index : static-web-server.git

ascending towards madness

author Jose Quintana <1700322+joseluisq@users.noreply.github.com> 2022-11-16 22:33:22.0 +00:00:00
committer GitHub <noreply@github.com> 2022-11-16 22:33:22.0 +00:00:00
commit
0e538dd9572de0e2165f484a84332962b1dee959 [patch]
tree
701cf943ef421c930d5185e1e7998c67fcb1fb14
parent
2828f58f40783c247f4289915c8cb1227294f923
download
0e538dd9572de0e2165f484a84332962b1dee959.tar.gz

refactor: http-related extension traits (#160)

https://rust-lang.github.io/rfcs/0445-extension-trait-conventions.html

Diff

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

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<Body>,
) -> Result<Response<Body>> {
    // 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<Output = Result<Response<Body>, 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<Body>, 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<Body>, 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