Skip to main content

mini_static/
server.rs

1use std::convert::Infallible;
2use std::fs;
3use std::io::Read;
4use std::net::SocketAddr;
5use std::path::{Path, PathBuf};
6use std::pin::Pin;
7use std::sync::Arc;
8use std::task::{Context, Poll};
9use std::time::{Duration, SystemTime};
10
11use bytes::{BufMut, Bytes, BytesMut};
12use hyper::{Method, Response, StatusCode, Request};
13use hyper::service::service_fn;
14use http_body::{Body, Frame};
15use http_body_util::{BodyExt, Full};
16use hyper::body::Incoming;
17use hyper_util::rt::TokioExecutor;
18use hyper_util::rt::TokioIo;
19use hyper_util::server::conn::auto::Builder as AutoBuilder;
20use tokio::fs::File;
21use tokio::io::{AsyncRead, ReadBuf};
22use tokio::net::{TcpListener, TcpStream};
23use tokio::sync::{OwnedSemaphorePermit, Semaphore};
24use tokio::time::timeout;
25
26use crate::error::StaticError;
27use crate::handler::ResponseBody;
28use crate::resolve;
29
30const ACCEPT_BACKOFF_INITIAL: Duration = Duration::from_millis(10);
31const ACCEPT_BACKOFF_MAX: Duration = Duration::from_secs(1);
32
33/// A source of accepted TCP connections. Abstracted so the accept-error backoff below
34/// can be exercised against a listener that fails on demand, without needing to provoke
35/// real OS-level accept errors (e.g. EMFILE) in tests.
36trait TcpAccept {
37    async fn accept(&self) -> std::io::Result<(TcpStream, SocketAddr)>;
38}
39
40impl TcpAccept for TcpListener {
41    async fn accept(&self) -> std::io::Result<(TcpStream, SocketAddr)> {
42        TcpListener::accept(self).await
43    }
44}
45
46/// Exponential backoff for retrying `accept()` after an error, so a sustained failure
47/// (e.g. the process is out of file descriptors) degrades into periodic retries instead
48/// of a CPU-bound busy spin or, worse, silently ending the accept loop for good. Resets
49/// to the initial delay as soon as an accept succeeds.
50struct Backoff {
51    delay: Duration,
52}
53
54impl Backoff {
55    fn new() -> Self {
56        Backoff { delay: ACCEPT_BACKOFF_INITIAL }
57    }
58
59    fn next_delay(&mut self) -> Duration {
60        let delay = self.delay;
61        self.delay = (self.delay * 2).min(ACCEPT_BACKOFF_MAX);
62        delay
63    }
64
65    fn reset(&mut self) {
66        self.delay = ACCEPT_BACKOFF_INITIAL;
67    }
68}
69
70/// Accept a connection and reserve it a connection-limit permit, retrying transient
71/// `accept()` errors with `Backoff` instead of ending the accept loop on the first one.
72/// Returns `None` only if the semaphore itself has been closed (never happens in normal
73/// operation, since nothing ever calls `close()` on it — handled so a caller can still
74/// fail safely rather than panic).
75async fn accept_and_permit<L: TcpAccept>(
76    listener: &L,
77    backoff: &mut Backoff,
78    semaphore: &Arc<Semaphore>,
79) -> Option<(TcpStream, OwnedSemaphorePermit)> {
80    loop {
81        let stream = match listener.accept().await {
82            Ok((stream, _)) => {
83                backoff.reset();
84                stream
85            }
86            Err(_) => {
87                tokio::time::sleep(backoff.next_delay()).await;
88                continue;
89            }
90        };
91        return match semaphore.clone().acquire_owned().await {
92            Ok(permit) => Some((stream, permit)),
93            Err(_) => None,
94        };
95    }
96}
97
98/// A static file server for serving files securely from a root directory.
99///
100/// `Server` canonicalizes the root directory once at creation time and uses the
101/// canonical form for all subsequent requests, avoiding repeated filesystem calls.
102///
103/// # Security
104///
105/// The server protects against:
106/// - Path traversal attacks (e.g., `../../etc/passwd`)
107/// - Accessing files outside the root via symlinks
108/// - Disclosing filesystem structure (traversal and missing files both return 404)
109///
110/// # Cloning
111///
112/// `Server` is cheap to clone (a `PathBuf` and a `usize`). Multiple clones can be used
113/// concurrently in async tasks without synchronization overhead.
114///
115/// # Example
116///
117/// ```no_run
118/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
119/// use mini_static::Server;
120/// use std::path::Path;
121/// use std::time::Duration;
122///
123/// let server = Server::new(Path::new("./public"))?;
124/// let (port, _handle) = server.run(Duration::from_secs(30)).await?;
125/// println!("Server running on port {}", port);
126/// # Ok(())
127/// # }
128/// ```
129#[derive(Clone)]
130pub struct Server {
131    root_canon: PathBuf,
132    max_connections: usize,
133}
134
135impl Server {
136    /// Create a new server with the given root directory.
137    ///
138    /// Canonicalizes the root once at startup. All subsequent requests use the
139    /// canonical root without re-canonicalizing it, making this suitable for long-lived servers.
140    ///
141    /// # Arguments
142    ///
143    /// * `root` - The root directory to serve files from.
144    ///
145    /// # Errors
146    ///
147    /// Returns `Err(StaticError::Io)` if the root cannot be canonicalized (e.g., doesn't exist,
148    /// no read permissions).
149    pub fn new(root: &Path) -> Result<Self, StaticError> {
150        let root_canon = root.canonicalize().map_err(StaticError::Io)?;
151        Ok(Server { root_canon, max_connections: DEFAULT_MAX_CONNECTIONS })
152    }
153
154    /// Set the maximum number of connections served concurrently (default 1024).
155    ///
156    /// Once this many connections are in flight, `run()`'s accept loop stops accepting
157    /// new ones — without pausing the accept loop, a client that opens a connection and
158    /// sends nothing (see the header-read timeout docs on `run()`) could otherwise be
159    /// used, in enough parallel copies, to exhaust the process's file descriptors or
160    /// memory with no bound at all.
161    pub fn with_max_connections(mut self, max: usize) -> Self {
162        self.max_connections = max;
163        self
164    }
165
166    /// Resolve a request path under the server's root.
167    ///
168    /// This is a lower-level API for resolving paths without generating HTTP responses.
169    /// For most use cases, prefer `handle_request_with_method()` or the `run()` methods.
170    ///
171    /// # Arguments
172    ///
173    /// * `request_path` - The HTTP request path (e.g., `/path/to/file.html`).
174    ///
175    /// # Returns
176    ///
177    /// - `Ok(PathBuf)` if the path resolves to a file within root.
178    /// - `Err(StaticError)` if the path is invalid, missing, or attempts traversal.
179    pub fn resolve(&self, request_path: &str) -> Result<PathBuf, StaticError> {
180        resolve::resolve_with_canonical_root(&self.root_canon, request_path)
181    }
182
183    /// Handle an HTTP GET request for a resource path.
184    ///
185    /// Convenience method equivalent to `handle_request_with_method(&Method::GET, request_path)`.
186    ///
187    /// # Arguments
188    ///
189    /// * `request_path` - The HTTP request path (e.g., `/index.html`).
190    pub fn handle_request(&self, request_path: &str) -> Response<ResponseBody> {
191        self.handle_request_with_method(&Method::GET, request_path)
192    }
193
194    /// Handle an HTTP request with an explicit method.
195    ///
196    /// Only GET and HEAD methods are allowed. Other methods return 405 Method Not Allowed
197    /// with an Allow header listing the permitted methods.
198    ///
199    /// # Arguments
200    ///
201    /// * `method` - The HTTP method (GET and HEAD are allowed; others return 405).
202    /// * `request_path` - The HTTP request path (e.g., `/index.html`).
203    pub fn handle_request_with_method(
204        &self,
205        method: &Method,
206        request_path: &str,
207    ) -> Response<ResponseBody> {
208        self.handle_request_with_headers(method, request_path, None, None)
209    }
210
211    /// Run the server on a specific address with a configurable header-read timeout.
212    ///
213    /// Spawns the server in a background Tokio task and returns immediately with the
214    /// assigned port number and a [`ServerHandle`]. Call `handle.shutdown().await` to
215    /// stop accepting new connections and wait for in-flight connections to finish.
216    ///
217    /// # Header-Read Timeout
218    ///
219    /// Connections that don't send complete HTTP headers within `header_timeout` are closed.
220    /// This prevents slowloris attacks and resource exhaustion from incomplete requests.
221    ///
222    /// # Arguments
223    ///
224    /// * `addr` - Socket address to bind to (e.g., `127.0.0.1:0` for loopback ephemeral,
225    ///   or `0.0.0.0:8080` to bind all interfaces on a fixed port).
226    /// * `header_timeout` - Maximum time to wait for complete HTTP headers on each connection.
227    ///
228    /// # Returns
229    ///
230    /// - `Ok((u16, ServerHandle))` with the assigned port number and a handle for graceful shutdown.
231    /// - `Err(StaticError::Io)` if binding to the socket fails.
232    pub async fn run_on(&self, addr: SocketAddr, header_timeout: Duration) -> Result<(u16, ServerHandle), StaticError> {
233        let listener = TcpListener::bind(addr)
234            .await
235            .map_err(StaticError::Io)?;
236        let port = listener
237            .local_addr()
238            .map_err(StaticError::Io)?
239            .port();
240
241        let server = self.clone();
242        let semaphore = Arc::new(Semaphore::new(server.max_connections));
243        let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
244
245        let accept_task = tokio::spawn(async move {
246            let mut backoff = Backoff::new();
247            let mut join_set: tokio::task::JoinSet<()> = tokio::task::JoinSet::new();
248            let mut shutdown_pin = std::pin::pin!(shutdown_rx);
249            let mut shutting_down = false;
250
251            loop {
252                if !shutting_down {
253                    // The accept-and-permit step and the shutdown signal race in a single
254                    // `select!` so shutdown can preempt a pending accept or a permit wait
255                    // cleanly, at any point — not just between loop iterations.
256                    tokio::select! {
257                        accepted = accept_and_permit(&listener, &mut backoff, &semaphore) => {
258                            match accepted {
259                                Some((stream, permit)) => {
260                                    let server = server.clone();
261                                    join_set.spawn(async move {
262                                        let _permit = permit;
263                                        serve_connection(stream, server, header_timeout).await;
264                                    });
265                                }
266                                None => shutting_down = true,
267                            }
268                        }
269                        _ = shutdown_pin.as_mut() => {
270                            shutting_down = true;
271                        }
272                    }
273                    continue;
274                }
275
276                // Stop accepting; drain already-spawned connections before returning.
277                match join_set.join_next().await {
278                    Some(_) => continue,
279                    None => break,
280                }
281            }
282        });
283
284        Ok((port, ServerHandle { shutdown_tx: Some(shutdown_tx), accept_task }))
285    }
286
287    /// Run the server on loopback (127.0.0.1) with a configurable header-read timeout.
288    ///
289    /// Binds to an ephemeral port and spawns the server in a background Tokio task.
290    /// Returns immediately with the assigned port number and a [`ServerHandle`]. Dropping
291    /// the handle without calling `shutdown()` leaves the server running in the
292    /// background for the life of the process — the same behavior `run()` always had.
293    /// Call `handle.shutdown().await` to stop accepting new connections and wait for
294    /// in-flight connections to finish.
295    ///
296    /// # Header-Read Timeout
297    ///
298    /// Connections that don't send complete HTTP headers within `header_timeout` are closed.
299    /// This prevents slowloris attacks and resource exhaustion from incomplete requests.
300    ///
301    /// # Arguments
302    ///
303    /// * `header_timeout` - Maximum time to wait for complete HTTP headers on each connection.
304    ///
305    /// # Returns
306    ///
307    /// - `Ok((u16, ServerHandle))` with the ephemeral port number assigned by the OS and
308    ///   a handle for graceful shutdown.
309    /// - `Err(StaticError::Io)` if binding to the socket fails.
310    ///
311    /// # Example
312    ///
313    /// ```no_run
314    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
315    /// use mini_static::Server;
316    /// use std::path::Path;
317    /// use std::time::Duration;
318    ///
319    /// let server = Server::new(Path::new("./public"))?;
320    /// let (port, handle) = server.run(Duration::from_secs(30)).await?;
321    /// println!("Server running on http://127.0.0.1:{}", port);
322    /// // ... later, to stop it gracefully:
323    /// handle.shutdown().await;
324    /// # Ok(())
325    /// # }
326    /// ```
327    pub async fn run(&self, header_timeout: Duration) -> Result<(u16, ServerHandle), StaticError> {
328        let addr: SocketAddr = ([127, 0, 0, 1], 0).into();
329        self.run_on(addr, header_timeout).await
330    }
331
332    /// Run the server on all interfaces (0.0.0.0) with a configurable header-read timeout.
333    ///
334    /// Binds to a specified port on all network interfaces. Useful for containerized
335    /// deployments, reverse-proxy setups, or services that need to accept connections
336    /// from anywhere. Spawns the server in a background Tokio task and returns immediately
337    /// with the assigned port and a [`ServerHandle`].
338    ///
339    /// # Header-Read Timeout
340    ///
341    /// Connections that don't send complete HTTP headers within `header_timeout` are closed.
342    /// This prevents slowloris attacks and resource exhaustion from incomplete requests.
343    ///
344    /// # Arguments
345    ///
346    /// * `port` - Port number to bind to (0 for ephemeral port assignment).
347    /// * `header_timeout` - Maximum time to wait for complete HTTP headers on each connection.
348    ///
349    /// # Returns
350    ///
351    /// - `Ok((u16, ServerHandle))` with the assigned port number and a handle for graceful shutdown.
352    /// - `Err(StaticError::Io)` if binding to the socket fails.
353    ///
354    /// # Example
355    ///
356    /// ```no_run
357    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
358    /// use mini_static::Server;
359    /// use std::path::Path;
360    /// use std::time::Duration;
361    ///
362    /// let server = Server::new(Path::new("./public"))?;
363    /// let (_port, handle) = server.run_all(8080, Duration::from_secs(30)).await?;
364    /// println!("Server listening on 0.0.0.0:8080");
365    /// handle.shutdown().await;
366    /// # Ok(())
367    /// # }
368    /// ```
369    pub async fn run_all(&self, port: u16, header_timeout: Duration) -> Result<(u16, ServerHandle), StaticError> {
370        let addr: SocketAddr = ([0, 0, 0, 0], port).into();
371        self.run_on(addr, header_timeout).await
372    }
373
374    /// Run the server on loopback (127.0.0.1) with a default header-read timeout.
375    ///
376    /// Convenience wrapper around `run()` that uses a default 30-second header-read timeout.
377    /// Returns immediately with the ephemeral port number and a [`ServerHandle`]; the server
378    /// continues in a background Tokio task until the handle's `shutdown()` is awaited or
379    /// the Tokio runtime shuts down.
380    ///
381    /// This is the recommended method for tests and lightweight services that don't require
382    /// custom timeout configuration.
383    ///
384    /// # Returns
385    ///
386    /// - `Ok((u16, ServerHandle))` with the ephemeral port number assigned by the OS and
387    ///   a handle for graceful shutdown.
388    /// - `Err(StaticError::Io)` if binding to the socket fails.
389    ///
390    /// # Example
391    ///
392    /// ```no_run
393    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
394    /// use mini_static::Server;
395    /// use std::path::Path;
396    ///
397    /// let server = Server::new(Path::new("./public"))?;
398    /// let (port, handle) = server.run_ephemeral().await?;
399    /// println!("Server ready on http://127.0.0.1:{}", port);
400    /// handle.shutdown().await;
401    /// # Ok(())
402    /// # }
403    /// ```
404    pub async fn run_ephemeral(&self) -> Result<(u16, ServerHandle), StaticError> {
405        self.run(Duration::from_secs(30)).await
406    }
407
408    /// Handle an HTTP request asynchronously, streaming file bodies to the client.
409    ///
410    /// File responses are backed by `FileBody`, which reads and hands off one 64 KB
411    /// chunk to hyper at a time as `poll_frame` is driven — memory use stays bounded to
412    /// one chunk per in-flight response regardless of file size, and no chunk is copied
413    /// or zero-filled beyond what the read syscall itself writes.
414    ///
415    /// Conditional requests (If-None-Match, If-Modified-Since) are honored: if the
416    /// request includes a validator that matches the file's ETag, returns 304 Not Modified.
417    async fn handle_request_async(
418        &self,
419        method: &Method,
420        request_path: &str,
421        if_none_match: Option<&str>,
422        if_modified_since: Option<&str>,
423    ) -> Response<ResponseBody> {
424        // Gate on HTTP method
425        if method != Method::GET && method != Method::HEAD {
426            return finish(Response::builder()
427                .status(StatusCode::METHOD_NOT_ALLOWED)
428                .header("Allow", "GET, HEAD")
429                .header("X-Content-Type-Options", "nosniff")
430                .body(into_response_body(Full::new(Bytes::from("method not allowed\n")))));
431        }
432
433        // `resolve()` does blocking filesystem syscalls (`canonicalize()`, up to two per
434        // request). Running those directly in this `async fn` would block whichever
435        // Tokio worker thread happens to be driving it, stalling every other task
436        // scheduled on that thread for the duration of the syscalls. `spawn_blocking`
437        // moves the work onto Tokio's dedicated blocking thread pool instead.
438        let server = self.clone();
439        let owned_request_path = request_path.to_string();
440        let resolved = tokio::task::spawn_blocking(move || server.resolve(&owned_request_path)).await;
441        let resolved = match resolved {
442            Ok(r) => r,
443            Err(_) => return internal_error_response(),
444        };
445
446        match resolved {
447            Ok(path) => {
448                let decoded_request_path = resolve::decode_request_path(request_path);
449                if path.file_name().is_some_and(|name| name == "index.html")
450                    && !decoded_request_path.ends_with('/')
451                    && !decoded_request_path.ends_with("index.html")
452                {
453                    let location = format!("{}/", request_path.trim_end_matches('/'));
454                    // `location` is built from the (attacker-controlled) request path;
455                    // `finish()` degrades to 400 instead of panicking if it ever contains
456                    // bytes invalid in a header value.
457                    return finish(Response::builder()
458                        .status(StatusCode::MOVED_PERMANENTLY)
459                        .header("Location", location)
460                        .header("X-Content-Type-Options", "nosniff")
461                        .body(into_response_body(Full::new(Bytes::from("moved\n")))));
462                }
463
464                // Use async file operations for streaming
465                let file = match File::open(&path).await {
466                    Ok(f) => f,
467                    Err(_) => return internal_error_response(),
468                };
469
470                let metadata = match file.metadata().await {
471                    Ok(m) => m,
472                    Err(_) => return internal_error_response(),
473                };
474
475                let file_size = metadata.len();
476                let etag = generate_etag(&metadata);
477
478                // Check If-None-Match (ETag) for 304 Not Modified
479                if let Some(if_none_match) = if_none_match {
480                    if is_etag_match(if_none_match, &etag) {
481                        return finish(Response::builder()
482                            .status(StatusCode::NOT_MODIFIED)
483                            .header("ETag", etag)
484                            .body(into_response_body(Full::new(Bytes::new()))));
485                    }
486                }
487
488                // Check If-Modified-Since (mtime) for 304 Not Modified
489                if let Some(if_modified_since) = if_modified_since {
490                    if is_not_modified_since(if_modified_since, &metadata) {
491                        return finish(Response::builder()
492                            .status(StatusCode::NOT_MODIFIED)
493                            .header("ETag", etag)
494                            .body(into_response_body(Full::new(Bytes::new()))));
495                    }
496                }
497
498                // HEAD must not return a body (RFC 9110); skip opening the read stream
499                // entirely since we'd just discard every chunk.
500                let body: ResponseBody = if *method == Method::HEAD {
501                    into_response_body(Full::new(Bytes::new()))
502                } else {
503                    FileBody::new(file).boxed()
504                };
505
506                let content_type = mime_type_for_path(&path);
507                finish(Response::builder()
508                    .status(StatusCode::OK)
509                    .header("X-Content-Type-Options", "nosniff")
510                    .header("Content-Type", content_type)
511                    .header("Content-Length", file_size.to_string())
512                    .header("ETag", etag)
513                    .body(body))
514            }
515            Err(e) => {
516                let message = e.user_message();
517                let body = format!("{}\n", message);
518
519                finish(Response::builder()
520                    .status(StatusCode::NOT_FOUND)
521                    .header("X-Content-Type-Options", "nosniff")
522                    .body(into_response_body(Full::new(Bytes::from(body)))))
523            }
524        }
525    }
526
527    /// Handle an HTTP request with method and optional Range/If-Range headers (synchronous API).
528    ///
529    /// This is the synchronous version of request handling used internally by the
530    /// async server loop. For most use cases, prefer using `run()` or `run_ephemeral()`
531    /// which handle the full async lifecycle.
532    ///
533    /// Only GET and HEAD methods are allowed; other methods return 405 Method Not Allowed.
534    /// All errors (missing files, traversal attempts, I/O failures) are returned as 404
535    /// to avoid leaking filesystem structure information.
536    ///
537    /// # Range Request Handling
538    ///
539    /// mini-static does not yet serve `206 Partial Content` — every request,
540    /// ranged or not, gets the full body with `200`. This is RFC 9110-correct behavior
541    /// (as opposed to incorrectly answering `416`), but partial-content serving is
542    /// deferred to a later phase.
543    ///
544    /// # Arguments
545    ///
546    /// * `method` - The HTTP method (GET and HEAD only).
547    /// * `request_path` - The HTTP request path (e.g., `/index.html`).
548    /// * `_range_header` - Optional Range header (currently unused).
549    /// * `_if_range_header` - Optional If-Range header (currently unused).
550    pub fn handle_request_with_headers(
551        &self,
552        method: &Method,
553        request_path: &str,
554        _range_header: Option<&str>,
555        _if_range_header: Option<&str>,
556    ) -> Response<ResponseBody> {
557        // Gate on HTTP method
558        if method != Method::GET && method != Method::HEAD {
559            return finish(Response::builder()
560                .status(StatusCode::METHOD_NOT_ALLOWED)
561                .header("Allow", "GET, HEAD")
562                .header("X-Content-Type-Options", "nosniff")
563                .body(into_response_body(Full::new(Bytes::from("method not allowed\n")))));
564        }
565
566        // Method is allowed; resolve the path
567        match self.resolve(request_path) {
568            Ok(path) => {
569                // Check if resolved path is index.html but request_path doesn't end with /
570                // If so, redirect to path/ to establish correct base for relative links.
571                // Compare against the *decoded* request path so a percent-encoded explicit
572                // request for index.html (e.g. `/docs/index.htm%6c`) is recognized as such
573                // instead of producing a redirect to a still-encoded, broken Location.
574                let decoded_request_path = resolve::decode_request_path(request_path);
575                if path.file_name().is_some_and(|name| name == "index.html")
576                    && !decoded_request_path.ends_with('/')
577                    && !decoded_request_path.ends_with("index.html")
578                {
579                    let location = format!("{}/", request_path.trim_end_matches('/'));
580
581                    // Location is built from the (attacker-controlled) request path;
582                    // `finish()` degrades to 400 instead of panicking if it ever contains
583                    // bytes invalid in a header value.
584                    return finish(Response::builder()
585                        .status(StatusCode::MOVED_PERMANENTLY)
586                        .header("Location", location)
587                        .header("X-Content-Type-Options", "nosniff")
588                        .body(into_response_body(Full::new(Bytes::from("moved\n")))));
589                }
590
591                let file = match fs::File::open(&path) {
592                    Ok(f) => f,
593                    Err(_) => return internal_error_response(),
594                };
595                let metadata = match file.metadata() {
596                    Ok(m) => m,
597                    Err(_) => return internal_error_response(),
598                };
599                let file_size = metadata.len();
600                let etag = generate_etag(&metadata);
601
602                // HEAD must not return a body (RFC 9110); avoid reading file content we'd
603                // just discard.
604                let body_bytes = if *method == Method::HEAD {
605                    Bytes::new()
606                } else {
607                    let mut buf = Vec::with_capacity(file_size as usize);
608                    let mut file = file;
609                    if file.read_to_end(&mut buf).is_err() {
610                        return internal_error_response();
611                    }
612                    Bytes::from(buf)
613                };
614
615                finish(Response::builder()
616                    .status(StatusCode::OK)
617                    .header("X-Content-Type-Options", "nosniff")
618                    .header("Content-Length", file_size.to_string())
619                    .header("ETag", etag)
620                    .body(into_response_body(Full::new(body_bytes))))
621            }
622            Err(e) => {
623                let message = e.user_message();
624                let body = format!("{}\n", message);
625
626                finish(Response::builder()
627                    .status(StatusCode::NOT_FOUND)
628                    .header("X-Content-Type-Options", "nosniff")
629                    .body(into_response_body(Full::new(Bytes::from(body)))))
630            }
631        }
632    }
633}
634
635/// Wires an accepted connection up to the hyper HTTP/1 service and drives it to
636/// completion, bounded by `header_timeout`. Shared by every accept loop so the
637/// framing/timeout setup is defined exactly once.
638async fn serve_connection(stream: TcpStream, server: Server, header_timeout: Duration) {
639    let io = TokioIo::new(stream);
640    let svc = service_fn(move |req: Request<Incoming>| {
641        let server = server.clone();
642        async move {
643            let method = req.method().clone();
644            let path = req.uri().path().to_string();
645            let if_none_match = req.headers().get("if-none-match").and_then(|v| v.to_str().ok());
646            let if_modified_since = req.headers().get("if-modified-since").and_then(|v| v.to_str().ok());
647            let resp = server.handle_request_async(&method, &path, if_none_match, if_modified_since).await;
648            Ok::<_, Infallible>(resp)
649        }
650    });
651    let _ = timeout(
652        header_timeout,
653        AutoBuilder::new(TokioExecutor::new()).serve_connection(io, svc),
654    ).await;
655}
656
657/// A handle to a server started by `Server::run()` or `Server::run_ephemeral()`.
658///
659/// Dropping this handle without calling `shutdown()` leaves the server running in the
660/// background for the life of the process — the same behavior `run()` always had before
661/// this handle existed. Call `shutdown()` to stop accepting new connections and wait for
662/// already-accepted connections to finish before returning.
663pub struct ServerHandle {
664    shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
665    accept_task: tokio::task::JoinHandle<()>,
666}
667
668impl ServerHandle {
669    /// Stop accepting new connections and wait for in-flight connections to finish.
670    pub async fn shutdown(mut self) {
671        if let Some(tx) = self.shutdown_tx.take() {
672            let _ = tx.send(());
673        }
674        let _ = self.accept_task.await;
675    }
676}
677
678fn into_response_body(body: Full<Bytes>) -> ResponseBody {
679    body.map_err(|never| match never {}).boxed()
680}
681
682/// Finishes building a response, degrading to a generic 400 instead of panicking if any
683/// header value turns out to be invalid for use as an HTTP header value.
684///
685/// Every header value that reaches `Response::builder()` in this module is either a
686/// static string or formatted from internal, already-validated data (a byte count, an
687/// mtime, a fixed method list) — none of it can actually fail today. But `.unwrap()`
688/// on that assumption is exactly the kind of thing that turns "can't happen" into a
689/// production panic the day someone adds a header built from new input without
690/// re-deriving that guarantee. Routing every response through this one fallible path
691/// means that mistake fails safe instead of panicking.
692fn finish(built: Result<Response<ResponseBody>, hyper::http::Error>) -> Response<ResponseBody> {
693    built.unwrap_or_else(|_| bad_request_response())
694}
695
696/// Chunk size for streaming file reads — 64 KB per frame.
697const FILE_CHUNK_SIZE: usize = 65_536;
698
699/// Default maximum concurrent connections, overridable via `Server::with_max_connections`.
700const DEFAULT_MAX_CONNECTIONS: usize = 1024;
701
702/// An `http_body::Body` that streams a `tokio::fs::File` to the client one chunk at a
703/// time, instead of buffering the whole file before the response body is polled.
704///
705/// Each `poll_frame` call reads directly into `buf`'s spare (uninitialized) capacity via
706/// `ReadBuf::uninit` and marks only the bytes the read syscall actually wrote as
707/// initialized via `advance_mut` — there's no `resize`-driven zero-fill and no extra
708/// copy: `split_to(n).freeze()` hands the just-filled bytes to the caller and leaves
709/// `buf`'s already-reserved spare capacity in place for the next read.
710struct FileBody {
711    file: File,
712    buf: BytesMut,
713}
714
715impl FileBody {
716    fn new(file: File) -> Self {
717        FileBody { file, buf: BytesMut::new() }
718    }
719}
720
721impl Body for FileBody {
722    type Data = Bytes;
723    type Error = StaticError;
724
725    fn poll_frame(
726        self: Pin<&mut Self>,
727        cx: &mut Context<'_>,
728    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
729        let this = self.get_mut();
730
731        if this.buf.capacity() - this.buf.len() < FILE_CHUNK_SIZE {
732            this.buf.reserve(FILE_CHUNK_SIZE);
733        }
734
735        let mut read_buf = ReadBuf::uninit(this.buf.spare_capacity_mut());
736        let file = Pin::new(&mut this.file);
737
738        match file.poll_read(cx, &mut read_buf) {
739            Poll::Ready(Ok(())) => {
740                let n = read_buf.filled().len();
741                if n == 0 {
742                    return Poll::Ready(None);
743                }
744                // Safety: `poll_read` reported exactly `n` bytes filled into the spare
745                // capacity we handed it via `ReadBuf::uninit`; advancing by that same
746                // `n` only marks bytes the reader actually initialized.
747                unsafe { this.buf.advance_mut(n) };
748                let chunk = this.buf.split_to(n).freeze();
749                Poll::Ready(Some(Ok(Frame::data(chunk))))
750            }
751            Poll::Ready(Err(e)) => Poll::Ready(Some(Err(StaticError::Io(e)))),
752            Poll::Pending => Poll::Pending,
753        }
754    }
755}
756
757// `internal_error_response()` and `bad_request_response()` are the fallback responses
758// `finish()` itself degrades to — every header and body here is a fixed string with no
759// external input, so `.body(...)` cannot fail. They can't be routed through `finish()`
760// without it degrading to itself on failure.
761fn internal_error_response() -> Response<ResponseBody> {
762    Response::builder()
763        .status(StatusCode::INTERNAL_SERVER_ERROR)
764        .header("X-Content-Type-Options", "nosniff")
765        .body(into_response_body(Full::new(Bytes::from(
766            "internal server error\n",
767        ))))
768        .unwrap()
769}
770
771fn bad_request_response() -> Response<ResponseBody> {
772    Response::builder()
773        .status(StatusCode::BAD_REQUEST)
774        .header("X-Content-Type-Options", "nosniff")
775        .body(into_response_body(Full::new(Bytes::from("bad request\n"))))
776        .unwrap()
777}
778
779/// Generate an ETag for a file based on modification time and size.
780///
781/// Format: `"<size>-<mtime_secs>"`
782fn generate_etag(metadata: &fs::Metadata) -> String {
783    let size = metadata.len();
784    let mtime = metadata
785        .modified()
786        .ok()
787        .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
788        .map(|d| d.as_secs())
789        .unwrap_or(0);
790    format!("\"{}-{}\"", size, mtime)
791}
792
793/// Determine MIME type from file path extension.
794fn mime_type_for_path(path: &Path) -> &'static str {
795    path.extension()
796        .and_then(|ext| ext.to_str())
797        .and_then(|ext| match ext.to_lowercase().as_str() {
798            "html" | "htm" => Some("text/html; charset=utf-8"),
799            "css" => Some("text/css; charset=utf-8"),
800            "js" => Some("application/javascript; charset=utf-8"),
801            "json" => Some("application/json; charset=utf-8"),
802            "svg" => Some("image/svg+xml"),
803            "png" => Some("image/png"),
804            "jpg" | "jpeg" => Some("image/jpeg"),
805            "gif" => Some("image/gif"),
806            "webp" => Some("image/webp"),
807            "ico" => Some("image/x-icon"),
808            "woff" => Some("font/woff"),
809            "woff2" => Some("font/woff2"),
810            "ttf" => Some("font/ttf"),
811            "md" | "markdown" => Some("text/markdown; charset=utf-8"),
812            "txt" => Some("text/plain; charset=utf-8"),
813            "xml" => Some("application/xml"),
814            "pdf" => Some("application/pdf"),
815            "zip" => Some("application/zip"),
816            _ => None,
817        })
818        .unwrap_or("application/octet-stream")
819}
820
821/// Check if the If-None-Match header matches the current ETag.
822/// Handles both exact match and wildcard (*) comparison per RFC 9110.
823fn is_etag_match(if_none_match: &str, etag: &str) -> bool {
824    if if_none_match == "*" {
825        return true;
826    }
827    if_none_match.split(',').any(|tag| tag.trim() == etag)
828}
829
830/// Check if If-Modified-Since indicates the file hasn't been modified.
831/// Returns true if the file's mtime is before/equal to the If-Modified-Since timestamp.
832fn is_not_modified_since(if_modified_since: &str, metadata: &fs::Metadata) -> bool {
833    let file_mtime = metadata
834        .modified()
835        .ok()
836        .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
837        .map(|d| d.as_secs())
838        .unwrap_or(0);
839
840    // Parse the If-Modified-Since header as an HTTP-date (RFC 9110 Section 5.6.7).
841    // For simplicity, try to parse as a simple Unix timestamp first, then fall back to
842    // a basic string comparison. A production implementation would use a proper
843    // RFC 2822 / RFC 9110 date parser, but for testing we can be lenient.
844    if let Ok(client_time) = if_modified_since.parse::<u64>() {
845        return file_mtime <= client_time;
846    }
847
848    // Fallback: if parsing fails, be conservative and don't return 304.
849    false
850}
851
852#[cfg(test)]
853mod file_body_tests {
854    use super::*;
855    use http_body_util::BodyExt;
856
857    // Disproves the prior implementation, which read every chunk into a `Vec` and
858    // only wrapped the whole result in a single `Full` frame at the end — that
859    // implementation would fail this test with `frame_count == 1` and
860    // `max_frame_len == file size`, regardless of `FILE_CHUNK_SIZE`.
861    #[tokio::test]
862    async fn file_body_yields_multiple_bounded_chunks_not_one_buffered_frame() {
863        let dir = tempfile::TempDir::new().unwrap();
864        let path = dir.path().join("big.bin");
865        let content = vec![7u8; FILE_CHUNK_SIZE * 3 + 12_345];
866        fs::write(&path, &content).unwrap();
867
868        let file = File::open(&path).await.unwrap();
869        let mut body = FileBody::new(file);
870
871        let mut frame_count = 0usize;
872        let mut max_frame_len = 0usize;
873        let mut reassembled = Vec::new();
874
875        while let Some(frame) = body.frame().await {
876            let frame = frame.unwrap();
877            let data = frame.into_data().unwrap();
878            frame_count += 1;
879            max_frame_len = max_frame_len.max(data.len());
880            reassembled.extend_from_slice(&data);
881        }
882
883        assert!(
884            frame_count > 1,
885            "expected the file to be delivered as multiple frames, got {frame_count}"
886        );
887        assert!(
888            max_frame_len <= FILE_CHUNK_SIZE,
889            "no single frame should exceed the chunk size ({FILE_CHUNK_SIZE}), got {max_frame_len}"
890        );
891        assert_eq!(reassembled, content, "reassembled chunks must match original file content exactly");
892    }
893}
894
895#[cfg(test)]
896mod accept_tests {
897    use super::*;
898    use std::sync::atomic::{AtomicUsize, Ordering};
899    use std::sync::Mutex;
900
901    #[test]
902    fn backoff_doubles_up_to_max() {
903        let mut backoff = Backoff::new();
904        let mut last = backoff.next_delay();
905        assert_eq!(last, ACCEPT_BACKOFF_INITIAL);
906
907        // Double repeatedly; it must stop growing once it hits the cap rather than
908        // continuing to double forever (a fixed upper bound, not an unbounded retry).
909        for _ in 0..20 {
910            last = backoff.next_delay();
911        }
912        assert_eq!(last, ACCEPT_BACKOFF_MAX);
913    }
914
915    #[test]
916    fn backoff_reset_returns_to_initial_delay() {
917        let mut backoff = Backoff::new();
918        backoff.next_delay();
919        backoff.next_delay();
920        backoff.reset();
921        assert_eq!(backoff.next_delay(), ACCEPT_BACKOFF_INITIAL);
922    }
923
924    /// Fails `accept()` a fixed number of times, recording the (paused, virtual)
925    /// instant of each attempt, before delegating to a real listener so the caller can
926    /// eventually succeed.
927    struct FlakyListener {
928        inner: TcpListener,
929        remaining_failures: AtomicUsize,
930        attempts: Mutex<Vec<tokio::time::Instant>>,
931    }
932
933    impl TcpAccept for FlakyListener {
934        async fn accept(&self) -> std::io::Result<(TcpStream, SocketAddr)> {
935            self.attempts.lock().unwrap().push(tokio::time::Instant::now());
936            if self.remaining_failures.fetch_sub(1, Ordering::SeqCst) > 0 {
937                Err(std::io::Error::other("simulated accept error"))
938            } else {
939                TcpAccept::accept(&self.inner).await
940            }
941        }
942    }
943
944    // Disproves the prior implementation, which broke out of the accept loop entirely
945    // on the first `accept()` error — permanently ending the server. This test would
946    // also fail against a naive `continue`-only fix (no backoff): the recorded gaps
947    // between attempts would collapse to ~0 (a busy spin) instead of the expected
948    // exponentially growing delays.
949    #[tokio::test(start_paused = true)]
950    async fn accept_loop_backs_off_between_repeated_errors_instead_of_busy_spinning() {
951        let inner = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
952        let addr = inner.local_addr().unwrap();
953
954        let flaky = FlakyListener {
955            inner,
956            remaining_failures: AtomicUsize::new(5),
957            attempts: Mutex::new(Vec::new()),
958        };
959
960        tokio::spawn(async move {
961            let _ = TcpStream::connect(addr).await;
962        });
963
964        let semaphore = Arc::new(Semaphore::new(1));
965        let mut backoff = Backoff::new();
966        let result = accept_and_permit(&flaky, &mut backoff, &semaphore).await;
967        assert!(result.is_some(), "accept should eventually succeed once the flaky listener stops failing");
968
969        let recorded = flaky.attempts.lock().unwrap();
970        assert_eq!(recorded.len(), 6, "5 failures then 1 success");
971
972        let expected_gaps = [
973            ACCEPT_BACKOFF_INITIAL,
974            ACCEPT_BACKOFF_INITIAL * 2,
975            ACCEPT_BACKOFF_INITIAL * 4,
976            ACCEPT_BACKOFF_INITIAL * 8,
977            ACCEPT_BACKOFF_INITIAL * 16,
978        ];
979        for (i, expected) in expected_gaps.iter().enumerate() {
980            let gap = recorded[i + 1] - recorded[i];
981            assert_eq!(
982                gap, *expected,
983                "gap between attempt {i} and {} should reflect the backoff delay, not a busy spin",
984                i + 1
985            );
986        }
987    }
988}
989
990#[cfg(test)]
991mod finish_tests {
992    use super::*;
993
994    // Disproves a bare `.unwrap()` on the same builder: CR/LF is not a legal header
995    // value byte (it would enable header/response splitting), so this construction is
996    // guaranteed to make `.body(...)` return `Err`. Every real call site in this module
997    // only ever builds header values from static strings or internally-formatted
998    // numbers, so this test can't happen through normal use — it exists to prove
999    // `finish()`'s fallback path actually works, not to exercise a reachable case.
1000    #[test]
1001    fn finish_degrades_to_400_on_invalid_header_value_instead_of_panicking() {
1002        let built = Response::builder()
1003            .status(StatusCode::OK)
1004            .header("X-Test", "invalid\r\nvalue")
1005            .body(into_response_body(Full::new(Bytes::new())));
1006        assert!(built.is_err(), "CR/LF in a header value should be rejected by the builder");
1007
1008        let response = finish(built);
1009        assert_eq!(
1010            response.status(),
1011            StatusCode::BAD_REQUEST,
1012            "finish() should degrade to 400 rather than panicking on an invalid header value"
1013        );
1014    }
1015}