From af77e4a3cf6cda193ce0e1f9f6d249adb1fadcbc Mon Sep 17 00:00:00 2001 From: Jose Quintana <1700322+joseluisq@users.noreply.github.com> Date: Mon, 8 May 2023 00:05:11 +0200 Subject: [PATCH] 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] --- 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 * -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) -> Option { - if let Some(ref accept_encoding) = headers.typed_get::() { - 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: ` 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, @@ -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, @@ -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, @@ -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, @@ -184,6 +195,14 @@ pub fn create_encoding_header(existing: Option, coding: ContentCodi coding.into() } +/// Try to get the prefered `content-encoding` via the `accept-encoding` header. +pub fn get_prefered_encoding(headers: &HeaderMap) -> Option { + if let Some(ref accept_encoding) = headers.typed_get::() { + 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, + #[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, /// Compression. + #[cfg(feature = "compression")] pub compression: Option, /// Check for a pre-compressed file on disk. + #[cfg(feature = "compression")] pub compression_static: Option, /// 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( mut file_path: &'a mut PathBuf, - headers: &'a HeaderMap, - compression_static: bool, + _headers: &'a HeaderMap, + _compression_static: bool, ) -> Result, 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; (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); } -- libgit2 1.7.2