From ed0d6ac9989a14704c414eefd1aca16315e2fcca Mon Sep 17 00:00:00 2001 From: Jose Quintana Date: Thu, 21 Jan 2021 23:58:40 +0100 Subject: [PATCH] feat: custom error pages support --- README.md | 2 +- src/bin/server.rs | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------ src/core/cache.rs | 7 +++++++ src/core/config.rs | 16 ++++++++++++++++ src/core/helpers.rs | 14 ++++++++++++-- src/core/rejection.rs | 39 +++++++++++++++++++++++++++------------ 6 files changed, 157 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index e43c974..f41622b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ For stable `v1` and contributions please refer to [1.x](https://github.com/josel - Built with [Rust](https://rust-lang.org) which is focused on [safety, speed, and concurrency](https://kornel.ski/rust-c-speed). - Memory safety and very reduced CPU and RAM overhead. -- Blazing fast static files-serving powered by [Warp](https://github.com/seanmonstar/warp/) `v0.2` ([Hyper](https://github.com/hyperium/hyper/) `v0.13`), [Tokio](https://github.com/tokio-rs/tokio) `v0.2` and a set of [awesome crates](./Cargo.toml). +- Blazing fast static files-serving powered by [Warp](https://github.com/seanmonstar/warp/) `v0.3` ([Hyper](https://github.com/hyperium/hyper/) `v0.14`), [Tokio](https://github.com/tokio-rs/tokio) `v1` and a set of [awesome crates](./Cargo.toml). - Suitable for lightweight [GNU/Linux Docker containers](https://hub.docker.com/r/joseluisq/static-web-server/tags). It's a fully __5MB__ static binary thanks to [Rust and Musl libc](https://doc.rust-lang.org/edition-guide/rust-2018/platform-and-target-support/musl-support-for-fully-static-binaries.html). - Opt-in GZip, Deflate and Brotli compression for text-based web files only. - Compression on demand via [Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) header. diff --git a/src/bin/server.rs b/src/bin/server.rs index 8d0292f..c8375e1 100644 --- a/src/bin/server.rs +++ b/src/bin/server.rs @@ -16,71 +16,117 @@ use self::static_web_server::core::*; async fn server(opts: config::Options) -> Result { logger::init(&opts.log_level)?; + // Check a valid root directory + let root_dir = helpers::get_valid_dirpath(opts.root)?; + + // Read custom error pages content + let page404 = helpers::read_file_content(opts.page404.as_ref()); + let page50x = helpers::read_file_content(opts.page50x.as_ref()); + + // Public HEAD endpoint + let page404_a = page404.clone(); + let page50x_a = page50x.clone(); let public_head = warp::head().and( - warp::fs::dir(opts.root.clone()) + warp::fs::dir(root_dir.clone()) .map(cache::control_headers) .with(warp::trace::request()) - .recover(rejection::handle_rejection), + .recover(move |r| { + let page404_a = page404_a.clone(); + let page50x_a = page50x_a.clone(); + async move { rejection::handle_rejection(page404_a, page50x_a, r).await } + }), ); + // Public GET endpoint (default) + let page404_b = page404.clone(); + let page50x_b = page50x.clone(); let public_get_default = warp::get().and( - warp::fs::dir(opts.root.clone()) + warp::fs::dir(root_dir.clone()) .map(cache::control_headers) .with(warp::trace::request()) - .recover(rejection::handle_rejection), + .recover(move |r| { + let page404_b = page404_b.clone(); + let page50x_b = page50x_b.clone(); + async move { rejection::handle_rejection(page404_b, page50x_b, r).await } + }), ); let host = opts.host.parse::()?; let port = opts.port; - let accept_encoding = |v: &'static str| warp::header::contains("accept-encoding", v); - + // Public GET/HEAD endpoints with compression (deflate, gzip, brotli, none) + let page404_c = page404.clone(); + let page50x_c = page50x.clone(); match opts.compression.as_ref() { - "brotli" => tokio::task::spawn( - warp::serve( - public_head.or(warp::get() - .and(accept_encoding("br")) - .and( - warp::fs::dir(opts.root.clone()) - .map(cache::control_headers) - .with(warp::trace::request()) - .with(warp::compression::brotli(true)) - .recover(rejection::handle_rejection), - ) - .or(public_get_default)), + "brotli" => { + tokio::task::spawn( + warp::serve( + public_head.or(warp::get() + .and(cache::accept_encoding("br")) + .and( + warp::fs::dir(root_dir.clone()) + .map(cache::control_headers) + .with(warp::trace::request()) + .with(warp::compression::brotli(true)) + .recover(move |r| { + let page404_c = page404_c.clone(); + let page50x_c = page50x_c.clone(); + async move { + rejection::handle_rejection(page404_c, page50x_c, r).await + } + }), + ) + .or(public_get_default)), + ) + .run((host, port)), ) - .run((host, port)), - ), - "deflate" => tokio::task::spawn( - warp::serve( - public_head.or(warp::get() - .and(accept_encoding("deflate")) - .and( - warp::fs::dir(opts.root.clone()) - .map(cache::control_headers) - .with(warp::trace::request()) - .with(warp::compression::deflate(true)) - .recover(rejection::handle_rejection), - ) - .or(public_get_default)), + } + "deflate" => { + tokio::task::spawn( + warp::serve( + public_head.or(warp::get() + .and(cache::accept_encoding("deflate")) + .and( + warp::fs::dir(root_dir.clone()) + .map(cache::control_headers) + .with(warp::trace::request()) + .with(warp::compression::deflate(true)) + .recover(move |r| { + let page404_c = page404_c.clone(); + let page50x_c = page50x_c.clone(); + async move { + rejection::handle_rejection(page404_c, page50x_c, r).await + } + }), + ) + .or(public_get_default)), + ) + .run((host, port)), ) - .run((host, port)), - ), - "gzip" => tokio::task::spawn( - warp::serve( - public_head.or(warp::get() - .and(accept_encoding("gzip")) - .and( - warp::fs::dir(opts.root.clone()) - .map(cache::control_headers) - .with(warp::trace::request()) - .with(warp::compression::gzip(true)) - .recover(rejection::handle_rejection), - ) - .or(public_get_default)), + } + "gzip" => { + tokio::task::spawn( + warp::serve( + public_head.or(warp::get() + .and(cache::accept_encoding("gzip")) + .and( + warp::fs::dir(root_dir.clone()) + .map(cache::control_headers) + .with(warp::trace::request()) + .with(warp::compression::gzip(true)) + .recover(move |r| { + let page404_c = page404_c.clone(); + let page50x_c = page50x_c.clone(); + async move { + rejection::handle_rejection(page404_c, page50x_c, r).await + } + }), + ) + .or(public_get_default)), + ) + .run((host, port)), ) - .run((host, port)), - ), + } _ => tokio::task::spawn(warp::serve(public_head.or(public_get_default)).run((host, port))), }; diff --git a/src/core/cache.rs b/src/core/cache.rs index c771cb3..148e7d5 100644 --- a/src/core/cache.rs +++ b/src/core/cache.rs @@ -36,3 +36,10 @@ pub fn control_headers(res: warp::fs::File) -> warp::reply::WithHeader u32 { std::cmp::min(n, u32::MAX as u64) as u32 } + +/// Warp filter in order to check for an `Accept-Encoding` header value. +pub fn accept_encoding( + val: &'static str, +) -> impl warp::Filter + Copy { + warp::header::contains("accept-encoding", val) +} diff --git a/src/core/config.rs b/src/core/config.rs index 63e9f23..406f43c 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -27,6 +27,22 @@ pub struct Options { /// Root directory path of static files pub root: String, + #[structopt( + long, + default_value = "./public/50x.html", + env = "SERVER_ERROR_PAGE_50X" + )] + /// HTML file path for 50x errors. If path is not specified or simply don't exists then server will use a generic HTML error message. + pub page50x: String, + + #[structopt( + long, + default_value = "./public/404.html", + env = "SERVER_ERROR_PAGE_404" + )] + /// HTML file path for 404 errors. If path is not specified or simply don't exists then server will use a generic HTML error message. + pub page404: String, + #[structopt(long, short = "c", default_value = "gzip", env = "SERVER_COMPRESSION")] /// Compression body support for web text-based file types. Values: "gzip", "deflate" or "brotli". /// Use an empty value to skip compression. diff --git a/src/core/helpers.rs b/src/core/helpers.rs index 96f2eff..9abc9b1 100644 --- a/src/core/helpers.rs +++ b/src/core/helpers.rs @@ -1,3 +1,4 @@ +use std::fs; use std::path::{Path, PathBuf}; use super::Result; @@ -8,8 +9,8 @@ where PathBuf: From

, { match PathBuf::from(path) { - v if !v.exists() => bail!("path \"{:?}\" was not found", &v), - v if !v.is_dir() => bail!("path \"{:?}\" is not a directory", &v), + v if !v.exists() => bail!("path \"{:?}\" was not found or inaccessible", &v), + v if !v.is_dir() => bail!("path \"{:?}\" is not a valid directory", &v), v => Ok(v), } } @@ -25,3 +26,12 @@ where _ => bail!("directory name for path \"{:?}\" was not determined", path), } } + +// Read the entire contents of a file into a string if it's valid or empty otherwise. +pub fn read_file_content(p: &str) -> String { + if !p.is_empty() && Path::new(p).exists() { + return fs::read_to_string(p).unwrap_or(String::new()); + } + + String::new() +} diff --git a/src/core/rejection.rs b/src/core/rejection.rs index 666eaef..78e8502 100644 --- a/src/core/rejection.rs +++ b/src/core/rejection.rs @@ -4,22 +4,37 @@ use warp::http::StatusCode; use warp::{Rejection, Reply}; // It receives a `Rejection` and tries to return a HTML error reply. -pub async fn handle_rejection(err: Rejection) -> Result { +pub async fn handle_rejection( + page_404: String, + page_50x: String, + err: Rejection, +) -> Result { + let mut content = String::new(); let code = if err.is_not_found() { + content = page_404; StatusCode::NOT_FOUND - } else if err + } else { + if err .find::() .is_some() - { - StatusCode::BAD_REQUEST - } else if err.find::().is_some() { - StatusCode::METHOD_NOT_ALLOWED - } else { - StatusCode::INTERNAL_SERVER_ERROR + { + StatusCode::BAD_REQUEST + } else if err.find::().is_some() { + StatusCode::METHOD_NOT_ALLOWED + } else if err.find::().is_some() { + StatusCode::UNSUPPORTED_MEDIA_TYPE + } else { + content = page_50x; + StatusCode::INTERNAL_SERVER_ERROR + } }; - let content = format!( - "{}

{}

", - code, code - ); + + if content.is_empty() { + content = format!( + "{}

{}

", + code, code + ); + } + Ok(warp::reply::with_status(warp::reply::html(content), code)) } -- libgit2 1.7.2