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