Skip to main content

static_web_server/
maintenance_mode.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//! Provides maintenance mode functionality.
7//!
8
9use hyper::{Method, Request, Response, StatusCode};
10use std::path::{Path, PathBuf};
11
12use crate::body::Body;
13use crate::error_page::build_html_response;
14use crate::{Error, Result, handler::RequestHandlerOpts, helpers};
15
16const DEFAULT_BODY_CONTENT: &str = "The server is in maintenance mode.";
17
18/// Initializes maintenance mode handling
19pub(crate) fn init(
20    maintenance_mode: bool,
21    maintenance_mode_status: StatusCode,
22    maintenance_mode_file: PathBuf,
23    handler_opts: &mut RequestHandlerOpts,
24) {
25    handler_opts.maintenance_mode = maintenance_mode;
26    handler_opts.maintenance_mode_status = maintenance_mode_status;
27    handler_opts.maintenance_mode_file = maintenance_mode_file;
28    tracing::info!(
29        "maintenance mode: enabled={}",
30        handler_opts.maintenance_mode
31    );
32    tracing::info!(
33        "maintenance mode status: {}",
34        handler_opts.maintenance_mode_status.as_str()
35    );
36    tracing::info!(
37        "maintenance mode file: \"{}\"",
38        handler_opts.maintenance_mode_file.display()
39    );
40    // SECURITY/PERF: Pre-cache the maintenance body so we never touch disk
41    // from inside the async request hot path. See `error_page::PAGE_CACHE`.
42    crate::error_page::cache_page(&handler_opts.maintenance_mode_file);
43}
44
45/// Produces maintenance mode response if necessary
46pub(crate) fn pre_process<T>(
47    opts: &RequestHandlerOpts,
48    req: &Request<T>,
49) -> Option<Result<Response<Body>, Error>> {
50    if opts.maintenance_mode {
51        Some(get_response(
52            req.method(),
53            &opts.maintenance_mode_status,
54            &opts.maintenance_mode_file,
55        ))
56    } else {
57        None
58    }
59}
60
61/// Get the a server maintenance mode response.
62pub fn get_response(
63    method: &Method,
64    status_code: &StatusCode,
65    file_path: &Path,
66) -> Result<Response<Body>> {
67    tracing::debug!("server has entered into maintenance mode");
68    tracing::debug!("maintenance mode file path to use: {}", file_path.display());
69
70    let body_content = if let Some(cached) = crate::error_page::cached_page(file_path) {
71        cached.as_str().to_owned()
72    } else if file_path.is_file() {
73        // Cache miss (e.g. called directly without going through `init`).
74        crate::error_page::cache_page(file_path);
75        helpers::read_text_default(file_path)
76    } else {
77        tracing::debug!(
78            "maintenance mode file path not found or not a regular file, using a default message"
79        );
80        format!(
81            "<html><head><title>{status_code}</title></head><body><center><h1>{DEFAULT_BODY_CONTENT}</h1></center></body></html>"
82        )
83    };
84
85    Ok(build_html_response(
86        body_content,
87        *status_code,
88        Some(method),
89    ))
90}
91
92#[cfg(test)]
93mod tests {
94    use super::pre_process;
95    use crate::body::Body;
96    use crate::{Error, handler::RequestHandlerOpts};
97    use hyper::{Request, Response, StatusCode};
98
99    fn make_request() -> Request<Body> {
100        Request::builder()
101            .method("GET")
102            .uri("/")
103            .body(crate::body::empty())
104            .unwrap()
105    }
106
107    fn get_status(result: Option<Result<Response<Body>, Error>>) -> Option<StatusCode> {
108        if let Some(Ok(response)) = result {
109            Some(response.status())
110        } else {
111            None
112        }
113    }
114
115    #[test]
116    fn test_maintenance_disabled() {
117        assert!(
118            pre_process(
119                &RequestHandlerOpts {
120                    maintenance_mode: false,
121                    ..Default::default()
122                },
123                &make_request()
124            )
125            .is_none()
126        );
127    }
128
129    #[test]
130    fn test_maintenance_default() {
131        assert_eq!(
132            get_status(pre_process(
133                &RequestHandlerOpts {
134                    maintenance_mode: true,
135                    ..Default::default()
136                },
137                &make_request()
138            )),
139            Some(StatusCode::SERVICE_UNAVAILABLE)
140        );
141    }
142
143    #[test]
144    fn test_maintenance_custom_status() {
145        assert_eq!(
146            get_status(pre_process(
147                &RequestHandlerOpts {
148                    maintenance_mode: true,
149                    maintenance_mode_status: StatusCode::IM_A_TEAPOT,
150                    ..Default::default()
151                },
152                &make_request()
153            )),
154            Some(StatusCode::IM_A_TEAPOT)
155        );
156    }
157}