From 0e538dd9572de0e2165f484a84332962b1dee959 Mon Sep 17 00:00:00 2001 From: Jose Quintana <1700322+joseluisq@users.noreply.github.com> Date: Wed, 16 Nov 2022 23:33:22 +0100 Subject: [PATCH] refactor: http-related extension traits (#160) https://rust-lang.github.io/rfcs/0445-extension-trait-conventions.html --- src/compression.rs | 4 ++-- src/directory_listing.rs | 5 ++--- src/error_page.rs | 4 ++-- src/exts/http.rs | 36 ++++++++++++++++++++++++++++++++++++ src/exts/mod.rs | 3 +++ src/handler.rs | 12 +++++++----- src/lib.rs | 1 + src/static_files.rs | 97 +++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------------------- 8 files changed, 100 insertions(+), 62 deletions(-) create mode 100644 src/exts/http.rs create mode 100644 src/exts/mod.rs diff --git a/src/compression.rs b/src/compression.rs index 54e0212..a85b6ff 100644 --- a/src/compression.rs +++ b/src/compression.rs @@ -14,7 +14,7 @@ use std::pin::Pin; use std::task::{Context, Poll}; use tokio_util::io::{ReaderStream, StreamReader}; -use crate::Result; +use crate::{exts::http::MethodExt, Result}; /// Contains a fixed list of common text-based MIME types in order to apply compression. pub const TEXT_MIME_TYPES: [&str; 24] = [ @@ -62,7 +62,7 @@ pub fn auto( resp: Response, ) -> Result> { // Skip compression for HEAD and OPTIONS request methods - if method == Method::HEAD || method == Method::OPTIONS { + if method.is_head() || method.is_options() { return Ok(resp); } diff --git a/src/directory_listing.rs b/src/directory_listing.rs index 788e9a0..052b576 100644 --- a/src/directory_listing.rs +++ b/src/directory_listing.rs @@ -13,7 +13,7 @@ use std::path::Path; use std::time::{SystemTime, UNIX_EPOCH}; use structopt::clap::arg_enum; -use crate::Result; +use crate::{exts::http::MethodExt, Result}; arg_enum! { #[derive(Debug, Serialize, Deserialize, Clone)] @@ -36,8 +36,6 @@ pub fn auto_index<'a>( dir_listing_order: u8, dir_listing_format: &'a DirListFmt, ) -> impl Future, StatusCode>> + Send + 'a { - let is_head = method == Method::HEAD; - // Note: it's safe to call `parent()` here since `filepath` // value always refer to a path with file ending and under // a root directory boundary. @@ -47,6 +45,7 @@ pub fn auto_index<'a>( tokio::fs::read_dir(parent).then(move |res| match res { Ok(dir_reader) => Either::Left(async move { + let is_head = method.is_head(); match read_dir_entries( dir_reader, current_path, diff --git a/src/error_page.rs b/src/error_page.rs index e8a665f..fd2f783 100644 --- a/src/error_page.rs +++ b/src/error_page.rs @@ -2,7 +2,7 @@ use headers::{AcceptRanges, ContentLength, ContentType, HeaderMapExt}; use hyper::{Body, Method, Response, StatusCode, Uri}; use mime_guess::mime; -use crate::Result; +use crate::{exts::http::MethodExt, Result}; /// It returns a HTTP error response which also handles available `404` or `50x` HTML content. pub fn error_response( @@ -83,7 +83,7 @@ pub fn error_response( let mut body = Body::empty(); let len = error_page_content.len() as u64; - if method != Method::HEAD { + if !method.is_head() { body = Body::from(error_page_content) } diff --git a/src/exts/http.rs b/src/exts/http.rs new file mode 100644 index 0000000..51274bc --- /dev/null +++ b/src/exts/http.rs @@ -0,0 +1,36 @@ +//! HTTP-related extension traits. + +use hyper::Method; + +/// A fixed list of HTTP methods supported by SWS. +pub const HTTP_SUPPORTED_METHODS: [Method; 3] = [Method::OPTIONS, Method::HEAD, Method::GET]; + +/// SWS HTTP Method extensions trait. +pub trait MethodExt { + fn is_allowed(&self) -> bool; + fn is_get(&self) -> bool; + fn is_head(&self) -> bool; + fn is_options(&self) -> bool; +} + +impl MethodExt for Method { + /// Checks if the HTTP method is allowed (supported) by SWS. + fn is_allowed(&self) -> bool { + HTTP_SUPPORTED_METHODS.iter().any(|h| self == h) + } + + /// Checks if the HTTP method is `GET`. + fn is_get(&self) -> bool { + self == Method::GET + } + + /// Checks if the HTTP method is `HEAD`. + fn is_head(&self) -> bool { + self == Method::HEAD + } + + /// Checks if the HTTP method is `OPTIONS`. + fn is_options(&self) -> bool { + self == Method::OPTIONS + } +} diff --git a/src/exts/mod.rs b/src/exts/mod.rs new file mode 100644 index 0000000..5f2e89e --- /dev/null +++ b/src/exts/mod.rs @@ -0,0 +1,3 @@ +//! Some extension traits for various use cases. + +pub mod http; diff --git a/src/handler.rs b/src/handler.rs index 6f5f9bd..7af5324 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,11 +1,13 @@ use headers::HeaderValue; -use hyper::{header::WWW_AUTHENTICATE, Body, Method, Request, Response, StatusCode}; +use hyper::{header::WWW_AUTHENTICATE, Body, Request, Response, StatusCode}; use std::{future::Future, net::IpAddr, net::SocketAddr, path::PathBuf, sync::Arc}; use crate::{ basic_auth, compression, control_headers, cors, custom_headers, directory_listing::DirListFmt, - error_page, fallback_page, redirects, rewrites, security_headers, + error_page, + exts::http::MethodExt, + fallback_page, redirects, rewrites, security_headers, settings::Advanced, static_files::{self, HandleOpts}, Error, Result, @@ -86,8 +88,8 @@ impl RequestHandler { ); async move { - // Check for disallowed HTTP methods and reject requests accordingly - if !(method == Method::GET || method == Method::HEAD || method == Method::OPTIONS) { + // Reject in case of incoming HTTP request method is not allowed + if !method.is_allowed() { return error_page::error_response( uri, method, @@ -246,7 +248,7 @@ impl RequestHandler { } Err(status) => { // Check for a fallback response - if method == Method::GET + if method.is_get() && status == StatusCode::NOT_FOUND && !self.opts.page_fallback.is_empty() { diff --git a/src/lib.rs b/src/lib.rs index 43ef7fb..0c4c385 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,7 @@ pub mod cors; pub mod custom_headers; pub mod directory_listing; pub mod error_page; +pub mod exts; pub mod fallback_page; pub mod handler; pub mod helpers; diff --git a/src/static_files.rs b/src/static_files.rs index 6c0736f..dcb1db7 100644 --- a/src/static_files.rs +++ b/src/static_files.rs @@ -22,6 +22,7 @@ use std::sync::Mutex; use std::task::{Context, Poll}; use crate::directory_listing::DirListFmt; +use crate::exts::http::{MethodExt, HTTP_SUPPORTED_METHODS}; use crate::{compression_static, directory_listing, Result}; /// Defines all options needed by the static-files handler. @@ -44,8 +45,8 @@ pub async fn handle<'a>(opts: &HandleOpts<'a>) -> Result<(Response, bool), let method = opts.method; let uri_path = opts.uri_path; - // Check for disallowed HTTP methods and reject request accordently - if !(method == Method::GET || method == Method::HEAD || method == Method::OPTIONS) { + // Check if current HTTP method for incoming request is supported + if !method.is_allowed() { return Err(StatusCode::METHOD_NOT_ALLOWED); } @@ -60,59 +61,55 @@ pub async fn handle<'a>(opts: &HandleOpts<'a>) -> Result<(Response, bool), // `is_precompressed` relates to `opts.compression_static` value let is_precompressed = precompressed_variant.is_some(); - // NOTE: `is_dir` means an "auto index" for the current directory request - - // Check for a trailing slash on the current directory path - // and redirect if that path doesn't end with the slash char - if opts.redirect_trailing_slash && is_dir && !uri_path.ends_with('/') { - let uri = [uri_path, "/"].concat(); - let loc = match HeaderValue::from_str(uri.as_str()) { - Ok(val) => val, - Err(err) => { - tracing::error!("invalid header value from current uri: {:?}", err); - return Err(StatusCode::INTERNAL_SERVER_ERROR); - } - }; - - let mut resp = Response::new(Body::empty()); - resp.headers_mut().insert(hyper::header::LOCATION, loc); - *resp.status_mut() = StatusCode::PERMANENT_REDIRECT; + if is_dir { + // Check for a trailing slash on the current directory path + // and redirect if that path doesn't end with the slash char + if opts.redirect_trailing_slash && !uri_path.ends_with('/') { + let uri = [uri_path, "/"].concat(); + let loc = match HeaderValue::from_str(uri.as_str()) { + Ok(val) => val, + Err(err) => { + tracing::error!("invalid header value from current uri: {:?}", err); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + }; - tracing::trace!("uri doesn't end with a slash so redirecting permanently"); - return Ok((resp, is_precompressed)); - } + let mut resp = Response::new(Body::empty()); + resp.headers_mut().insert(hyper::header::LOCATION, loc); + *resp.status_mut() = StatusCode::PERMANENT_REDIRECT; - // Respond with the permitted communication options - if method == Method::OPTIONS { - let mut resp = Response::new(Body::empty()); - *resp.status_mut() = StatusCode::NO_CONTENT; - resp.headers_mut() - .typed_insert(headers::Allow::from_iter(vec![ - Method::OPTIONS, - Method::HEAD, - Method::GET, - ])); - resp.headers_mut().typed_insert(AcceptRanges::bytes()); + tracing::trace!("uri doesn't end with a slash so redirecting permanently"); + return Ok((resp, is_precompressed)); + } - return Ok((resp, is_precompressed)); - } + // Respond with the permitted communication options + if method.is_options() { + let mut resp = Response::new(Body::empty()); + *resp.status_mut() = StatusCode::NO_CONTENT; + resp.headers_mut() + .typed_insert(headers::Allow::from_iter(HTTP_SUPPORTED_METHODS)); + resp.headers_mut().typed_insert(AcceptRanges::bytes()); - // Directory listing - // Check if "directory listing" feature is enabled, - // if current path is a valid directory and - // if it does not contain an `index.html` file (if a proper auto index is generated) - if opts.dir_listing && is_dir && !file_path.exists() { - let resp = directory_listing::auto_index( - method, - uri_path, - opts.uri_query, - file_path.as_ref(), - opts.dir_listing_order, - opts.dir_listing_format, - ) - .await?; + return Ok((resp, is_precompressed)); + } - return Ok((resp, is_precompressed)); + // Directory listing + // Check if "directory listing" feature is enabled, + // if current path is a valid directory and + // if it does not contain an `index.html` file (if a proper auto index is generated) + if opts.dir_listing && !file_path.exists() { + let resp = directory_listing::auto_index( + method, + uri_path, + opts.uri_query, + file_path.as_ref(), + opts.dir_listing_order, + opts.dir_listing_format, + ) + .await?; + + return Ok((resp, is_precompressed)); + } } // Check for a pre-compressed file variant if present under the `opts.compression_static` context -- libgit2 1.7.2