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