From 5ef3b623fec908aae282ccd3089e27e621507864 Mon Sep 17 00:00:00 2001 From: Jose Quintana Date: Sat, 9 Jul 2022 21:53:36 +0200 Subject: [PATCH] chore: redirects with pattern matching --- Cargo.lock | 12 ++++++++++++ Cargo.toml | 1 + docs/content/features/url-redirects.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/mkdocs.yml | 1 + src/handler.rs | 32 ++++++++++++++++++++++++++++++-- src/lib.rs | 1 + src/redirects.rs | 21 +++++++++++++++++++++ src/settings/file.rs | 20 ++++++++++++++++++++ src/settings/mod.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ tests/toml/config.toml | 12 ++++++++++++ 10 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 docs/content/features/url-redirects.md create mode 100644 src/redirects.rs diff --git a/Cargo.lock b/Cargo.lock index 0318910..f4bd63c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -860,6 +860,17 @@ dependencies = [ ] [[package]] +name = "serde_repr" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ad84e47328a31223de7fed7a4f5087f2d6ddfe586cf3ca25b7a165bc0a5aed" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "sha-1" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -963,6 +974,7 @@ dependencies = [ "rustls-pemfile", "serde", "serde_ignored", + "serde_repr", "signal-hook", "signal-hook-tokio", "structopt", diff --git a/Cargo.toml b/Cargo.toml index 03d840d..7d777c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ pin-project = "1.0" rustls-pemfile = "0.2" serde = { version = "1.0", default-features = false, features = ["derive"] } serde_ignored = "0.1" +serde_repr = "0.1" structopt = { version = "0.3", default-features = false } time = { version = "0.1", default-features = false } tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros", "fs", "io-util", "signal"] } diff --git a/docs/content/features/url-redirects.md b/docs/content/features/url-redirects.md new file mode 100644 index 0000000..9fe033e --- /dev/null +++ b/docs/content/features/url-redirects.md @@ -0,0 +1,52 @@ +# URL Redirects + +**SWS** provides the ability to redirect request URLs with pattern matching support. + +URI redirects are particularly useful with pattern matching ([globs](https://en.wikipedia.org/wiki/Glob_(programming))). Use them for example to prevent broken links if you've moved a page or to shorten URLs. + +## Structure + +The URL redirect 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 the following key/value pairs: + +- One `source` key containing a string _glob pattern_. +- One `destination` string containing the local file path or a full URL. +- One `kind` number containing the HTTP response code. + +!!! 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 must exist. It can be a local path `/some/directory/file.html` or a full URL. It is worth noting that the `/` at the beginning indicates the server's root directory. + +### Kind + +It indicates the HTTP response code. +The values can be: + +- `301` for "Moved Permanently" +- `302` for "Found" (Temporary Redirect) + +## Examples + +```toml +[advanced] + +### URL Redirects + +[[advanced.redirects]] +source = "**/*.{jpg,jpeg}" +destination = "/images/generic1.png" +kind = 301 + +[[advanced.redirects]] +source = "/index.html" +destination = "https://sws.joseluisq.net" +kind = 302 +``` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 72f9399..7ab7b5a 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -140,6 +140,7 @@ nav: - 'Error Pages': 'features/error-pages.md' - 'Custom HTTP Headers': 'features/custom-http-headers.md' - 'URL Rewrites': 'features/url-rewrites.md' + - 'URL Redirects': 'features/url-redirects.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 a749670..a68e1e7 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,9 +1,10 @@ +use headers::HeaderValue; use hyper::{header::WWW_AUTHENTICATE, Body, Method, Request, Response, StatusCode}; use std::{future::Future, net::SocketAddr, path::PathBuf, sync::Arc}; use crate::{ basic_auth, compression, control_headers, cors, custom_headers, error_page, fallback_page, - rewrites, security_headers, settings::Advanced, static_files, Error, Result, + redirects, rewrites, security_headers, settings::Advanced, static_files, Error, Result, }; /// It defines options for a request handler. @@ -128,8 +129,35 @@ impl RequestHandler { } } - // Rewrites if let Some(advanced) = &self.opts.advanced_opts { + // Redirects + if let Some(parts) = redirects::get_redirection(uri_path, &advanced.redirects) { + let (uri_dest, status) = parts; + match HeaderValue::from_str(uri_dest) { + Ok(loc) => { + let mut resp = Response::new(Body::empty()); + resp.headers_mut().insert(hyper::header::LOCATION, loc); + *resp.status_mut() = *status; + tracing::trace!( + "uri matches redirect pattern, redirecting with status {}", + status.canonical_reason().unwrap_or_default() + ); + return Ok(resp); + } + Err(err) => { + tracing::error!("invalid header value from current uri: {:?}", err); + return error_page::error_response( + uri, + method, + &StatusCode::INTERNAL_SERVER_ERROR, + &self.opts.page404, + &self.opts.page50x, + ); + } + }; + } + + // Rewrites if let Some(uri) = rewrites::rewrite_uri_path(uri_path, &advanced.rewrites) { uri_path = uri } diff --git a/src/lib.rs b/src/lib.rs index 8b72a33..5f9ecda 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 redirects; pub mod rewrites; pub mod security_headers; pub mod server; diff --git a/src/redirects.rs b/src/redirects.rs new file mode 100644 index 0000000..c87d1d1 --- /dev/null +++ b/src/redirects.rs @@ -0,0 +1,21 @@ +use hyper::StatusCode; + +use crate::settings::Redirects; + +/// It returns a redirect's destination path and status code if the current request uri +/// matches againt the provided redirect's array. +pub fn get_redirection<'a>( + uri_path: &'a str, + redirects_opts_vec: &'a Option>, +) -> Option<(&'a str, &'a StatusCode)> { + if let Some(redirects_vec) = redirects_opts_vec { + for redirect_entry in redirects_vec.iter() { + // Match source glob pattern against the request uri path + if redirect_entry.source.is_match(uri_path) { + return Some((redirect_entry.destination.as_str(), &redirect_entry.kind)); + } + } + } + + None +} diff --git a/src/settings/file.rs b/src/settings/file.rs index d54f356..f2bcedf 100644 --- a/src/settings/file.rs +++ b/src/settings/file.rs @@ -2,6 +2,7 @@ use headers::HeaderMap; use serde::Deserialize; +use serde_repr::{Deserialize_repr, Serialize_repr}; use std::path::Path; use std::{collections::BTreeSet, path::PathBuf}; @@ -37,6 +38,23 @@ pub struct Headers { pub headers: HeaderMap, } +#[derive(Debug, Serialize_repr, Deserialize_repr, Clone)] +#[repr(u16)] +pub enum RedirectsKind { + /// Moved Permanently + Permanent = 301, + /// Found + Temporary = 302, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct Redirects { + pub source: String, + pub destination: String, + pub kind: RedirectsKind, +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "kebab-case")] pub struct Rewrites { @@ -52,6 +70,8 @@ pub struct Advanced { pub headers: Option>, // Rewrites pub rewrites: Option>, + // Redirects + pub redirects: Option>, } /// General server options available in configuration file mode. diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 1806a02..45faf46 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -1,5 +1,6 @@ use globset::{Glob, GlobMatcher}; use headers::HeaderMap; +use hyper::StatusCode; use structopt::StructOpt; use crate::{Context, Result}; @@ -28,10 +29,21 @@ pub struct Rewrites { pub destination: String, } +/// The `Redirects` file options. +pub struct Redirects { + /// Source pattern glob matcher + pub source: GlobMatcher, + /// A local file that must exist + pub destination: String, + /// Redirection type either 301 (Moved Permanently) or 302 (Found) + pub kind: StatusCode, +} + /// The `advanced` file options. pub struct Advanced { pub headers: Option>, pub rewrites: Option>, + pub redirects: Option>, } /// The full server CLI and File options. @@ -214,9 +226,40 @@ impl Settings { _ => None, }; + // 3. Redirects assignment + let redirects_entries = match advanced.redirects { + Some(redirects_entries) => { + let mut redirects_vec: Vec = Vec::new(); + + // Compile a glob pattern for each redirect sources entry + for redirects_entry in redirects_entries.iter() { + let source = Glob::new(&redirects_entry.source) + .with_context(|| { + format!( + "can not compile glob pattern for redirect source: {}", + &redirects_entry.source + ) + })? + .compile_matcher(); + + let status_code = redirects_entry.kind.to_owned() as u16; + redirects_vec.push(Redirects { + source, + destination: redirects_entry.destination.to_owned(), + kind: StatusCode::from_u16(status_code).with_context(|| { + format!("invalid redirect status code: {}", status_code) + })?, + }); + } + Some(redirects_vec) + } + _ => None, + }; + settings_advanced = Some(Advanced { headers: headers_entries, rewrites: rewrites_entries, + redirects: redirects_entries, }); } } diff --git a/tests/toml/config.toml b/tests/toml/config.toml index 838c37c..34b4ded 100644 --- a/tests/toml/config.toml +++ b/tests/toml/config.toml @@ -78,6 +78,18 @@ source = "**/*.{jpg,jpeg,png,ico,gif}" headers.Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload" +### URL Redirects + +[[advanced.redirects]] +source = "**/*.{jpg,jpeg}" +destination = "/images/generic1.png" +kind = 301 + +[[advanced.redirects]] +source = "/index.html" +destination = "https://sws.joseluisq.net" +kind = 302 + ### URL Rewrites [[advanced.rewrites]] -- libgit2 1.7.2