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 headers::{AcceptRanges, ContentLength, ContentType, HeaderMapExt};
10use hyper::{Body, Method, Request, Response, StatusCode};
11use mime_guess::mime;
12use std::path::{Path, PathBuf};
13
14use crate::{handler::RequestHandlerOpts, helpers, http_ext::MethodExt, Error, Result};
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}
41
42/// Produces maintenance mode response if necessary
43pub(crate) fn pre_process<T>(
44    opts: &RequestHandlerOpts,
45    req: &Request<T>,
46) -> Option<Result<Response<Body>, Error>> {
47    if opts.maintenance_mode {
48        Some(get_response(
49            req.method(),
50            &opts.maintenance_mode_status,
51            &opts.maintenance_mode_file,
52        ))
53    } else {
54        None
55    }
56}
57
58/// Get the a server maintenance mode response.
59pub fn get_response(
60    method: &Method,
61    status_code: &StatusCode,
62    file_path: &Path,
63) -> Result<Response<Body>> {
64    tracing::debug!("server has entered into maintenance mode");
65    tracing::debug!("maintenance mode file path to use: {}", file_path.display());
66
67    let body_content = if file_path.is_file() {
68        String::from_utf8_lossy(&helpers::read_bytes_default(file_path))
69            .trim()
70            .to_owned()
71    } else {
72        tracing::debug!(
73            "maintenance mode file path not found or not a regular file, using a default message"
74        );
75        format!("<html><head><title>{status_code}</title></head><body><center><h1>{DEFAULT_BODY_CONTENT}</h1></center></body></html>")
76    };
77
78    let mut body = Body::empty();
79    let len = body_content.len() as u64;
80
81    if !method.is_head() {
82        body = Body::from(body_content)
83    }
84
85    let mut resp = Response::new(body);
86    *resp.status_mut() = *status_code;
87    resp.headers_mut()
88        .typed_insert(ContentType::from(mime::TEXT_HTML_UTF_8));
89    resp.headers_mut().typed_insert(ContentLength(len));
90    resp.headers_mut().typed_insert(AcceptRanges::bytes());
91
92    Ok(resp)
93}
94
95#[cfg(test)]
96mod tests {
97    use super::pre_process;
98    use crate::{handler::RequestHandlerOpts, Error};
99    use hyper::{Body, Request, Response, StatusCode};
100
101    fn make_request() -> Request<Body> {
102        Request::builder()
103            .method("GET")
104            .uri("/")
105            .body(Body::empty())
106            .unwrap()
107    }
108
109    fn get_status(result: Option<Result<Response<Body>, Error>>) -> Option<StatusCode> {
110        if let Some(Ok(response)) = result {
111            Some(response.status())
112        } else {
113            None
114        }
115    }
116
117    #[test]
118    fn test_maintenance_disabled() {
119        assert!(pre_process(
120            &RequestHandlerOpts {
121                maintenance_mode: false,
122                ..Default::default()
123            },
124            &make_request()
125        )
126        .is_none());
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}