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

Issue:
Support for URL customization
I'd like to customize my URLs (e.g. `https://foo.com/bar.html` to `https://foo.com/pretty-bar`, or even `https://foo.com/logical/hierarchical/structure/pretty-bar`). Allowing arbitary mappings between URL requests and files on the server would be nice.


Discussion / hints:
In fact, something like this is already on my TODO list which will make it possible to rewrite URLs via glob patterns at convenience.

Fix patch:
diff --git a/docs/content/features/url-rewrites.md b/docs/content/features/url-rewrites.md
new file mode 100644
index 00000000..47d99eab
--- /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 42e4759d..72f93999 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 dbb7905e..a7496701 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 9caa7ea1..8b72a338 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 00000000..a71eee31
--- /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 22be141b..d54f356b 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 4b793ded..1806a023 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,
                     });
                 }
             }


Test patch:
diff --git a/tests/toml/config.toml b/tests/toml/config.toml
index 47ff0905..838c37cd 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"