From f59a9c5aea8b1ab2b0bf12619b97c0bab5174e5a Mon Sep 17 00:00:00 2001 From: Jose Quintana <1700322+joseluisq@users.noreply.github.com> Date: Wed, 6 Jul 2022 22:25:22 +0200 Subject: [PATCH] Merge pull request #122 from joseluisq/feature/Rewrites_with_pattern_matching feat: url rewrites with pattern matching support --- 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(-) create mode 100644 docs/content/features/url-rewrites.md create mode 100644 src/rewrites.rs 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>, +) -> 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>, + // Rewrites + pub rewrites: Option>, } /// 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>, + pub rewrites: Option>, } /// 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 = 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" -- libgit2 1.7.2