static-web-server 3.0.0-beta.1

A cross-platform, high-performance and asynchronous web server for static files-serving.
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0
// This file is part of Static Web Server.
// See https://static-web-server.net/ for more information
// Copyright (C) 2019-present Jose Quintana <joseluisq.net>

//! Provides maintenance mode functionality.
//!

use hyper::{Method, Request, Response, StatusCode};
use std::path::{Path, PathBuf};

use crate::body::Body;
use crate::error_page::build_html_response;
use crate::{Error, Result, handler::RequestHandlerOpts, helpers};

const DEFAULT_BODY_CONTENT: &str = "The server is in maintenance mode.";

/// Initializes maintenance mode handling
pub(crate) fn init(
    maintenance_mode: bool,
    maintenance_mode_status: StatusCode,
    maintenance_mode_file: PathBuf,
    handler_opts: &mut RequestHandlerOpts,
) {
    handler_opts.maintenance_mode = maintenance_mode;
    handler_opts.maintenance_mode_status = maintenance_mode_status;
    handler_opts.maintenance_mode_file = maintenance_mode_file;
    tracing::info!(
        "maintenance mode: enabled={}",
        handler_opts.maintenance_mode
    );
    tracing::info!(
        "maintenance mode status: {}",
        handler_opts.maintenance_mode_status.as_str()
    );
    tracing::info!(
        "maintenance mode file: \"{}\"",
        handler_opts.maintenance_mode_file.display()
    );
    // SECURITY/PERF: Pre-cache the maintenance body so we never touch disk
    // from inside the async request hot path. See `error_page::PAGE_CACHE`.
    crate::error_page::cache_page(&handler_opts.maintenance_mode_file);
}

/// Produces maintenance mode response if necessary
pub(crate) fn pre_process<T>(
    opts: &RequestHandlerOpts,
    req: &Request<T>,
) -> Option<Result<Response<Body>, Error>> {
    if opts.maintenance_mode {
        Some(get_response(
            req.method(),
            &opts.maintenance_mode_status,
            &opts.maintenance_mode_file,
        ))
    } else {
        None
    }
}

/// Get the a server maintenance mode response.
pub fn get_response(
    method: &Method,
    status_code: &StatusCode,
    file_path: &Path,
) -> Result<Response<Body>> {
    tracing::debug!("server has entered into maintenance mode");
    tracing::debug!("maintenance mode file path to use: {}", file_path.display());

    let body_content = if let Some(cached) = crate::error_page::cached_page(file_path) {
        cached.as_str().to_owned()
    } else if file_path.is_file() {
        // Cache miss (e.g. called directly without going through `init`).
        crate::error_page::cache_page(file_path);
        helpers::read_text_default(file_path)
    } else {
        tracing::debug!(
            "maintenance mode file path not found or not a regular file, using a default message"
        );
        format!(
            "<html><head><title>{status_code}</title></head><body><center><h1>{DEFAULT_BODY_CONTENT}</h1></center></body></html>"
        )
    };

    Ok(build_html_response(
        body_content,
        *status_code,
        Some(method),
    ))
}

#[cfg(test)]
mod tests {
    use super::pre_process;
    use crate::body::Body;
    use crate::{Error, handler::RequestHandlerOpts};
    use hyper::{Request, Response, StatusCode};

    fn make_request() -> Request<Body> {
        Request::builder()
            .method("GET")
            .uri("/")
            .body(crate::body::empty())
            .unwrap()
    }

    fn get_status(result: Option<Result<Response<Body>, Error>>) -> Option<StatusCode> {
        if let Some(Ok(response)) = result {
            Some(response.status())
        } else {
            None
        }
    }

    #[test]
    fn test_maintenance_disabled() {
        assert!(
            pre_process(
                &RequestHandlerOpts {
                    maintenance_mode: false,
                    ..Default::default()
                },
                &make_request()
            )
            .is_none()
        );
    }

    #[test]
    fn test_maintenance_default() {
        assert_eq!(
            get_status(pre_process(
                &RequestHandlerOpts {
                    maintenance_mode: true,
                    ..Default::default()
                },
                &make_request()
            )),
            Some(StatusCode::SERVICE_UNAVAILABLE)
        );
    }

    #[test]
    fn test_maintenance_custom_status() {
        assert_eq!(
            get_status(pre_process(
                &RequestHandlerOpts {
                    maintenance_mode: true,
                    maintenance_mode_status: StatusCode::IM_A_TEAPOT,
                    ..Default::default()
                },
                &make_request()
            )),
            Some(StatusCode::IM_A_TEAPOT)
        );
    }
}