chore: redirects with pattern matching
Diff
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(-)
@@ -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",
@@ -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"] }
@@ -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
```
@@ -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'
@@ -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,
};
@@ -128,8 +129,35 @@ impl RequestHandler {
}
}
if let Some(advanced) = &self.opts.advanced_opts {
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,
);
}
};
}
if let Some(uri) = rewrites::rewrite_uri_path(uri_path, &advanced.rewrites) {
uri_path = uri
}
@@ -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;
@@ -0,0 +1,21 @@
use hyper::StatusCode;
use crate::settings::Redirects;
pub fn get_redirection<'a>(
uri_path: &'a str,
redirects_opts_vec: &'a Option<Vec<Redirects>>,
) -> Option<(&'a str, &'a StatusCode)> {
if let Some(redirects_vec) = redirects_opts_vec {
for redirect_entry in redirects_vec.iter() {
if redirect_entry.source.is_match(uri_path) {
return Some((redirect_entry.destination.as_str(), &redirect_entry.kind));
}
}
}
None
}
@@ -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 {
Permanent = 301,
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<Vec<Headers>>,
pub rewrites: Option<Vec<Rewrites>>,
pub redirects: Option<Vec<Redirects>>,
}
@@ -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,
}
pub struct Redirects {
pub source: GlobMatcher,
pub destination: String,
pub kind: StatusCode,
}
pub struct Advanced {
pub headers: Option<Vec<Headers>>,
pub rewrites: Option<Vec<Rewrites>>,
pub redirects: Option<Vec<Redirects>>,
}
@@ -214,9 +226,40 @@ impl Settings {
_ => None,
};
let redirects_entries = match advanced.redirects {
Some(redirects_entries) => {
let mut redirects_vec: Vec<Redirects> = Vec::new();
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,
});
}
}
@@ -78,6 +78,18 @@ source = "**/*.{jpg,jpeg,png,ico,gif}"
headers.Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
[[advanced.redirects]]
source = "**/*.{jpg,jpeg}"
destination = "/images/generic1.png"
kind = 301
[[advanced.redirects]]
source = "/index.html"
destination = "https://sws.joseluisq.net"
kind = 302
[[advanced.rewrites]]