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 headers::{AcceptRanges, ContentLength, ContentType, HeaderMapExt};
10use hyper::{Body, Method, Request, Response, StatusCode};
11use mime_guess::mime;
12use std::path::{Path, PathBuf};
13
14use crate::{Error, Result, handler::RequestHandlerOpts, helpers, http_ext::MethodExt};
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!(
76            "<html><head><title>{status_code}</title></head><body><center><h1>{DEFAULT_BODY_CONTENT}</h1></center></body></html>"
77        )
78    };
79
80    let mut body = Body::empty();
81    let len = body_content.len() as u64;
82
83    if !method.is_head() {
84        body = Body::from(body_content)
85    }
86
87    let mut resp = Response::new(body);
88    *resp.status_mut() = *status_code;
89    resp.headers_mut()
90        .typed_insert(ContentType::from(mime::TEXT_HTML_UTF_8));
91    resp.headers_mut().typed_insert(ContentLength(len));
92    resp.headers_mut().typed_insert(AcceptRanges::bytes());
93
94    Ok(resp)
95}
96
97#[cfg(test)]
98mod tests {
99    use super::pre_process;
100    use crate::{Error, handler::RequestHandlerOpts};
101    use hyper::{Body, Request, Response, StatusCode};
102
103    fn make_request() -> Request<Body> {
104        Request::builder()
105            .method("GET")
106            .uri("/")
107            .body(Body::empty())
108            .unwrap()
109    }
110
111    fn get_status(result: Option<Result<Response<Body>, Error>>) -> Option<StatusCode> {
112        if let Some(Ok(response)) = result {
113            Some(response.status())
114        } else {
115            None
116        }
117    }
118
119    #[test]
120    fn test_maintenance_disabled() {
121        assert!(
122            pre_process(
123                &RequestHandlerOpts {
124                    maintenance_mode: false,
125                    ..Default::default()
126                },
127                &make_request()
128            )
129            .is_none()
130        );
131    }
132
133    #[test]
134    fn test_maintenance_default() {
135        assert_eq!(
136            get_status(pre_process(
137                &RequestHandlerOpts {
138                    maintenance_mode: true,
139                    ..Default::default()
140                },
141                &make_request()
142            )),
143            Some(StatusCode::SERVICE_UNAVAILABLE)
144        );
145    }
146
147    #[test]
148    fn test_maintenance_custom_status() {
149        assert_eq!(
150            get_status(pre_process(
151                &RequestHandlerOpts {
152                    maintenance_mode: true,
153                    maintenance_mode_status: StatusCode::IM_A_TEAPOT,
154                    ..Default::default()
155                },
156                &make_request()
157            )),
158            Some(StatusCode::IM_A_TEAPOT)
159        );
160    }
161}