Skip to main content

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