index : static-web-server.git

ascending towards madness

author Jose Quintana <1700322+joseluisq@users.noreply.github.com> 2023-10-12 20:26:52.0 +00:00:00
committer GitHub <noreply@github.com> 2023-10-12 20:26:52.0 +00:00:00
commit
9e5049110c3835cad4fb5e404b88fe6ecffd35fb [patch]
tree
88774ffa432a6b7f7153c4b1eb289f695f0de7d6
parent
7c5df010c9f32a299edba98153d4e6e9a5ba8c4b
download
9e5049110c3835cad4fb5e404b88fe6ecffd35fb.tar.gz

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(-)

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[=<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 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<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 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 <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 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<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 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<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 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