Skip to main content

boatramp_server/
http_serve.rs

1//! The unified serving front door: every accepted connection — plaintext or TLS,
2//! HTTP/1.1 or HTTP/2 — is driven by [`boatramp_http::serve_connection`], boatramp's
3//! own hand-rolled h1+h2 stack. This is what replaced hyper/`axum_server` on the
4//! serving path (hyper stays only as the reverse-proxy *client*).
5//!
6//! The pieces:
7//! - [`RouterHandler`] bridges the dispatcher to the axum [`Router`] (a tower
8//!   `Service`): it swaps body types and attaches the peer, nothing else. It is
9//!   **protocol-agnostic** — it never strips hop-by-hop headers, because each codec
10//!   owns its own framing rules (the h1 loop reframes and preserves a `101`
11//!   upgrade's `Connection`/`Upgrade` verbatim; the h2 codec drops the
12//!   connection-specific headers HTTP/2 forbids). One bridge feeds both.
13//! - [`serve_tls`] terminates TLS ourselves (rustls) and serves the negotiated
14//!   protocol; it also transparently completes ACME `acme-tls/1` challenge
15//!   handshakes. [`ReloadableTls`] lets the ACME renewal loop hot-swap the served
16//!   certificate live, the way `axum_server`'s `RustlsConfig::reload` did.
17//! - [`serve_plaintext`] is the same accept→`serve_connection` loop without TLS
18//!   (the `:80` HTTP→HTTPS redirect listener).
19
20use std::future::Future;
21use std::net::SocketAddr;
22use std::sync::atomic::{AtomicUsize, Ordering};
23use std::sync::Arc;
24
25use arc_swap::ArcSwap;
26use axum::Router;
27use boatramp_http::{
28    Body as HttpBody, BodyError, Handler, Request as HttpRequest, Response as HttpResponse,
29};
30use futures::StreamExt as _;
31use http_body_util::BodyStream;
32use rustls::ServerConfig;
33use tokio::net::{TcpListener, TcpStream};
34use tokio_rustls::TlsAcceptor;
35
36/// The ALPN identifier for the ACME TLS-ALPN-01 challenge (RFC 8737). A challenge
37/// connection negotiates only this; a completed challenge handshake carries no
38/// request, so [`serve_tls`] drops it rather than handing it to the h1/h2 driver.
39const ACME_TLS_ALPN: &[u8] = b"acme-tls/1";
40
41/// The ALPN protocols our HTTPS listeners advertise, in server-preference order:
42/// HTTP/2 first, then HTTP/1.1. (`serve_connection` also sniffs the h2 preface, so
43/// a client that negotiates neither still gets the right codec — ALPN is the fast
44/// path, the sniff is the backstop.)
45pub fn alpn_h1_h2() -> Vec<Vec<u8>> {
46    vec![b"h2".to_vec(), b"http/1.1".to_vec()]
47}
48
49/// A [`Router`] plus the optional hot-path handle, flowed through the serve loops as
50/// one unit. A bare `Router` converts in with **no** fast path (`From<Router>`), so
51/// auxiliary listeners (the `:80` HTTP→HTTPS redirect, ACME challenge) are unchanged;
52/// the main site listeners pass `(router, fast)` (via `From<(Router, FastServe)>`) to
53/// enable the bypass. Cheap to clone (an `Arc`-y `Router` clone + an `Option`).
54#[derive(Clone)]
55pub struct ServeInput {
56    router: Router,
57    fast: Option<crate::FastServe>,
58}
59
60impl From<Router> for ServeInput {
61    fn from(router: Router) -> Self {
62        Self { router, fast: None }
63    }
64}
65
66impl From<(Router, crate::FastServe)> for ServeInput {
67    fn from((router, fast): (Router, crate::FastServe)) -> Self {
68        Self {
69            router,
70            fast: Some(fast),
71        }
72    }
73}
74
75/// Bridges [`boatramp_http`]'s serving surface to the axum [`Router`] (a tower
76/// `Service`). Both codecs produce a native `http::Request`, so the bridge only
77/// swaps the body type and attaches the peer address; method / URI / headers pass
78/// through untouched. Constructed once per connection. Carries the optional
79/// [`FastServe`](crate::FastServe) hot-path handle: an eligible plain site GET/HEAD is
80/// dispatched straight to `serve_by_host`, skipping the axum router + middleware
81/// composition; everything else falls through to the router unchanged.
82pub struct RouterHandler {
83    serve: ServeInput,
84    peer: SocketAddr,
85}
86
87impl RouterHandler {
88    pub fn new(serve: impl Into<ServeInput>, peer: SocketAddr) -> Self {
89        Self {
90            serve: serve.into(),
91            peer,
92        }
93    }
94}
95
96impl Handler for RouterHandler {
97    async fn handle(&self, req: HttpRequest) -> HttpResponse {
98        // Wrap the streaming request body as an axum body (cheap; `ReqBody` is an
99        // `http_body::Body`) and attach the peer as `ConnectInfo` (IP rules / rate
100        // limiting / access logs read it). Nothing else is rebuilt.
101        let mut request = req.map(axum::body::Body::new);
102        request
103            .extensions_mut()
104            .insert(axum::extract::ConnectInfo(self.peer));
105
106        // Hot path: an eligible plain site GET/HEAD skips the axum router + middleware
107        // future-composition (~15–20% of per-core CPU, profiled) and dispatches straight
108        // to `serve_by_host`. `FastServe::dispatch` still applies the request-id +
109        // access-log/metrics guarantees, and rate-limit / visitor-auth / host-routing /
110        // preview-auth / DV all run inside `serve_by_host_inner` — so the bypass can never
111        // skip a security check. Everything else (and every build without a fast handle)
112        // falls through to the router, byte-identical to before.
113        let resp = match &self.serve.fast {
114            Some(fast) if fast.eligible(&request) => fast.dispatch(request, self.peer).await,
115            _ => {
116                // Call the router as a tower Service (axum's Router is always ready).
117                use tower_service::Service as _;
118                let mut router = self.serve.router.clone();
119                match router.call(request).await {
120                    Ok(r) => r,
121                    Err(_) => return boatramp_http::response(502, b"bad gateway".to_vec()),
122                }
123            }
124        };
125        let (mut parts, body) = resp.into_parts();
126        // Zero-copy static: `serve_entry` attached the open file as an extension for a
127        // large plaintext static blob. Hand the codec a `Body::File` — it moves the
128        // bytes with `sendfile` over a plaintext socket (no userspace copy) and reads +
129        // writes them otherwise. Only reachable for plaintext large static, so the h1
130        // codec's write half is a bare TCP socket and `sendfile` applies.
131        if let Some(src) = parts
132            .extensions
133            .remove::<crate::serve_pipeline::SendfileSource>()
134        {
135            return axum::http::Response::from_parts(
136                parts,
137                HttpBody::file(src.file, src.offset, src.len),
138            );
139        }
140        // Hand the router's body to the codec as a pull `Stream` it polls itself —
141        // no producer task, no channel, no buffering (unbounded bodies stream too).
142        // Data frames pass through; a mid-stream body error (an upstream that dropped)
143        // becomes a `BodyError` so the codec aborts the response instead of framing a
144        // truncated body as complete; trailer/empty frames are dropped. We do NOT
145        // strip hop-by-hop headers here: the h1 loop reframes them (and preserves a
146        // `101` upgrade's verbatim), and the h2 codec drops the ones it forbids.
147        let chunks = BodyStream::new(body).filter_map(|frame| {
148            std::future::ready(match frame {
149                Ok(f) => f.into_data().ok().filter(|b| !b.is_empty()).map(Ok),
150                Err(_) => Some(Err(BodyError)),
151            })
152        });
153        axum::http::Response::from_parts(parts, HttpBody::try_stream(chunks))
154    }
155}
156
157/// Serve one accepted connection (plaintext or already-TLS-terminated) by driving
158/// it through the unified [`boatramp_http::serve_connection`] dispatcher, bridged
159/// into `router`. A clean close is silent; an unexpected IO error is logged at
160/// debug. Generic over the IO so a raw `TcpStream`, a rewound stream, or a
161/// `TlsStream` all serve identically.
162pub async fn serve_router_conn<IO>(io: IO, peer: SocketAddr, serve: impl Into<ServeInput>)
163where
164    IO: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
165{
166    if let Err(err) = boatramp_http::serve_connection(io, RouterHandler::new(serve, peer)).await {
167        tracing::debug!(%peer, %err, "connection served with error");
168    }
169}
170
171/// Serve one TLS-terminated connection, routing on the **negotiated ALPN** rather
172/// than re-sniffing the stream: ALPN already told us the protocol, so we hand the
173/// decrypted stream straight to the h2 mux driver or the h1 loop with no preface
174/// sniff and no [`Rewind`] wrapper (that wrapper would otherwise sit on the write
175/// path and break the h2 writer's vectored `IoSlice` fast-path). A completed
176/// `acme-tls/1` challenge handshake carries no request and is dropped. A connection
177/// that negotiated no ALPN falls back to the sniffing dispatcher.
178async fn serve_tls_stream(
179    stream: tokio_rustls::server::TlsStream<TcpStream>,
180    peer: SocketAddr,
181    serve: ServeInput,
182) {
183    let alpn = stream.get_ref().1.alpn_protocol().map(<[u8]>::to_vec);
184    let handler = RouterHandler::new(serve, peer);
185    let result = match alpn.as_deref() {
186        // The challenge handshake alone satisfies the CA — nothing to serve.
187        Some(p) if p == ACME_TLS_ALPN => {
188            tracing::debug!(%peer, "completed an ACME tls-alpn-01 challenge");
189            return;
190        }
191        Some(b"h2") => boatramp_http::h2::serve_connection_mux(stream, handler).await,
192        Some(b"http/1.1") => boatramp_http::h1::serve_connection(stream, handler).await,
193        // No ALPN negotiated (a bare TLS client): let the dispatcher sniff h2c-vs-h1.
194        _ => boatramp_http::serve_connection(stream, handler).await,
195    };
196    if let Err(err) = result {
197        tracing::debug!(%peer, %err, "TLS connection served with error");
198    }
199}
200
201/// A rustls [`ServerConfig`] the accept loop reads afresh per connection, so a
202/// background task (ACME renewal) can hot-swap the served certificate without a
203/// restart — the same capability `axum_server`'s `RustlsConfig` gave us. Reads are
204/// a single lock-free atomic load ([`ArcSwap`]).
205#[derive(Clone)]
206pub struct ReloadableTls(Arc<ArcSwap<ServerConfig>>);
207
208impl ReloadableTls {
209    /// Wrap a config that will be served (and may later be [`reload`](Self::reload)ed).
210    pub fn new(config: ServerConfig) -> Self {
211        Self(Arc::new(ArcSwap::from_pointee(config)))
212    }
213
214    /// Atomically replace the served config; connections accepted after this use it,
215    /// in-flight ones are unaffected.
216    pub fn reload(&self, config: ServerConfig) {
217        self.0.store(Arc::new(config));
218    }
219
220    fn current(&self) -> Arc<ServerConfig> {
221        self.0.load_full()
222    }
223}
224
225impl From<ServerConfig> for ReloadableTls {
226    fn from(config: ServerConfig) -> Self {
227        Self::new(config)
228    }
229}
230
231/// How long the serve loops wait for in-flight connections to finish after the
232/// shutdown signal before dropping them — matches the plaintext drain deadline.
233const DRAIN_DEADLINE: std::time::Duration = std::time::Duration::from_secs(30);
234
235/// Serve HTTPS on `addr`: accept TCP, terminate TLS with the (reloadable) rustls
236/// config, and drive the negotiated protocol through [`serve_router_conn`]. An
237/// ACME `acme-tls/1` challenge handshake completes and is dropped (it carries no
238/// request). Returns once `shutdown` resolves, after a bounded drain of in-flight
239/// connections. The config SHOULD advertise ALPN `h2`/`http/1.1` ([`alpn_h1_h2`]);
240/// an ACME config additionally carries `acme-tls/1`.
241pub async fn serve_tls<S>(
242    addr: SocketAddr,
243    tls: ReloadableTls,
244    serve: impl Into<ServeInput>,
245    shutdown: S,
246) -> std::io::Result<()>
247where
248    S: Future<Output = ()> + Send,
249{
250    let listener = TcpListener::bind(addr).await?;
251    serve_tls_listener(listener, tls, serve, shutdown).await
252}
253
254/// [`serve_tls`] on an already-bound [`TcpListener`] — for callers that must learn
255/// the bound port first (an ephemeral `:0` bind) or that inherit the socket
256/// (systemd activation, tests).
257pub async fn serve_tls_listener<S>(
258    listener: TcpListener,
259    tls: ReloadableTls,
260    serve: impl Into<ServeInput>,
261    shutdown: S,
262) -> std::io::Result<()>
263where
264    S: Future<Output = ()> + Send,
265{
266    if let Ok(addr) = listener.local_addr() {
267        tracing::info!(%addr, "serving HTTPS (boatramp-http)");
268    }
269    let serve = serve.into();
270    let inflight = Arc::new(AtomicUsize::new(0));
271    tokio::pin!(shutdown);
272    loop {
273        tokio::select! {
274            _ = &mut shutdown => break,
275            accepted = listener.accept() => {
276                let (mut tcp, peer) = match accepted {
277                    Ok(v) => v,
278                    Err(err) => {
279                        tracing::debug!(%err, "TLS serve: accept error");
280                        continue;
281                    }
282                };
283                crate::disable_nagle(&mut tcp);
284                let acceptor = TlsAcceptor::from(tls.current());
285                let serve = serve.clone();
286                let inflight = inflight.clone();
287                inflight.fetch_add(1, Ordering::SeqCst);
288                tokio::spawn(async move {
289                    match acceptor.accept(tcp).await {
290                        Ok(stream) => serve_tls_stream(stream, peer, serve).await,
291                        Err(err) => tracing::debug!(%peer, %err, "TLS handshake failed"),
292                    }
293                    inflight.fetch_sub(1, Ordering::SeqCst);
294                });
295            }
296        }
297    }
298    drain(&inflight).await;
299    Ok(())
300}
301
302/// Serve plaintext HTTP on `addr` through the unified dispatcher (h1, or h2c via
303/// the preface sniff), bridged into `router`. Used by the `:80` HTTP→HTTPS
304/// redirect listener. Returns once `shutdown` resolves, after a bounded drain.
305pub async fn serve_plaintext<S>(
306    addr: SocketAddr,
307    serve: impl Into<ServeInput>,
308    shutdown: S,
309) -> std::io::Result<()>
310where
311    S: Future<Output = ()> + Send,
312{
313    let listener = TcpListener::bind(addr).await?;
314    serve_plaintext_listener(listener, serve, shutdown).await
315}
316
317/// [`serve_plaintext`] on an already-bound [`TcpListener`] (see
318/// [`serve_tls_listener`]).
319pub async fn serve_plaintext_listener<S>(
320    listener: TcpListener,
321    serve: impl Into<ServeInput>,
322    shutdown: S,
323) -> std::io::Result<()>
324where
325    S: Future<Output = ()> + Send,
326{
327    let serve = serve.into();
328    let inflight = Arc::new(AtomicUsize::new(0));
329    tokio::pin!(shutdown);
330    loop {
331        tokio::select! {
332            _ = &mut shutdown => break,
333            accepted = listener.accept() => {
334                let (mut tcp, peer) = match accepted {
335                    Ok(v) => v,
336                    Err(err) => {
337                        tracing::debug!(%err, "plaintext serve: accept error");
338                        continue;
339                    }
340                };
341                crate::disable_nagle(&mut tcp);
342                let serve = serve.clone();
343                let inflight = inflight.clone();
344                inflight.fetch_add(1, Ordering::SeqCst);
345                tokio::spawn(async move {
346                    serve_router_conn(tcp, peer, serve).await;
347                    inflight.fetch_sub(1, Ordering::SeqCst);
348                });
349            }
350        }
351    }
352    drain(&inflight).await;
353    Ok(())
354}
355
356/// Wait for in-flight connections to drop to zero, or the [`DRAIN_DEADLINE`],
357/// whichever comes first (then the caller returns and any stragglers are dropped).
358async fn drain(inflight: &AtomicUsize) {
359    let deadline = tokio::time::Instant::now() + DRAIN_DEADLINE;
360    while inflight.load(Ordering::SeqCst) > 0 {
361        if tokio::time::Instant::now() >= deadline {
362            tracing::warn!("TLS/plaintext drain deadline exceeded; dropping in-flight connections");
363            break;
364        }
365        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
366    }
367}