index : static-web-server.git

ascending towards madness

author Jose Quintana <1700322+joseluisq@users.noreply.github.com> 2024-01-15 23:43:14.0 +00:00:00
committer GitHub <noreply@github.com> 2024-01-15 23:43:14.0 +00:00:00
commit
8c6ab533fd23fb766c13ceddd2b0640b776ad4db [patch]
tree
d9e4e497377729e5287ae4cb43d76033a449c9c3
parent
67a2403253b1bdf29653c8e73d206ab73ddeca78
download
8c6ab533fd23fb766c13ceddd2b0640b776ad4db.tar.gz

feat: optional `Host` URI support for the URL Redirects feature (#301)

* feat: optional `host` uri support for URL redirects

which allows redirecting based on a host's incoming uri making it
possible to perform for example www to non-www redirects.

config example:

```toml
[advanced]

[[advanced.redirects]]
host = "127.0.0.1:4433"
source = "/{*}"
destination = "https://localhost:4433/$1"
kind = 301
```

* chore: add test cases

Diff

 src/handler.rs                     | 10 +++++-
 src/lib.rs                         |  2 +-
 src/redirects.rs                   | 11 ++++++-
 src/settings/file.rs               |  2 +-
 src/settings/mod.rs                |  3 ++-
 src/testing.rs                     | 65 ++++++++++++++++++++++++++++++++++++-
 tests/fixtures/toml/redirects.toml | 17 +++++++++-
 tests/redirects.rs                 | 71 +++++++++++++++++++++++++++++++++++++++-
 tests/toml/config.toml             |  5 +++-
 9 files changed, 186 insertions(+)

diff --git a/src/handler.rs b/src/handler.rs
index 1e5606c..a2f5177 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -163,6 +163,11 @@ impl RequestHandler {
            );
        }

        let host = headers
            .get(http::header::HOST)
            .and_then(|v| v.to_str().ok())
            .unwrap_or("");

        async move {
            // Health endpoint check
            if health_request {
@@ -252,7 +257,12 @@ impl RequestHandler {
            // Advanced options
            if let Some(advanced) = &self.opts.advanced_opts {
                // Redirects
                let mut uri_host = uri.host().unwrap_or(host).to_owned();
                if let Some(uri_port) = uri.port_u16() {
                    uri_host.push_str(&format!(":{}", uri_port));
                }
                if let Some(redirects) = redirects::get_redirection(
                    &uri_host,
                    uri_path.clone().as_str(),
                    advanced.redirects.as_deref(),
                ) {
diff --git a/src/lib.rs b/src/lib.rs
index 56698cd..ebc37aa 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -157,6 +157,8 @@ pub mod error;
// Private modules
#[doc(hidden)]
mod helpers;
#[doc(hidden)]
pub mod testing;

// Re-exports
pub use error::*;
diff --git a/src/redirects.rs b/src/redirects.rs
index 38f5435..ea5c59a 100644
--- a/src/redirects.rs
+++ b/src/redirects.rs
@@ -11,11 +11,22 @@ use crate::settings::Redirects;
/// It returns a redirect's destination path and status code if the current request uri
/// matches against the provided redirect's array.
pub fn get_redirection<'a>(
    uri_host: &'a str,
    uri_path: &'a str,
    redirects_opts: Option<&'a [Redirects]>,
) -> Option<&'a Redirects> {
    if let Some(redirects_vec) = redirects_opts {
        for redirect_entry in redirects_vec.iter() {
            // Match `host` redirect against `uri_host` if specified
            if let Some(host) = &redirect_entry.host {
                tracing::debug!(
                    "checking host '{host}' redirect entry against uri host '{uri_host}'"
                );
                if !host.eq(uri_host) {
                    continue;
                }
            }

            // Match source glob pattern against the request uri path
            if redirect_entry.source.is_match(uri_path) {
                return Some(redirect_entry);
diff --git a/src/settings/file.rs b/src/settings/file.rs
index 2e047c5..6dd57db 100644
--- a/src/settings/file.rs
+++ b/src/settings/file.rs
@@ -70,6 +70,8 @@ pub enum RedirectsKind {
#[serde(rename_all = "kebab-case")]
/// Represents redirects types.
pub struct Redirects {
    /// Optional host to match against an incoming URI host if specified
    pub host: Option<String>,
    /// Source of the redirect.
    pub source: String,
    /// Redirect destination.
diff --git a/src/settings/mod.rs b/src/settings/mod.rs
index e7f00f5..f1379cb 100644
--- a/src/settings/mod.rs
+++ b/src/settings/mod.rs
@@ -45,6 +45,8 @@ pub struct Rewrites {

/// The `Redirects` file options.
pub struct Redirects {
    /// Optional host to match against an incoming URI host if specified
    pub host: Option<String>,
    /// Source pattern Regex matcher
    pub source: Regex,
    /// A local file that must exist
@@ -439,6 +441,7 @@ impl Settings {

                            let status_code = redirects_entry.kind.to_owned() as u16;
                            redirects_vec.push(Redirects {
                                host: redirects_entry.host.to_owned(),
                                source,
                                destination: redirects_entry.destination.to_owned(),
                                kind: StatusCode::from_u16(status_code).with_context(|| {
diff --git a/src/testing.rs b/src/testing.rs
new file mode 100644
index 0000000..9621210
--- /dev/null
+++ b/src/testing.rs
@@ -0,0 +1,65 @@
// 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>

//! Development utilities for testing of SWS.
//!

/// SWS fixtures module.
#[doc(hidden)]
pub mod fixtures {
    use std::{path::PathBuf, sync::Arc};

    use crate::{
        handler::{RequestHandler, RequestHandlerOpts},
        Settings,
    };

    /// Testing Remote address
    pub const REMOTE_ADDR: &str = "127.0.0.1:1234";

    /// Create a `RequestHandler` from a custom TOML config file (fixture).
    pub fn fixture_req_handler(fixture_toml: &str) -> RequestHandler {
        // Replace default config file and load the fixture TOML settings
        let f = PathBuf::from("tests/fixtures").join(fixture_toml);
        std::env::set_var("SERVER_CONFIG_FILE", f);
        let opts = Settings::get(false).unwrap();

        let req_handler_opts = RequestHandlerOpts {
            root_dir: opts.general.root,
            compression: opts.general.compression,
            compression_static: opts.general.compression_static,
            #[cfg(feature = "directory-listing")]
            dir_listing: opts.general.directory_listing,
            #[cfg(feature = "directory-listing")]
            dir_listing_order: opts.general.directory_listing_order,
            #[cfg(feature = "directory-listing")]
            dir_listing_format: opts.general.directory_listing_format,
            // TODO: add support or `cors` when required
            cors: None,
            security_headers: opts.general.security_headers,
            cache_control_headers: opts.general.cache_control_headers,
            page404: opts.general.page404,
            page50x: opts.general.page50x,
            // TODO: add support or `page_fallback` when required
            #[cfg(feature = "fallback-page")]
            page_fallback: vec![],
            #[cfg(feature = "basic-auth")]
            basic_auth: opts.general.basic_auth,
            log_remote_address: opts.general.log_remote_address,
            redirect_trailing_slash: opts.general.redirect_trailing_slash,
            ignore_hidden_files: opts.general.ignore_hidden_files,
            index_files: vec![opts.general.index_files],
            health: opts.general.health,
            maintenance_mode: opts.general.maintenance_mode,
            maintenance_mode_status: opts.general.maintenance_mode_status,
            maintenance_mode_file: opts.general.maintenance_mode_file,
            advanced_opts: opts.advanced,
        };

        RequestHandler {
            opts: Arc::from(req_handler_opts),
        }
    }
}
diff --git a/tests/fixtures/toml/redirects.toml b/tests/fixtures/toml/redirects.toml
new file mode 100644
index 0000000..d6a6563
--- /dev/null
+++ b/tests/fixtures/toml/redirects.toml
@@ -0,0 +1,17 @@
[general]

root = "docker/public"

[advanced]

### URL Redirects
[[advanced.redirects]]
host = "127.0.0.1:1234"
source = "/{*}"
destination = "http://localhost:1234/$1"
kind = 301

[[advanced.redirects]]
source = "**/{*}.{*}"
destination = "http://localhost:1234/files/$1.$2"
kind = 302
diff --git a/tests/redirects.rs b/tests/redirects.rs
new file mode 100644
index 0000000..4329e2e
--- /dev/null
+++ b/tests/redirects.rs
@@ -0,0 +1,71 @@
#![forbid(unsafe_code)]
#![deny(warnings)]
#![deny(rust_2018_idioms)]
#![deny(dead_code)]

pub mod tests {
    use hyper::Request;
    use std::net::SocketAddr;

    use static_web_server::testing::fixtures::{fixture_req_handler, REMOTE_ADDR};

    #[tokio::test]
    async fn redirects_default() {
        let req_handler = fixture_req_handler("toml/redirects.toml");
        let remote_addr = Some(REMOTE_ADDR.parse::<SocketAddr>().unwrap());

        let mut req = Request::default();
        *req.uri_mut() = "http://localhost:1234/assets/favicon.ico".parse().unwrap();

        match req_handler.handle(&mut req, remote_addr).await {
            Ok(res) => {
                assert_eq!(res.status(), 302);
                assert_eq!(
                    res.headers()["location"],
                    "http://localhost:1234/files/assets/favicon.ico"
                );
            }
            Err(status) => {
                panic!("expected a status 302 but got {status}")
            }
        };
    }

    #[tokio::test]
    async fn redirects_host() {
        let req_handler = fixture_req_handler("toml/redirects.toml");
        let remote_addr = Some(REMOTE_ADDR.parse::<SocketAddr>().unwrap());

        let mut req = Request::default();
        *req.uri_mut() = "http://127.0.0.1:1234".parse().unwrap();

        match req_handler.handle(&mut req, remote_addr).await {
            Ok(res) => {
                assert_eq!(res.status(), 301);
                assert_eq!(res.headers()["location"], "http://localhost:1234/");
            }
            Err(status) => {
                panic!("expected a status 301 but got {status}")
            }
        };
    }

    #[tokio::test]
    async fn redirects_skipped() {
        let req_handler = fixture_req_handler("toml/redirects.toml");
        let remote_addr = Some(REMOTE_ADDR.parse::<SocketAddr>().unwrap());

        let mut req = Request::default();
        *req.uri_mut() = "http://localhost:1234".parse().unwrap();

        match req_handler.handle(&mut req, remote_addr).await {
            Ok(res) => {
                assert_eq!(res.status(), 200);
                assert_eq!(res.headers()["content-type"], "text/html");
            }
            Err(status) => {
                panic!("expected a status 200 but got {status}")
            }
        };
    }
}
diff --git a/tests/toml/config.toml b/tests/toml/config.toml
index 8b29a20..c8702c7 100644
--- a/tests/toml/config.toml
+++ b/tests/toml/config.toml
@@ -103,6 +103,11 @@ headers.Strict-Transport-Security = "max-age=63072000; includeSubDomains; preloa


### URL Redirects
[[advanced.redirects]]
host = "127.0.0.1:4433"
source = "/{*}"
destination = "https://localhost:4433/$1"
kind = 301

[[advanced.redirects]]
source = "**/{*}.{jpg,jpeg}"