Repository: static-web-server/static-web-server (language: rust)

Issue:
Ability to rewrite HTTP status
**Is your feature request related to a problem? Please describe.**
Would like to be able to rewrite HTTP status so present a maintenance page giving a 503.

**Describe the solution you'd like**
Would like the ability to force the index.html to throw a 503 (or maybe any other error) just for hosting a maintenance page. Via Env vars would be preferable due to the constraints I'm under since I'm using a TrueNAS Scale helms chart (don't have as much access to the configuration)

**Describe alternatives you've considered**
Just about everything, but the way my setup is right now I have no ability to use a different web server since I need traefik's ingress and am limited to the selection of helms charts that work on truenas, and caddy (downstream reverse proxy) can't load balance while rewriting HTTP status codes.



Discussion / hints:
Because maintenance is a frequent task, I think that a `--maintenance-mode` feature flag with an additional custom status flag could work.

Here is just an idea.

```env
# required envs
SERVER_MAINTENANCE_MODE=true
SERVER_MAINTENANCE_MODE_STATUS=503
# optional env
SERVER_MAINTENANCE_MODE_FILE="./my_maintenance.html"
```

Let me know if this is what you are interested in.
That's a great idea to do a maintenance flag and customizing the status code. And I'm assuming if the optional  `SERVER_MAINTENANCE_MODE_FILE` env var isn't there it'll just display the index.html but with a 503?
> I'm assuming if the optional `SERVER_MAINTENANCE_MODE_FILE` env var isn't there it'll just display the index.html but with a 503?

No, it won't.
I thought that when using the defaults (`SERVER_MAINTENANCE_MODE=true`) we could display a generic message like `The server is under maintenance mode.` with a `503` status.
So we could even make the status optional too.

For example, I propose the following.

```env
# required env, false by default
SERVER_MAINTENANCE_MODE=true

# optional env, 503 by default
SERVER_MAINTENANCE_MODE_STATUS=503

# optional env, empty by default, server uses a generic message instead
SERVER_MAINTENANCE_MODE_FILE="./my_maintenance.html"
```

> No, it won't. I thought that when using the defaults (`SERVER_MAINTENANCE_MODE=true`) we could display a generic message like `The server is under maintenance mode.` with a `503` status. So we could even make the status optional too.
>

That makes more sense. Looks like a perfect simple solution!


Fix patch:
diff --git a/README.md b/README.md
index b3144b49..8d3a9ad9 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 3d740c66..f854ad4b 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[=<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
diff --git a/docs/content/configuration/config-file.md b/docs/content/configuration/config-file.md
index b5c3a430..4b29f152 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 4a804e18..a32308a4 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 00000000..696149a6
--- /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 47e1d63e..9d4a3a6b 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 cbc05a15..e2cf6145 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 fb548c5e..65257111 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<Advanced>,
@@ -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 5c20b201..56698cd1 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 00000000..06ea0990
--- /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 <joseluisq.net>
+
+//! 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<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)
+}
diff --git a/src/server.rs b/src/server.rs
index 4cae50f7..8d958760 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 c1320d35..a7d8d8af 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<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()),
+    }
+}
diff --git a/src/settings/file.rs b/src/settings/file.rs
index 69bc865e..2e047c57 100644
--- a/src/settings/file.rs
+++ b/src/settings/file.rs
@@ -237,6 +237,15 @@ pub struct General {
     /// Health endpoint feature.
     pub health: Option<bool>,
 
+    /// Maintenance mode feature.
+    pub maintenance_mode: Option<bool>,
+
+    /// Custom HTTP status for when entering into maintenance mode.
+    pub maintenance_mode_status: Option<u16>,
+
+    /// Custom maintenance mode HTML file.
+    pub maintenance_mode_file: Option<PathBuf>,
+
     #[cfg(windows)]
     /// windows service feature.
     pub windows_service: Option<bool>,
diff --git a/src/settings/mod.rs b/src/settings/mod.rs
index 4de7f7d6..2e43f08b 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)]


Test patch:
diff --git a/tests/toml/config.toml b/tests/toml/config.toml
index 3bbf5a66..a1dc5c9c 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