feat: maintenance mode support (#272)
maintenance mode support via new options:
--maintenance-mode=false
--maintenance-mode-status=503
--maintenance-mode-file="./my_maintenance.html"
Diff
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(-)
@@ -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)).
@@ -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[=<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[=<MAINTENANCE_MODE>]
Enable the server's maintenance mode functionality [env: SERVER_MAINTENANCE_MODE=] [default: false] [possible values: true, false]
--maintenance-mode-status <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 <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
@@ -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
@@ -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.
@@ -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"
```
@@ -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)).
@@ -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'
@@ -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,
pub health: bool,
pub maintenance_mode: bool,
pub maintenance_mode_status: StatusCode,
pub maintenance_mode_file: PathBuf,
pub advanced_opts: Option<Advanced>,
@@ -140,6 +146,7 @@ impl RequestHandler {
}
}
if health_request {
tracing::debug!(
"incoming request: method={} uri={}{}",
@@ -157,6 +164,7 @@ impl RequestHandler {
}
async move {
if health_request {
let body = if method.is_get() {
Body::from("OK")
@@ -232,6 +240,15 @@ impl RequestHandler {
}
}
if self.opts.maintenance_mode {
return maintenance_mode::get_response(
method,
&self.opts.maintenance_mode_status,
&self.opts.maintenance_mode_file,
);
}
if let Some(advanced) = &self.opts.advanced_opts {
@@ -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;
@@ -0,0 +1,62 @@
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";
pub fn get_response(
method: &Method,
status_code: &StatusCode,
file_path: &Path,
) -> Result<Response<Body>> {
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 = [
"<html><head><title>",
status_code.as_str(),
" ",
status_code.canonical_reason().unwrap_or_default(),
"</title></head><body><center><h1>",
DEFAULT_BODY_CONTENT,
"</h1></center></body></html>",
]
.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)
}
@@ -267,6 +267,20 @@ impl Server {
let health = general.health;
server_info!("health endpoint: enabled={}", health);
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()
);
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,
}),
});
@@ -6,6 +6,7 @@
use clap::Parser;
use hyper::StatusCode;
use std::path::PathBuf;
#[cfg(feature = "directory-listing")]
@@ -396,6 +397,38 @@ pub struct General {
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"
)]
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"
)]
pub maintenance_mode_status: StatusCode,
#[arg(
long,
default_value = "",
value_parser = value_parser_pathbuf,
requires_if("true", "maintenance_mode"),
env = "SERVER_MAINTENANCE_MODE_FILE"
)]
pub maintenance_mode_file: PathBuf,
@@ -433,7 +466,13 @@ pub enum Commands {
Uninstall {},
}
#[cfg(feature = "fallback-page")]
fn value_parser_pathbuf(s: &str) -> crate::Result<PathBuf, String> {
Ok(PathBuf::from(s))
}
fn value_parser_status_code(s: &str) -> Result<StatusCode, String> {
match s.parse::<u16>() {
Ok(code) => StatusCode::from_u16(code).map_err(|err| err.to_string()),
Err(err) => Err(err.to_string()),
}
}
@@ -237,6 +237,15 @@ pub struct General {
pub health: Option<bool>,
pub maintenance_mode: Option<bool>,
pub maintenance_mode_status: Option<u16>,
pub maintenance_mode_file: Option<PathBuf>,
#[cfg(windows)]
pub windows_service: Option<bool>,
@@ -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;
#[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
}
#[cfg(windows)]
@@ -514,6 +528,9 @@ impl Settings {
ignore_hidden_files,
index_files,
health,
maintenance_mode,
maintenance_mode_status,
maintenance_mode_file,
#[cfg(windows)]
@@ -68,6 +68,11 @@ compression-static = false
index-files = "index.html, index.htm"
maintenance-mode = false