//! Bounded listeners. HTTP/2 is negotiated through TLS ALPN. Cleartext HTTP/2 //! is opt-in per listener and accepts both the HTTP/2 prior-knowledge preface and //! a validated HTTP/1.1 `Upgrade: h2c` bootstrap. use crate::{metrics::Metrics, runtime::App, settings::Http2Connections}; use anyhow::{Context, Result}; use axum::{body::Body, Router}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use bytes::{Buf, Bytes, BytesMut}; use hyper::{ body::Incoming, server::conn::{http1, http2}, service::service_fn, Request, Response, }; use hyper_util::rt::{TokioExecutor, TokioIo, TokioTimer}; use http_body::Body as HttpBody; use std::{ convert::Infallible, io, net::SocketAddr, pin::Pin, sync::{ atomic::{AtomicU64, AtomicUsize, Ordering}, Arc, }, task::{Context as TaskContext, Poll}, time::Duration, }; use tokio::{ io::{AsyncRead, AsyncWrite, ReadBuf}, net::TcpListener, sync::{Notify, OwnedSemaphorePermit, Semaphore}, task::JoinSet, }; use tokio_util::sync::CancellationToken; use tower::ServiceExt; const HTTP2_PRIOR_KNOWLEDGE_PREFACE: &[u8; 24] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"; const H2_FRAME_HEADER_LEN: usize = 9; fn header_has_token(headers: &http::HeaderMap, name: &str, token: &str) -> bool { headers .get_all(name) .iter() .filter_map(|value| value.to_str().ok()) .flat_map(|value| value.split(',')) .any(|value| value.trim().eq_ignore_ascii_case(token)) } fn single_header<'a>(headers: &'a http::HeaderMap, name: &str) -> std::result::Result<&'a str, &'static str> { let mut values = headers.get_all(name).iter(); let value = values.next().ok_or("missing required h2c header")?; if values.next().is_some() { return Err("duplicate h2c header"); } value.to_str().map_err(|_| "invalid h2c header encoding") } fn h2c_attempt(request: &Request) -> bool { request .headers() .get_all(http::header::UPGRADE) .iter() .filter_map(|value| value.to_str().ok()) .flat_map(|value| value.split(',')) .any(|value| value.trim().eq_ignore_ascii_case("h2c")) || request.headers().contains_key("http2-settings") } fn hpack_integer(out: &mut Vec, value: usize, prefix_bits: u8, first: u8) { let limit = (1usize << prefix_bits) - 1; if value < limit { out.push(first | value as u8); return; } out.push(first | limit as u8); let mut rest = value - limit; while rest >= 128 { out.push((rest as u8 & 0x7f) | 0x80); rest >>= 7; } out.push(rest as u8); } fn hpack_string(out: &mut Vec, value: &[u8]) { // Huffman bit stays clear; the bootstrap encoder is deliberately simple. hpack_integer(out, value.len(), 7, 0); out.extend_from_slice(value); } fn hpack_literal(out: &mut Vec, name: &str, value: &str) { // Literal Header Field without Indexing, new name (RFC 7541 section 6.2.2). out.push(0); hpack_string(out, name.as_bytes()); hpack_string(out, value.as_bytes()); } fn h2_headers_frame(request: &Request) -> std::result::Result { if request.version() != http::Version::HTTP_11 { return Err("h2c upgrade requires HTTP/1.1"); } if !header_has_token(request.headers(), http::header::CONNECTION.as_str(), "upgrade") || !header_has_token(request.headers(), http::header::CONNECTION.as_str(), "http2-settings") { return Err("Connection must nominate Upgrade and HTTP2-Settings"); } if !single_header(request.headers(), http::header::UPGRADE.as_str())? .eq_ignore_ascii_case("h2c") { return Err("Upgrade must be exactly h2c"); } let settings = single_header(request.headers(), "http2-settings")?; let settings = URL_SAFE_NO_PAD .decode(settings.as_bytes()) .map_err(|_| "invalid HTTP2-Settings base64url")?; if settings.len() > 36 || settings.len() % 6 != 0 { return Err("invalid HTTP2-Settings payload"); } if request.headers().contains_key(http::header::TRANSFER_ENCODING) { return Err("h2c upgrade request body is not supported"); } if request.headers().get_all(http::header::CONTENT_LENGTH).iter().count() > 1 { return Err("duplicate Content-Length on h2c upgrade"); } if request .headers() .get(http::header::CONTENT_LENGTH) .is_some_and(|value| value.as_bytes() != b"0") { return Err("h2c upgrade request body is not supported"); } let authority = request .uri() .authority() .map(|value| value.as_str()) .or_else(|| request.headers().get(http::header::HOST).and_then(|value| value.to_str().ok())) .ok_or("h2c upgrade requires Host/:authority")?; let path = request .uri() .path_and_query() .map(|value| value.as_str()) .unwrap_or("/"); let method = request.method().as_str(); if method.len() > 32 || authority.len() > 1024 || path.len() > 8192 { return Err("h2c bootstrap request fields exceed limits"); } let mut block = Vec::with_capacity(1024); hpack_literal(&mut block, ":method", method); hpack_literal(&mut block, ":scheme", "http"); hpack_literal(&mut block, ":authority", authority); hpack_literal(&mut block, ":path", path); for (name, value) in request.headers() { let name = name.as_str(); if matches!( name, "connection" | "upgrade" | "http2-settings" | "host" | "keep-alive" | "proxy-connection" | "transfer-encoding" ) || header_has_token(request.headers(), http::header::CONNECTION.as_str(), name) { continue; } if name == "te" && !value.as_bytes().eq_ignore_ascii_case(b"trailers") { continue; } let value = value.to_str().map_err(|_| "non-text header in h2c bootstrap")?; hpack_literal(&mut block, name, value); } if block.len() > 32_768 { return Err("h2c bootstrap header block exceeds limit"); } let length = block.len(); let mut frame = Vec::with_capacity(H2_FRAME_HEADER_LEN + length); frame.extend_from_slice(&[ ((length >> 16) & 0xff) as u8, ((length >> 8) & 0xff) as u8, (length & 0xff) as u8, 0x1, // HEADERS 0x5, // END_STREAM | END_HEADERS 0, 0, 0, 1, // stream 1 is the HTTP/1.1 upgrade request ]); frame.extend_from_slice(&block); Ok(Bytes::from(frame)) } /// After a successful 101 response the client sends the normal HTTP/2 client /// connection preface. Hyper still needs to observe the upgrade request as /// stream 1, so this adapter injects one synthetic HEADERS frame immediately /// after the client's mandatory initial SETTINGS frame. All later streams are /// handled by Hyper's normal HTTP/2 state machine. struct H2cUpgradeIo { inner: I, injected_headers: Bytes, bootstrap: BytesMut, ready: Bytes, bootstrap_done: bool, } impl H2cUpgradeIo { fn new(inner: I, injected_headers: Bytes) -> Self { Self { inner, injected_headers, bootstrap: BytesMut::with_capacity(4096), ready: Bytes::new(), bootstrap_done: false, } } fn try_finish_bootstrap(&mut self) -> io::Result<()> { if self.bootstrap_done || self.bootstrap.len() < HTTP2_PRIOR_KNOWLEDGE_PREFACE.len() + H2_FRAME_HEADER_LEN { return Ok(()); } if &self.bootstrap[..HTTP2_PRIOR_KNOWLEDGE_PREFACE.len()] != HTTP2_PRIOR_KNOWLEDGE_PREFACE { return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid HTTP/2 client preface after h2c upgrade")); } let offset = HTTP2_PRIOR_KNOWLEDGE_PREFACE.len(); let header = &self.bootstrap[offset..offset + H2_FRAME_HEADER_LEN]; let length = ((header[0] as usize) << 16) | ((header[1] as usize) << 8) | header[2] as usize; if length > 16_384 || header[3] != 0x4 || header[4] != 0 || header[5] != 0 || header[6] != 0 || header[7] != 0 || header[8] != 0 { return Err(io::Error::new(io::ErrorKind::InvalidData, "h2c client preface must begin with a non-ACK SETTINGS frame")); } let end = offset + H2_FRAME_HEADER_LEN + length; if self.bootstrap.len() < end { return Ok(()); } let mut ready = BytesMut::with_capacity(self.bootstrap.len() + self.injected_headers.len()); ready.extend_from_slice(&self.bootstrap[..end]); ready.extend_from_slice(&self.injected_headers); ready.extend_from_slice(&self.bootstrap[end..]); self.ready = ready.freeze(); self.bootstrap.clear(); self.bootstrap_done = true; Ok(()) } } impl AsyncRead for H2cUpgradeIo { fn poll_read( mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { loop { if !self.ready.is_empty() { let take = self.ready.len().min(buf.remaining()); buf.put_slice(&self.ready[..take]); self.ready.advance(take); return Poll::Ready(Ok(())); } if self.bootstrap_done { return Pin::new(&mut self.inner).poll_read(cx, buf); } let mut temp = [0u8; 8192]; let mut temp_buf = ReadBuf::new(&mut temp); match Pin::new(&mut self.inner).poll_read(cx, &mut temp_buf) { Poll::Pending => return Poll::Pending, Poll::Ready(Err(error)) => return Poll::Ready(Err(error)), Poll::Ready(Ok(())) => { let bytes = temp_buf.filled(); if bytes.is_empty() { return Poll::Ready(Err(io::Error::new( io::ErrorKind::UnexpectedEof, "connection closed during h2c client preface", ))); } self.bootstrap.extend_from_slice(bytes); if self.bootstrap.len() > 128 * 1024 { return Poll::Ready(Err(io::Error::new( io::ErrorKind::InvalidData, "h2c bootstrap exceeds limit", ))); } if let Err(error) = self.try_finish_bootstrap() { return Poll::Ready(Err(error)); } } } } } } impl AsyncWrite for H2cUpgradeIo { fn poll_write(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>, buf: &[u8]) -> Poll> { Pin::new(&mut self.inner).poll_write(cx, buf) } fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { Pin::new(&mut self.inner).poll_flush(cx) } fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { Pin::new(&mut self.inner).poll_shutdown(cx) } } /// Shared HTTP/2 activity state. Idle shutdown is allowed only when there are /// no active streams and no recent frame traffic. Lifetime shutdown ignores idle /// state but still uses the same GOAWAY -> drain -> TCP close sequence. struct H2ConnectionState { started: std::time::Instant, last_activity_ms: AtomicU64, active_streams: AtomicUsize, notify: Notify, } impl H2ConnectionState { fn new() -> Arc { Arc::new(Self { started: std::time::Instant::now(), last_activity_ms: AtomicU64::new(0), active_streams: AtomicUsize::new(0), notify: Notify::new(), }) } fn elapsed_ms(&self) -> u64 { self.started.elapsed().as_millis().min(u128::from(u64::MAX)) as u64 } fn mark_activity(&self) { self.last_activity_ms .fetch_max(self.elapsed_ms(), Ordering::Relaxed); self.notify.notify_waiters(); } fn idle_for(&self) -> Duration { Duration::from_millis( self.elapsed_ms() .saturating_sub(self.last_activity_ms.load(Ordering::Relaxed)), ) } fn is_idle(&self, idle: Duration) -> bool { self.active_streams.load(Ordering::Acquire) == 0 && self.idle_for() >= idle } } struct H2ActivityIo { inner: I, state: Arc, } impl H2ActivityIo { fn new(inner: I, state: Arc) -> Self { Self { inner, state } } } impl Unpin for H2ActivityIo {} impl AsyncRead for H2ActivityIo { fn poll_read( self: Pin<&mut Self>, cx: &mut TaskContext<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { let this = self.get_mut(); let before = buf.filled().len(); match Pin::new(&mut this.inner).poll_read(cx, buf) { Poll::Ready(Ok(())) => { if buf.filled().len() > before { this.state.mark_activity(); } Poll::Ready(Ok(())) } other => other, } } } impl AsyncWrite for H2ActivityIo { fn poll_write( self: Pin<&mut Self>, cx: &mut TaskContext<'_>, buf: &[u8], ) -> Poll> { let this = self.get_mut(); match Pin::new(&mut this.inner).poll_write(cx, buf) { Poll::Ready(Ok(written)) => { if written > 0 { this.state.mark_activity(); } Poll::Ready(Ok(written)) } other => other, } } fn poll_flush(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { Pin::new(&mut self.get_mut().inner).poll_flush(cx) } fn poll_shutdown(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { Pin::new(&mut self.get_mut().inner).poll_shutdown(cx) } } struct H2StreamLease { state: Arc, } impl H2StreamLease { fn begin(state: Arc) -> Self { state.active_streams.fetch_add(1, Ordering::AcqRel); state.mark_activity(); Self { state } } } impl Drop for H2StreamLease { fn drop(&mut self) { self.state.active_streams.fetch_sub(1, Ordering::AcqRel); self.state.mark_activity(); } } struct H2TrackedBody { inner: Pin>, stream: Option, } impl H2TrackedBody { fn new(inner: B, stream: H2StreamLease) -> Self { Self { inner: Box::pin(inner), stream: Some(stream), } } } impl HttpBody for H2TrackedBody where B: HttpBody, { type Data = B::Data; type Error = B::Error; fn poll_frame( self: Pin<&mut Self>, cx: &mut TaskContext<'_>, ) -> Poll, Self::Error>>> { let this = self.get_mut(); match this.inner.as_mut().poll_frame(cx) { Poll::Ready(None) => { this.stream.take(); Poll::Ready(None) } Poll::Ready(Some(Err(error))) => { this.stream.take(); Poll::Ready(Some(Err(error))) } other => other, } } fn is_end_stream(&self) -> bool { self.inner.as_ref().get_ref().is_end_stream() } fn size_hint(&self) -> http_body::SizeHint { self.inner.as_ref().get_ref().size_hint() } } async fn wait_for_h2_idle(state: Arc, idle: Duration) { loop { // Register for a notification before inspecting the state. This avoids // losing the final stream-drop notification between the load and await. let notified = state.notify.notified(); tokio::pin!(notified); if state.active_streams.load(Ordering::Acquire) > 0 { notified.as_mut().await; continue; } let idle_for = state.idle_for(); if idle_for >= idle { return; } tokio::select! { _ = tokio::time::sleep(idle.saturating_sub(idle_for)) => {}, _ = notified.as_mut() => {}, } } } struct ActiveHttp2Connection(Option>); impl ActiveHttp2Connection { fn begin(metrics: Option>) -> Self { if let Some(metrics) = &metrics { metrics .http2_active_connections .fetch_add(1, Ordering::Relaxed); } Self(metrics) } } impl Drop for ActiveHttp2Connection { fn drop(&mut self) { if let Some(metrics) = &self.0 { metrics .http2_active_connections .fetch_sub(1, Ordering::Relaxed); } } } fn h2_prior_knowledge_prefix(bytes: &[u8]) -> bool { bytes.len() <= HTTP2_PRIOR_KNOWLEDGE_PREFACE.len() && HTTP2_PRIOR_KNOWLEDGE_PREFACE.starts_with(bytes) } #[derive(Clone, Copy)] pub struct PeerAddr(pub SocketAddr); #[derive(Clone, Copy)] pub struct LocalAddr(pub SocketAddr); #[derive(Clone, Copy, Default)] pub struct Transport { pub tls: bool, } #[derive(Clone)] pub struct ServerShutdown(pub CancellationToken); #[derive(Clone)] enum TlsMode { Frontend(Arc, bool), Fixed(Arc, Option), } /// Upgraded tunnels hold this lease after Hyper finishes its HTTP connection. /// This prevents WebSockets from escaping the public connection limit/counter. pub struct ConnectionLease { _permit: OwnedSemaphorePermit, metrics: Option>, } impl Drop for ConnectionLease { fn drop(&mut self) { if let Some(m) = &self.metrics { m.active_connections.fetch_sub(1, Ordering::Relaxed); } } } pub async fn serve( listener: TcpListener, router: Router, shutdown: CancellationToken, max_connections: usize, grace: Duration, metrics: Option>, ) -> Result<()> { serve_inner( listener, router, shutdown, Arc::new(Semaphore::new(max_connections)), grace, metrics, None, false, None, Http2Connections::default(), ) .await } pub async fn serve_public( listener: TcpListener, router: Router, shutdown: CancellationToken, permits: Arc, grace: Duration, metrics: Arc, tls: Option>, ) -> Result<()> { serve_frontend( listener, router, shutdown, permits, grace, metrics, tls.clone().map(|app| (app, true)), false, tls, ) .await } pub async fn serve_frontend( listener: TcpListener, router: Router, shutdown: CancellationToken, permits: Arc, grace: Duration, metrics: Arc, tls: Option<(Arc, bool)>, h2c: bool, frontend_state: Option>, ) -> Result<()> { let http2_settings = frontend_state .as_ref() .map(|state| state.config.load().envelope.config.http2) .unwrap_or_default(); serve_frontend_with_http2( listener, router, shutdown, permits, grace, metrics, tls, h2c, frontend_state, http2_settings, ) .await } /// Start one compiled frontend listener with its immutable per-listener H2 /// lifecycle policy. The public `serve_frontend` wrapper retains the legacy /// global fallback for callers that do not provide a compiled listener plan. pub async fn serve_frontend_with_http2( listener: TcpListener, router: Router, shutdown: CancellationToken, permits: Arc, grace: Duration, metrics: Arc, tls: Option<(Arc, bool)>, h2c: bool, frontend_state: Option>, http2_settings: Http2Connections, ) -> Result<()> { serve_inner( listener, router, shutdown, permits, grace, Some(metrics), tls.map(|(app, http2)| TlsMode::Frontend(app, http2)), h2c, frontend_state, http2_settings, ) .await } pub async fn serve_tls( listener: TcpListener, router: Router, shutdown: CancellationToken, max_connections: usize, grace: Duration, tls: Arc, ) -> Result<()> { serve_inner( listener, router, shutdown, Arc::new(Semaphore::new(max_connections)), grace, None, Some(TlsMode::Fixed(tls, None)), false, None, Http2Connections::default(), ) .await } /// Serve an HTTPS socket while turning accidental/plain HTTP requests on the /// same port into redirects. No administration handler is exposed through the /// plaintext router. pub async fn serve_tls_with_plain_redirect( listener: TcpListener, router: Router, plaintext_router: Router, shutdown: CancellationToken, max_connections: usize, grace: Duration, tls: Arc, ) -> Result<()> { serve_inner( listener, router, shutdown, Arc::new(Semaphore::new(max_connections)), grace, None, Some(TlsMode::Fixed(tls, Some(plaintext_router))), false, None, Http2Connections::default(), ) .await } async fn serve_inner( listener: TcpListener, router: Router, shutdown: CancellationToken, permits: Arc, grace: Duration, metrics: Option>, tls: Option, h2c: bool, frontend_state: Option>, http2_settings: Http2Connections, ) -> Result<()> { let mut tasks = JoinSet::new(); loop { tokio::select! { biased; _ = shutdown.cancelled() => break, _ = tasks.join_next(), if !tasks.is_empty() => {}, accepted = listener.accept() => { let (stream, peer) = accepted.context("listener accept failed")?; // Never substitute the wildcard listener address or a forwarded // header for the destination IP of this accepted connection. let local = stream.local_addr().context("accepted local address unavailable")?; let Ok(permit) = permits.clone().try_acquire_owned() else { if let Some(m) = &metrics { m.rejected_connections.fetch_add(1, Ordering::Relaxed); } drop(stream); continue; }; let _ = stream.set_nodelay(true); if let Some(m) = &metrics { m.connections.fetch_add(1, Ordering::Relaxed); m.active_connections.fetch_add(1, Ordering::Relaxed); } let lease = Arc::new(ConnectionLease { _permit: permit, metrics: metrics.clone() }); let router = router.clone(); let stop = shutdown.clone(); let tls = tls.clone(); let metrics = metrics.clone(); let frontend_state = frontend_state.clone(); tasks.spawn(async move { match tls { Some(TlsMode::Frontend(state, http2)) => { // Read ClientHello before committing to ALPN so a vhost can opt out // of HTTP/2 or narrow TLS policy while sharing one TLS listener. let deadline = tokio::time::Instant::now() + Duration::from_secs(10); let acceptor = tokio_rustls::LazyConfigAcceptor::new( rustls::server::Acceptor::default(), stream, ); let started = tokio::select! { _ = stop.cancelled() => return, started = tokio::time::timeout_at(deadline, acceptor) => started, }; let Ok(Ok(start)) = started else { state.metrics.tls_handshake_errors.fetch_add(1, Ordering::Relaxed); return; }; let config = { let snapshot = state.config.load(); let host = start .client_hello() .server_name() .and_then(|name| crate::config::request_host(name).ok()); let frontend = host .as_deref() .and_then(|host| snapshot.frontend(host, local, true)); let local_host = host .as_deref() .and_then(|host| snapshot.not_found_host_on(host, local, true)); if let Some(frontend) = frontend { if http2 && frontend.http2_enabled { frontend.tls.clone() } else { frontend.tls_http1.clone() } } else if http2 && local_host.is_none_or(|item| item.http2_enabled) { snapshot.tls.clone() } else { snapshot.tls_http1.clone() } }; let Some(config) = config else { return; }; let accepted = tokio::select! { _ = stop.cancelled() => return, accepted = tokio::time::timeout_at(deadline, start.into_stream(config)) => accepted, }; match accepted { Ok(Ok(stream)) => { let h2 = stream.get_ref().1.alpn_protocol() == Some(b"h2".as_slice()); state.metrics.tls_connections.fetch_add(1, Ordering::Relaxed); if h2 { state.metrics.http2_connections.fetch_add(1, Ordering::Relaxed); } let extended_connect = h2 && state.config.load().websocket_rfc8441_on(local, true); connection( stream, router, peer, local, Transport { tls: true }, h2, extended_connect, false, None, lease, stop, grace, http2_settings, ) .await; }, _ => { state.metrics.tls_handshake_errors.fetch_add(1, Ordering::Relaxed); }, } } Some(TlsMode::Fixed(config, plaintext_router)) => { let deadline = tokio::time::Instant::now() + Duration::from_secs(10); if let Some(plain) = plaintext_router { let mut first = [0u8; 1]; let peeked = tokio::select! { _ = stop.cancelled() => return, peeked = tokio::time::timeout_at(deadline, stream.peek(&mut first)) => peeked, }; match peeked { Ok(Ok(n)) if n > 0 && first[0] != 0x16 => { connection( stream, plain, peer, local, Transport { tls: false }, false, false, false, None, lease, stop, grace, Http2Connections::default(), ) .await; return; } Ok(Ok(n)) if n > 0 => {} _ => return, } } let acceptor = tokio_rustls::TlsAcceptor::from(config); let accepted = tokio::select! { _ = stop.cancelled() => return, accepted = tokio::time::timeout_at(deadline, acceptor.accept(stream)) => accepted, }; if let Ok(Ok(stream)) = accepted { let h2 = stream.get_ref().1.alpn_protocol() == Some(b"h2".as_slice()); connection( stream, router, peer, local, Transport { tls: true }, h2, false, false, None, lease, stop, grace, Http2Connections::default(), ) .await; } } None => { let prior_h2 = if h2c { let detected = tokio::time::timeout(Duration::from_secs(10), async { let mut preface = [0u8; HTTP2_PRIOR_KNOWLEDGE_PREFACE.len()]; loop { let n = stream.peek(&mut preface).await?; if n == 0 { return Ok::(false); } let prefix = &preface[..n.min(HTTP2_PRIOR_KNOWLEDGE_PREFACE.len())]; if !h2_prior_knowledge_prefix(prefix) { return Ok(false); } if n >= HTTP2_PRIOR_KNOWLEDGE_PREFACE.len() { return Ok(prefix == HTTP2_PRIOR_KNOWLEDGE_PREFACE); } tokio::task::yield_now().await; } }).await; matches!(detected, Ok(Ok(true))) } else { false }; if prior_h2 { if let Some(m) = &metrics { m.http2_connections.fetch_add(1, Ordering::Relaxed); } } let extended_connect = prior_h2 && frontend_state .as_ref() .is_some_and(|state| state.config.load().websocket_rfc8441_on(local, false)); connection( stream, router, peer, local, Transport { tls: false }, prior_h2, extended_connect, h2c && !prior_h2, frontend_state, lease, stop, grace, http2_settings, ) .await; } } }); }, } } let _ = tokio::time::timeout(grace + Duration::from_secs(1), async { while tasks.join_next().await.is_some() {} }) .await; tasks.abort_all(); while tasks.join_next().await.is_some() {} Ok(()) } #[allow(clippy::too_many_arguments)] async fn connection( stream: I, router: Router, peer: SocketAddr, local: SocketAddr, transport: Transport, h2: bool, extended_connect: bool, h2c_upgrade: bool, h2c_state: Option>, lease: Arc, stop: CancellationToken, grace: Duration, http2_settings: Http2Connections, ) where I: AsyncRead + AsyncWrite + Unpin + Send + 'static, { if h2 { http2_connection( stream, router, peer, local, transport, extended_connect, lease, stop, grace, http2_settings, ) .await; return; } let request_stop = stop.clone(); let service = service_fn(move |mut request: Request| { let router = router.clone(); let request_stop = request_stop.clone(); let request_lease = lease.clone(); let upgrade_state = h2c_state.clone(); async move { request.extensions_mut().insert(PeerAddr(peer)); request.extensions_mut().insert(LocalAddr(local)); request.extensions_mut().insert(transport); request.extensions_mut().insert(request_lease.clone()); request .extensions_mut() .insert(ServerShutdown(request_stop.clone())); if h2c_attempt(&request) { if !h2c_upgrade { return Ok::<_, Infallible>( Response::builder() .status(http::StatusCode::BAD_REQUEST) .header(http::header::CACHE_CONTROL, "no-store") .body(Body::from("h2c upgrade is disabled on this listener")) .expect("static h2c disabled response"), ); } let injected_headers = match h2_headers_frame(&request) { Ok(frame) => frame, Err(message) => { return Ok::<_, Infallible>( Response::builder() .status(http::StatusCode::BAD_REQUEST) .header(http::header::CACHE_CONTROL, "no-store") .body(Body::from(message)) .expect("static h2c validation response"), ) } }; let on_upgrade = hyper::upgrade::on(&mut request); let upgrade_router = router.clone(); let upgrade_stop = request_stop.clone(); let upgrade_lease = request_lease.clone(); let upgrade_state_for_task = upgrade_state.clone(); let task = async move { match on_upgrade.await { Ok(upgraded) => { if let Some(state) = &upgrade_state_for_task { state.metrics.http2_connections.fetch_add(1, Ordering::Relaxed); } let extended_connect = upgrade_state_for_task.as_ref().is_some_and(|state| { state.config.load().websocket_rfc8441_on(local, false) }); let io = H2cUpgradeIo::new(TokioIo::new(upgraded), injected_headers); http2_connection( io, upgrade_router, peer, local, Transport { tls: false }, extended_connect, upgrade_lease, upgrade_stop, grace, http2_settings, ) .await; } Err(error) => tracing::debug!(%error, "h2c upgrade transport failed"), } }; if let Some(state) = upgrade_state { state.tunnels.spawn(task); } else { tokio::spawn(task); } return Ok::<_, Infallible>( Response::builder() .status(http::StatusCode::SWITCHING_PROTOCOLS) .header(http::header::CONNECTION, "Upgrade") .header(http::header::UPGRADE, "h2c") .body(Body::empty()) .expect("static h2c switching response"), ); } router.oneshot(request.map(Body::new)).await } }); let mut builder = http1::Builder::new(); builder .timer(TokioTimer::new()) .keep_alive(true) .max_headers(64) .max_buf_size(65_536) .header_read_timeout(Duration::from_secs(10)); let conn = builder .serve_connection(TokioIo::new(stream), service) .with_upgrades(); tokio::pin!(conn); tokio::select! { result = &mut conn => { if let Err(error) = result { tracing::debug!(%error, "HTTP/1 connection closed"); } }, _ = stop.cancelled() => { conn.as_mut().graceful_shutdown(); let _ = tokio::time::timeout(grace, &mut conn).await; }, } } #[derive(Clone, Copy, Debug)] enum H2ShutdownCause { Idle, Lifetime, Process, } #[allow(clippy::too_many_arguments)] async fn http2_connection( stream: I, router: Router, peer: SocketAddr, local: SocketAddr, transport: Transport, extended_connect: bool, lease: Arc, stop: CancellationToken, grace: Duration, settings: Http2Connections, ) where I: AsyncRead + AsyncWrite + Unpin + Send + 'static, { let request_stop = stop.clone(); let connection_metrics = lease.metrics.clone(); let activity = H2ConnectionState::new(); let service_activity = activity.clone(); let service = service_fn(move |mut request: Request| { let stream_lease = H2StreamLease::begin(service_activity.clone()); request.extensions_mut().insert(PeerAddr(peer)); request.extensions_mut().insert(LocalAddr(local)); request.extensions_mut().insert(transport); request.extensions_mut().insert(lease.clone()); request .extensions_mut() .insert(ServerShutdown(request_stop.clone())); let router = router.clone(); async move { router .oneshot(request.map(Body::new)) .await .map(|response| { response.map(|body| Body::new(H2TrackedBody::new(body, stream_lease))) }) } }); let _active_http2 = ActiveHttp2Connection::begin(connection_metrics); let stream = H2ActivityIo::new(stream, activity.clone()); let mut builder = http2::Builder::new(TokioExecutor::new()); builder .timer(TokioTimer::new()) .max_concurrent_streams(64) .max_header_list_size(32_768) .max_send_buf_size(65_536) .initial_stream_window_size(65_535) .initial_connection_window_size(1_048_576); if extended_connect { builder.enable_connect_protocol(); } let conn = builder.serve_connection(TokioIo::new(stream), service); tokio::pin!(conn); let max_lifetime = settings.max_connection_lifetime(); let lifetime = async move { if let Some(duration) = max_lifetime { tokio::time::sleep(duration).await; } else { std::future::pending::<()>().await; } }; tokio::pin!(lifetime); let cause = loop { tokio::select! { result = &mut conn => { if let Err(error) = result { tracing::debug!(%error, "HTTP/2 connection closed"); } return; }, _ = stop.cancelled() => break H2ShutdownCause::Process, _ = &mut lifetime => break H2ShutdownCause::Lifetime, _ = wait_for_h2_idle(activity.clone(), settings.idle_timeout()) => { // Re-check immediately before GOAWAY so activity racing the timer // cannot turn a busy connection into an idle shutdown. if activity.is_idle(settings.idle_timeout()) { break H2ShutdownCause::Idle; } }, } }; // Hyper's graceful shutdown sends GOAWAY and refuses newer streams while // continuing to poll streams that were already accepted. conn.as_mut().graceful_shutdown(); let configured_drain = settings.drain_timeout(); let drain = if matches!(cause, H2ShutdownCause::Process) { std::cmp::min(configured_drain, grace) } else { configured_drain }; match tokio::time::timeout(drain, &mut conn).await { Ok(Ok(())) => tracing::debug!(?cause, "HTTP/2 graceful shutdown drained"), Ok(Err(error)) => tracing::debug!(?cause, %error, "HTTP/2 graceful shutdown ended with protocol error"), Err(_) => tracing::debug!(?cause, drain_seconds = drain.as_secs_f64(), "HTTP/2 drain timeout reached; closing transport"), } } #[cfg(test)] mod h2c_tests { use super::*; use axum::routing::get; use tokio::io::{AsyncReadExt, AsyncWriteExt}; #[test] fn h2_prior_knowledge_preface_is_distinguished_from_http1() { assert!(h2_prior_knowledge_prefix(b"P")); assert!(h2_prior_knowledge_prefix(b"PRI * HTTP/2.0\r\n")); assert!(h2_prior_knowledge_prefix(HTTP2_PRIOR_KNOWLEDGE_PREFACE)); assert!(!h2_prior_knowledge_prefix(b"POST / HTTP/1.1\r\n")); assert!(!h2_prior_knowledge_prefix(b"GET / HTTP/1.1\r\n")); } #[test] fn h2c_upgrade_validation_rejects_missing_settings_and_bodies() { let missing = Request::builder() .method("GET") .uri("/upgrade") .version(http::Version::HTTP_11) .header("host", "localhost") .header("connection", "Upgrade") .header("upgrade", "h2c") .body(()) .unwrap(); assert!(h2_headers_frame(&missing).is_err()); let body = Request::builder() .method("POST") .uri("/upgrade") .version(http::Version::HTTP_11) .header("host", "localhost") .header("connection", "Upgrade, HTTP2-Settings") .header("upgrade", "h2c") .header("http2-settings", "") .header("content-length", "1") .body(()) .unwrap(); assert!(h2_headers_frame(&body).is_err()); } async fn read_http1_head(stream: &mut tokio::net::TcpStream) -> Vec { let mut all = Vec::new(); let mut byte = [0u8; 1]; while !all.ends_with(b"\r\n\r\n") { stream.read_exact(&mut byte).await.unwrap(); all.push(byte[0]); assert!(all.len() < 8192, "HTTP/1 response head exceeded test bound"); } all } async fn try_read_h2_frame( stream: &mut tokio::net::TcpStream, ) -> io::Result<(u8, u8, u32, Vec)> { let mut header = [0u8; 9]; stream.read_exact(&mut header).await?; let length = ((header[0] as usize) << 16) | ((header[1] as usize) << 8) | header[2] as usize; if length > 64 * 1024 { return Err(io::Error::new(io::ErrorKind::InvalidData, "unexpectedly large HTTP/2 test frame")); } let mut payload = vec![0u8; length]; stream.read_exact(&mut payload).await?; let stream_id = u32::from_be_bytes([header[5], header[6], header[7], header[8]]) & 0x7fff_ffff; Ok((header[3], header[4], stream_id, payload)) } async fn read_h2_frame(stream: &mut tokio::net::TcpStream) -> (u8, u8, u32, Vec) { try_read_h2_frame(stream).await.unwrap() } async fn start_direct_h2( router: Router, settings: Http2Connections, ) -> (tokio::net::TcpStream, CancellationToken, tokio::task::JoinHandle<()>) { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let address = listener.local_addr().unwrap(); let stop = CancellationToken::new(); let server_stop = stop.clone(); let server = tokio::spawn(async move { let (stream, peer) = listener.accept().await.unwrap(); let local = stream.local_addr().unwrap(); let permit = Arc::new(Semaphore::new(1)).acquire_owned().await.unwrap(); let lease = Arc::new(ConnectionLease { _permit: permit, metrics: None, }); http2_connection( stream, router, peer, local, Transport { tls: false }, false, lease, server_stop, Duration::from_secs(5), settings, ) .await; }); let mut client = tokio::net::TcpStream::connect(address).await.unwrap(); client.write_all(HTTP2_PRIOR_KNOWLEDGE_PREFACE).await.unwrap(); client .write_all(&[0, 0, 0, 0x4, 0, 0, 0, 0, 0]) .await .unwrap(); (client, stop, server) } #[tokio::test] async fn http1_h2c_upgrade_bootstraps_request_as_http2_stream_one() { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let address = listener.local_addr().unwrap(); let stop = CancellationToken::new(); let server_stop = stop.clone(); let router = Router::new().route("/upgrade", get(|| async { "h2c-ok" })); let server = tokio::spawn(async move { serve_inner( listener, router, server_stop, Arc::new(Semaphore::new(8)), Duration::from_secs(1), None, None, true, None, ) .await .unwrap(); }); let mut client = tokio::net::TcpStream::connect(address).await.unwrap(); client .write_all( b"GET /upgrade HTTP/1.1\r\nHost: localhost\r\nConnection: Upgrade, HTTP2-Settings\r\nUpgrade: h2c\r\nHTTP2-Settings: \r\n\r\n", ) .await .unwrap(); let response = read_http1_head(&mut client).await; let response = String::from_utf8(response).unwrap(); assert!(response.starts_with("HTTP/1.1 101"), "{response}"); assert!(response.to_ascii_lowercase().contains("upgrade: h2c")); client.write_all(HTTP2_PRIOR_KNOWLEDGE_PREFACE).await.unwrap(); // Client connection preface must begin with a non-ACK SETTINGS frame. client .write_all(&[0, 0, 0, 0x4, 0, 0, 0, 0, 0]) .await .unwrap(); let mut saw_stream_one_headers = false; let mut body = Vec::new(); for _ in 0..16 { let (kind, flags, stream_id, payload) = tokio::time::timeout( Duration::from_secs(2), read_h2_frame(&mut client), ) .await .expect("HTTP/2 response frame timeout"); if kind == 0x1 && stream_id == 1 { saw_stream_one_headers = true; } if kind == 0x0 && stream_id == 1 { body.extend_from_slice(&payload); if flags & 0x1 != 0 { break; } } } assert!(saw_stream_one_headers, "missing stream 1 response HEADERS"); assert_eq!(body, b"h2c-ok"); stop.cancel(); server.await.unwrap(); } #[tokio::test] async fn h2_idle_connection_sends_goaway_before_closing() { let settings = Http2Connections { idle_timeout_seconds: 1, max_connection_lifetime_seconds: 0, drain_timeout_seconds: 1, }; let (mut client, stop, server) = start_direct_h2(Router::new(), settings).await; let saw_goaway = tokio::time::timeout(Duration::from_secs(3), async { loop { match try_read_h2_frame(&mut client).await { Ok((0x7, _, 0, _)) => return true, Ok(_) => {} Err(_) => return false, } } }) .await .expect("idle HTTP/2 shutdown did not complete"); assert!(saw_goaway, "idle shutdown must send GOAWAY"); stop.cancel(); server.await.unwrap(); } #[tokio::test] async fn h2_idle_wait_does_not_close_an_active_stream() { let state = H2ConnectionState::new(); let stream = H2StreamLease::begin(state.clone()); tokio::time::sleep(Duration::from_millis(30)).await; assert!(tokio::time::timeout( Duration::from_millis(30), wait_for_h2_idle(state.clone(), Duration::from_millis(10)) ) .await .is_err()); drop(stream); tokio::time::timeout( Duration::from_millis(100), wait_for_h2_idle(state, Duration::from_millis(10)), ) .await .expect("idle shutdown should become eligible after the stream finishes"); } #[tokio::test] async fn h2_lifetime_goaway_is_bounded_by_drain_timeout() { let blocker = Arc::new(Notify::new()); let wait = blocker.clone(); let router = Router::new().route( "/hold", get(move || { let wait = wait.clone(); async move { wait.notified().await; "done" } }), ); let settings = Http2Connections { idle_timeout_seconds: 60, max_connection_lifetime_seconds: 1, drain_timeout_seconds: 1, }; let (mut client, _stop, server) = start_direct_h2(router, settings).await; let upgrade = Request::builder() .method("GET") .uri("/hold") .version(http::Version::HTTP_11) .header("host", "localhost") .header("connection", "Upgrade, HTTP2-Settings") .header("upgrade", "h2c") .header("http2-settings", "") .body(()) .unwrap(); client.write_all(&h2_headers_frame(&upgrade).unwrap()).await.unwrap(); let saw_goaway_and_close = tokio::time::timeout(Duration::from_secs(4), async { let mut saw_goaway = false; loop { match try_read_h2_frame(&mut client).await { Ok((0x7, _, 0, _)) => saw_goaway = true, Ok(_) => {} Err(error) if matches!(error.kind(), io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset) => { return saw_goaway; } Err(_) => return saw_goaway, } } }) .await .expect("lifetime/drain shutdown exceeded its bound"); assert!(saw_goaway_and_close, "lifetime shutdown must send GOAWAY before drain timeout closes TCP"); server.await.unwrap(); drop(blocker); } #[tokio::test] async fn h2_lifetime_allows_active_stream_to_finish_during_drain() { let blocker = Arc::new(Notify::new()); let wait = blocker.clone(); let router = Router::new().route( "/hold", get(move || { let wait = wait.clone(); async move { wait.notified().await; "done" } }), ); let settings = Http2Connections { idle_timeout_seconds: 60, max_connection_lifetime_seconds: 1, drain_timeout_seconds: 3, }; let (mut client, _stop, server) = start_direct_h2(router, settings).await; let request = Request::builder() .method("GET") .uri("/hold") .version(http::Version::HTTP_11) .header("host", "localhost") .header("connection", "Upgrade, HTTP2-Settings") .header("upgrade", "h2c") .header("http2-settings", "") .body(()) .unwrap(); client.write_all(&h2_headers_frame(&request).unwrap()).await.unwrap(); let release = blocker.clone(); tokio::spawn(async move { tokio::time::sleep(Duration::from_millis(1_250)).await; release.notify_waiters(); }); let (saw_goaway, saw_end_stream) = tokio::time::timeout(Duration::from_secs(4), async { let mut saw_goaway = false; let mut saw_end_stream = false; loop { match try_read_h2_frame(&mut client).await { Ok((0x7, _, 0, _)) => saw_goaway = true, Ok((kind, flags, 1, _)) if (kind == 0x0 || kind == 0x1) && flags & 0x1 != 0 => { saw_end_stream = true; if saw_goaway { return (true, true); } } Ok(_) => {} Err(_) => return (saw_goaway, saw_end_stream), } } }) .await .expect("active stream did not drain within the configured bound"); assert!(saw_goaway, "lifetime shutdown must send GOAWAY"); assert!(saw_end_stream, "an accepted stream must be allowed to finish during drain"); server.await.unwrap(); } #[tokio::test] async fn malformed_h2c_upgrade_is_rejected_before_switching_protocols() { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let address = listener.local_addr().unwrap(); let stop = CancellationToken::new(); let server_stop = stop.clone(); let server = tokio::spawn(async move { serve_inner( listener, Router::new(), server_stop, Arc::new(Semaphore::new(8)), Duration::from_secs(1), None, None, true, None, ) .await .unwrap(); }); let mut client = tokio::net::TcpStream::connect(address).await.unwrap(); client .write_all( b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: Upgrade\r\nUpgrade: h2c\r\n\r\n", ) .await .unwrap(); let response = String::from_utf8(read_http1_head(&mut client).await).unwrap(); assert!(response.starts_with("HTTP/1.1 400"), "{response}"); stop.cancel(); server.await.unwrap(); } }