index : static-web-server.git

ascending towards madness

author Jose Quintana <joseluisquintana20@gmail.com> 2021-04-29 21:00:49.0 +00:00:00
committer Jose Quintana <joseluisquintana20@gmail.com> 2021-04-29 21:04:01.0 +00:00:00
commit
4290e21c76e2904a489be14fa08878aeafac141b [patch]
tree
d00a661d2948cd3a8f93794ec207c8a8ec814a49
parent
c47b898f004382f81e30a9bb8bd83c072cb15ae1
download
4290e21c76e2904a489be14fa08878aeafac141b.tar.gz

refactor: http1 with static files

part of #37

Diff

 Cargo.lock         | 432 +++---------------------------------------------------
 Cargo.toml         |  16 +-
 README.md          |   2 +-
 src/cache.rs       |  38 +-----
 src/compression.rs |  19 +--
 src/controller.rs  |  38 +++++-
 src/cors.rs        |  36 +-----
 src/error_page.rs  |   4 +-
 src/fs.rs          | 382 ++++++++++++++++++++++++++++++++++++++++++++++++-
 src/lib.rs         |   8 +-
 src/rejection.rs   |  46 +------
 src/server.rs      | 161 ++++----------------
 src/signals.rs     |  51 +------
 13 files changed, 504 insertions(+), 729 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index a4d5eb2..1d8ce00 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,27 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"

[[package]]
name = "alloc-no-stdlib"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192ec435945d87bc2f70992b4d818154b5feede43c09fb7592146374eac90a6"

[[package]]
name = "alloc-stdlib"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2"
dependencies = [
 "alloc-no-stdlib",
]

[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -37,20 +16,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"

[[package]]
name = "async-compression"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443ccbb270374a2b1055fc72da40e1f237809cd6bb0e97e66d264cd138473a6"
dependencies = [
 "brotli",
 "flate2",
 "futures-core",
 "memchr",
 "pin-project-lite",
 "tokio",
]

[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -78,33 +43,6 @@ dependencies = [
]

[[package]]
name = "brotli"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f29919120f08613aadcd4383764e00526fc9f18b6c0895814faeed0dd78613e"
dependencies = [
 "alloc-no-stdlib",
 "alloc-stdlib",
 "brotli-decompressor",
]

[[package]]
name = "brotli-decompressor"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1052e1c3b8d4d80eb84a8b94f0a1498797b5fb96314c001156a1c761940ef4ec"
dependencies = [
 "alloc-no-stdlib",
 "alloc-stdlib",
]

[[package]]
name = "bumpalo"
version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe"

[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -124,12 +62,6 @@ checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"

[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"

[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
@@ -164,15 +96,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634"

[[package]]
name = "crc32fast"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
dependencies = [
 "cfg-if 1.0.0",
]

[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -182,40 +105,12 @@ dependencies = [
]

[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"

[[package]]
name = "flate2"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0"
dependencies = [
 "cfg-if 1.0.0",
 "crc32fast",
 "libc",
 "miniz_oxide",
]

[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"

[[package]]
name = "form_urlencoded"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
dependencies = [
 "matches",
 "percent-encoding",
]

[[package]]
name = "fs_extra"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -293,41 +188,16 @@ dependencies = [
]

[[package]]
name = "h2"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc018e188373e2777d0ef2467ebff62a08e66c3f5857b23c8fbec3018210dc00"
dependencies = [
 "bytes",
 "fnv",
 "futures-core",
 "futures-sink",
 "futures-util",
 "http",
 "indexmap",
 "slab",
 "tokio",
 "tokio-util",
 "tracing",
]

[[package]]
name = "hashbrown"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"

[[package]]
name = "headers"
version = "0.3.4"
source = "git+https://github.com/joseluisq/hyper-headers.git?branch=headers_encoding#ca704fcb605adf33f327d0f5a41d5072606058a1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0b7591fb62902706ae8e7aaff416b1b0fa2c0fd0878b46dc13baa3712d8a855"
dependencies = [
 "base64",
 "bitflags",
 "bytes",
 "headers-core",
 "http",
 "itertools",
 "mime",
 "sha-1",
 "time",
@@ -336,7 +206,8 @@ dependencies = [
[[package]]
name = "headers-core"
version = "0.2.0"
source = "git+https://github.com/joseluisq/hyper-headers.git?branch=headers_encoding#ca704fcb605adf33f327d0f5a41d5072606058a1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
dependencies = [
 "http",
]
@@ -403,7 +274,6 @@ dependencies = [
 "futures-channel",
 "futures-core",
 "futures-util",
 "h2",
 "http",
 "http-body",
 "httparse",
@@ -418,25 +288,6 @@ dependencies = [
]

[[package]]
name = "indexmap"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
dependencies = [
 "autocfg",
 "hashbrown",
]

[[package]]
name = "itertools"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
dependencies = [
 "either",
]

[[package]]
name = "itoa"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -464,15 +315,6 @@ dependencies = [
]

[[package]]
name = "js-sys"
version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c"
dependencies = [
 "wasm-bindgen",
]

[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -490,7 +332,7 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
 "cfg-if 1.0.0",
 "cfg-if",
]

[[package]]
@@ -503,12 +345,6 @@ dependencies = [
]

[[package]]
name = "matches"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"

[[package]]
name = "memchr"
version = "2.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -531,16 +367,6 @@ dependencies = [
]

[[package]]
name = "miniz_oxide"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
dependencies = [
 "adler",
 "autocfg",
]

[[package]]
name = "mio"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -563,19 +389,6 @@ dependencies = [
]

[[package]]
name = "nix"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c722bee1037d430d0f8e687bbdbf222f27cc6e4e68d5caf630857bb2b6dbdce"
dependencies = [
 "bitflags",
 "cc",
 "cfg-if 0.1.10",
 "libc",
 "void",
]

[[package]]
name = "ntapi"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -731,56 +544,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548"

[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
 "cc",
 "libc",
 "once_cell",
 "spin",
 "untrusted",
 "web-sys",
 "winapi",
]

[[package]]
name = "rustls"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7"
dependencies = [
 "base64",
 "log",
 "ring",
 "sct",
 "webpki",
]

[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"

[[package]]
name = "scoped-tls"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"

[[package]]
name = "sct"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce"
dependencies = [
 "ring",
 "untrusted",
]

[[package]]
name = "serde"
version = "1.0.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -798,25 +567,13 @@ dependencies = [
]

[[package]]
name = "serde_urlencoded"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
dependencies = [
 "form_urlencoded",
 "itoa",
 "ryu",
 "serde",
]

[[package]]
name = "sha-1"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfebf75d25bd900fd1e7d11501efab59bc846dbc76196839663e6637bba9f25f"
dependencies = [
 "block-buffer",
 "cfg-if 1.0.0",
 "cfg-if",
 "cpuid-bool",
 "digest",
 "opaque-debug",
@@ -832,22 +589,15 @@ dependencies = [
]

[[package]]
name = "signal"
version = "0.7.0"
name = "signal-hook-registry"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f6ce83b159ab6984d2419f495134972b48754d13ff2e3f8c998339942b56ed9"
checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6"
dependencies = [
 "libc",
 "nix",
]

[[package]]
name = "slab"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527"

[[package]]
name = "smallvec"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -864,26 +614,24 @@ dependencies = [
]

[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"

[[package]]
name = "static-web-server"
version = "2.0.0-beta.3"
dependencies = [
 "anyhow",
 "bytes",
 "futures",
 "headers",
 "hyper",
 "jemallocator",
 "nix",
 "mime_guess",
 "num_cpus",
 "once_cell",
 "signal",
 "percent-encoding",
 "structopt",
 "tokio",
 "tokio-util",
 "tracing",
 "tracing-subscriber",
 "warp",
]

[[package]]
@@ -912,9 +660,9 @@ dependencies = [

[[package]]
name = "syn"
version = "1.0.70"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9505f307c872bab8eb46f77ae357c8eba1fdacead58ee5a850116b1d7f82883"
checksum = "ad184cc9470f9117b2ac6817bfe297307418819ba40552f9b3846f05c33d5373"
dependencies = [
 "proc-macro2",
 "quote",
@@ -962,29 +710,22 @@ dependencies = [
 "memchr",
 "mio",
 "num_cpus",
 "once_cell",
 "pin-project-lite",
 "signal-hook-registry",
 "tokio-macros",
 "winapi",
]

[[package]]
name = "tokio-rustls"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6"
dependencies = [
 "rustls",
 "tokio",
 "webpki",
]

[[package]]
name = "tokio-stream"
version = "0.1.5"
name = "tokio-macros"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e177a5d8c3bf36de9ebe6d58537d8879e964332f93fb3339e43f618c81361af0"
checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57"
dependencies = [
 "futures-core",
 "pin-project-lite",
 "tokio",
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
@@ -1013,8 +754,7 @@ version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01ebdc2bb4498ab1ab5f5b73c5803825e60199229ccba0698170e3be0e7f959f"
dependencies = [
 "cfg-if 1.0.0",
 "log",
 "cfg-if",
 "pin-project-lite",
 "tracing-attributes",
 "tracing-core",
@@ -1123,24 +863,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"

[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"

[[package]]
name = "version_check"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"

[[package]]
name = "void"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"

[[package]]
name = "want"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1151,114 +879,12 @@ dependencies = [
]

[[package]]
name = "warp"
version = "0.3.1"
source = "git+https://github.com/joseluisq/warp.git?branch=0.3.x#f638f8958addb953501a08d427aae64a4c4f5a21"
dependencies = [
 "async-compression",
 "bytes",
 "futures",
 "headers",
 "http",
 "hyper",
 "log",
 "mime",
 "mime_guess",
 "percent-encoding",
 "pin-project",
 "scoped-tls",
 "serde",
 "serde_json",
 "serde_urlencoded",
 "tokio",
 "tokio-rustls",
 "tokio-stream",
 "tokio-util",
 "tower-service",
 "tracing",
]

[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"

[[package]]
name = "wasm-bindgen"
version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9"
dependencies = [
 "cfg-if 1.0.0",
 "wasm-bindgen-macro",
]

[[package]]
name = "wasm-bindgen-backend"
version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae"
dependencies = [
 "bumpalo",
 "lazy_static",
 "log",
 "proc-macro2",
 "quote",
 "syn",
 "wasm-bindgen-shared",
]

[[package]]
name = "wasm-bindgen-macro"
version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f"
dependencies = [
 "quote",
 "wasm-bindgen-macro-support",
]

[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "wasm-bindgen-backend",
 "wasm-bindgen-shared",
]

[[package]]
name = "wasm-bindgen-shared"
version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489"

[[package]]
name = "web-sys"
version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be"
dependencies = [
 "js-sys",
 "wasm-bindgen",
]

[[package]]
name = "webpki"
version = "0.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
dependencies = [
 "ring",
 "untrusted",
]

[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 44fc6c2..3e393ce 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,21 +25,21 @@ name = "static-web-server"
path = "src/bin/server.rs"

[dependencies]
tokio = { version = "1", features = ["rt-multi-thread"], default-features = false }
warp = { git = "https://github.com/joseluisq/warp.git", branch = "0.3.x", features = ["tls", "compression"], default-features = false }
hyper = { version = "0.14", features = ["stream", "http1", "tcp", "server"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "fs", "io-util"], default-features = false }
futures = { version = "0.3", default-features = false }
headers = "0.3"
tokio-util = { version = "0.6", features = ["io"] }
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = "0.2"
mime_guess = "2.0"
bytes = "1.0"
percent-encoding = "2.1"
structopt = { version = "0.3", default-features = false }
num_cpus = { version = "1.13" }
once_cell = "1.7"

[target.'cfg(not(windows))'.dependencies.nix]
version = "0.14"

[target.'cfg(not(windows))'.dependencies.signal]
version = "0.7"

[target.'cfg(all(target_env = "musl", target_pointer_width = "64"))'.dependencies.jemallocator]
version = "0.3"

diff --git a/README.md b/README.md
index 73985e6..69a6a2e 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@

- 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 and asynchronous 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.
- Blazing fast static files-serving and asynchronous powered by [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.
- GZip, Deflate or 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/cache.rs b/src/cache.rs
deleted file mode 100644
index c32e1c1..0000000
--- a/src/cache.rs
+++ /dev/null
@@ -1,38 +0,0 @@
const CACHE_EXT_ONE_HOUR: [&str; 4] = ["atom", "json", "rss", "xml"];
const CACHE_EXT_ONE_YEAR: [&str; 30] = [
    "bmp", "bz2", "css", "doc", "gif", "gz", "htc", "ico", "jpeg", "jpg", "js", "map", "mjs",
    "mp3", "mp4", "ogg", "ogv", "pdf", "png", "rar", "rtf", "tar", "tgz", "wav", "weba", "webm",
    "webp", "woff", "woff2", "zip",
];

/// It applies the corresponding Cache-Control headers based on a set of file types.
pub fn control_headers(res: warp::fs::File) -> warp::reply::WithHeader<warp::fs::File> {
    // Default max-age value in seconds (one day)
    let mut max_age = 60 * 60 * 24_u64;

    if let Some(ext) = res.path().extension() {
        if let Some(ext) = ext.to_str() {
            if CACHE_EXT_ONE_HOUR.iter().any(|x| *x == ext) {
                max_age = 60 * 60;
            } else if CACHE_EXT_ONE_YEAR.iter().any(|x| *x == ext) {
                max_age = 60 * 60 * 24 * 365;
            }
        }
    }

    // HTML file types and others
    warp::reply::with_header(
        res,
        "cache-control",
        [
            "public, max-age=".to_string(),
            duration(max_age).to_string(),
        ]
        .concat(),
    )
}

/// It caps a duration value at ~136 years.
fn duration(n: u64) -> u32 {
    std::cmp::min(n, u32::MAX as u64) as u32
}
diff --git a/src/compression.rs b/src/compression.rs
deleted file mode 100644
index 7dd180d..0000000
--- a/src/compression.rs
+++ /dev/null
@@ -1,19 +0,0 @@
/// Contains a common fixed list of text-based MIME types in order to apply compression.
pub const TEXT_MIME_TYPES: [&str; 16] = [
    "text/html",
    "text/css",
    "text/javascript",
    "text/xml",
    "text/plain",
    "text/x-component",
    "application/javascript",
    "application/x-javascript",
    "application/json",
    "application/xml",
    "application/rss+xml",
    "application/atom+xml",
    "font/truetype",
    "font/opentype",
    "application/vnd.ms-fontobject",
    "image/svg+xml",
];
diff --git a/src/controller.rs b/src/controller.rs
new file mode 100644
index 0000000..bb44d2f
--- /dev/null
+++ b/src/controller.rs
@@ -0,0 +1,38 @@
use headers::{AcceptRanges, ContentLength, ContentType, HeaderMapExt};
use hyper::{Body, Request, Response};
use std::path::Path;

use crate::error::Result;
use crate::fs;

/// Main server request entry point.
pub async fn handle(base: &Path, req: Request<Body>) -> Result<Response<Body>> {
    let path = req.uri().path();
    let resp = fs::handle_request(base, req.headers(), path).await;
    match resp {
        Ok(resp) => Ok(resp),
        Err(status) => {
            let method = req.method();
            tracing::warn!(method = ?method, status = status.as_u16(), error = ?status.to_string());

            let mut body = Body::empty();
            let mut len = 0_u64;
            if method == hyper::Method::GET {
                let content = format!(
                    "<html><head><title>{}</title></head><body><center><h1>{}</h1></center></body></html>",
                    status, status
                );
                len = content.len() as u64;
                body = Body::from(content)
            }

            let mut resp = Response::new(body);
            *resp.status_mut() = status;
            resp.headers_mut().typed_insert(ContentLength(len));
            resp.headers_mut().typed_insert(ContentType::html());
            resp.headers_mut().typed_insert(AcceptRanges::bytes());

            Ok(resp)
        }
    }
}
diff --git a/src/cors.rs b/src/cors.rs
deleted file mode 100644
index 9482ba9..0000000
--- a/src/cors.rs
+++ /dev/null
@@ -1,36 +0,0 @@
use std::collections::HashSet;
use warp::filters::cors::Builder;

/// Warp filter which provides an optional CORS if its supported.
pub fn get_opt_cors_filter(origins: &str) -> (Option<Builder>, String) {
    let mut cors_allowed_hosts = String::new();
    let cors_filter = if origins.is_empty() {
        None
    } else if origins == "*" {
        cors_allowed_hosts = origins.into();
        Some(
            warp::cors()
                .allow_any_origin()
                .allow_methods(vec!["GET", "HEAD", "OPTIONS"]),
        )
    } else {
        cors_allowed_hosts = origins.into();
        let hosts = cors_allowed_hosts
            .split(',')
            .map(|s| s.trim().as_ref())
            .collect::<HashSet<_>>();

        if hosts.is_empty() {
            cors_allowed_hosts = hosts.into_iter().collect::<Vec<&str>>().join(", ");
            None
        } else {
            Some(
                warp::cors()
                    .allow_origins(hosts)
                    .allow_methods(vec!["GET", "HEAD", "OPTIONS"]),
            )
        }
    };

    (cors_filter, cors_allowed_hosts)
}
diff --git a/src/error_page.rs b/src/error_page.rs
new file mode 100644
index 0000000..db455f2
--- /dev/null
+++ b/src/error_page.rs
@@ -0,0 +1,4 @@
use once_cell::sync::OnceCell;

pub static PAGE_404: OnceCell<String> = OnceCell::new();
pub static PAGE_50X: OnceCell<String> = OnceCell::new();
diff --git a/src/fs.rs b/src/fs.rs
new file mode 100644
index 0000000..4f5ff62
--- /dev/null
+++ b/src/fs.rs
@@ -0,0 +1,382 @@
// Most of the this file is borrowed from https://github.com/seanmonstar/warp/blob/master/src/filters/fs.rs

use bytes::{Bytes, BytesMut};
use futures::future::Either;
use futures::{future, ready, stream, FutureExt, Stream, StreamExt, TryFutureExt};
use headers::{
    AcceptRanges, ContentLength, ContentRange, ContentType, HeaderMap, HeaderMapExt, HeaderValue,
    IfModifiedSince, IfRange, IfUnmodifiedSince, LastModified, Range,
};
use hyper::{Body, Response, StatusCode};
use percent_encoding::percent_decode_str;
use std::fs::Metadata;
use std::future::Future;
use std::io;
use std::ops::Bound;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use std::task::Poll;
use std::{cmp, path::Path};
use tokio::fs::File as TkFile;
use tokio::io::AsyncSeekExt;
use tokio_util::io::poll_read_buf;

/// A small Arch `PathBuf` wrapper since Arc<PathBuf> doesn't implement AsRef<Path>.
#[derive(Clone, Debug)]
pub struct ArcPath(pub Arc<PathBuf>);

impl AsRef<Path> for ArcPath {
    fn as_ref(&self) -> &Path {
        (*self.0).as_ref()
    }
}

/// Entry point to handle web server requests which map to specific files
/// on file system and return a file response.
pub async fn handle_request(
    base: &Path,
    headers: &HeaderMap<HeaderValue>,
    path: &str,
) -> Result<Response<Body>, StatusCode> {
    let base = Arc::new(base.into());
    let path = path_from_tail(base, path).await?;
    file_reply(headers, path).await
}

/// Reply with a file content.
fn file_reply(
    headers: &HeaderMap<HeaderValue>,
    path: ArcPath,
) -> impl Future<Output = Result<Response<Body>, StatusCode>> + Send {
    let conditionals = get_conditional_headers(headers);
    TkFile::open(path.clone()).then(move |res| match res {
        Ok(f) => Either::Left(file_conditional(f, path, conditionals)),
        Err(err) => {
            let rej = match err.kind() {
                io::ErrorKind::NotFound => {
                    tracing::debug!("file not found: {:?}", path.as_ref().display());
                    StatusCode::NOT_FOUND
                }
                io::ErrorKind::PermissionDenied => {
                    tracing::warn!("file permission denied: {:?}", path.as_ref().display());
                    StatusCode::FORBIDDEN
                }
                _ => {
                    tracing::error!(
                        "file open error (path={:?}): {} ",
                        path.as_ref().display(),
                        err
                    );
                    StatusCode::INTERNAL_SERVER_ERROR
                }
            };
            Either::Right(future::err(rej))
        }
    })
}

fn get_conditional_headers(header_list: &HeaderMap<HeaderValue>) -> Conditionals {
    let if_modified_since = header_list.typed_get::<IfModifiedSince>();
    let if_unmodified_since = header_list.typed_get::<IfUnmodifiedSince>();
    let if_range = header_list.typed_get::<IfRange>();
    let range = header_list.typed_get::<Range>();

    Conditionals {
        if_modified_since,
        if_unmodified_since,
        if_range,
        range,
    }
}

fn path_from_tail(
    base: Arc<PathBuf>,
    tail: &str,
) -> impl Future<Output = Result<ArcPath, StatusCode>> + Send {
    future::ready(sanitize_path(base.as_ref(), tail)).and_then(|mut buf| async {
        let is_dir = tokio::fs::metadata(buf.clone())
            .await
            .map(|m| m.is_dir())
            .unwrap_or(false);

        if is_dir {
            tracing::debug!("dir: appending index.html to directory path");
            buf.push("index.html");
        }
        tracing::trace!("dir: {:?}", buf);
        Ok(ArcPath(Arc::new(buf)))
    })
}

fn sanitize_path(base: impl AsRef<Path>, tail: &str) -> Result<PathBuf, StatusCode> {
    let mut buf = PathBuf::from(base.as_ref());
    let p = match percent_decode_str(tail).decode_utf8() {
        Ok(p) => p,
        Err(err) => {
            tracing::debug!("dir: failed to decode route={:?}: {:?}", tail, err);
            return Err(StatusCode::UNSUPPORTED_MEDIA_TYPE);
        }
    };
    tracing::trace!("dir? base={:?}, route={:?}", base.as_ref(), p);
    for seg in p.split('/') {
        if seg.starts_with("..") {
            tracing::warn!("dir: rejecting segment starting with '..'");
            return Err(StatusCode::NOT_FOUND);
        } else if seg.contains('\\') {
            tracing::warn!("dir: rejecting segment containing with backslash (\\)");
            return Err(StatusCode::NOT_FOUND);
        } else {
            buf.push(seg);
        }
    }
    Ok(buf)
}

#[derive(Debug)]
struct Conditionals {
    if_modified_since: Option<IfModifiedSince>,
    if_unmodified_since: Option<IfUnmodifiedSince>,
    if_range: Option<IfRange>,
    range: Option<Range>,
}

enum Cond {
    NoBody(Response<Body>),
    WithBody(Option<Range>),
}

impl Conditionals {
    fn check(self, last_modified: Option<LastModified>) -> Cond {
        if let Some(since) = self.if_unmodified_since {
            let precondition = last_modified
                .map(|time| since.precondition_passes(time.into()))
                .unwrap_or(false);

            tracing::trace!(
                "if-unmodified-since? {:?} vs {:?} = {}",
                since,
                last_modified,
                precondition
            );
            if !precondition {
                let mut res = Response::new(Body::empty());
                *res.status_mut() = StatusCode::PRECONDITION_FAILED;
                return Cond::NoBody(res);
            }
        }

        if let Some(since) = self.if_modified_since {
            tracing::trace!(
                "if-modified-since? header = {:?}, file = {:?}",
                since,
                last_modified
            );
            let unmodified = last_modified
                .map(|time| !since.is_modified(time.into()))
                // no last_modified means its always modified
                .unwrap_or(false);
            if unmodified {
                let mut res = Response::new(Body::empty());
                *res.status_mut() = StatusCode::NOT_MODIFIED;
                return Cond::NoBody(res);
            }
        }

        if let Some(if_range) = self.if_range {
            tracing::trace!("if-range? {:?} vs {:?}", if_range, last_modified);
            let can_range = !if_range.is_modified(None, last_modified.as_ref());
            if !can_range {
                return Cond::WithBody(None);
            }
        }

        Cond::WithBody(self.range)
    }
}

fn file_conditional(
    f: TkFile,
    path: ArcPath,
    conditionals: Conditionals,
) -> impl Future<Output = Result<Response<Body>, StatusCode>> + Send {
    file_metadata(f).map_ok(move |(file, meta)| {
        let mut len = meta.len();
        let modified = meta.modified().ok().map(LastModified::from);

        match conditionals.check(modified) {
            Cond::NoBody(resp) => resp,
            Cond::WithBody(range) => {
                bytes_range(range, len)
                    .map(|(start, end)| {
                        let sub_len = end - start;
                        let buf_size = optimal_buf_size(&meta);
                        let stream = file_stream(file, buf_size, (start, end));
                        let body = Body::wrap_stream(stream);

                        let mut resp = Response::new(body);

                        if sub_len != len {
                            *resp.status_mut() = StatusCode::PARTIAL_CONTENT;
                            resp.headers_mut().typed_insert(
                                ContentRange::bytes(start..end, len).expect("valid ContentRange"),
                            );

                            len = sub_len;
                        }

                        let mime = mime_guess::from_path(path.as_ref()).first_or_octet_stream();

                        resp.headers_mut().typed_insert(ContentLength(len));
                        resp.headers_mut().typed_insert(ContentType::from(mime));
                        resp.headers_mut().typed_insert(AcceptRanges::bytes());

                        if let Some(last_modified) = modified {
                            resp.headers_mut().typed_insert(last_modified);
                        }

                        resp
                    })
                    .unwrap_or_else(|BadRange| {
                        // bad byte range
                        let mut resp = Response::new(Body::empty());
                        *resp.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
                        resp.headers_mut()
                            .typed_insert(ContentRange::unsatisfied_bytes(len));
                        resp
                    })
            }
        }
    })
}

async fn file_metadata(f: TkFile) -> Result<(TkFile, Metadata), StatusCode> {
    match f.metadata().await {
        Ok(meta) => Ok((f, meta)),
        Err(err) => {
            tracing::debug!("file metadata error: {}", err);
            Err(StatusCode::INTERNAL_SERVER_ERROR)
        }
    }
}

struct BadRange;

fn bytes_range(range: Option<Range>, max_len: u64) -> Result<(u64, u64), BadRange> {
    let range = if let Some(range) = range {
        range
    } else {
        return Ok((0, max_len));
    };

    let ret = range
        .iter()
        .map(|(start, end)| {
            let start = match start {
                Bound::Unbounded => 0,
                Bound::Included(s) => s,
                Bound::Excluded(s) => s + 1,
            };

            let end = match end {
                Bound::Unbounded => max_len,
                Bound::Included(s) => s + 1,
                Bound::Excluded(s) => s,
            };

            if start < end && end <= max_len {
                Ok((start, end))
            } else {
                tracing::trace!("unsatisfiable byte range: {}-{}/{}", start, end, max_len);
                Err(BadRange)
            }
        })
        .next()
        .unwrap_or(Ok((0, max_len)));
    ret
}

fn file_stream(
    mut file: TkFile,
    buf_size: usize,
    (start, end): (u64, u64),
) -> impl Stream<Item = Result<Bytes, io::Error>> + Send {
    let seek = async move {
        if start != 0 {
            file.seek(io::SeekFrom::Start(start)).await?;
        }
        Ok(file)
    };

    seek.into_stream()
        .map(move |result| {
            let mut buf = BytesMut::new();
            let mut len = end - start;
            let mut f = match result {
                Ok(f) => f,
                Err(f) => return Either::Left(stream::once(future::err(f))),
            };

            Either::Right(stream::poll_fn(move |cx| {
                if len == 0 {
                    return Poll::Ready(None);
                }
                reserve_at_least(&mut buf, buf_size);

                let n = match ready!(poll_read_buf(Pin::new(&mut f), cx, &mut buf)) {
                    Ok(n) => n as u64,
                    Err(err) => {
                        tracing::debug!("file read error: {}", err);
                        return Poll::Ready(Some(Err(err)));
                    }
                };

                if n == 0 {
                    tracing::debug!("file read found EOF before expected length");
                    return Poll::Ready(None);
                }

                let mut chunk = buf.split().freeze();
                if n > len {
                    chunk = chunk.split_to(len as usize);
                    len = 0;
                } else {
                    len -= n;
                }

                Poll::Ready(Some(Ok(chunk)))
            }))
        })
        .flatten()
}

fn reserve_at_least(buf: &mut BytesMut, cap: usize) {
    if buf.capacity() - buf.len() < cap {
        buf.reserve(cap);
    }
}

const DEFAULT_READ_BUF_SIZE: usize = 8_192;

fn optimal_buf_size(metadata: &Metadata) -> usize {
    let block_size = get_block_size(metadata);

    // If file length is smaller than block size, don't waste space
    // reserving a bigger-than-needed buffer.
    cmp::min(block_size as u64, metadata.len()) as usize
}

#[cfg(unix)]
fn get_block_size(metadata: &Metadata) -> usize {
    use std::os::unix::fs::MetadataExt;
    //TODO: blksize() returns u64, should handle bad cast...
    //(really, a block size bigger than 4gb?)

    // Use device blocksize unless it's really small.
    cmp::max(metadata.blksize() as usize, DEFAULT_READ_BUF_SIZE)
}

#[cfg(not(unix))]
fn get_block_size(_metadata: &Metadata) -> usize {
    DEFAULT_READ_BUF_SIZE
}
diff --git a/src/lib.rs b/src/lib.rs
index 57c9027..b2bc1a3 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,15 +3,13 @@
#[macro_use]
extern crate anyhow;

pub mod cache;
pub mod compression;
pub mod config;
pub mod cors;
pub mod controller;
pub mod error_page;
pub mod fs;
pub mod helpers;
pub mod logger;
pub mod rejection;
pub mod server;
pub mod signals;

#[macro_use]
pub mod error;
diff --git a/src/rejection.rs b/src/rejection.rs
deleted file mode 100644
index 75a825a..0000000
--- a/src/rejection.rs
+++ /dev/null
@@ -1,46 +0,0 @@
use anyhow::Result;
use once_cell::sync::OnceCell;
use std::convert::Infallible;
use warp::http::StatusCode;
use warp::{Rejection, Reply};

pub static PAGE_404: OnceCell<String> = OnceCell::new();
pub static PAGE_50X: OnceCell<String> = OnceCell::new();

/// It receives a `Rejection` and tries to return the corresponding HTML error reply.
pub async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> {
    let mut content = String::new();
    let code = if err.is_not_found() {
        content = PAGE_404
            .get()
            .expect("page 404 is not initialized")
            .to_string();
        StatusCode::NOT_FOUND
    } else if err
        .find::<warp::filters::body::BodyDeserializeError>()
        .is_some()
    {
        StatusCode::BAD_REQUEST
    } else if err.find::<warp::reject::MethodNotAllowed>().is_some() {
        StatusCode::METHOD_NOT_ALLOWED
    } else if err.find::<warp::filters::cors::CorsForbidden>().is_some() {
        StatusCode::FORBIDDEN
    } else if err.find::<warp::reject::UnsupportedMediaType>().is_some() {
        StatusCode::UNSUPPORTED_MEDIA_TYPE
    } else {
        content = PAGE_50X
            .get()
            .expect("page 50x is not initialized")
            .to_string();
        StatusCode::INTERNAL_SERVER_ERROR
    };

    if content.is_empty() {
        content = format!(
            "<html><head><title>{}</title></head><body><center><h1>{}</h1></center></body></html>",
            code, code
        );
    }

    Ok(warp::reply::with_status(warp::reply::html(content), code))
}
diff --git a/src/server.rs b/src/server.rs
index bc887fe..c18aaba 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -1,13 +1,16 @@
use hyper::server::Server as HyperServer;
use hyper::service::{make_service_fn, service_fn};
use std::net::{IpAddr, SocketAddr};
use std::path::PathBuf;
use std::sync::Arc;
use structopt::StructOpt;
use warp::Filter;
use tokio::signal;

use crate::{cache, cors, helpers, logger, rejection, Result};
use crate::{
    compression::TEXT_MIME_TYPES,
    config::{Config, CONFIG},
    error_page,
};
use crate::{controller::handle, fs::ArcPath};
use crate::{error, helpers, logger, Result};

/// Define a multi-thread HTTP or HTTP/2 web server.
pub struct Server {
@@ -25,10 +28,11 @@ impl Server {
            0 | 1 => 1,
            _ => num_cpus::get() * opts.threads_multiplier,
        };

        Self { threads }
    }

    /// Build and run the multi-thread `Server` spawning a new Tokio asynchronous task for it.
    /// Build and run the multi-thread `Server`.
    pub fn run(self) -> Result {
        tokio::runtime::Builder::new_multi_thread()
            .enable_all()
@@ -36,7 +40,7 @@ impl Server {
            .worker_threads(self.threads)
            .build()?
            .block_on(async {
                let r = self.run_server_with_config().await;
                let r = self.start_server().await;
                if r.is_err() {
                    panic!("Server error during start up: {:?}", r.unwrap_err())
                }
@@ -45,8 +49,8 @@ impl Server {
        Ok(())
    }

    /// Create and run the `Warp` server spawning a new Tokio asynchronous task with the given configuration.
    async fn run_server_with_config(self) -> Result {
    /// Run the inner Hyper `HyperServer` forever on the current thread with the given configuration.
    async fn start_server(self) -> Result {
        let opts = Config::global();

        logger::init(&opts.log_level)?;
@@ -61,34 +65,41 @@ impl Server {
        let root_dir = helpers::get_valid_dirpath(&opts.root)?;

        // Custom error pages content
        rejection::PAGE_404
        error_page::PAGE_404
            .set(helpers::read_file_content(opts.page404.as_ref()))
            .expect("page 404 is not initialized");
        rejection::PAGE_50X
        error_page::PAGE_50X
            .set(helpers::read_file_content(opts.page50x.as_ref()))
            .expect("page 50x is not initialized");

        // CORS support
        let (cors_filter_opt, cors_allowed_origins) =
            cors::get_opt_cors_filter(opts.cors_allow_origins.as_ref());
        // TODO: CORS support

        // HTTP/2 + TLS
        let http2 = opts.http2;
        let http2_tls_cert_path = &opts.http2_tls_cert;
        let http2_tls_key_path = &opts.http2_tls_key;
        // TODO: HTTP/2 + TLS

        // Spawn a new Tokio asynchronous server task determined by the given options
        tokio::task::spawn(run_server_with_options(
            addr,
            root_dir,
            http2,
            http2_tls_cert_path,
            http2_tls_key_path,
            cors_filter_opt,
            cors_allowed_origins,
        ));

        handle_signals();

        let span = tracing::info_span!("Server::run", ?addr, threads = ?self.threads);
        tracing::info!(parent: &span, "listening on http://{}", addr);

        let root_dir = ArcPath(Arc::new(root_dir));
        let create_service = make_service_fn(move |_| {
            let root_dir = root_dir.clone();
            async move {
                Ok::<_, error::Error>(service_fn(move |req| {
                    let root_dir = root_dir.clone();
                    async move { handle(root_dir.as_ref(), req).await }
                }))
            }
        });
        HyperServer::bind(&addr)
            .serve(create_service)
            .with_graceful_shutdown(async {
                signal::ctrl_c()
                    .await
                    .expect("failed to install CTRL+C signal handler");
                tracing::warn!(parent: &span, "CTRL+C signal caught and execution exited");
            })
            .await?;

        Ok(())
    }
@@ -99,97 +110,3 @@ impl Default for Server {
        Self::new()
    }
}

/// It creates and starts a Warp HTTP or HTTP/2 server with its options.
pub async fn run_server_with_options(
    addr: SocketAddr,
    root_dir: PathBuf,
    http2: bool,
    http2_tls_cert_path: &'static str,
    http2_tls_key_path: &'static str,
    cors_filter_opt: Option<warp::filters::cors::Builder>,
    cors_allowed_origins: String,
) {
    // Base fs directory filter
    let base_fs_dir_filter = warp::fs::dir(root_dir.clone())
        .map(cache::control_headers)
        .with(warp::trace::request())
        .recover(rejection::handle_rejection);

    // Public HEAD endpoint
    let public_head = warp::head().and(base_fs_dir_filter.clone());

    // Public GET endpoint (default)
    let public_get_default = warp::get().and(base_fs_dir_filter);

    // Current fs directory filter
    let fs_dir_filter = warp::fs::dir(root_dir)
        .map(cache::control_headers)
        .with(warp::compression::auto(|headers| {
            // Skip compression for non-text-based MIME types
            if let Some(content_type) = headers.get("content-type") {
                !TEXT_MIME_TYPES.iter().any(|h| h == content_type)
            } else {
                false
            }
        }))
        .with(warp::trace::request())
        .recover(rejection::handle_rejection);

    // Determine CORS filter
    if let Some(cors_filter) = cors_filter_opt {
        tracing::info!(
            cors_enabled = ?true,
            allowed_origins = ?cors_allowed_origins
        );

        let public_head = public_head.with(cors_filter.clone());
        let public_get_default = public_get_default.with(cors_filter.clone());
        let public_get = warp::get().and(fs_dir_filter).with(cors_filter.clone());

        let server = warp::serve(public_head.or(public_get).or(public_get_default));

        if http2 {
            server
                .tls()
                .cert_path(http2_tls_cert_path)
                .key_path(http2_tls_key_path)
                .run(addr)
                .await
        } else {
            server.run(addr).await
        }
    } else {
        let public_get = warp::get().and(fs_dir_filter);

        let server = warp::serve(public_head.or(public_get).or(public_get_default));

        if http2 {
            server
                .tls()
                .cert_path(http2_tls_cert_path)
                .key_path(http2_tls_key_path)
                .run(addr)
                .await
        } else {
            server.run(addr).await
        }
    }
}

#[cfg(not(windows))]
/// Handle incoming signals for Unix-like OS's only
fn handle_signals() {
    use crate::signals;

    signals::wait(|sig: signals::Signal| {
        let code = signals::as_int(sig);
        tracing::warn!("Signal {} caught. Server execution exited.", code);
        std::process::exit(code)
    });
}

#[cfg(windows)]
fn handle_signals() {
    // TODO: Windows signals...
}
diff --git a/src/signals.rs b/src/signals.rs
deleted file mode 100644
index 6947b11..0000000
--- a/src/signals.rs
+++ /dev/null
@@ -1,51 +0,0 @@
use nix::errno::Errno;
use nix::libc::c_int;
use nix::sys::signal::{SIGCHLD, SIGINT, SIGTERM};
use nix::sys::wait::WaitStatus::{Exited, Signaled, StillAlive};
use nix::sys::wait::{waitpid, WaitPidFlag};
use nix::Error;

pub use signal::Signal;

/// It waits for an incoming Termination Signal like Ctrl+C (SIGINT), SIGTERM, etc
pub fn wait<F>(func: F)
where
    F: Fn(signal::Signal),
{
    let sig_trap = signal::trap::Trap::trap(&[SIGTERM, SIGINT, SIGCHLD]);
    for sig in sig_trap {
        match sig {
            SIGCHLD => {
                // Current std::process::Command ip does not have a way to find
                // process id, so we just wait until we have no children
                loop {
                    match waitpid(None, Some(WaitPidFlag::WNOHANG)) {
                        Ok(Exited(pid, status)) => {
                            println!("{} exited with status {}", pid, status);
                            continue;
                        }
                        Ok(Signaled(pid, sig, _)) => {
                            println!("{} killed by {}", pid, sig as c_int);
                            continue;
                        }
                        Ok(StillAlive) => break,
                        Ok(status) => {
                            println!("Temporary status {:?}", status);
                            continue;
                        }
                        Err(Error::Sys(Errno::ECHILD)) => return,
                        Err(e) => {
                            panic!("Error {:?}", e);
                        }
                    }
                }
            }
            sig => func(sig),
        }
    }
}

/// It casts a given `signal::Signal` to `i32`.
pub fn as_int(sig: signal::Signal) -> i32 {
    sig as c_int
}