index : static-web-server.git

ascending towards madness

author Jose Quintana <1700322+joseluisq@users.noreply.github.com> 2022-07-06 20:25:22.0 +00:00:00
committer GitHub <noreply@github.com> 2022-07-06 20:25:22.0 +00:00:00
commit
f59a9c5aea8b1ab2b0bf12619b97c0bab5174e5a [patch]
tree
b3380a68e46879ec61a86a3536219272a3db87b6
parent
8353f19dc223c90e104a2925718b43372d29247c
parent
37c9ffc56b568aaadd0885268f2cb17292973bff
download
f59a9c5aea8b1ab2b0bf12619b97c0bab5174e5a.tar.gz

Merge pull request #122 from joseluisq/feature/Rewrites_with_pattern_matching

feat: url rewrites with pattern matching support

Diff

 docs/content/features/url-rewrites.md | 41 ++++++++++++++++++++++++++++++++++++-
 docs/mkdocs.yml                       |  1 +-
 src/handler.rs                        | 13 ++++++++---
 src/lib.rs                            |  1 +-
 src/rewrites.rs                       | 19 +++++++++++++++++-
 src/settings/file.rs                  | 11 +++++++++-
 src/settings/mod.rs                   | 40 +++++++++++++++++++++++++++++++++--
 tests/toml/config.toml                | 16 ++++++++++----
 8 files changed, 132 insertions(+), 10 deletions(-)

diff --git a/docs/content/features/url-rewrites.md b/docs/content/features/url-rewrites.md
new file mode 100644
index 0000000..47d99ea
--- /dev/null
+++ b/docs/content/features/url-rewrites.md
@@ -0,0 +1,41 @@
# URL Rewrites 

**SWS** provides the ability to rewrite request URLs with pattern matching support.

URI rewrites are particularly useful with pattern matching ([globs]https://en.wikipedia.org/wiki/Glob_(programming)), as the server can accept any URL that matches the pattern and let the client-side code decide what to display.

## Structure

The URL rewrite rules should be defined mainly as an [Array of Tables]https://toml.io/en/v1.0.0#array-of-tables.

Each table entry should have two key/value pairs:

- One `source` key containing a string _glob pattern_.
- One `destination` string containing the local file path.

!!! info "Note"
    The incoming request(s) will reach the `destination` only if the request(s) URI matches the `source` pattern.

### Source

The source is a [Glob pattern]https://en.wikipedia.org/wiki/Glob_(programming) that should match against the URI that is requesting a resource file.

### Destination

A local file path which must exist. It has to look something like `/some/directory/file.html`. It is worth noting that the `/` at the beginning indicates the server's root directory.

## Examples

```toml
[advanced]

### URL Rewrites

[[advanced.rewrites]]
source = "**/*.{png,ico,gif}"
destination = "/assets/generic1.png"

[[advanced.rewrites]]
source = "**/*.{jpg,jpeg}"
destination = "/images/generic2.png"
```
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 42e4759..72f9399 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -139,6 +139,7 @@ nav:
    - 'Worker Threads Customization': 'features/worker-threads.md'
    - 'Error Pages': 'features/error-pages.md'
    - 'Custom HTTP Headers': 'features/custom-http-headers.md'
    - 'URL Rewrites': 'features/url-rewrites.md'
    - 'Windows Service': 'features/windows-service.md'
  - 'Platforms & Architectures': 'platforms-architectures.md'
  - 'Migration from v1 to v2': 'migration.md'
diff --git a/src/handler.rs b/src/handler.rs
index dbb7905..a749670 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -3,7 +3,7 @@ use std::{future::Future, net::SocketAddr, path::PathBuf, sync::Arc};

use crate::{
    basic_auth, compression, control_headers, cors, custom_headers, error_page, fallback_page,
    security_headers, settings::Advanced, static_files, Error, Result,
    rewrites, security_headers, settings::Advanced, static_files, Error, Result,
};

/// It defines options for a request handler.
@@ -43,7 +43,7 @@ impl RequestHandler {
        let uri = req.uri();

        let root_dir = &self.opts.root_dir;
        let uri_path = uri.path();
        let mut uri_path = uri.path();
        let uri_query = uri.query();
        let dir_listing = self.opts.dir_listing;
        let dir_listing_order = self.opts.dir_listing_order;
@@ -65,7 +65,7 @@ impl RequestHandler {
        );

        async move {
            // Check for disallowed HTTP methods and reject request accordently
            // Check for disallowed HTTP methods and reject requests accordingly
            if !(method == Method::GET || method == Method::HEAD || method == Method::OPTIONS) {
                return error_page::error_response(
                    uri,
@@ -128,6 +128,13 @@ impl RequestHandler {
                }
            }

            // Rewrites
            if let Some(advanced) = &self.opts.advanced_opts {
                if let Some(uri) = rewrites::rewrite_uri_path(uri_path, &advanced.rewrites) {
                    uri_path = uri
                }
            }

            // Static files
            match static_files::handle(
                method,
diff --git a/src/lib.rs b/src/lib.rs
index 9caa7ea..8b72a33 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -19,6 +19,7 @@ pub mod fallback_page;
pub mod handler;
pub mod helpers;
pub mod logger;
pub mod rewrites;
pub mod security_headers;
pub mod server;
pub mod service;
diff --git a/src/rewrites.rs b/src/rewrites.rs
new file mode 100644
index 0000000..a71eee3
--- /dev/null
+++ b/src/rewrites.rs
@@ -0,0 +1,19 @@
use crate::settings::Rewrites;

/// It returns a rewrite's destination path if the current request uri
/// matches againt the provided rewrites array.
pub fn rewrite_uri_path<'a>(
    uri_path: &'a str,
    rewrites_opts_vec: &'a Option<Vec<Rewrites>>,
) -> Option<&'a str> {
    if let Some(rewrites_vec) = rewrites_opts_vec {
        for rewrites_entry in rewrites_vec.iter() {
            // Match source glob pattern against request uri path
            if rewrites_entry.source.is_match(uri_path) {
                return Some(rewrites_entry.destination.as_str());
            }
        }
    }

    None
}
diff --git a/src/settings/file.rs b/src/settings/file.rs
index 22be141..d54f356 100644
--- a/src/settings/file.rs
+++ b/src/settings/file.rs
@@ -7,7 +7,7 @@ use std::{collections::BTreeSet, path::PathBuf};

use crate::{helpers, Context, Result};

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub enum LogLevel {
    Error,
@@ -37,12 +37,21 @@ pub struct Headers {
    pub headers: HeaderMap,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Rewrites {
    pub source: String,
    pub destination: String,
}

/// Advanced server options only available in configuration file mode.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Advanced {
    // Headers
    pub headers: Option<Vec<Headers>>,
    // Rewrites
    pub rewrites: Option<Vec<Rewrites>>,
}

/// General server options available in configuration file mode.
diff --git a/src/settings/mod.rs b/src/settings/mod.rs
index 4b793de..1806a02 100644
--- a/src/settings/mod.rs
+++ b/src/settings/mod.rs
@@ -20,9 +20,18 @@ pub struct Headers {
    pub headers: HeaderMap,
}

/// The `Rewrites` file options.
pub struct Rewrites {
    /// Source pattern glob matcher
    pub source: GlobMatcher,
    /// A local file that must exist
    pub destination: String,
}

/// The `advanced` file options.
pub struct Advanced {
    pub headers: Option<Vec<Headers>>,
    pub rewrites: Option<Vec<Rewrites>>,
}

/// The full server CLI and File options.
@@ -81,7 +90,7 @@ impl Settings {

                config_file = Some(path_resolved);

                // Assign the corresponding file option values
                // File-based "general" options
                if let Some(general) = settings.general {
                    if let Some(v) = general.host {
                        host = v
@@ -151,7 +160,7 @@ impl Settings {
                    }
                }

                // Prepare the "advanced" options
                // File-based "advanced" options
                if let Some(advanced) = settings.advanced {
                    // 1. Custom HTTP headers assignment
                    let headers_entries = match advanced.headers {
@@ -179,8 +188,35 @@ impl Settings {
                        _ => None,
                    };

                    // 2. Rewrites assignment
                    let rewrites_entries = match advanced.rewrites {
                        Some(rewrites_entries) => {
                            let mut rewrites_vec: Vec<Rewrites> = Vec::new();

                            // Compile a glob pattern for each rewrite sources entry
                            for rewrites_entry in rewrites_entries.iter() {
                                let source = Glob::new(&rewrites_entry.source)
                                    .with_context(|| {
                                        format!(
                                            "can not compile glob pattern for rewrite source: {}",
                                            &rewrites_entry.source
                                        )
                                    })?
                                    .compile_matcher();

                                rewrites_vec.push(Rewrites {
                                    source,
                                    destination: rewrites_entry.destination.to_owned(),
                                });
                            }
                            Some(rewrites_vec)
                        }
                        _ => None,
                    };

                    settings_advanced = Some(Advanced {
                        headers: headers_entries,
                        rewrites: rewrites_entries,
                    });
                }
            }
diff --git a/tests/toml/config.toml b/tests/toml/config.toml
index 47ff090..838c37c 100644
--- a/tests/toml/config.toml
+++ b/tests/toml/config.toml
@@ -2,7 +2,7 @@

#### Address & Root dir
host = "::"
port = 8087
port = 8787
root = "docker/public"

#### Logging
@@ -45,9 +45,6 @@ grace-period = 0
#### Page fallback for 404s
page-fallback = ""

#### Page fallback for 404s
page-fallback = ""

#### Log request Remote Address if available
log-remote-address = false

@@ -79,3 +76,14 @@ Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
[[advanced.headers]]
source = "**/*.{jpg,jpeg,png,ico,gif}"
headers.Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"


### URL Rewrites

[[advanced.rewrites]]
source = "**/*.{png,ico,gif}"
destination = "/assets/favicon.ico"

[[advanced.rewrites]]
source = "**/*.{jpg,jpeg}"
destination = "/images/nomad.png"