index : static-web-server.git

ascending towards madness

author Jose Quintana <joseluisquintana20@gmail.com> 2022-07-09 19:53:36.0 +00:00:00
committer Jose Quintana <joseluisquintana20@gmail.com> 2022-07-09 19:53:36.0 +00:00:00
commit
5ef3b623fec908aae282ccd3089e27e621507864 [patch]
tree
ef05d0c303aa42cf92603f4a89b03c765b0547f7
parent
f59a9c5aea8b1ab2b0bf12619b97c0bab5174e5a
download
5ef3b623fec908aae282ccd3089e27e621507864.tar.gz

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(-)

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<Vec<Redirects>>,
) -> 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<Vec<Headers>>,
    // Rewrites
    pub rewrites: Option<Vec<Rewrites>>,
    // Redirects
    pub redirects: Option<Vec<Redirects>>,
}

/// 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<Vec<Headers>>,
    pub rewrites: Option<Vec<Rewrites>>,
    pub redirects: Option<Vec<Redirects>>,
}

/// 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<Redirects> = 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]]