static_web_server/
handler.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//! Request handler module intended to manage incoming HTTP requests.
7//!
8
9use hyper::{Body, Request, Response, StatusCode};
10use std::{
11    future::Future,
12    net::{IpAddr, SocketAddr},
13    path::PathBuf,
14    sync::Arc,
15};
16
17#[cfg(any(
18    feature = "compression",
19    feature = "compression-gzip",
20    feature = "compression-brotli",
21    feature = "compression-zstd",
22    feature = "compression-deflate"
23))]
24use crate::{compression, compression_static};
25
26#[cfg(feature = "basic-auth")]
27use crate::basic_auth;
28
29#[cfg(feature = "fallback-page")]
30use crate::fallback_page;
31
32#[cfg(all(unix, feature = "experimental"))]
33use crate::metrics;
34
35#[cfg(feature = "experimental")]
36use crate::mem_cache::cache::MemCacheOpts;
37
38use crate::{
39    control_headers, cors, custom_headers, error_page, health,
40    http_ext::MethodExt,
41    log_addr, maintenance_mode, redirects, rewrites, security_headers,
42    settings::Advanced,
43    static_files::{self, HandleOpts},
44    virtual_hosts, Error, Result,
45};
46
47#[cfg(feature = "directory-listing")]
48use crate::directory_listing::DirListFmt;
49
50/// It defines options for a request handler.
51pub struct RequestHandlerOpts {
52    // General options
53    /// Root directory of static files.
54    pub root_dir: PathBuf,
55    #[cfg(feature = "experimental")]
56    /// In-memory cache feature (experimental).
57    pub memory_cache: Option<MemCacheOpts>,
58    /// Compression feature.
59    pub compression: bool,
60    #[cfg(any(
61        feature = "compression",
62        feature = "compression-gzip",
63        feature = "compression-brotli",
64        feature = "compression-zstd",
65        feature = "compression-deflate"
66    ))]
67    /// Compression level.
68    pub compression_level: crate::settings::CompressionLevel,
69    /// Compression static feature.
70    pub compression_static: bool,
71    /// Directory listing feature.
72    #[cfg(feature = "directory-listing")]
73    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
74    pub dir_listing: bool,
75    /// Directory listing order feature.
76    #[cfg(feature = "directory-listing")]
77    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
78    pub dir_listing_order: u8,
79    #[cfg(feature = "directory-listing")]
80    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
81    /// Directory listing format feature.
82    pub dir_listing_format: DirListFmt,
83    /// CORS feature.
84    pub cors: Option<cors::Configured>,
85    /// Security headers feature.
86    pub security_headers: bool,
87    /// Cache control headers feature.
88    pub cache_control_headers: bool,
89    /// Page for 404 errors.
90    pub page404: PathBuf,
91    /// Page for 50x errors.
92    pub page50x: PathBuf,
93    /// Page fallback feature.
94    #[cfg(feature = "fallback-page")]
95    #[cfg_attr(docsrs, doc(cfg(feature = "fallback-page")))]
96    pub page_fallback: Vec<u8>,
97    /// Basic auth feature.
98    #[cfg(feature = "basic-auth")]
99    #[cfg_attr(docsrs, doc(cfg(feature = "basic-auth")))]
100    pub basic_auth: String,
101    /// Index files feature.
102    pub index_files: Vec<String>,
103    /// Log remote address feature.
104    pub log_remote_address: bool,
105    /// Log the X-Real-IP header.
106    pub log_x_real_ip: bool,
107    /// Log the X-Forwarded-For header.
108    pub log_forwarded_for: bool,
109    /// Trusted IPs for remote addresses.
110    pub trusted_proxies: Vec<IpAddr>,
111    /// Redirect trailing slash feature.
112    pub redirect_trailing_slash: bool,
113    /// Ignore hidden files feature.
114    pub ignore_hidden_files: bool,
115    /// Prevent following symlinks for files and directories.
116    pub disable_symlinks: bool,
117    /// Health endpoint feature.
118    pub health: bool,
119    /// Metrics endpoint feature (experimental).
120    #[cfg(all(unix, feature = "experimental"))]
121    pub experimental_metrics: bool,
122    /// Maintenance mode feature.
123    pub maintenance_mode: bool,
124    /// Custom HTTP status for when entering into maintenance mode.
125    pub maintenance_mode_status: StatusCode,
126    /// Custom maintenance mode HTML file.
127    pub maintenance_mode_file: PathBuf,
128
129    /// Advanced options from the config file.
130    pub advanced_opts: Option<Advanced>,
131}
132
133impl Default for RequestHandlerOpts {
134    fn default() -> Self {
135        Self {
136            root_dir: PathBuf::from("./public"),
137            compression: true,
138            compression_static: false,
139            #[cfg(any(
140                feature = "compression",
141                feature = "compression-gzip",
142                feature = "compression-brotli",
143                feature = "compression-zstd",
144                feature = "compression-deflate"
145            ))]
146            compression_level: crate::settings::CompressionLevel::Default,
147            #[cfg(feature = "directory-listing")]
148            dir_listing: false,
149            #[cfg(feature = "directory-listing")]
150            dir_listing_order: 6, // unordered
151            #[cfg(feature = "directory-listing")]
152            dir_listing_format: DirListFmt::Html,
153            cors: None,
154            #[cfg(feature = "experimental")]
155            memory_cache: None,
156            security_headers: false,
157            cache_control_headers: true,
158            page404: PathBuf::from("./404.html"),
159            page50x: PathBuf::from("./50x.html"),
160            #[cfg(feature = "fallback-page")]
161            page_fallback: Vec::new(),
162            #[cfg(feature = "basic-auth")]
163            basic_auth: String::new(),
164            index_files: vec!["index.html".into()],
165            log_remote_address: false,
166            log_x_real_ip: false,
167            log_forwarded_for: false,
168            trusted_proxies: Vec::new(),
169            redirect_trailing_slash: true,
170            ignore_hidden_files: false,
171            disable_symlinks: false,
172            health: false,
173            #[cfg(all(unix, feature = "experimental"))]
174            experimental_metrics: false,
175            maintenance_mode: false,
176            maintenance_mode_status: StatusCode::SERVICE_UNAVAILABLE,
177            maintenance_mode_file: PathBuf::new(),
178            advanced_opts: None,
179        }
180    }
181}
182
183/// It defines the main request handler used by the Hyper service request.
184pub struct RequestHandler {
185    /// Request handler options.
186    pub opts: Arc<RequestHandlerOpts>,
187}
188
189impl RequestHandler {
190    /// Main entry point for incoming requests.
191    pub fn handle<'a>(
192        &'a self,
193        req: &'a mut Request<Body>,
194        remote_addr: Option<SocketAddr>,
195    ) -> impl Future<Output = Result<Response<Body>, Error>> + Send + 'a {
196        let mut base_path = &self.opts.root_dir;
197        #[cfg(feature = "directory-listing")]
198        let dir_listing = self.opts.dir_listing;
199        #[cfg(feature = "directory-listing")]
200        let dir_listing_order = self.opts.dir_listing_order;
201        #[cfg(feature = "directory-listing")]
202        let dir_listing_format = &self.opts.dir_listing_format;
203        let redirect_trailing_slash = self.opts.redirect_trailing_slash;
204        let compression_static = self.opts.compression_static;
205        let ignore_hidden_files = self.opts.ignore_hidden_files;
206        let disable_symlinks = self.opts.disable_symlinks;
207        let index_files: Vec<&str> = self.opts.index_files.iter().map(|s| s.as_str()).collect();
208        #[cfg(feature = "experimental")]
209        let memory_cache = self.opts.memory_cache.as_ref();
210
211        log_addr::pre_process(&self.opts, req, remote_addr);
212
213        async move {
214            // Reject if the HTTP request method is not allowed
215            if !req.method().is_allowed() {
216                return error_page::error_response(
217                    req.uri(),
218                    req.method(),
219                    &StatusCode::METHOD_NOT_ALLOWED,
220                    &self.opts.page404,
221                    &self.opts.page50x,
222                );
223            }
224
225            // Health endpoint check
226            if let Some(result) = health::pre_process(&self.opts, req) {
227                return result;
228            }
229
230            // Metrics endpoint check
231            #[cfg(all(unix, feature = "experimental"))]
232            if let Some(result) = metrics::pre_process(&self.opts, req) {
233                return result;
234            }
235
236            // CORS
237            if let Some(result) = cors::pre_process(&self.opts, req) {
238                return result;
239            }
240
241            // `Basic` HTTP Authorization Schema
242            #[cfg(feature = "basic-auth")]
243            if let Some(response) = basic_auth::pre_process(&self.opts, req) {
244                return response;
245            }
246
247            // Maintenance Mode
248            if let Some(response) = maintenance_mode::pre_process(&self.opts, req) {
249                return response;
250            }
251
252            // Redirects
253            if let Some(result) = redirects::pre_process(&self.opts, req) {
254                return result;
255            }
256
257            // Rewrites
258            if let Some(result) = rewrites::pre_process(&self.opts, req) {
259                return result;
260            }
261
262            // Advanced options
263            if let Some(advanced) = &self.opts.advanced_opts {
264                // If the "Host" header matches any virtual_host, change the root directory
265                if let Some(root) =
266                    virtual_hosts::get_real_root(req, advanced.virtual_hosts.as_deref())
267                {
268                    base_path = root;
269                }
270            }
271
272            let index_files = index_files.as_ref();
273
274            // Static files
275            let (resp, file_path) = match static_files::handle(&HandleOpts {
276                method: req.method(),
277                headers: req.headers(),
278                #[cfg(feature = "experimental")]
279                memory_cache,
280                base_path,
281                uri_path: req.uri().path(),
282                uri_query: req.uri().query(),
283                #[cfg(feature = "directory-listing")]
284                dir_listing,
285                #[cfg(feature = "directory-listing")]
286                dir_listing_order,
287                #[cfg(feature = "directory-listing")]
288                dir_listing_format,
289                redirect_trailing_slash,
290                compression_static,
291                ignore_hidden_files,
292                index_files,
293                disable_symlinks,
294            })
295            .await
296            {
297                Ok(result) => (result.resp, Some(result.file_path)),
298                Err(status) => (
299                    error_page::error_response(
300                        req.uri(),
301                        req.method(),
302                        &status,
303                        &self.opts.page404,
304                        &self.opts.page50x,
305                    )?,
306                    None,
307                ),
308            };
309
310            // Check for a fallback response
311            #[cfg(feature = "fallback-page")]
312            let resp = fallback_page::post_process(&self.opts, req, resp)?;
313
314            // Append CORS headers if they are present
315            let resp = cors::post_process(&self.opts, req, resp)?;
316
317            // Add a `Vary` header if static compression is used
318            #[cfg(any(
319                feature = "compression",
320                feature = "compression-gzip",
321                feature = "compression-brotli",
322                feature = "compression-zstd",
323                feature = "compression-deflate"
324            ))]
325            let resp = compression_static::post_process(&self.opts, req, resp)?;
326
327            // Auto compression based on the `Accept-Encoding` header
328            #[cfg(any(
329                feature = "compression",
330                feature = "compression-gzip",
331                feature = "compression-brotli",
332                feature = "compression-zstd",
333                feature = "compression-deflate"
334            ))]
335            let resp = compression::post_process(&self.opts, req, resp)?;
336
337            // Append `Cache-Control` headers for web assets
338            let resp = control_headers::post_process(&self.opts, req, resp)?;
339
340            // Append security headers
341            let resp = security_headers::post_process(&self.opts, req, resp)?;
342
343            // Add/update custom headers
344            let resp = custom_headers::post_process(&self.opts, req, resp, file_path.as_ref())?;
345
346            Ok(resp)
347        }
348    }
349}