From 9e5049110c3835cad4fb5e404b88fe6ecffd35fb Mon Sep 17 00:00:00 2001 From: Jose Quintana <1700322+joseluisq@users.noreply.github.com> Date: Thu, 12 Oct 2023 22:26:52 +0200 Subject: [PATCH] feat: maintenance mode support (#272) maintenance mode support via new options: --maintenance-mode=false --maintenance-mode-status=503 --maintenance-mode-file="./my_maintenance.html" --- README.md | 1 + docs/content/configuration/command-line-arguments.md | 6 ++++++ docs/content/configuration/config-file.md | 5 +++++ docs/content/configuration/environment-variables.md | 6 ++++++ docs/content/features/maintenance-mode.md | 39 +++++++++++++++++++++++++++++++++++++++ docs/content/index.md | 1 + docs/mkdocs.yml | 1 + src/handler.rs | 19 ++++++++++++++++++- src/lib.rs | 1 + src/maintenance_mode.rs | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/server.rs | 17 +++++++++++++++++ src/settings/cli.rs | 41 ++++++++++++++++++++++++++++++++++++++++- src/settings/file.rs | 9 +++++++++ src/settings/mod.rs | 17 +++++++++++++++++ tests/toml/config.toml | 5 +++++ 15 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 docs/content/features/maintenance-mode.md create mode 100644 src/maintenance_mode.rs diff --git a/README.md b/README.md index b3144b4..8d3a9ad 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ Cross-platform and available for `Linux`, `macOS`, `Windows`, `FreeBSD`, `NetBSD - Custom URL rewrites and redirects via glob patterns with replacements. - Virtual hosting support. - Multiple index files. +- Maintenance Mode functionality. - Available as a library crate with opt-in features. - First-class [Docker](https://docs.docker.com/get-started/overview/) support. [Scratch](https://hub.docker.com/_/scratch), latest [Alpine Linux](https://hub.docker.com/_/alpine) and [Debian](https://hub.docker.com/_/alpine) Docker images. - Ability to accept a socket listener as a file descriptor for sandboxing and on-demand applications (e.g. [systemd](http://0pointer.de/blog/projects/socket-activation.html)). diff --git a/docs/content/configuration/command-line-arguments.md b/docs/content/configuration/command-line-arguments.md index 3d740c6..f854ad4 100644 --- a/docs/content/configuration/command-line-arguments.md +++ b/docs/content/configuration/command-line-arguments.md @@ -85,6 +85,12 @@ Options: Ignore hidden files/directories (dotfiles), preventing them to be served and being included in auto HTML index pages (directory listing) [env: SERVER_IGNORE_HIDDEN_FILES=] [default: false] [possible values: true, false] --health[=] Add a /health endpoint that doesn't generate any log entry and returns a 200 status code. This is especially useful with Kubernetes liveness and readiness probes [env: SERVER_HEALTH=] [default: false] [possible values: true, false] + --maintenance-mode[=] + Enable the server's maintenance mode functionality [env: SERVER_MAINTENANCE_MODE=] [default: false] [possible values: true, false] + --maintenance-mode-status + Provide a custom HTTP status code when entering into maintenance mode. Default 503 [env: SERVER_MAINTENANCE_MODE_STATUS=] [default: 503] + --maintenance-mode-file + Provide a custom maintenance mode HTML file. If not provided then a generic message will be displayed [env: SERVER_MAINTENANCE_MODE_FILE=] [default: ] -h, --help Print help (see more with '--help') -V, --version diff --git a/docs/content/configuration/config-file.md b/docs/content/configuration/config-file.md index b5c3a43..4b29f15 100644 --- a/docs/content/configuration/config-file.md +++ b/docs/content/configuration/config-file.md @@ -80,6 +80,11 @@ health = false #### List of index files # index-files = "index.html, index.htm" +#### Maintenance Mode + +maintenance-mode = false +# maintenance-mode-status = 503 +# maintenance-mode-file = "./maintenance.html" ### Windows Only diff --git a/docs/content/configuration/environment-variables.md b/docs/content/configuration/environment-variables.md index 4a804e1..a32308a 100644 --- a/docs/content/configuration/environment-variables.md +++ b/docs/content/configuration/environment-variables.md @@ -110,6 +110,12 @@ Activate the health endpoint. ### SERVER_INDEX_FILES List of files that will be used as an index for requests ending with the slash character (‘/’). Files are checked in the specified order. Default `index.html`. +### SERVER_MAINTENANCE_MODE +Enable the server's maintenance mode functionality. +### SERVER_MAINTENANCE_MODE_STATUS +Provide a custom HTTP status code when entering into maintenance mode. Default `503`. +### SERVER_MAINTENANCE_MODE_FILE +Provide a custom maintenance mode HTML file. If not provided then a generic message will be displayed. ## Windows The following options and commands are Windows platform-specific. diff --git a/docs/content/features/maintenance-mode.md b/docs/content/features/maintenance-mode.md new file mode 100644 index 0000000..696149a --- /dev/null +++ b/docs/content/features/maintenance-mode.md @@ -0,0 +1,39 @@ +# Maintenance Mode + +**`SWS`** provides a way to put a server into a maintenance mode. Allowing the server to respond with a custom HTTP status code and HTML content always by default. + +This is useful to allow the server to be taken offline without disrupting the service. + +The feature is disabled by default and can be controlled by the boolean `--maintenance-mode` option or the equivalent [SERVER_MAINTENANCE_MODE](./../configuration/environment-variables.md#server_maintenance_mode) env. + +## How it works + +When the feature is enabled, SWS will respond *always* with the specified (or default) status code and HTML content to every request ignoring all SWS features. Except the [Health check](./health-endpoint.md), [CORS](./cors.md) and [Basic Authentication](./basic-authentication.md) features. + +## HTTP Status Code + +The `--maintenance-mode-status` or the equivalent [SERVER_MAINTENANCE_MODE_STATUS](./../configuration/environment-variables.md#server_maintenance_mode_status) env variable can be used to tell SWS to reply with a specific status code. + +When not specified, the server will reply with the `503 Service Unavailable` status. + +## HTML Page + +The `--maintenance-mode-file` or the equivalent [SERVER_MAINTENANCE_MODE_FILE](./../configuration/environment-variables.md#server_maintenance_mode_file) env variable can be also used to customize the response content. + +The value should be an existing local HTML file path. When not provided a generic message will be displayed. + +!!! tip "Optional" + Remember that either `--maintenance-mode-status` and `--maintenance-mode-file` are optional and can be omitted as needed. + +## Example + +For instance, the server will respond with a `503 Service Unavailable` status code and a custom message. + +```sh +static-web-server -p 8787 -d ./public \ + --maintenance-mode \ + # optional status code, `503` by default + --maintenance-mode-status=503 \ + # optional HTML page, generic message by default + --maintenance-mode-file="./maintenance.html" +``` diff --git a/docs/content/index.md b/docs/content/index.md index 47e1d63..9d4a3a6 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -70,6 +70,7 @@ Cross-platform and available for `Linux`, `macOS`, `Windows`, `FreeBSD`, `NetBSD - Custom URL rewrites and redirects via glob patterns with replacements. - Virtual hosting support. - Multiple index files. +- Maintenance Mode functionality. - Available as a library crate with opt-in features. - First-class [Docker](https://docs.docker.com/get-started/overview/) support. [Scratch](https://hub.docker.com/_/scratch), latest [Alpine Linux](https://hub.docker.com/_/alpine) and [Debian](https://hub.docker.com/_/alpine) Docker images. - Ability to accept a socket listener as a file descriptor for sandboxing and on-demand applications (e.g. [systemd](http://0pointer.de/blog/projects/socket-activation.html)). diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index cbc05a1..e2cf614 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -164,6 +164,7 @@ nav: - 'Health endpoint': 'features/health-endpoint.md' - 'Virtual Hosting': 'features/virtual-hosting.md' - 'Multiple Index Files': 'features/multiple-index-files.md' + - 'Maintenance Mode': 'features/maintenance-mode.md' - 'WebAssembly': 'features/webassembly.md' - 'Platforms & Architectures': 'platforms-architectures.md' - 'Migrating from v1 to v2': 'migration.md' diff --git a/src/handler.rs b/src/handler.rs index fb548c5..6525711 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -22,7 +22,7 @@ use crate::fallback_page; use crate::{ control_headers, cors, custom_headers, error_page, exts::http::MethodExt, - redirects, rewrites, security_headers, + maintenance_mode, redirects, rewrites, security_headers, settings::{file::RedirectsKind, Advanced}, static_files::{self, HandleOpts}, virtual_hosts, Error, Result, @@ -80,6 +80,12 @@ pub struct RequestHandlerOpts { pub ignore_hidden_files: bool, /// Health endpoint feature. pub health: bool, + /// Maintenance mode feature. + pub maintenance_mode: bool, + /// Custom HTTP status for when entering into maintenance mode. + pub maintenance_mode_status: StatusCode, + /// Custom maintenance mode HTML file. + pub maintenance_mode_file: PathBuf, /// Advanced options from the config file. pub advanced_opts: Option, @@ -140,6 +146,7 @@ impl RequestHandler { } } + // Health endpoint logs if health_request { tracing::debug!( "incoming request: method={} uri={}{}", @@ -157,6 +164,7 @@ impl RequestHandler { } async move { + // Health endpoint check if health_request { let body = if method.is_get() { Body::from("OK") @@ -232,6 +240,15 @@ impl RequestHandler { } } + // Maintenance Mode + if self.opts.maintenance_mode { + return maintenance_mode::get_response( + method, + &self.opts.maintenance_mode_status, + &self.opts.maintenance_mode_file, + ); + } + // Advanced options if let Some(advanced) = &self.opts.advanced_opts { // Redirects diff --git a/src/lib.rs b/src/lib.rs index 5c20b20..56698cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -132,6 +132,7 @@ pub mod handler; pub mod https_redirect; #[macro_use] pub mod logger; +pub mod maintenance_mode; pub mod redirects; pub mod rewrites; pub mod security_headers; diff --git a/src/maintenance_mode.rs b/src/maintenance_mode.rs new file mode 100644 index 0000000..06ea099 --- /dev/null +++ b/src/maintenance_mode.rs @@ -0,0 +1,62 @@ +// 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 + +//! Provides maintenance mode functionality. +//! + +use headers::{AcceptRanges, ContentLength, ContentType, HeaderMapExt}; +use hyper::{Body, Method, Response, StatusCode}; +use mime_guess::mime; +use std::path::Path; + +use crate::{exts::http::MethodExt, helpers, Result}; + +const DEFAULT_BODY_CONTENT: &str = "The server is under maintenance mode"; + +/// Get the a server maintenance mode response. +pub fn get_response( + method: &Method, + status_code: &StatusCode, + file_path: &Path, +) -> Result> { + tracing::debug!("server has entered into maintenance mode"); + + let mut body_content = String::new(); + if file_path.exists() { + body_content = String::from_utf8_lossy(&helpers::read_bytes_default(file_path)) + .to_string() + .trim() + .to_owned(); + } + + if body_content.is_empty() { + body_content = [ + "", + status_code.as_str(), + " ", + status_code.canonical_reason().unwrap_or_default(), + "

", + DEFAULT_BODY_CONTENT, + "

", + ] + .concat(); + } + + let mut body = Body::empty(); + let len = body_content.len() as u64; + + if !method.is_head() { + body = Body::from(body_content) + } + + let mut resp = Response::new(body); + *resp.status_mut() = *status_code; + resp.headers_mut() + .typed_insert(ContentType::from(mime::TEXT_HTML_UTF_8)); + resp.headers_mut().typed_insert(ContentLength(len)); + resp.headers_mut().typed_insert(AcceptRanges::bytes()); + + Ok(resp) +} diff --git a/src/server.rs b/src/server.rs index 4cae50f..8d95876 100644 --- a/src/server.rs +++ b/src/server.rs @@ -267,6 +267,20 @@ impl Server { let health = general.health; server_info!("health endpoint: enabled={}", health); + // Maintenance mode option + let maintenance_mode = general.maintenance_mode; + let maintenance_mode_status = general.maintenance_mode_status; + let maintenance_mode_file = general.maintenance_mode_file; + server_info!("maintenance mode: enabled={}", maintenance_mode); + server_info!( + "maintenance mode status: {}", + maintenance_mode_status.as_str() + ); + server_info!( + "maintenance mode file: \"{}\"", + maintenance_mode_file.display() + ); + // Create a service router for Hyper let router_service = RouterService::new(RequestHandler { opts: Arc::from(RequestHandlerOpts { @@ -293,6 +307,9 @@ impl Server { ignore_hidden_files, index_files, health, + maintenance_mode, + maintenance_mode_status, + maintenance_mode_file, advanced_opts, }), }); diff --git a/src/settings/cli.rs b/src/settings/cli.rs index c1320d3..a7d8d8a 100644 --- a/src/settings/cli.rs +++ b/src/settings/cli.rs @@ -6,6 +6,7 @@ //! The server CLI options use clap::Parser; +use hyper::StatusCode; use std::path::PathBuf; #[cfg(feature = "directory-listing")] @@ -396,6 +397,38 @@ pub struct General { /// This is especially useful with Kubernetes liveness and readiness probes. pub health: bool, + #[arg( + long, + default_value = "false", + default_missing_value("true"), + num_args(0..=1), + require_equals(true), + action = clap::ArgAction::Set, + env = "SERVER_MAINTENANCE_MODE" + )] + /// Enable the server's maintenance mode functionality. + pub maintenance_mode: bool, + + #[arg( + long, + default_value = "503", + value_parser = value_parser_status_code, + requires_if("true", "maintenance_mode"), + env = "SERVER_MAINTENANCE_MODE_STATUS" + )] + /// Provide a custom HTTP status code when entering into maintenance mode. Default 503. + pub maintenance_mode_status: StatusCode, + + #[arg( + long, + default_value = "", + value_parser = value_parser_pathbuf, + requires_if("true", "maintenance_mode"), + env = "SERVER_MAINTENANCE_MODE_FILE" + )] + /// Provide a custom maintenance mode HTML file. If not provided then a generic message will be displayed. + pub maintenance_mode_file: PathBuf, + // // Windows specific arguments and commands // @@ -433,7 +466,13 @@ pub enum Commands { Uninstall {}, } -#[cfg(feature = "fallback-page")] fn value_parser_pathbuf(s: &str) -> crate::Result { Ok(PathBuf::from(s)) } + +fn value_parser_status_code(s: &str) -> Result { + match s.parse::() { + Ok(code) => StatusCode::from_u16(code).map_err(|err| err.to_string()), + Err(err) => Err(err.to_string()), + } +} diff --git a/src/settings/file.rs b/src/settings/file.rs index 69bc865..2e047c5 100644 --- a/src/settings/file.rs +++ b/src/settings/file.rs @@ -237,6 +237,15 @@ pub struct General { /// Health endpoint feature. pub health: Option, + /// Maintenance mode feature. + pub maintenance_mode: Option, + + /// Custom HTTP status for when entering into maintenance mode. + pub maintenance_mode_status: Option, + + /// Custom maintenance mode HTML file. + pub maintenance_mode_file: Option, + #[cfg(windows)] /// windows service feature. pub windows_service: Option, diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 4de7f7d..2e43f08 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -148,6 +148,10 @@ impl Settings { let mut index_files = opts.index_files; let mut health = opts.health; + let mut maintenance_mode = opts.maintenance_mode; + let mut maintenance_mode_status = opts.maintenance_mode_status; + let mut maintenance_mode_file = opts.maintenance_mode_file; + // Windows-only options #[cfg(windows)] let mut windows_service = opts.windows_service; @@ -288,6 +292,16 @@ impl Settings { if let Some(v) = general.index_files { index_files = v } + if let Some(v) = general.maintenance_mode { + maintenance_mode = v + } + if let Some(v) = general.maintenance_mode_status { + maintenance_mode_status = + StatusCode::from_u16(v).with_context(|| "invalid HTTP status code")? + } + if let Some(v) = general.maintenance_mode_file { + maintenance_mode_file = v + } // Windows-only options #[cfg(windows)] @@ -514,6 +528,9 @@ impl Settings { ignore_hidden_files, index_files, health, + maintenance_mode, + maintenance_mode_status, + maintenance_mode_file, // Windows-only options and commands #[cfg(windows)] diff --git a/tests/toml/config.toml b/tests/toml/config.toml index 3bbf5a6..a1dc5c9 100644 --- a/tests/toml/config.toml +++ b/tests/toml/config.toml @@ -68,6 +68,11 @@ compression-static = false #### List of index files index-files = "index.html, index.htm" +#### Maintenance Mode +maintenance-mode = false +# maintenance-mode-status = 503 +# maintenance-mode-file = "maintenance.html" + ### Windows Only #### Run the web server as a Windows Service -- libgit2 1.7.2