Skip to main content

tachyon_web/server/
http.rs

1use crate::http::response::Body;
2#[cfg(feature = "tls")]
3use crate::server::TLS_HANDSHAKE_TIMEOUT;
4use crate::server::{IS_LOCAL_WORKER, REQUEST_TIMEOUT, Server};
5use bytes::Bytes;
6use hyper::body::{Body as HyperBody, Frame, SizeHint};
7use hyper::service::service_fn;
8use hyper::{Request, Response};
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12use std::task::{Context, Poll};
13use tokio::net::TcpListener;
14#[cfg(feature = "tls")]
15use tokio_rustls::TlsAcceptor;
16
17pin_project_lite::pin_project! {
18    /// Wraps an incoming request body with an absolute deadline for fully reading it.
19    ///
20    /// Bodies are streamed lazily now (see `hyper_handler`), so a handler that never
21    /// touches the body (e.g. an arity-0 route) would otherwise never be bounded by any
22    /// timeout — the client could send headers, declare a `Content-Length`, and simply
23    /// never send the body, holding the connection open indefinitely. This restores the
24    /// same bound `REQUEST_TIMEOUT` previously enforced by eager buffering, without
25    /// requiring anything to actually poll the body for it to apply.
26    struct DeadlineBody {
27        #[pin]
28        inner: hyper::body::Incoming,
29        #[pin]
30        deadline: tokio::time::Sleep,
31    }
32}
33
34impl HyperBody for DeadlineBody {
35    type Data = Bytes;
36    type Error = crate::http::error::Error;
37
38    fn poll_frame(
39        self: Pin<&mut Self>,
40        cx: &mut Context<'_>,
41    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
42        let this = self.project();
43        if this.deadline.poll(cx).is_ready() {
44            return Poll::Ready(Some(Err(crate::http::error::Error::Rejection {
45                status: hyper::StatusCode::REQUEST_TIMEOUT,
46                message: "Timed out reading request body".to_string(),
47            })));
48        }
49        match this.inner.poll_frame(cx) {
50            Poll::Ready(Some(Ok(frame))) => Poll::Ready(Some(Ok(frame))),
51            Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e.into()))),
52            Poll::Ready(None) => Poll::Ready(None),
53            Poll::Pending => Poll::Pending,
54        }
55    }
56
57    fn is_end_stream(&self) -> bool {
58        self.inner.is_end_stream()
59    }
60
61    fn size_hint(&self) -> SizeHint {
62        self.inner.size_hint()
63    }
64}
65
66#[cfg(feature = "http2")]
67#[derive(Clone, Copy, Debug)]
68struct LocalExecutor;
69
70#[cfg(feature = "http2")]
71impl<F> hyper::rt::Executor<F> for LocalExecutor
72where
73    F: Future + Send + 'static,
74    F::Output: Send + 'static,
75{
76    fn execute(&self, fut: F) {
77        IS_LOCAL_WORKER.with(|flag| {
78            if flag.get() {
79                drop(tokio::task::spawn_local(fut));
80            } else {
81                drop(tokio::spawn(fut));
82            }
83        });
84    }
85}
86
87impl<S> Server<S>
88where
89    S: Clone + Send + Sync + 'static,
90{
91    /// Serve HTTP/1.1 (and, with the `http2` feature, HTTP/2 over cleartext —
92    /// "h2c", detected via the connection preface with no ALPN needed) over
93    /// plaintext TCP on the given listener.
94    ///
95    /// Without the `http2` feature this uses `hyper::server::conn::http1::Builder`
96    /// directly — no protocol sniffing, no `auto` dispatch overhead. With it, it
97    /// uses `hyper_util`'s `auto::Builder`, which peeks at the first bytes of each
98    /// connection to detect an HTTP/2 client connection preface and falls back to
99    /// HTTP/1.1 otherwise. Either builder is constructed once and cloned per
100    /// connection (cheap: only pointer-sized fields).
101    ///
102    /// h2c has no browser support (browsers only ever negotiate HTTP/2 via TLS
103    /// ALPN) but is exactly what most non-browser HTTP/2 clients (gRPC, `curl
104    /// --http2-prior-knowledge`, many internal service meshes) expect when TLS is
105    /// terminated upstream (e.g. behind a load balancer) or simply not wanted.
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if FIPS compliance enforcement fails. Per-connection I/O
110    /// errors (accept failures, handshake failures, etc.) are logged and do not
111    /// terminate the accept loop.
112    pub async fn serve_http(self, listener: TcpListener) -> Result<(), std::io::Error> {
113        crate::server::enforce_fips_compliance()?;
114        let state = Arc::new(self);
115        let connection_semaphore = Arc::new(tokio::sync::Semaphore::new(state.max_connections));
116
117        // Build once outside the loop — `clone()` inside is a few pointer copies.
118        // Three cases, matching whichever of `http1`/`http2` are enabled (at least
119        // one always is — see the crate-level `compile_error!` in `lib.rs`):
120        #[cfg(all(feature = "http1", feature = "http2"))]
121        let builder = {
122            // Both enabled: `auto::Builder` sniffs each connection's first bytes
123            // for the HTTP/2 client preface and falls back to HTTP/1.1 otherwise.
124            let mut b = hyper_util::server::conn::auto::Builder::new(LocalExecutor);
125            let _ = b
126                .http1()
127                .timer(hyper_util::rt::TokioTimer::new())
128                .header_read_timeout(REQUEST_TIMEOUT)
129                .keep_alive(true)
130                .max_buf_size(8192)
131                .writev(true);
132            let _ = b
133                .http2()
134                .timer(hyper_util::rt::TokioTimer::new())
135                .initial_stream_window_size(65535)
136                .initial_connection_window_size(1024 * 1024)
137                .max_frame_size(16384)
138                .max_concurrent_streams(200)
139                .keep_alive_timeout(REQUEST_TIMEOUT);
140            b
141        };
142        #[cfg(all(feature = "http1", not(feature = "http2")))]
143        let builder = {
144            // http1 only: the low-level builder directly, no protocol-sniffing overhead.
145            let mut b = hyper::server::conn::http1::Builder::new();
146            let _ = b
147                .timer(hyper_util::rt::TokioTimer::new())
148                .header_read_timeout(REQUEST_TIMEOUT)
149                .keep_alive(true)
150                .max_buf_size(8192)
151                .writev(true);
152            b
153        };
154        #[cfg(all(feature = "http2", not(feature = "http1")))]
155        let builder = {
156            // http2 only: h2c with no HTTP/1.1 fallback at all — a client that
157            // isn't speaking HTTP/2 with prior knowledge simply fails to connect.
158            let mut b = hyper::server::conn::http2::Builder::new(LocalExecutor);
159            let _ = b
160                .timer(hyper_util::rt::TokioTimer::new())
161                .initial_stream_window_size(65535)
162                .initial_connection_window_size(1024 * 1024)
163                .max_frame_size(16384)
164                .max_concurrent_streams(200)
165                .keep_alive_timeout(REQUEST_TIMEOUT);
166            b
167        };
168
169        loop {
170            let Ok(permit) = connection_semaphore.clone().acquire_owned().await else {
171                break;
172            };
173
174            let (stream, peer) = match listener.accept().await {
175                Ok(c) => c,
176                Err(e) => {
177                    drop(permit);
178                    tracing::error!("[http] Accept error: {}", e);
179                    if crate::server::is_resource_exhaustion(&e) {
180                        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
181                    }
182                    continue;
183                }
184            };
185            let _ = stream.set_nodelay(true);
186            #[cfg(target_os = "linux")]
187            {
188                let sock_ref = socket2::SockRef::from(&stream);
189                let _ = sock_ref.set_tcp_quickack(true);
190            }
191            let state = state.clone();
192            let builder = builder.clone();
193
194            let serve_fut = async move {
195                let io = hyper_util::rt::TokioIo::new(stream);
196                let svc = service_fn(move |req| hyper_handler(state.clone(), req, peer));
197                #[cfg(all(feature = "http1", feature = "http2"))]
198                let result = builder.serve_connection_with_upgrades(io, svc).await;
199                #[cfg(all(feature = "http1", not(feature = "http2")))]
200                let result = builder.serve_connection(io, svc).with_upgrades().await;
201                #[cfg(all(feature = "http2", not(feature = "http1")))]
202                let result = builder.serve_connection(io, svc).await;
203                if let Err(e) = result {
204                    tracing::debug!("[http] Connection error: {}", e);
205                }
206                drop(permit);
207            };
208
209            IS_LOCAL_WORKER.with(|flag| {
210                if flag.get() {
211                    drop(tokio::task::spawn_local(serve_fut));
212                } else {
213                    drop(tokio::spawn(serve_fut));
214                }
215            });
216        }
217        Ok(())
218    }
219
220    /// Serve HTTP/1.1 and HTTP/2 over TLS (HTTPS) on the given listener and acceptor.
221    ///
222    /// # Errors
223    ///
224    /// Returns an error if FIPS compliance enforcement fails. Per-connection I/O
225    /// errors (accept failures, handshake failures, etc.) are logged and do not
226    /// terminate the accept loop.
227    #[cfg(feature = "tls")]
228    pub async fn serve_https(
229        self,
230        listener: TcpListener,
231        acceptor: TlsAcceptor,
232    ) -> Result<(), std::io::Error> {
233        crate::server::enforce_fips_compliance()?;
234        let state = Arc::new(self);
235        let connection_semaphore = Arc::new(tokio::sync::Semaphore::new(state.max_connections));
236
237        loop {
238            let Ok(permit) = connection_semaphore.clone().acquire_owned().await else {
239                break;
240            };
241
242            let (tcp_stream, peer) = match listener.accept().await {
243                Ok(c) => c,
244                Err(e) => {
245                    drop(permit);
246                    tracing::error!("[https] Accept error: {}", e);
247                    if crate::server::is_resource_exhaustion(&e) {
248                        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
249                    }
250                    continue;
251                }
252            };
253            let _ = tcp_stream.set_nodelay(true);
254            #[cfg(target_os = "linux")]
255            {
256                let sock_ref = socket2::SockRef::from(&tcp_stream);
257                let _ = sock_ref.set_tcp_quickack(true);
258            }
259            let acceptor = acceptor.clone();
260            let state = state.clone();
261
262            let serve_fut = async move {
263                let tls_stream =
264                    match tokio::time::timeout(TLS_HANDSHAKE_TIMEOUT, acceptor.accept(tcp_stream))
265                        .await
266                    {
267                        Ok(Ok(stream)) => stream,
268                        Ok(Err(e)) => {
269                            tracing::debug!("[https] TLS handshake error: {}", e);
270                            drop(permit);
271                            return;
272                        }
273                        Err(_) => {
274                            tracing::debug!("[https] TLS handshake timed out");
275                            drop(permit);
276                            return;
277                        }
278                    };
279
280                // Inspect TLS connection ALPN before consuming the stream.
281                // Copy bytes out so the borrow ends before the move.
282                #[cfg(feature = "http2")]
283                let is_h2 = {
284                    let (_, connection) = tls_stream.get_ref();
285                    connection.alpn_protocol() == Some(b"h2")
286                };
287
288                let io = hyper_util::rt::TokioIo::new(tls_stream);
289                let svc = service_fn(move |req| hyper_handler(state.clone(), req, peer));
290
291                #[cfg(feature = "http2")]
292                if is_h2 {
293                    // Use low-level HTTP/2 connection builder
294                    let mut builder = hyper::server::conn::http2::Builder::new(LocalExecutor);
295                    let _ = builder
296                        .timer(hyper_util::rt::TokioTimer::new())
297                        .initial_stream_window_size(65535)
298                        .initial_connection_window_size(1024 * 1024)
299                        .max_frame_size(16384)
300                        .max_concurrent_streams(200)
301                        .keep_alive_timeout(REQUEST_TIMEOUT);
302
303                    if let Err(e) = builder.serve_connection(io, svc).await {
304                        tracing::debug!("[https] HTTP/2 Connection error: {}", e);
305                    }
306                    drop(permit);
307                    return;
308                }
309
310                // Fallback path for a connection that didn't negotiate h2 over ALPN.
311                // With the `http1` feature this is the common case (HTTP/1.1 over
312                // TLS); without it, ALPN only ever advertised "h2" (see
313                // `alpn_protocols` in `server/mod.rs`), so a non-h2 connection here
314                // means a non-compliant client picked a protocol we didn't offer —
315                // there's no builder to serve it with, so the connection is dropped.
316                #[cfg(feature = "http1")]
317                {
318                    // Use low-level HTTP/1.1 connection builder (bypasses auto-negotiation overhead)
319                    let mut builder = hyper::server::conn::http1::Builder::new();
320                    let _ = builder
321                        .timer(hyper_util::rt::TokioTimer::new())
322                        .header_read_timeout(REQUEST_TIMEOUT)
323                        .keep_alive(true)
324                        .max_buf_size(8192);
325
326                    if let Err(e) = builder.serve_connection(io, svc).with_upgrades().await {
327                        tracing::debug!("[https] HTTP/1.1 Connection error: {}", e);
328                    }
329                }
330                drop(permit);
331            };
332
333            IS_LOCAL_WORKER.with(|flag| {
334                if flag.get() {
335                    drop(tokio::task::spawn_local(serve_fut));
336                } else {
337                    drop(tokio::spawn(serve_fut));
338                }
339            });
340        }
341        Ok(())
342    }
343
344    /// Serve HTTP/1.1 and HTTP/2 over TLS (HTTPS) on the given listener with a custom `rustls::ServerConfig`.
345    ///
346    /// # Errors
347    ///
348    /// Returns an error if FIPS compliance enforcement fails. Per-connection I/O
349    /// errors (accept failures, handshake failures, etc.) are logged and do not
350    /// terminate the accept loop.
351    #[cfg(feature = "tls")]
352    pub async fn serve_https_config(
353        self,
354        listener: TcpListener,
355        config: rustls::ServerConfig,
356    ) -> Result<(), std::io::Error> {
357        crate::server::enforce_fips_compliance()?;
358        let acceptor = TlsAcceptor::from(Arc::new(config));
359        self.serve_https(listener, acceptor).await
360    }
361}
362
363pub(super) async fn hyper_handler<S>(
364    state: Arc<Server<S>>,
365    req: Request<hyper::body::Incoming>,
366    peer: std::net::SocketAddr,
367) -> Result<Response<Body>, std::io::Error>
368where
369    S: Clone + Send + Sync + 'static,
370{
371    let (parts, incoming_body) = req.into_parts();
372
373    let body = Body::stream(DeadlineBody {
374        inner: incoming_body,
375        deadline: tokio::time::sleep(REQUEST_TIMEOUT),
376    });
377
378    let mut rebuild_req = Request::from_parts(parts, body);
379    #[cfg(feature = "original-uri")]
380    {
381        let orig_uri = rebuild_req.uri().clone();
382        rebuild_req
383            .extensions_mut()
384            .insert(crate::routing::extract::OriginalUri(orig_uri));
385    }
386    rebuild_req
387        .extensions_mut()
388        .insert(crate::routing::extract::ConnectInfo(peer));
389    rebuild_req
390        .extensions_mut()
391        .insert(crate::routing::extract::MaxBodySize(state.max_body_size));
392
393    let resp = state.router.handle_request(rebuild_req).await;
394
395    if let Some((min, max)) = state.response_jitter {
396        tokio::time::sleep(crate::server::jittered_delay(min, max)).await;
397    }
398
399    Ok(resp)
400}