diff --git a/pingora-core/src/apps/mod.rs b/pingora-core/src/apps/mod.rs index 93bea8b4..d19e2f7b 100644 --- a/pingora-core/src/apps/mod.rs +++ b/pingora-core/src/apps/mod.rs @@ -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); } diff --git a/pingora-core/src/protocols/http/server.rs b/pingora-core/src/protocols/http/server.rs index bdf2bdc5..71c0ebaa 100644 --- a/pingora-core/src/protocols/http/server.rs +++ b/pingora-core/src/protocols/http/server.rs @@ -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. diff --git a/pingora-core/src/protocols/http/v1/server.rs b/pingora-core/src/protocols/http/v1/server.rs index 9144c6e5..952e8c09 100644 --- a/pingora-core/src/protocols/http/v1/server.rs +++ b/pingora-core/src/protocols/http/v1/server.rs @@ -128,6 +128,8 @@ pub struct HttpSession { connection_user_context: Option>, /// 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 @@ -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, } @@ -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) diff --git a/pingora-core/src/protocols/tls/boringssl_openssl/stream.rs b/pingora-core/src/protocols/tls/boringssl_openssl/stream.rs index 894244c0..49e0f59a 100644 --- a/pingora-core/src/protocols/tls/boringssl_openssl/stream.rs +++ b/pingora-core/src/protocols/tls/boringssl_openssl/stream.rs @@ -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) } } diff --git a/pingora-core/src/protocols/tls/digest.rs b/pingora-core/src/protocols/tls/digest.rs index 58ecf3b6..d0c6b314 100644 --- a/pingora-core/src/protocols/tls/digest.rs +++ b/pingora-core/src/protocols/tls/digest.rs @@ -31,6 +31,8 @@ pub struct SslDigest { pub serial_number: Option, /// The digest of the peer's certificate pub cert_digest: Vec, + /// The SNI (Server Name Indication) of this connection + pub server_name: Option, /// The user-defined TLS data pub extension: SslDigestExtension, } @@ -43,6 +45,7 @@ impl SslDigest { organization: Option, serial_number: Option, cert_digest: Vec, + server_name: Option, ) -> Self where S: Into>, @@ -53,6 +56,7 @@ impl SslDigest { organization, serial_number, cert_digest, + server_name, extension: SslDigestExtension::default(), } } diff --git a/pingora-core/src/protocols/tls/rustls/stream.rs b/pingora-core/src/protocols/tls/rustls/stream.rs index f2a0ddae..1b41f4c8 100644 --- a/pingora-core/src/protocols/tls/rustls/stream.rs +++ b/pingora-core/src/protocols/tls/rustls/stream.rs @@ -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, + ) } } diff --git a/pingora-core/src/protocols/tls/s2n/stream.rs b/pingora-core/src/protocols/tls/s2n/stream.rs index 3f12ea44..92965045 100644 --- a/pingora-core/src/protocols/tls/s2n/stream.rs +++ b/pingora-core/src/protocols/tls/s2n/stream.rs @@ -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, ) } } diff --git a/pingora-core/tests/test_basic.rs b/pingora-core/tests/test_basic.rs index ae6ee810..e1a6d6aa 100644 --- a/pingora-core/tests/test_basic.rs +++ b/pingora-core/tests/test_basic.rs @@ -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" + ); +} diff --git a/pingora-core/tests/utils/mod.rs b/pingora-core/tests/utils/mod.rs index a8d526b7..61ea7432 100644 --- a/pingora-core/tests/utils/mod.rs +++ b/pingora-core/tests/utils/mod.rs @@ -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() } diff --git a/pingora-http/src/lib.rs b/pingora-http/src/lib.rs index 954be81b..e9d1b08d 100644 --- a/pingora-http/src/lib.rs +++ b/pingora-http/src/lib.rs @@ -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::() .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::() .explain_err(InvalidHTTPHeader, |_| format!("invalid uri {}", lossy_str))?; self.base.uri = uri; self.raw_path_fallback = path.to_vec(); @@ -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()) } } @@ -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 = vec![]; @@ -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(); diff --git a/pingora-proxy/src/lib.rs b/pingora-proxy/src/lib.rs index 4ce9e5e5..f1284b95 100644 --- a/pingora-proxy/src/lib.rs +++ b/pingora-proxy/src/lib.rs @@ -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) => { diff --git a/pingora-rustls/src/lib.rs b/pingora-rustls/src/lib.rs index deb0c88b..d8916b57 100644 --- a/pingora-rustls/src/lib.rs +++ b/pingora-rustls/src/lib.rs @@ -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.