index : static-web-server.git

ascending towards madness

author Jose Quintana <1700322+joseluisq@users.noreply.github.com> 2023-05-07 22:05:11.0 +00:00:00
committer GitHub <noreply@github.com> 2023-05-07 22:05:11.0 +00:00:00
commit
af77e4a3cf6cda193ce0e1f9f6d249adb1fadcbc [patch]
tree
30862d6bcfe372d97a7feb7edd815501f08ddf02
parent
6876a75e1ea2e25f7973f1cc614f76bd80b5d16c
download
af77e4a3cf6cda193ce0e1f9f6d249adb1fadcbc.tar.gz

feat: cargo feature for compression and compression static (#201)

* feat: cargo feature for compression and compression static
   compression = ["compression-brotli", "compression-deflate", "compression-gzip", "compression-zstd"]
* docs: describe compression cargo feature [skip ci]

Diff

 Cargo.toml                           | 11 ++++++++--
 docs/content/building-from-source.md | 33 +++++++++++++++++++++++-------
 src/compression.rs                   | 39 ++++++++++++++++++++++++++---------
 src/compression_static.rs            |  3 +++-
 src/handler.rs                       | 11 +++++++---
 src/lib.rs                           |  2 ++-
 src/server.rs                        |  8 +++++++-
 src/settings/cli.rs                  |  6 +++--
 src/settings/file.rs                 |  2 ++-
 src/settings/mod.rs                  |  8 +++++++-
 src/static_files.rs                  | 41 ++++++++++++++++++++++++++-----------
 tests/compression_static.rs          |  4 ++++-
 tests/static_files.rs                |  9 +++++---
 13 files changed, 138 insertions(+), 39 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 48fb5cf..20f9975 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -37,13 +37,20 @@ path = "src/bin/server.rs"
doc = false

[features]
default = ["http2"]
default = ["compression", "http2"]
# HTTP2
tls = ["tokio-rustls"]
http2 = ["tls"]
# Compression
compression = ["compression-brotli", "compression-deflate", "compression-gzip", "compression-zstd"]
compression-brotli = ["async-compression/brotli"]
compression-deflate = ["async-compression/deflate"]
compression-gzip = ["async-compression/deflate"]
compression-zstd = ["async-compression/zstd"]

[dependencies]
anyhow = "1.0"
async-compression = { version = "0.3", default-features = false, features = ["brotli", "deflate", "gzip", "zstd", "tokio"] }
async-compression = { version = "0.3", default-features = false, optional = true, features = ["brotli", "deflate", "gzip", "zstd", "tokio"] }
bcrypt = "0.14"
bytes = "1.4"
form_urlencoded = "1.1"
diff --git a/docs/content/building-from-source.md b/docs/content/building-from-source.md
index b90b8a8..037be78 100644
--- a/docs/content/building-from-source.md
+++ b/docs/content/building-from-source.md
@@ -21,22 +21,41 @@ Finally, the release binary should be available at `target/release/static-web-se
!!! info "Don't use the project's `Makefile`"
    Please don't use the project's `Makefile` since it's only intended for development and some on-demand tasks.

## Building documentation from source
## Cargo features

All HTML documentation is located in the `docs/` project's directory and is built using [Material for MkDocs]https://github.com/squidfunk/mkdocs-material.
When building from the source, all features are enabled by default.
However, you can disable just the ones you don't need from the lists below.

It's only necessary to have [Docker]https://www.docker.com/get-started/ installed.
Feature | Description
---------|------
**Deafult** |
`default` | Activates all features by default.
[**HTTP2/TLS**]./features/http2-tls.md |
`http2` | Activates the HTTP2 and TLS feature.
`tls` | Activates only the TLS feature.
[**Compression**]./features/compression.md |
`compression` | Activates auto-compression and compression static with all supported algorithms.
`compression-brotli` | Activates auto-compression/compression static with only the `brotli` algorithm.
`compression-deflate` | Activates auto-compression/compression static with only the `deflate` algorithm.
`compression-gzip` | Activates auto-compression/compression static with only the `gzip` algorithm.
`compression-zstd` | Activates auto-compression/compression static with only the `zstd` algorithm.

## Cargo features
### Disable all default features

Some features are optional when running or building from the source.
For example, if you want to run without the default features like `http2` just try.
For example, if you want to run or build SWS without the default features like `compression`, `http2`, etc then just try:

```sh
# run
cargo run --no-default-features -- -h
# or build
cargo build --release --no-default-features
```

For more optional features take a look a the `[features]` section of the `cargo.toml` file adjusting them on demand.
## Building documentation from source

All HTML documentation is located in the `docs/` project's directory and is built using [Material for MkDocs]https://github.com/squidfunk/mkdocs-material.

It's only necessary to have [Docker]https://www.docker.com/get-started/ installed.

### Building documentation

diff --git a/src/compression.rs b/src/compression.rs
index 0dc0f7a..3b7109c 100644
--- a/src/compression.rs
+++ b/src/compression.rs
@@ -8,7 +8,15 @@

// Part of the file is borrowed from <https://github.com/seanmonstar/warp/pull/513>*

use async_compression::tokio::bufread::{BrotliEncoder, DeflateEncoder, GzipEncoder, ZstdEncoder};
#[cfg(feature = "compression-brotli")]
use async_compression::tokio::bufread::BrotliEncoder;
#[cfg(feature = "compression-deflate")]
use async_compression::tokio::bufread::DeflateEncoder;
#[cfg(feature = "compression-gzip")]
use async_compression::tokio::bufread::GzipEncoder;
#[cfg(feature = "compression-zstd")]
use async_compression::tokio::bufread::ZstdEncoder;

use bytes::Bytes;
use futures_util::Stream;
use headers::{AcceptEncoding, ContentCoding, ContentType, HeaderMap, HeaderMapExt};
@@ -52,16 +60,8 @@ pub const TEXT_MIME_TYPES: [&str; 24] = [
    "application/wasm",
];

/// Try to get the prefered `content-encoding` via the `accept-encoding` header.
pub fn get_prefered_encoding(headers: &HeaderMap<HeaderValue>) -> Option<ContentCoding> {
    if let Some(ref accept_encoding) = headers.typed_get::<AcceptEncoding>() {
        return accept_encoding.prefered_encoding();
    }
    None
}

/// Create a wrapping handler that compresses the Body of a [`Response`](hyper::Response)
/// using `gzip`, `deflate` or `brotli` if is specified in the `Accept-Encoding` header, adding
/// using `gzip`, `deflate`, `brotli` or `zstd` if is specified in the `Accept-Encoding` header, adding
/// `content-encoding: <coding>` to the Response's [`HeaderMap`](hyper::HeaderMap)
/// It also provides the ability to apply compression for text-based MIME types only.
pub fn auto(
@@ -84,18 +84,25 @@ pub fn auto(
            }
        }

        #[cfg(feature = "compression-gzip")]
        if encoding == ContentCoding::GZIP {
            let (head, body) = resp.into_parts();
            return Ok(gzip(head, body.into()));
        }

        #[cfg(feature = "compression-deflate")]
        if encoding == ContentCoding::DEFLATE {
            let (head, body) = resp.into_parts();
            return Ok(deflate(head, body.into()));
        }

        #[cfg(feature = "compression-brotli")]
        if encoding == ContentCoding::BROTLI {
            let (head, body) = resp.into_parts();
            return Ok(brotli(head, body.into()));
        }

        #[cfg(feature = "compression-zstd")]
        if encoding == ContentCoding::ZSTD {
            let (head, body) = resp.into_parts();
            return Ok(zstd(head, body.into()));
@@ -107,6 +114,7 @@ pub fn auto(

/// Create a wrapping handler that compresses the Body of a [`Response`](hyper::Response)
/// using gzip, adding `content-encoding: gzip` to the Response's [`HeaderMap`](hyper::HeaderMap)
#[cfg(feature = "compression-gzip")]
pub fn gzip(
    mut head: http::response::Parts,
    body: CompressableBody<Body, hyper::Error>,
@@ -122,6 +130,7 @@ pub fn gzip(

/// Create a wrapping handler that compresses the Body of a [`Response`](hyper::Response)
/// using deflate, adding `content-encoding: deflate` to the Response's [`HeaderMap`](hyper::HeaderMap)
#[cfg(feature = "compression-deflate")]
pub fn deflate(
    mut head: http::response::Parts,
    body: CompressableBody<Body, hyper::Error>,
@@ -142,6 +151,7 @@ pub fn deflate(

/// Create a wrapping handler that compresses the Body of a [`Response`](hyper::Response)
/// using brotli, adding `content-encoding: br` to the Response's [`HeaderMap`](hyper::HeaderMap)
#[cfg(feature = "compression-brotli")]
pub fn brotli(
    mut head: http::response::Parts,
    body: CompressableBody<Body, hyper::Error>,
@@ -160,6 +170,7 @@ pub fn brotli(

/// Create a wrapping handler that compresses the Body of a [`Response`](hyper::Response)
/// using zstd, adding `content-encoding: zstd` to the Response's [`HeaderMap`](hyper::HeaderMap)
#[cfg(feature = "compression-zstd")]
pub fn zstd(
    mut head: http::response::Parts,
    body: CompressableBody<Body, hyper::Error>,
@@ -184,6 +195,14 @@ pub fn create_encoding_header(existing: Option<HeaderValue>, coding: ContentCodi
    coding.into()
}

/// Try to get the prefered `content-encoding` via the `accept-encoding` header.
pub fn get_prefered_encoding(headers: &HeaderMap<HeaderValue>) -> Option<ContentCoding> {
    if let Some(ref accept_encoding) = headers.typed_get::<AcceptEncoding>() {
        return accept_encoding.prefered_encoding();
    }
    None
}

/// A wrapper around any type that implements [`Stream`](futures_util::Stream) to be
/// compatible with async_compression's `Stream` based encoders.
#[pin_project]
diff --git a/src/compression_static.rs b/src/compression_static.rs
index c3be592..f24195f 100644
--- a/src/compression_static.rs
+++ b/src/compression_static.rs
@@ -38,10 +38,13 @@ pub async fn precompressed_variant<'a>(
    // Determine prefered-encoding extension if available
    let comp_ext = match compression::get_prefered_encoding(headers) {
        // https://zlib.net/zlib_faq.html#faq39
        #[cfg(feature = "compression-gzip")]
        Some(ContentCoding::GZIP | ContentCoding::DEFLATE) => "gz",
        // https://peazip.github.io/brotli-compressed-file-format.html
        #[cfg(feature = "compression-brotli")]
        Some(ContentCoding::BROTLI) => "br",
        // https://datatracker.ietf.org/doc/html/rfc8878
        #[cfg(feature = "compression-zstd")]
        Some(ContentCoding::ZSTD) => "zst",
        _ => {
            tracing::trace!(
diff --git a/src/handler.rs b/src/handler.rs
index 26cc42f..0dac15f 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -10,8 +10,11 @@ use headers::HeaderValue;
use hyper::{header::WWW_AUTHENTICATE, Body, Request, Response, StatusCode};
use std::{future::Future, net::IpAddr, net::SocketAddr, path::PathBuf, sync::Arc};

#[cfg(feature = "compression")]
use crate::compression;

use crate::{
    basic_auth, compression, control_headers, cors, custom_headers,
    basic_auth, control_headers, cors, custom_headers,
    directory_listing::DirListFmt,
    error_page,
    exts::http::MethodExt,
@@ -229,7 +232,7 @@ impl RequestHandler {
            })
            .await
            {
                Ok((mut resp, is_precompressed)) => {
                Ok((mut resp, _is_precompressed)) => {
                    // Append CORS headers if they are present
                    if let Some(cors_headers) = cors_headers {
                        if !cors_headers.is_empty() {
@@ -241,6 +244,7 @@ impl RequestHandler {
                    }

                    // Compression content encoding varies so use a `Vary` header
                    #[cfg(feature = "compression")]
                    if self.opts.compression || compression_static {
                        resp.headers_mut().append(
                            hyper::header::VARY,
@@ -249,7 +253,8 @@ impl RequestHandler {
                    }

                    // Auto compression based on the `Accept-Encoding` header
                    if self.opts.compression && !is_precompressed {
                    #[cfg(feature = "compression")]
                    if self.opts.compression && !_is_precompressed {
                        resp = match compression::auto(method, headers, resp) {
                            Ok(res) => res,
                            Err(err) => {
diff --git a/src/lib.rs b/src/lib.rs
index c758a13..e6aa663 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -82,7 +82,9 @@ extern crate serde;

// Public modules
pub mod basic_auth;
#[cfg(feature = "compression")]
pub mod compression;
#[cfg(feature = "compression")]
pub mod compression_static;
pub mod control_headers;
pub mod cors;
diff --git a/src/server.rs b/src/server.rs
index 9c1759b..cb792a3 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -166,11 +166,19 @@ impl Server {
        tracing::info!("security headers: enabled={}", security_headers);

        // Auto compression based on the `Accept-Encoding` header
        #[cfg(not(feature = "compression"))]
        let compression = false;
        #[cfg(feature = "compression")]
        let compression = general.compression;
        #[cfg(feature = "compression")]
        tracing::info!("auto compression: enabled={}", compression);

        // Check pre-compressed files based on the `Accept-Encoding` header
        #[cfg(not(feature = "compression"))]
        let compression_static = false;
        #[cfg(feature = "compression")]
        let compression_static = general.compression_static;
        #[cfg(feature = "compression")]
        tracing::info!("compression static: enabled={}", compression_static);

        // Directory listing option
diff --git a/src/settings/cli.rs b/src/settings/cli.rs
index b346558..2053fd1 100644
--- a/src/settings/cli.rs
+++ b/src/settings/cli.rs
@@ -157,6 +157,7 @@ pub struct General {
    /// Specify the file path to read the private key.
    pub http2_tls_key: Option<PathBuf>,

    #[cfg(feature = "compression")]
    #[structopt(
        long,
        short = "x",
@@ -164,16 +165,17 @@ pub struct General {
        default_value = "true",
        env = "SERVER_COMPRESSION"
    )]
    /// Gzip, Deflate or Brotli compression on demand determined by the Accept-Encoding header and applied to text-based web file types only.
    /// Gzip, Deflate, Brotli or Zstd compression on demand determined by the Accept-Encoding header and applied to text-based web file types only.
    pub compression: bool,

    #[cfg(feature = "compression")]
    #[structopt(
        long,
        parse(try_from_str),
        default_value = "false",
        env = "SERVER_COMPRESSION_STATIC"
    )]
    /// Look up the pre-compressed file variant (`.gz` or `.br`) on disk of a requested file and serves it directly if available.
    /// Look up the pre-compressed file variant (`.gz`, `.br` or `.zst`) on disk of a requested file and serves it directly if available.
    /// The compression type is determined by the `Accept-Encoding` header.
    pub compression_static: bool,

diff --git a/src/settings/file.rs b/src/settings/file.rs
index 37298a6..2de11dc 100644
--- a/src/settings/file.rs
+++ b/src/settings/file.rs
@@ -117,9 +117,11 @@ pub struct General {
    pub cache_control_headers: Option<bool>,

    /// Compression.
    #[cfg(feature = "compression")]
    pub compression: Option<bool>,

    /// Check for a pre-compressed file on disk.
    #[cfg(feature = "compression")]
    pub compression_static: Option<bool>,

    /// Error 404 pages.
diff --git a/src/settings/mod.rs b/src/settings/mod.rs
index 24b8cf8..c616489 100644
--- a/src/settings/mod.rs
+++ b/src/settings/mod.rs
@@ -77,8 +77,12 @@ impl Settings {
        let mut log_level = opts.log_level;
        let mut config_file = opts.config_file.clone();
        let mut cache_control_headers = opts.cache_control_headers;

        #[cfg(feature = "compression")]
        let mut compression = opts.compression;
        #[cfg(feature = "compression")]
        let mut compression_static = opts.compression_static;

        let mut page404 = opts.page404;
        let mut page50x = opts.page50x;
        #[cfg(feature = "http2")]
@@ -143,9 +147,11 @@ impl Settings {
                    if let Some(v) = general.cache_control_headers {
                        cache_control_headers = v
                    }
                    #[cfg(feature = "compression")]
                    if let Some(v) = general.compression {
                        compression = v
                    }
                    #[cfg(feature = "compression")]
                    if let Some(v) = general.compression_static {
                        compression_static = v
                    }
@@ -324,7 +330,9 @@ impl Settings {
                log_level,
                config_file,
                cache_control_headers,
                #[cfg(feature = "compression")]
                compression,
                #[cfg(feature = "compression")]
                compression_static,
                page404,
                page50x,
diff --git a/src/static_files.rs b/src/static_files.rs
index bdf4034..c48aa24 100644
--- a/src/static_files.rs
+++ b/src/static_files.rs
@@ -26,10 +26,12 @@ use std::path::{Component, Path, PathBuf};
use std::pin::Pin;
use std::task::{Context, Poll};

use crate::directory_listing::DirListFmt;
#[cfg(feature = "compression")]
use crate::compression_static;

use crate::exts::http::{MethodExt, HTTP_SUPPORTED_METHODS};
use crate::exts::path::PathExt;
use crate::{compression_static, directory_listing, Result};
use crate::{directory_listing, directory_listing::DirListFmt, Result};

/// Defines all options needed by the static-files handler.
pub struct HandleOpts<'a> {
@@ -198,8 +200,8 @@ fn suffix_file_html_metadata(file_path: &mut PathBuf) -> (&mut PathBuf, Option<M
/// as well as its optional pre-compressed variant.
async fn composed_file_metadata<'a>(
    mut file_path: &'a mut PathBuf,
    headers: &'a HeaderMap<HeaderValue>,
    compression_static: bool,
    _headers: &'a HeaderMap<HeaderValue>,
    _compression_static: bool,
) -> Result<FileMetadata<'a>, StatusCode> {
    tracing::trace!("getting metadata for file {}", file_path.display());

@@ -211,9 +213,10 @@ async fn composed_file_metadata<'a>(
                file_path.push("index.html");

                // Pre-compressed variant check for the autoindex
                if compression_static {
                #[cfg(feature = "compression")]
                if _compression_static {
                    if let Some(p) =
                        compression_static::precompressed_variant(file_path, headers).await
                        compression_static::precompressed_variant(file_path, _headers).await
                    {
                        return Ok(FileMetadata {
                            file_path,
@@ -243,9 +246,10 @@ async fn composed_file_metadata<'a>(
                }
            } else {
                // Fallback pre-compressed variant check for the specific file
                if compression_static {
                #[cfg(feature = "compression")]
                if _compression_static {
                    if let Some(p) =
                        compression_static::precompressed_variant(file_path, headers).await
                        compression_static::precompressed_variant(file_path, _headers).await
                    {
                        return Ok(FileMetadata {
                            file_path,
@@ -266,8 +270,10 @@ async fn composed_file_metadata<'a>(
        }
        Err(err) => {
            // Pre-compressed variant check for the file not found
            if compression_static {
                if let Some(p) = compression_static::precompressed_variant(file_path, headers).await
            #[cfg(feature = "compression")]
            if _compression_static {
                if let Some(p) =
                    compression_static::precompressed_variant(file_path, _headers).await
                {
                    return Ok(FileMetadata {
                        file_path,
@@ -283,6 +289,8 @@ async fn composed_file_metadata<'a>(
            // For example: `/posts/article` will fallback to `/posts/article.html`
            let new_meta: Option<Metadata>;
            (file_path, new_meta) = suffix_file_html_metadata(file_path);

            #[cfg(feature = "compression")]
            match new_meta {
                Some(new_meta) => {
                    return Ok(FileMetadata {
@@ -294,9 +302,9 @@ async fn composed_file_metadata<'a>(
                }
                _ => {
                    // Last pre-compressed variant check or the suffixed file not found
                    if compression_static {
                    if _compression_static {
                        if let Some(p) =
                            compression_static::precompressed_variant(file_path, headers).await
                            compression_static::precompressed_variant(file_path, _headers).await
                        {
                            return Ok(FileMetadata {
                                file_path,
@@ -308,6 +316,15 @@ async fn composed_file_metadata<'a>(
                    }
                }
            }
            #[cfg(not(feature = "compression"))]
            if let Some(new_meta) = new_meta {
                return Ok(FileMetadata {
                    file_path,
                    metadata: new_meta,
                    is_dir: false,
                    precompressed_variant: None,
                });
            }

            Err(err)
        }
diff --git a/tests/compression_static.rs b/tests/compression_static.rs
index 63715ad..1815581 100644
--- a/tests/compression_static.rs
+++ b/tests/compression_static.rs
@@ -3,6 +3,7 @@
#![deny(rust_2018_idioms)]
#![deny(dead_code)]

#[cfg(feature = "compression")]
#[cfg(test)]
mod tests {
    use bytes::Bytes;
@@ -42,6 +43,7 @@ mod tests {
            dir_listing_order: 6,
            dir_listing_format: &DirListFmt::Html,
            redirect_trailing_slash: true,
            #[cfg(feature = "compression")]
            compression_static: true,
            ignore_hidden_files: false,
        })
@@ -96,6 +98,7 @@ mod tests {
            dir_listing_order: 6,
            dir_listing_format: &DirListFmt::Html,
            redirect_trailing_slash: true,
            #[cfg(feature = "compression")]
            compression_static: true,
            ignore_hidden_files: false,
        })
@@ -147,6 +150,7 @@ mod tests {
            dir_listing_order: 6,
            dir_listing_format: &DirListFmt::Html,
            redirect_trailing_slash: true,
            #[cfg(feature = "compression")]
            compression_static: true,
            ignore_hidden_files: false,
        })
diff --git a/tests/static_files.rs b/tests/static_files.rs
index f54b9d5..1f0b01f 100644
--- a/tests/static_files.rs
+++ b/tests/static_files.rs
@@ -11,8 +11,10 @@ mod tests {
    use std::fs;
    use std::path::PathBuf;

    #[cfg(feature = "compression")]
    use static_web_server::compression;

    use static_web_server::{
        compression,
        directory_listing::DirListFmt,
        static_files::{self, HandleOpts},
    };
@@ -575,9 +577,10 @@ mod tests {
        }
    }

    #[cfg(feature = "compression")]
    #[tokio::test]
    async fn handle_file_compressions() {
        let encodings = ["gzip", "deflate", "br", "xyz"];
        let encodings = ["gzip", "deflate", "br", "zstd", "xyz"];
        let method = &Method::GET;

        for enc in encodings {
@@ -612,7 +615,7 @@ mod tests {

                    match enc {
                        // The handle only accepts `HEAD` or `GET` request methods
                        "gzip" | "deflate" | "br" => {
                        "gzip" | "deflate" | "br" | "zstd" => {
                            assert!(res.headers().get("content-length").is_none());
                            assert_eq!(res.headers()["content-encoding"], enc);
                        }