Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pingora-core/src/apps/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ where
let mut result = self.process_new_http(session, shutdown).await;
while let Some((stream, persistent_settings)) = result.map(|r| r.consume()) {
let mut session = ServerSession::new_http1(stream);
session.set_reused(true);
if let Some(persistent_settings) = persistent_settings {
persistent_settings.apply_to_session(&mut session);
}
Expand Down
16 changes: 16 additions & 0 deletions pingora-core/src/protocols/http/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,22 @@ impl Session {
}
}

/// Set whether this session is on a reused keepalive connection.
pub fn set_reused(&mut self, reused: bool) {
if let Self::H1(s) = self {
s.set_reused(reused);
}
}

/// Whether this session is on a reused keepalive connection.
pub fn is_reused(&self) -> bool {
if let Self::H1(s) = self {
s.is_reused()
} else {
false
}
}

/// Set user-defined context to carry across requests on the same keepalive connection.
///
/// Only applicable for HTTP/1.x connections; noop for h2, subrequest, and custom sessions.
Expand Down
13 changes: 13 additions & 0 deletions pingora-core/src/protocols/http/v1/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ pub struct HttpSession {
connection_user_context: Option<Box<dyn Any + Send + Sync>>,
/// Whether the client has closed the TCP connection (sent FIN / read returned 0).
half_closed: bool,
/// Whether this session is on a reused keepalive connection.
is_reused: bool,
/// When true (default), a client close after the request body is surfaced as a
/// `ConnectionClosed` error so the proxy aborts immediately. When false, the
/// close is tolerated and `read_body_or_idle` stays pending so the proxy can
Expand Down Expand Up @@ -179,6 +181,7 @@ impl HttpSession {
keepalive_reuses_remaining: None,
connection_user_context: None,
half_closed: false,
is_reused: false,
abort_on_close: true,
proxy_tasks_enabled: false,
}
Expand Down Expand Up @@ -1102,6 +1105,16 @@ impl HttpSession {
self.half_closed
}

/// Set whether this session is on a reused keepalive connection.
pub fn set_reused(&mut self, reused: bool) {
self.is_reused = reused;
}

/// Whether this session is on a reused keepalive connection.
pub fn is_reused(&self) -> bool {
self.is_reused
}

/// Return the raw bytes of the request header.
pub fn get_headers_raw_bytes(&self) -> Bytes {
self.raw_header.as_ref().unwrap().get_bytes(&self.buf)
Expand Down
6 changes: 5 additions & 1 deletion pingora-core/src/protocols/tls/boringssl_openssl/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,11 @@ impl SslDigest {
None => (Vec::new(), None, None),
};

SslDigest::new(cipher, ssl.version_str(), org, sn, cert_digest)
let server_name = ssl
.servername(ssl::NameType::HOST_NAME)
.map(|s| s.to_string());

SslDigest::new(cipher, ssl.version_str(), org, sn, cert_digest, server_name)
}
}

Expand Down
4 changes: 4 additions & 0 deletions pingora-core/src/protocols/tls/digest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ pub struct SslDigest {
pub serial_number: Option<String>,
/// The digest of the peer's certificate
pub cert_digest: Vec<u8>,
/// The SNI (Server Name Indication) of this connection
pub server_name: Option<String>,
/// The user-defined TLS data
pub extension: SslDigestExtension,
}
Expand All @@ -43,6 +45,7 @@ impl SslDigest {
organization: Option<String>,
serial_number: Option<String>,
cert_digest: Vec<u8>,
server_name: Option<String>,
) -> Self
where
S: Into<Cow<'static, str>>,
Expand All @@ -53,6 +56,7 @@ impl SslDigest {
organization,
serial_number,
cert_digest,
server_name,
extension: SslDigestExtension::default(),
}
}
Expand Down
14 changes: 13 additions & 1 deletion pingora-core/src/protocols/tls/rustls/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,19 @@ impl SslDigest {
.map(|(organization, serial)| (organization, Some(serial)))
.unwrap_or_default();

SslDigest::new(cipher, version, organization, serial_number, cert_digest)
let server_name = match stream {
RusTlsStream::Server(s) => s.get_ref().1.server_name().map(|s| s.to_string()),
RusTlsStream::Client(_) => None,
};

SslDigest::new(
cipher,
version,
organization,
serial_number,
cert_digest,
server_name,
)
}
}

Expand Down
3 changes: 3 additions & 0 deletions pingora-core/src/protocols/tls/s2n/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,12 +307,15 @@ impl SslDigest {
}
}

let server_name = conn.server_name().map(|s| s.to_string());

SslDigest::new(
cipher,
version,
organization,
serial_number,
cert_digest.unwrap_or_default(),
server_name,
)
}
}
Expand Down
26 changes: 26 additions & 0 deletions pingora-core/tests/test_basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,29 @@ async fn test_h2c_tcp_still_works() {
assert_eq!(res.status(), reqwest::StatusCode::OK);
assert_eq!(res.version(), reqwest::Version::HTTP_11);
}

#[cfg(feature = "any_tls")]
#[tokio::test]
async fn test_sni_digest_extraction() {
utils::init();

let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.resolve_to_addrs(
"myapp.example.com",
&[std::net::SocketAddr::from(([127, 0, 0, 1], 6146))],
)
.build()
.unwrap();

let res = client
.get("https://myapp.example.com:6146")
.send()
.await
.unwrap();
assert_eq!(res.status(), reqwest::StatusCode::OK);
assert_eq!(
res.headers().get("x-sni").unwrap().to_str().unwrap(),
"myapp.example.com"
);
}
7 changes: 7 additions & 0 deletions pingora-core/tests/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,17 @@ impl ServeHttp for EchoApp {
}
};

let sni = http_stream
.digest()
.and_then(|d| d.ssl_digest.as_ref())
.and_then(|s| s.server_name.as_deref())
.unwrap_or("none");

Response::builder()
.status(StatusCode::OK)
.header(http::header::CONTENT_TYPE, "text/html")
.header(http::header::CONTENT_LENGTH, body.len())
.header("x-sni", sni)
.body(body.to_vec())
.unwrap()
}
Expand Down
24 changes: 10 additions & 14 deletions pingora-http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,18 +258,16 @@ impl RequestHeader {
/// This API is to allow supporting non UTF-8 cases.
pub fn set_raw_path(&mut self, path: &[u8]) -> Result<()> {
if let Ok(p) = std::str::from_utf8(path) {
let uri = Uri::builder()
.path_and_query(p)
.build()
let uri = p
.parse::<Uri>()
.explain_err(InvalidHTTPHeader, |_| format!("invalid uri {}", p))?;
self.base.uri = uri;
// keep raw_path empty, no need to store twice
} else {
// put a valid utf-8 path into base for read only access
let lossy_str = String::from_utf8_lossy(path);
let uri = Uri::builder()
.path_and_query(lossy_str.as_ref())
.build()
let uri = lossy_str
.parse::<Uri>()
.explain_err(InvalidHTTPHeader, |_| format!("invalid uri {}", lossy_str))?;
self.base.uri = uri;
self.raw_path_fallback = path.to_vec();
Expand Down Expand Up @@ -297,15 +295,13 @@ impl RequestHeader {
pub fn raw_path(&self) -> &[u8] {
if !self.raw_path_fallback.is_empty() {
&self.raw_path_fallback
} else if let Some(pq) = self.base.uri.path_and_query() {
pq.as_str().as_bytes()
} else {
// Url should always be set
self.base
.uri
.path_and_query()
.as_ref()
.unwrap()
.as_str()
.as_bytes()
.authority()
.map_or(b"", |a| a.as_str().as_bytes())
}
}

Expand Down Expand Up @@ -771,7 +767,7 @@ mod tests {

#[test]
fn test_single_header() {
let mut req = RequestHeader::build("GET", b"\\", None).unwrap();
let mut req = RequestHeader::build("GET", b"/", None).unwrap();
req.insert_header("foo", "bar").unwrap();
req.insert_header("FoO", "Bar").unwrap();
let mut buf: Vec<u8> = vec![];
Expand Down Expand Up @@ -833,7 +829,7 @@ mod tests {

#[test]
fn test_multiple_header() {
let mut req = RequestHeader::build("GET", b"\\", None).unwrap();
let mut req = RequestHeader::build("GET", b"/", None).unwrap();
req.append_header("FoO", "Bar").unwrap();
req.append_header("fOO", "bar").unwrap();
req.append_header("BAZ", "baR").unwrap();
Expand Down
23 changes: 16 additions & 7 deletions pingora-proxy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,15 +233,24 @@ where
SV: ProxyHttp + Send + Sync,
SV::CTX: Send + Sync,
{
// phase 1 read request header
// 1. Lock-free check first: if already shutting down, abort immediately
if self.shutdown_flag.load(Ordering::Acquire) {
return None;
}

let res = tokio::select! {
biased; // biased select is cheaper, and we don't want to drop already buffered requests
res = downstream_session.read_request() => { res }
_ = self.shutdown.notified() => {
// service shutting down, dropping the connection to stop more req from coming in
return None;
// 2. Only select on the shutdown Notify primitive if the connection is reused (keepalive).
// For new connections, the client will send the request immediately, avoiding registration lock contention.
let res = if downstream_session.is_reused() {
tokio::select! {
biased; // biased select is cheaper, and we don't want to drop already buffered requests
res = downstream_session.read_request() => { res }
_ = self.shutdown.notified() => {
// service shutting down, dropping the connection to stop more req from coming in
return None;
}
}
} else {
downstream_session.read_request().await
};
match res {
Ok(true) => {
Expand Down
4 changes: 2 additions & 2 deletions pingora-rustls/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ pub use rustls::server::danger::{ClientCertVerified, ClientCertVerifier};
pub use rustls::server::{ClientCertVerifierBuilder, WebPkiClientVerifier};
pub use rustls::{
client::WebPkiServerVerifier, crypto::CryptoProvider, version, CertificateError, ClientConfig,
DigitallySignedStruct, Error as RusTlsError, KeyLogFile, RootCertStore, ServerConfig,
SignatureScheme, Stream,
Connection, DigitallySignedStruct, Error as RusTlsError, KeyLogFile, RootCertStore,
ServerConfig, SignatureScheme, Stream,
};

/// Install the default `ring` CryptoProvider for rustls.
Expand Down
Loading