static_web_server/static_files/mod.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2// This file is part of Static Web Server.
3// See https://static-web-server.net/ for more information
4// Copyright (C) 2019-present Jose Quintana <joseluisq.net>
5
6//! The static file module which powers the web server.
7//!
8//! The request pipeline is intentionally linear and reads top-to-bottom in
9//! [`handle`]:
10//!
11//! 1. **Method check** — `GET`, `HEAD` and `OPTIONS` only.
12//! 2. **Path sanitization** — strip traversal components from the URI path.
13//! 3. **In-memory cache lookup** — short-circuit hot files.
14//! 4. **File resolution** — directory → index, `.html` fallback,
15//! pre-compressed variant detection (see [`resolve`]).
16//! 5. **Security checks** — containment, symlink and hidden-file policy
17//! (see [`security`]).
18//! 6. **Short-circuit responses** — trailing-slash redirect, `OPTIONS`,
19//! directory listing or archive download.
20//! 7. **File reply** — stream the resolved file or its pre-compressed
21//! variant (see [`reply`]).
22
23// Part of the module is borrowed and adapted at a convenience from
24// https://github.com/seanmonstar/warp/blob/master/src/filters/fs.rs
25
26mod opts;
27mod reply;
28mod resolve;
29mod security;
30
31#[cfg(feature = "directory-listing")]
32mod listing;
33
34pub use opts::{HandleOpts, StaticFileResponse};
35
36// Re-export for benches/fuzzers/external integration tests that exercise
37// the path-sanitisation invariant directly.
38#[doc(hidden)]
39pub use crate::fs::path::sanitize_path;
40
41use hyper::StatusCode;
42
43use crate::Result;
44use crate::exts::http::MethodExt;
45use crate::fs::meta::FileMetadata;
46
47#[cfg(feature = "mem-cache")]
48use crate::mem_cache::cache;
49
50/// The server entry point to handle incoming requests which map to specific files
51/// on file system and return a file response.
52pub async fn handle(opts: &HandleOpts<'_>) -> Result<StaticFileResponse, StatusCode> {
53 if !opts.method.is_allowed() {
54 return Err(StatusCode::METHOD_NOT_ALLOWED);
55 }
56
57 let mut file_path = sanitize_path(opts.base_path, opts.uri_path)?;
58
59 // In-memory file cache lookup. A hit short-circuits the pipeline.
60 // On miss, the file is read from disk and the streaming pipeline
61 // populates the cache opportunistically (see `mem_cache::stream`).
62 #[cfg(feature = "mem-cache")]
63 if let Some(resp) = try_memory_cache(opts, &mut file_path) {
64 return Ok(resp);
65 }
66
67 let FileMetadata {
68 file_path,
69 metadata,
70 is_dir,
71 precompressed_variant,
72 file,
73 } = resolve::file_metadata(
74 &mut file_path,
75 opts.headers,
76 opts.compression_static,
77 opts.index_files,
78 )?;
79
80 security::enforce(file_path, is_dir, opts)?;
81
82 let resp_file_path = file_path.to_owned();
83
84 if let Some(resp) = reply::trailing_slash_redirect(is_dir, opts)? {
85 return Ok(StaticFileResponse::new(resp, resp_file_path));
86 }
87
88 if opts.method.is_options() {
89 return Ok(StaticFileResponse::new(
90 reply::options_reply(),
91 resp_file_path,
92 ));
93 }
94
95 #[cfg(feature = "directory-listing")]
96 if let Some(resp) = listing::try_listing(file_path, is_dir, opts)? {
97 return Ok(StaticFileResponse::new(resp, resp_file_path));
98 }
99
100 let resp =
101 reply::file_or_precompressed(opts, file_path, &metadata, precompressed_variant, file)?;
102 Ok(StaticFileResponse::new(resp, resp_file_path))
103}
104
105/// Tries to satisfy the request from the in-memory cache.
106///
107/// Returns `Some(response)` on a cache hit, `None` otherwise (cache disabled
108/// in the runtime config, no entry yet, or a non-UTF-8 path).
109///
110/// When a memory cache is configured and the request targets a directory
111/// with trailing-slash redirect on, the cache key is the implicit
112/// `<dir>/index.html`. The function may push that segment onto
113/// `file_path` before performing the lookup.
114#[cfg(feature = "mem-cache")]
115fn try_memory_cache(
116 opts: &HandleOpts<'_>,
117 file_path: &mut std::path::PathBuf,
118) -> Option<StaticFileResponse> {
119 // Runtime gate: if `[advanced.memory-cache]` is not configured in TOML,
120 // `opts.memory_cache` is `None` and we skip the lookup entirely.
121 opts.memory_cache.as_ref()?;
122
123 // NOTE: only the default auto-index is supported for directory
124 // requests inside the memory-cache context.
125 if opts.redirect_trailing_slash && opts.uri_path.ends_with('/') {
126 file_path.push("index.html");
127 }
128
129 let result = cache::lookup(file_path.as_path(), opts.headers)?;
130 match result {
131 Ok(resp) => Some(StaticFileResponse::new(resp, file_path.clone())),
132 // Hit, but the cached entry returned an error status (e.g. malformed Range).
133 // Fall through to the regular pipeline so the error path is consistent.
134 Err(_) => None,
135 }
136}