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 (parts, body) = resp.into_parts();
126 // Hand the router's body to the codec as a pull `Stream` it polls itself —
127 // no producer task, no channel, no buffering (unbounded bodies stream too).
128 // Data frames pass through; a mid-stream body error (an upstream that dropped)
129 // becomes a `BodyError` so the codec aborts the response instead of framing a
130 // truncated body as complete; trailer/empty frames are dropped. We do NOT
131 // strip hop-by-hop headers here: the h1 loop reframes them (and preserves a
132 // `101` upgrade's verbatim), and the h2 codec drops the ones it forbids.
133 let chunks = BodyStream::new(body).filter_map(|frame| {
134 std::future::ready(match frame {
135 Ok(f) => f.into_data().ok().filter(|b| !b.is_empty()).map(Ok),
136 Err(_) => Some(Err(BodyError)),
137 })
138 });
139 axum::http::Response::from_parts(parts, HttpBody::try_stream(chunks))
140 }
141}
142
143/// Serve one accepted connection (plaintext or already-TLS-terminated) by driving
144/// it through the unified [`boatramp_http::serve_connection`] dispatcher, bridged
145/// into `router`. A clean close is silent; an unexpected IO error is logged at
146/// debug. Generic over the IO so a raw `TcpStream`, a rewound stream, or a
147/// `TlsStream` all serve identically.
148pub async fn serve_router_conn<IO>(io: IO, peer: SocketAddr, serve: impl Into<ServeInput>)
149where
150 IO: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
151{
152 if let Err(err) = boatramp_http::serve_connection(io, RouterHandler::new(serve, peer)).await {
153 tracing::debug!(%peer, %err, "connection served with error");
154 }
155}
156
157/// Serve one TLS-terminated connection, routing on the **negotiated ALPN** rather
158/// than re-sniffing the stream: ALPN already told us the protocol, so we hand the
159/// decrypted stream straight to the h2 mux driver or the h1 loop with no preface
160/// sniff and no [`Rewind`] wrapper (that wrapper would otherwise sit on the write
161/// path and break the h2 writer's vectored `IoSlice` fast-path). A completed
162/// `acme-tls/1` challenge handshake carries no request and is dropped. A connection
163/// that negotiated no ALPN falls back to the sniffing dispatcher.
164async fn serve_tls_stream(
165 stream: tokio_rustls::server::TlsStream<TcpStream>,
166 peer: SocketAddr,
167 serve: ServeInput,
168) {
169 let alpn = stream.get_ref().1.alpn_protocol().map(<[u8]>::to_vec);
170 let handler = RouterHandler::new(serve, peer);
171 let result = match alpn.as_deref() {
172 // The challenge handshake alone satisfies the CA — nothing to serve.
173 Some(p) if p == ACME_TLS_ALPN => {
174 tracing::debug!(%peer, "completed an ACME tls-alpn-01 challenge");
175 return;
176 }
177 Some(b"h2") => boatramp_http::h2::serve_connection_mux(stream, handler).await,
178 Some(b"http/1.1") => boatramp_http::h1::serve_connection(stream, handler).await,
179 // No ALPN negotiated (a bare TLS client): let the dispatcher sniff h2c-vs-h1.
180 _ => boatramp_http::serve_connection(stream, handler).await,
181 };
182 if let Err(err) = result {
183 tracing::debug!(%peer, %err, "TLS connection served with error");
184 }
185}
186
187/// A rustls [`ServerConfig`] the accept loop reads afresh per connection, so a
188/// background task (ACME renewal) can hot-swap the served certificate without a
189/// restart — the same capability `axum_server`'s `RustlsConfig` gave us. Reads are
190/// a single lock-free atomic load ([`ArcSwap`]).
191#[derive(Clone)]
192pub struct ReloadableTls(Arc<ArcSwap<ServerConfig>>);
193
194impl ReloadableTls {
195 /// Wrap a config that will be served (and may later be [`reload`](Self::reload)ed).
196 pub fn new(config: ServerConfig) -> Self {
197 Self(Arc::new(ArcSwap::from_pointee(config)))
198 }
199
200 /// Atomically replace the served config; connections accepted after this use it,
201 /// in-flight ones are unaffected.
202 pub fn reload(&self, config: ServerConfig) {
203 self.0.store(Arc::new(config));
204 }
205
206 fn current(&self) -> Arc<ServerConfig> {
207 self.0.load_full()
208 }
209}
210
211impl From<ServerConfig> for ReloadableTls {
212 fn from(config: ServerConfig) -> Self {
213 Self::new(config)
214 }
215}
216
217/// How long the serve loops wait for in-flight connections to finish after the
218/// shutdown signal before dropping them — matches the plaintext drain deadline.
219const DRAIN_DEADLINE: std::time::Duration = std::time::Duration::from_secs(30);
220
221/// Serve HTTPS on `addr`: accept TCP, terminate TLS with the (reloadable) rustls
222/// config, and drive the negotiated protocol through [`serve_router_conn`]. An
223/// ACME `acme-tls/1` challenge handshake completes and is dropped (it carries no
224/// request). Returns once `shutdown` resolves, after a bounded drain of in-flight
225/// connections. The config SHOULD advertise ALPN `h2`/`http/1.1` ([`alpn_h1_h2`]);
226/// an ACME config additionally carries `acme-tls/1`.
227pub async fn serve_tls<S>(
228 addr: SocketAddr,
229 tls: ReloadableTls,
230 serve: impl Into<ServeInput>,
231 shutdown: S,
232) -> std::io::Result<()>
233where
234 S: Future<Output = ()> + Send,
235{
236 let listener = TcpListener::bind(addr).await?;
237 serve_tls_listener(listener, tls, serve, shutdown).await
238}
239
240/// [`serve_tls`] on an already-bound [`TcpListener`] — for callers that must learn
241/// the bound port first (an ephemeral `:0` bind) or that inherit the socket
242/// (systemd activation, tests).
243pub async fn serve_tls_listener<S>(
244 listener: TcpListener,
245 tls: ReloadableTls,
246 serve: impl Into<ServeInput>,
247 shutdown: S,
248) -> std::io::Result<()>
249where
250 S: Future<Output = ()> + Send,
251{
252 if let Ok(addr) = listener.local_addr() {
253 tracing::info!(%addr, "serving HTTPS (boatramp-http)");
254 }
255 let serve = serve.into();
256 let inflight = Arc::new(AtomicUsize::new(0));
257 tokio::pin!(shutdown);
258 loop {
259 tokio::select! {
260 _ = &mut shutdown => break,
261 accepted = listener.accept() => {
262 let (mut tcp, peer) = match accepted {
263 Ok(v) => v,
264 Err(err) => {
265 tracing::debug!(%err, "TLS serve: accept error");
266 continue;
267 }
268 };
269 crate::disable_nagle(&mut tcp);
270 let acceptor = TlsAcceptor::from(tls.current());
271 let serve = serve.clone();
272 let inflight = inflight.clone();
273 inflight.fetch_add(1, Ordering::SeqCst);
274 tokio::spawn(async move {
275 match acceptor.accept(tcp).await {
276 Ok(stream) => serve_tls_stream(stream, peer, serve).await,
277 Err(err) => tracing::debug!(%peer, %err, "TLS handshake failed"),
278 }
279 inflight.fetch_sub(1, Ordering::SeqCst);
280 });
281 }
282 }
283 }
284 drain(&inflight).await;
285 Ok(())
286}
287
288/// Serve plaintext HTTP on `addr` through the unified dispatcher (h1, or h2c via
289/// the preface sniff), bridged into `router`. Used by the `:80` HTTP→HTTPS
290/// redirect listener. Returns once `shutdown` resolves, after a bounded drain.
291pub async fn serve_plaintext<S>(
292 addr: SocketAddr,
293 serve: impl Into<ServeInput>,
294 shutdown: S,
295) -> std::io::Result<()>
296where
297 S: Future<Output = ()> + Send,
298{
299 let listener = TcpListener::bind(addr).await?;
300 serve_plaintext_listener(listener, serve, shutdown).await
301}
302
303/// [`serve_plaintext`] on an already-bound [`TcpListener`] (see
304/// [`serve_tls_listener`]).
305pub async fn serve_plaintext_listener<S>(
306 listener: TcpListener,
307 serve: impl Into<ServeInput>,
308 shutdown: S,
309) -> std::io::Result<()>
310where
311 S: Future<Output = ()> + Send,
312{
313 let serve = serve.into();
314 let inflight = Arc::new(AtomicUsize::new(0));
315 tokio::pin!(shutdown);
316 loop {
317 tokio::select! {
318 _ = &mut shutdown => break,
319 accepted = listener.accept() => {
320 let (mut tcp, peer) = match accepted {
321 Ok(v) => v,
322 Err(err) => {
323 tracing::debug!(%err, "plaintext serve: accept error");
324 continue;
325 }
326 };
327 crate::disable_nagle(&mut tcp);
328 let serve = serve.clone();
329 let inflight = inflight.clone();
330 inflight.fetch_add(1, Ordering::SeqCst);
331 tokio::spawn(async move {
332 serve_router_conn(tcp, peer, serve).await;
333 inflight.fetch_sub(1, Ordering::SeqCst);
334 });
335 }
336 }
337 }
338 drain(&inflight).await;
339 Ok(())
340}
341
342/// Wait for in-flight connections to drop to zero, or the [`DRAIN_DEADLINE`],
343/// whichever comes first (then the caller returns and any stragglers are dropped).
344async fn drain(inflight: &AtomicUsize) {
345 let deadline = tokio::time::Instant::now() + DRAIN_DEADLINE;
346 while inflight.load(Ordering::SeqCst) > 0 {
347 if tokio::time::Instant::now() >= deadline {
348 tracing::warn!("TLS/plaintext drain deadline exceeded; dropping in-flight connections");
349 break;
350 }
351 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
352 }
353}