From 1fd3e483a99d57dcbbdc48d31eaac87ed2c102f7 Mon Sep 17 00:00:00 2001 From: Jose Quintana <1700322+joseluisq@users.noreply.github.com> Date: Tue, 3 May 2022 15:33:25 +0200 Subject: [PATCH] Merge pull request #101 from joseluisq/feature/configration_file feat: configuration file support It allows to adjust the server using a settings file in TOML format via the new "-w, --config-file" CLI option and its "SERVER_CONFIG_FILE" env. This feature fundamentally provides two main option groups: "general" and "advanced" options. --- Cargo.lock | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 7 ++++++- LICENSE-MIT | 2 +- README.md | 6 ++++-- docs/content/configuration/command-line-arguments.md | 7 +++++-- docs/content/configuration/config-file.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/content/configuration/environment-variables.md | 3 +++ docs/content/features/custom-http-headers.md | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/content/getting-started.md | 6 +++--- docs/content/index.md | 4 +++- docs/content/license.md | 2 +- docs/mkdocs.yml | 6 +++++- src/bin/server.rs | 2 +- src/config.rs | 174 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ src/custom_headers.rs | 22 ++++++++++++++++++++++ src/handler.rs | 14 +++++++++++--- src/helpers.rs | 40 +++++++++++++++++++++++++++++++++++++++- src/lib.rs | 8 ++++++-- src/server.rs | 97 +++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------ src/settings/cli.rs | 182 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/settings/file.rs | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/settings/mod.rs | 210 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ tests/toml/config.toml | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 23 files changed, 1052 insertions(+), 241 deletions(-) create mode 100644 docs/content/configuration/config-file.md create mode 100644 docs/content/features/custom-http-headers.md delete mode 100644 src/config.rs create mode 100644 src/custom_headers.rs create mode 100644 src/settings/cli.rs create mode 100644 src/settings/file.rs create mode 100644 src/settings/mod.rs create mode 100644 tests/toml/config.toml diff --git a/Cargo.lock b/Cargo.lock index 7cd7654..57bfade 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] name = "alloc-no-stdlib" version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -123,6 +132,15 @@ dependencies = [ ] [[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + +[[package]] name = "bumpalo" version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -312,6 +330,20 @@ dependencies = [ ] [[package]] +name = "globset" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", + "serde", +] + +[[package]] name = "h2" version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -401,6 +433,16 @@ dependencies = [ ] [[package]] +name = "http-serde" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d98b3d9662de70952b14c4840ee0f37e23973542a363e2275f4b9d024ff6cca" +dependencies = [ + "http", + "serde", +] + +[[package]] name = "httparse" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -722,6 +764,23 @@ dependencies = [ ] [[package]] +name = "regex" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] name = "ring" version = "0.16.20" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -774,6 +833,35 @@ dependencies = [ ] [[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_ignored" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c2c7d39d14f2f2ea82239de71594782f186fd03501ac81f0ce08e674819ff2f" +dependencies = [ + "serde", +] + +[[package]] name = "sha-1" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -863,8 +951,10 @@ dependencies = [ "bytes", "form_urlencoded", "futures-util", + "globset", "headers", "http", + "http-serde", "humansize", "hyper", "listenfd", @@ -873,6 +963,8 @@ dependencies = [ "percent-encoding", "pin-project", "rustls-pemfile", + "serde", + "serde_ignored", "signal-hook", "signal-hook-tokio", "structopt", @@ -881,6 +973,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-util", + "toml", "tracing", "tracing-subscriber", ] @@ -1025,6 +1118,15 @@ dependencies = [ ] [[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] name = "tower-service" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml index d88e041..400d9a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "static-web-server" version = "2.7.1" -authors = ["Jose Quintana "] +authors = ["Jose Quintana "] license = "MIT OR Apache-2.0" description = "A blazing fast and asynchronous web server for static files-serving." repository = "https://github.com/joseluisq/static-web-server" @@ -51,6 +51,11 @@ tokio-util = { version = "0.7", default-features = false, features = ["io"] } tracing = { version = "0.1", default-features = false, features = ["std"] } tracing-subscriber = { version = "0.3", default-features = false, features = ["smallvec", "parking_lot", "fmt", "ansi", "tracing-log"] } form_urlencoded = "1.0" +serde = { version = "1.0", default-features = false, features = ["derive"] } +serde_ignored = "0.1" +toml = "0.5" +http-serde = "1.1" +globset = { version = "0.4", features = ["serde1"] } [target.'cfg(all(target_env = "musl", target_pointer_width = "64"))'.dependencies.tikv-jemallocator] version = "0.4" diff --git a/LICENSE-MIT b/LICENSE-MIT index 0e60829..5b9c9bb 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2019-present Jose Quintana +Copyright (c) 2019-present Jose Quintana Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index f78b287..67ee839 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,10 @@ It's cross-platform and available for `Linux`, `macOS`, `Windows` and `FreeBSD` - Optional directory listing. - [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) support. - Basic HTTP Authentication. +- Customizable HTTP Response Headers for specific file requests via glob patterns. +- Fallback pages for 404 errors useful for Single-page applications. +- Configurable using CLI arguments, environment variables or a file. - Default and custom error pages. -- Configurable using CLI arguments or environment variables. - First-class [Docker](https://docs.docker.com/get-started/overview/) support. [Scratch](https://hub.docker.com/_/scratch) and latest [Alpine Linux](https://hub.docker.com/_/alpine) Docker images available. - Ability to accept a socket listener as a file descriptor for use in sandboxing and on-demand applications (E.g [systemd](http://0pointer.de/blog/projects/socket-activation.html)). - Cross-platform. Binaries available for Linux, macOS, Windows & FreeBSD x86_64 / ARM. @@ -83,4 +85,4 @@ Feel free to send some [Pull request](https://github.com/joseluisq/static-web-se This work is primarily distributed under the terms of both the [MIT license](LICENSE-MIT) and the [Apache License (Version 2.0)](LICENSE-APACHE). -© 2019-present [Jose Quintana](https://git.io/joseluisq) +© 2019-present [Jose Quintana](https://joseluisq.net) diff --git a/docs/content/configuration/command-line-arguments.md b/docs/content/configuration/command-line-arguments.md index 00c6e3c..e167887 100644 --- a/docs/content/configuration/command-line-arguments.md +++ b/docs/content/configuration/command-line-arguments.md @@ -10,8 +10,8 @@ The server can be configured via the following command-line arguments. ``` $ static-web-server -h -static-web-server 2.7.0 -Jose Quintana +static-web-server 2.8.0 +Jose Quintana A blazing fast and asynchronous web server for static files-serving. USAGE: @@ -31,6 +31,9 @@ OPTIONS: -x, --compression Gzip, Deflate or Brotli compression on demand determined by the Accept-Encoding header and applied to text- based web file types only [env: SERVER_COMPRESSION=] [default: true] + -w, --config-file + Server TOML configuration file path [env: SERVER_CONFIG_FILE=] + -j, --cors-allow-headers Specify an optional CORS list of allowed headers separated by comas. Default "origin, content-type". It requires `--cors-allow-origins` to be used along with [env: SERVER_CORS_ALLOW_HEADERS=] [default: origin, diff --git a/docs/content/configuration/config-file.md b/docs/content/configuration/config-file.md new file mode 100644 index 0000000..9d0a63c --- /dev/null +++ b/docs/content/configuration/config-file.md @@ -0,0 +1,92 @@ +# TOML Configuration File + +**`SWS`** can be configured using a [TOML](https://toml.io/en/) file in order to adjust the general server features as well as other advanced ones. + +It's disabled by default and can be enabled by passing an *string file path* via the `-w, --config-file` option or its equivalent [SERVER_CONFIG_FILE](./../configuration/environment-variables.md#server_config_file) env. + +## TOML File (Manifest) + +Below just an example showing all features with its default values. + +```toml +[general] + +#### Address & Root dir +host = "::" +port = 8087 +root = "docker/public" + +#### Logging +log-level = "trace" + +#### Cache Control headers +cache-control-headers = true + +#### Auto Compression +compression = true + +#### Error pages +page404 = "docker/public/404.html" +page50x = "docker/public/50x.html" + +#### HTTP/2 + TLS +http2 = false +http2-tls-cert = "" +http2-tls-key = "" + +#### Security headers +security-headers = true + +#### CORS +cors-allow-origins = "" +cors-allow-headers = "" + +#### Directoy listing +directory-listing = false +directory-listing-order = 6 + +#### Basich Authentication +basic-auth = "" + +#### File descriptor binding +# fd = "" + +#### Worker threads +threads-multiplier = 1 + +#### Grace period after a graceful shutdown +grace-period = 0 + +#### Page fallback for 404s +page-fallback = "" + + +[advanced] + +#### .... +``` + +### General options + +The TOML `[general]` section allows to adjust the current options actually available via the CLI/ENVs ones. + +So they are equivalent each other **except** the `-w, --config-file` option which is omitted and can not be used for obvious reasons. + +!!! info "Config file based features are optional" + All server feature options via the configuration file are optional and can be omitted as needed. + +### Advanced options + +The TOML `[advanced]` section is intended for more complex features. + +### Precendence + +Whatever config file based feature option will take precedence over its CLI or ENV equivalent. + +## Usage + +The following command runs the server using an specific `config.toml` file. + +```sh +static-web-server -w config.toml +``` diff --git a/docs/content/configuration/environment-variables.md b/docs/content/configuration/environment-variables.md index 07f715a..bab3034 100644 --- a/docs/content/configuration/environment-variables.md +++ b/docs/content/configuration/environment-variables.md @@ -18,6 +18,9 @@ Optional file descriptor number (e.g. `0`) to inherit an already-opened TCP list ### SERVER_ROOT Relative or absolute root directory path of static files. Default `./public`. +### SERVER_CONFIG_FILE +The Server configuration file path in TOML format. See [The TOML Configuration File](../configuration/config-file.md). + ### SERVER_GRACE_PERIOD Defines a grace period in seconds after a `SIGTERM` signal is caught which will delay the server before to shut it down gracefully. The maximum value is `255` seconds. Default value is `0` (no delay). diff --git a/docs/content/features/custom-http-headers.md b/docs/content/features/custom-http-headers.md new file mode 100644 index 0000000..41cf931 --- /dev/null +++ b/docs/content/features/custom-http-headers.md @@ -0,0 +1,66 @@ +# Custom HTTP Headers + +**`SWS`** allows to customize the server [HTTP Response headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) on demand. + +## Structure + +The Server HTTP response headers should be defined mainly as [Array of Tables](https://toml.io/en/v1.0.0#array-of-tables). + +Each table entry should have two key/value pairs: + +- One `source` key containing an string *glob pattern*. +- One `headers` key containing a [set or hash table](https://toml.io/en/v1.0.0#table) describing plain HTTP headers to apply. + +A particular set of HTTP headers can only be applied when a `source` matches against the request uri. + +!!! info "Custom HTTP headers take precedence over existing ones" + Whatever custom HTTP header could **replace** an existing one if it was previously defined (E.g server default headers) and matches its `source`. + + The headers order is important since it determine its precedence. + + **Example:** if the feature `--cache-control-headers=true` is enabled but also a custom `cache-control` header was defined then the custom header will have priority. + +### Source + +Source is a [Glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)) that should match against the uri that is requesting a resource file. + +### Headers + +A set of valid plain [HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) to be applied. + +## Examples + +Below some examples of how to customize server HTTP headers in three variants. + +### Oneline version + +```toml +[advanced] + +[[advanced.headers]] +source = "**/*.{js,css}" +headers = { Access-Control-Allow-Origin = "*", X-XSS-PROTECTION = "1; mode=block" } +``` + +### Multiline version + +```toml +[advanced] + +[[advanced.headers]] +source = "*.html" +[advanced.headers.headers] +Cache-Control = "public, max-age=36000" +Content-Security-Policy = "frame-ancestors 'self'" +Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload" +``` + +### Multiline version with explicit header key (dotted) + +```toml +[advanced] + +[[advanced.headers]] +source = "**/*.{jpg,jpeg,png,ico,gif}" +headers.Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload" +``` diff --git a/docs/content/getting-started.md b/docs/content/getting-started.md index a6ac352..e9b125b 100644 --- a/docs/content/getting-started.md +++ b/docs/content/getting-started.md @@ -15,6 +15,6 @@ docker run --rm -it -p 8787:80 joseluisq/static-web-server:2 !!! info "Docker Tip" You can specify a Docker volume like `-v $HOME/my-public-dir:/public` in order to overwrite the default root directory. See [Docker examples](features/docker.md). -To see the available options type `static-web-server -h` or go to the [Command-line arguments](./configuration/command-line-arguments.md) section. - -Or if you are looking for more advanced examples then have a look at [the features](./features/http1.md) section. +- Type `static-web-server --help` or go to the [Command-line arguments](./configuration/command-line-arguments.md) section. +- See how to configure the server using a [configuration file](configuration/config-file.md). +- Have also a look at [the features](./features/http1.md) section for more advanced examples. diff --git a/docs/content/index.md b/docs/content/index.md index 4d34f64..96e4138 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -56,8 +56,10 @@ It's cross-platform and available for `Linux`, `macOS`, `Windows` and `FreeBSD` - Optional directory listing. - [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) support. - Basic HTTP Authentication. +- Customizable HTTP Response Headers for specific file requests via glob patterns. +- Fallback pages for 404 errors useful for Single-page applications. +- Configurable using CLI arguments, environment variables or a file. - Default and custom error pages. -- Configurable using CLI arguments or environment variables. - First-class [Docker](https://docs.docker.com/get-started/overview/) support. [Scratch](https://hub.docker.com/_/scratch) and latest [Alpine Linux](https://hub.docker.com/_/alpine) Docker images available. - Ability to accept a socket listener as a file descriptor for use in sandboxing and on-demand applications (E.g [systemd](http://0pointer.de/blog/projects/socket-activation.html)). - Cross-platform. Binaries available for Linux, macOS, Windows & FreeBSD x86_64 / ARM. diff --git a/docs/content/license.md b/docs/content/license.md index cb90e83..298f1e3 100644 --- a/docs/content/license.md +++ b/docs/content/license.md @@ -2,4 +2,4 @@ This work is primarily distributed under the terms of both the [MIT license](https://github.com/joseluisq/static-web-server/blob/master/LICENSE-MIT) and the [Apache License (Version 2.0)](https://github.com/joseluisq/static-web-server/blob/master/LICENSE-APACHE). -© 2019-present [Jose Quintana](https://git.io/joseluisq) +© 2019-present [Jose Quintana](https://github.com/joseluisq) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 9b74b66..898d8f5 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -28,7 +28,7 @@ theme: - content.code.annotate - content.tabs.link - header.autohide - - navigation.expand + # - navigation.expand - navigation.indexes # - navigation.instant # - navigation.sections @@ -103,6 +103,8 @@ markdown_extensions: - pymdownx.highlight: linenums: true linenums_style: pymdownx-inline + - pymdownx.tabbed: + alternate_style: true # Plugins plugins: @@ -118,6 +120,7 @@ nav: - 'Configuration': - 'Command Line Arguments': 'configuration/command-line-arguments.md' - 'Environment Variables': 'configuration/environment-variables.md' + - 'TOML Configuration File': 'configuration/config-file.md' - 'Building from Source': 'building-from-source.md' - 'Features': - 'HTTP/1': 'features/http1.md' @@ -135,6 +138,7 @@ nav: - 'File Descriptor Socket Passing': './features/file-descriptor-socket-passing.md' - 'Worker Threads Customization': 'features/worker-threads.md' - 'Error Pages': 'features/error-pages.md' + - 'Custom HTTP Headers': 'features/custom-http-headers.md' - 'Platforms & Architectures': 'platforms-architectures.md' - 'Migration from v1 to v2': 'migration.md' - 'Changelog v2 (latest stable)': 'https://github.com/joseluisq/static-web-server/blob/master/CHANGELOG.md' diff --git a/src/bin/server.rs b/src/bin/server.rs index b5ac46b..05663a6 100644 --- a/src/bin/server.rs +++ b/src/bin/server.rs @@ -10,7 +10,7 @@ static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; use static_web_server::{Result, Server}; fn main() -> Result { - Server::new().run()?; + Server::new()?.run()?; Ok(()) } diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index e04d838..0000000 --- a/src/config.rs +++ /dev/null @@ -1,174 +0,0 @@ -use structopt::StructOpt; - -#[derive(Debug, StructOpt)] -#[structopt(about, author)] -pub struct Config { - #[structopt(long, short = "a", default_value = "::", env = "SERVER_HOST")] - /// Host address (E.g 127.0.0.1 or ::1) - pub host: String, - - #[structopt(long, short = "p", default_value = "80", env = "SERVER_PORT")] - /// Host port - pub port: u16, - - #[structopt( - long, - short = "f", - env = "SERVER_LISTEN_FD", - conflicts_with_all(&["host", "port"]) - )] - /// Instead of binding to a TCP port, accept incoming connections to an already-bound TCP - /// socket listener on the specified file descriptor number (usually zero). Requires that the - /// parent process (e.g. inetd, launchd, or systemd) binds an address and port on behalf of - /// static-web-server, before arranging for the resulting file descriptor to be inherited by - /// static-web-server. Cannot be used in conjunction with the port and host arguments. The - /// included systemd unit file utilises this feature to increase security by allowing the - /// static-web-server to be sandboxed more completely. - pub fd: Option, - - #[structopt( - long, - short = "n", - default_value = "1", - env = "SERVER_THREADS_MULTIPLIER" - )] - /// Number of worker threads multiplier that'll be multiplied by the number of system CPUs - /// using the formula: `worker threads = number of CPUs * n` where `n` is the value that changes here. - /// When multiplier value is 0 or 1 then one thread per core is used. - /// Number of worker threads result should be a number between 1 and 32,768 though it is advised to keep this value on the smaller side. - pub threads_multiplier: usize, - - #[structopt(long, short = "d", default_value = "./public", env = "SERVER_ROOT")] - /// Root directory path of static files. - pub root: String, - - #[structopt( - long, - default_value = "./public/50x.html", - env = "SERVER_ERROR_PAGE_50X" - )] - /// HTML file path for 50x errors. If the path is not specified or simply doesn't exist then the server will use a generic HTML error message. - pub page50x: String, - - #[structopt( - long, - default_value = "./public/404.html", - env = "SERVER_ERROR_PAGE_404" - )] - /// HTML file path for 404 errors. If the path is not specified or simply doesn't exist then the server will use a generic HTML error message. - pub page404: String, - - #[structopt(long, default_value = "", env = "SERVER_FALLBACK_PAGE")] - /// HTML file path that is used for GET requests when the requested path doesn't exist. The fallback page is served with a 200 status code, useful when using client routers. If the path is not specified or simply doesn't exist then this feature will not be active. - pub page_fallback: String, - - #[structopt(long, short = "g", default_value = "error", env = "SERVER_LOG_LEVEL")] - /// Specify a logging level in lower case. Values: error, warn, info, debug or trace - pub log_level: String, - - #[structopt( - long, - short = "c", - default_value = "", - env = "SERVER_CORS_ALLOW_ORIGINS" - )] - /// Specify an optional CORS list of allowed origin hosts separated by comas. Host ports or protocols aren't being checked. Use an asterisk (*) to allow any host. - pub cors_allow_origins: String, - - #[structopt( - long, - short = "j", - default_value = "origin, content-type", - env = "SERVER_CORS_ALLOW_HEADERS" - )] - /// Specify an optional CORS list of allowed headers separated by comas. Default "origin, content-type". It requires `--cors-allow-origins` to be used along with. - pub cors_allow_headers: String, - - #[structopt( - long, - short = "t", - parse(try_from_str), - default_value = "false", - env = "SERVER_HTTP2_TLS" - )] - /// Enable HTTP/2 with TLS support. - pub http2: bool, - - #[structopt( - long, - required_if("http2", "true"), - default_value = "", - env = "SERVER_HTTP2_TLS_CERT" - )] - /// Specify the file path to read the certificate. - pub http2_tls_cert: String, - - #[structopt( - long, - required_if("http2", "true"), - default_value = "", - env = "SERVER_HTTP2_TLS_KEY" - )] - /// Specify the file path to read the private key. - pub http2_tls_key: String, - - #[structopt( - long, - short = "x", - parse(try_from_str), - 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. - pub compression: bool, - - #[structopt( - long, - short = "z", - parse(try_from_str), - default_value = "false", - env = "SERVER_DIRECTORY_LISTING" - )] - /// Enable directory listing for all requests ending with the slash character (‘/’). - pub directory_listing: bool, - - #[structopt( - long, - required_if("directory_listing", "true"), - default_value = "6", - env = "SERVER_DIRECTORY_LISTING_ORDER" - )] - /// Specify a default code number to order directory listing entries per `Name`, `Last modified` or `Size` attributes (columns). Code numbers supported: 0 (Name asc), 1 (Name desc), 2 (Last modified asc), 3 (Last modified desc), 4 (Size asc), 5 (Size desc). Default 6 (unordered) - pub directory_listing_order: u8, - - #[structopt( - long, - parse(try_from_str), - required_if("http2", "true"), - default_value_if("http2", Some("true"), "true"), - default_value = "false", - env = "SERVER_SECURITY_HEADERS" - )] - /// Enable security headers by default when HTTP/2 feature is activated. - /// Headers included: "Strict-Transport-Security: max-age=63072000; includeSubDomains; preload" (2 years max-age), - /// "X-Frame-Options: DENY", "X-XSS-Protection: 1; mode=block" and "Content-Security-Policy: frame-ancestors 'self'". - pub security_headers: bool, - - #[structopt( - long, - short = "e", - parse(try_from_str), - default_value = "true", - env = "SERVER_CACHE_CONTROL_HEADERS" - )] - /// Enable cache control headers for incoming requests based on a set of file types. The file type list can be found on `src/control_headers.rs` file. - pub cache_control_headers: bool, - - /// It provides The "Basic" HTTP Authentication scheme using credentials as "user-id:password" pairs. Password must be encoded using the "BCrypt" password-hashing function. - #[structopt(long, default_value = "", env = "SERVER_BASIC_AUTH")] - pub basic_auth: String, - - #[structopt(long, short = "q", default_value = "0", env = "SERVER_GRACE_PERIOD")] - /// Defines a grace period in seconds after a `SIGTERM` signal is caught which will delay the server before to shut it down gracefully. The maximum value is 255 seconds. - pub grace_period: u8, -} diff --git a/src/custom_headers.rs b/src/custom_headers.rs new file mode 100644 index 0000000..e6854c9 --- /dev/null +++ b/src/custom_headers.rs @@ -0,0 +1,22 @@ +use hyper::{Body, Response}; + +use crate::settings::Headers; + +/** Append custom HTTP headers to current response. */ +pub fn append_headers( + uri: &str, + headers_opts_vec: &Option>, + resp: &mut Response, +) { + if let Some(headers_vec) = headers_opts_vec { + for headers_entry in headers_vec.iter() { + // Match header glob pattern against request uri + if headers_entry.source.is_match(uri) { + // Add/update headers if uri matches + for (name, value) in &headers_entry.headers { + resp.headers_mut().insert(name, value.to_owned()); + } + } + } + } +} diff --git a/src/handler.rs b/src/handler.rs index 840db08..61eab62 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -2,13 +2,13 @@ use hyper::{header::WWW_AUTHENTICATE, Body, Method, Request, Response, StatusCod use std::{future::Future, path::PathBuf, sync::Arc}; use crate::{ - basic_auth, compression, control_headers, cors, error_page, fallback_page, security_headers, - static_files, + basic_auth, compression, control_headers, cors, custom_headers, error_page, fallback_page, + security_headers, settings::Advanced, static_files, Error, Result, }; -use crate::{Error, Result}; /// It defines options for a request handler. pub struct RequestHandlerOpts { + // General options pub root_dir: PathBuf, pub compression: bool, pub dir_listing: bool, @@ -20,6 +20,9 @@ pub struct RequestHandlerOpts { pub page50x: String, pub page_fallback: String, pub basic_auth: String, + + // Advanced options + pub advanced_opts: Option, } /// It defines the main request handler used by the Hyper service request. @@ -154,6 +157,11 @@ impl RequestHandler { security_headers::append_headers(&mut resp); } + // Add/update custom headers + if let Some(advanced) = &self.opts.advanced_opts { + custom_headers::append_headers(uri_path, &advanced.headers, &mut resp) + } + Ok(resp) } Err(status) => { diff --git a/src/helpers.rs b/src/helpers.rs index 375efa2..66fc20e 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,7 +1,7 @@ use std::fs; use std::path::{Path, PathBuf}; -use crate::Result; +use crate::{Context, Result}; /// Validate and return a directory path. pub fn get_valid_dirpath>(path: P) -> Result @@ -37,3 +37,41 @@ pub fn read_file_content(p: &str) -> String { } String::new() } + +/// Read the entire contents of a file into a bytes vector. +pub fn read_bytes(path: &Path) -> Result> { + fs::read(path).with_context(|| format!("failed to read `{}`", path.display())) +} + +/// Read an UTF-8 file from a specific path. +pub fn read_file(path: &Path) -> Result { + match String::from_utf8(read_bytes(path)?) { + Ok(s) => Ok(s), + Err(_) => bail!("path at `{}` was not valid utf-8", path.display()), + } +} + +pub fn stringify(dst: &mut String, path: &serde_ignored::Path<'_>) { + use serde_ignored::Path; + + match *path { + Path::Root => {} + Path::Seq { parent, index } => { + stringify(dst, parent); + if !dst.is_empty() { + dst.push('.'); + } + dst.push_str(&index.to_string()); + } + Path::Map { parent, ref key } => { + stringify(dst, parent); + if !dst.is_empty() { + dst.push('.'); + } + dst.push_str(key); + } + Path::Some { parent } + | Path::NewtypeVariant { parent } + | Path::NewtypeStruct { parent } => stringify(dst, parent), + } +} diff --git a/src/lib.rs b/src/lib.rs index 2cb08e1..49c9ddc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,11 +6,14 @@ #[macro_use] extern crate anyhow; +#[macro_use] +extern crate serde; + pub mod basic_auth; pub mod compression; -pub mod config; pub mod control_headers; pub mod cors; +pub mod custom_headers; pub mod error_page; pub mod fallback_page; pub mod handler; @@ -19,6 +22,7 @@ pub mod logger; pub mod security_headers; pub mod server; pub mod service; +pub mod settings; pub mod signals; pub mod static_files; pub mod tls; @@ -27,6 +31,6 @@ pub mod transport; #[macro_use] pub mod error; -pub use config::Config; pub use error::*; pub use server::Server; +pub use settings::Settings; diff --git a/src/server.rs b/src/server.rs index a46fb66..a39f298 100644 --- a/src/server.rs +++ b/src/server.rs @@ -3,33 +3,32 @@ use hyper::server::Server as HyperServer; use listenfd::ListenFd; use std::net::{IpAddr, SocketAddr, TcpListener}; use std::sync::Arc; -use structopt::StructOpt; use crate::handler::{RequestHandler, RequestHandlerOpts}; use crate::tls::{TlsAcceptor, TlsConfigBuilder}; -use crate::{config::Config, service::RouterService, Context, Result}; -use crate::{cors, helpers, logger, signals}; +use crate::{cors, helpers, logger, signals, Settings}; +use crate::{service::RouterService, Context, Result}; /// Define a multi-thread HTTP or HTTP/2 web server. pub struct Server { - opts: Config, + opts: Settings, threads: usize, } impl Server { /// Create new multi-thread server instance. - pub fn new() -> Server { + pub fn new() -> Result { // Get server config - let opts = Config::from_args(); + let opts = Settings::get()?; // Configure number of worker threads let cpus = num_cpus::get(); - let threads = match opts.threads_multiplier { + let threads = match opts.general.threads_multiplier { 0 | 1 => cpus, n => cpus * n, }; - Server { opts, threads } + Ok(Server { opts, threads }) } /// Build and run the multi-thread `Server`. @@ -53,33 +52,41 @@ impl Server { /// Run the inner Hyper `HyperServer` (HTTP1/HTTP2) forever on the current thread // using the given configuration. async fn start_server(self) -> Result { - let opts = &self.opts; + // Config "general" options + let general = self.opts.general; - // Initialize logging system - logger::init(&opts.log_level) - .with_context(|| "failed to initialize logging".to_string())?; + // Config-file "advanced" options + let advanced_opts = self.opts.advanced; + + // Logging system initialization + let log_level = &general.log_level.to_lowercase(); + logger::init(log_level).with_context(|| "failed to initialize logging")?; + tracing::info!("logging level: {}", log_level.to_lowercase()); + + // Config file + if general.config_file.is_some() && general.config_file.is_some() { + tracing::info!("config file: {}", general.config_file.unwrap().display()); + } // Determine TCP listener either file descriptor or TCP socket let (tcp_listener, addr_str); - match opts.fd { + match general.fd { Some(fd) => { addr_str = format!("@FD({})", fd); tcp_listener = ListenFd::from_env() .take_tcp_listener(fd)? - .with_context(|| { - "failed to convert inherited FD into a TCP listener".to_string() - })?; + .with_context(|| "failed to convert inherited FD into a TCP listener")?; tracing::info!( "converted inherited file descriptor {} to a TCP listener", fd ); } None => { - let ip = opts + let ip = general .host .parse::() - .with_context(|| format!("failed to parse {} address", opts.host))?; - let addr = SocketAddr::from((ip, opts.port)); + .with_context(|| format!("failed to parse {} address", general.host))?; + let addr = SocketAddr::from((ip, general.port)); tcp_listener = TcpListener::bind(addr) .with_context(|| format!("failed to bind to {} address", addr))?; addr_str = addr.to_string(); @@ -88,55 +95,55 @@ impl Server { } // Check for a valid root directory - let root_dir = helpers::get_valid_dirpath(&opts.root) - .with_context(|| "root directory was not found or inaccessible".to_string())?; + let root_dir = helpers::get_valid_dirpath(&general.root) + .with_context(|| "root directory was not found or inaccessible")?; // Custom error pages content - let page404 = helpers::read_file_content(&opts.page404); - let page50x = helpers::read_file_content(&opts.page50x); + let page404 = helpers::read_file_content(&general.page404); + let page50x = helpers::read_file_content(&general.page50x); // Fallback page content - let page_fallback = helpers::read_file_content(&opts.page_fallback); + let page_fallback = helpers::read_file_content(&general.page_fallback); // Number of worker threads option let threads = self.threads; tracing::info!("runtime worker threads: {}", self.threads); // Security Headers option - let security_headers = opts.security_headers; + let security_headers = general.security_headers; tracing::info!("security headers: enabled={}", security_headers); // Auto compression based on the `Accept-Encoding` header - let compression = opts.compression; + let compression = general.compression; tracing::info!("auto compression: enabled={}", compression); // Directory listing option - let dir_listing = opts.directory_listing; + let dir_listing = general.directory_listing; tracing::info!("directory listing: enabled={}", dir_listing); // Directory listing order number - let dir_listing_order = opts.directory_listing_order; + let dir_listing_order = general.directory_listing_order; tracing::info!("directory listing order code: {}", dir_listing_order); // Cache control headers option - let cache_control_headers = opts.cache_control_headers; + let cache_control_headers = general.cache_control_headers; tracing::info!("cache control headers: enabled={}", cache_control_headers); // CORS option let cors = cors::new( - opts.cors_allow_origins.trim(), - opts.cors_allow_headers.trim(), + general.cors_allow_origins.trim(), + general.cors_allow_headers.trim(), ); // `Basic` HTTP Authentication Schema option - let basic_auth = opts.basic_auth.trim().to_owned(); + let basic_auth = general.basic_auth.trim().to_owned(); tracing::info!( "basic authentication: enabled={}", - !self.opts.basic_auth.is_empty() + !general.basic_auth.is_empty() ); // Grace period option - let grace_period = opts.grace_period; + let grace_period = general.grace_period; tracing::info!("grace period before graceful shutdown: {}s", grace_period); // Create a service router for Hyper @@ -153,36 +160,36 @@ impl Server { page50x, page_fallback, basic_auth, + advanced_opts, }), }); // Run the corresponding HTTP Server asynchronously with its given options - if opts.http2 { + if general.http2 { // HTTP/2 + TLS tcp_listener .set_nonblocking(true) .expect("cannot set non-blocking"); let listener = tokio::net::TcpListener::from_std(tcp_listener) - .with_context(|| "failed to create tokio::net::TcpListener".to_string())?; + .with_context(|| "failed to create tokio::net::TcpListener")?; let mut incoming = AddrIncoming::from_listener(listener).with_context(|| { "failed to create an AddrIncoming from the current tokio::net::TcpListener" - .to_string() })?; incoming.set_nodelay(true); let tls = TlsConfigBuilder::new() - .cert_path(&opts.http2_tls_cert) - .key_path(&opts.http2_tls_key) + .cert_path(&general.http2_tls_cert) + .key_path(&general.http2_tls_key) .build() .with_context(|| { - "failed to initialize TLS, probably wrong cert/key or file missing".to_string() + "failed to initialize TLS, probably wrong cert/key or file missing" })?; #[cfg(unix)] let signals = signals::create_signals() - .with_context(|| "failed to register termination signals".to_string())?; + .with_context(|| "failed to register termination signals")?; #[cfg(unix)] let handle = signals.handle(); @@ -212,7 +219,7 @@ impl Server { #[cfg(unix)] let signals = signals::create_signals() - .with_context(|| "failed to register termination signals".to_string())?; + .with_context(|| "failed to register termination signals")?; #[cfg(unix)] let handle = signals.handle(); @@ -246,9 +253,3 @@ impl Server { Ok(()) } } - -impl Default for Server { - fn default() -> Self { - Self::new() - } -} diff --git a/src/settings/cli.rs b/src/settings/cli.rs new file mode 100644 index 0000000..4889cef --- /dev/null +++ b/src/settings/cli.rs @@ -0,0 +1,182 @@ +//! The server CLI options + +use std::path::PathBuf; +use structopt::StructOpt; + +/// General server configuration available in CLI and config file options. +#[derive(Debug, StructOpt)] +#[structopt(about, author)] +pub struct General { + #[structopt(long, short = "a", default_value = "::", env = "SERVER_HOST")] + /// Host address (E.g 127.0.0.1 or ::1) + pub host: String, + + #[structopt(long, short = "p", default_value = "80", env = "SERVER_PORT")] + /// Host port + pub port: u16, + + #[structopt( + long, + short = "f", + env = "SERVER_LISTEN_FD", + conflicts_with_all(&["host", "port"]) + )] + /// Instead of binding to a TCP port, accept incoming connections to an already-bound TCP + /// socket listener on the specified file descriptor number (usually zero). Requires that the + /// parent process (e.g. inetd, launchd, or systemd) binds an address and port on behalf of + /// static-web-server, before arranging for the resulting file descriptor to be inherited by + /// static-web-server. Cannot be used in conjunction with the port and host arguments. The + /// included systemd unit file utilises this feature to increase security by allowing the + /// static-web-server to be sandboxed more completely. + pub fd: Option, + + #[structopt( + long, + short = "n", + default_value = "1", + env = "SERVER_THREADS_MULTIPLIER" + )] + /// Number of worker threads multiplier that'll be multiplied by the number of system CPUs + /// using the formula: `worker threads = number of CPUs * n` where `n` is the value that changes here. + /// When multiplier value is 0 or 1 then one thread per core is used. + /// Number of worker threads result should be a number between 1 and 32,768 though it is advised to keep this value on the smaller side. + pub threads_multiplier: usize, + + #[structopt(long, short = "d", default_value = "./public", env = "SERVER_ROOT")] + /// Root directory path of static files. + pub root: String, + + #[structopt( + long, + default_value = "./public/50x.html", + env = "SERVER_ERROR_PAGE_50X" + )] + /// HTML file path for 50x errors. If the path is not specified or simply doesn't exist then the server will use a generic HTML error message. + pub page50x: String, + + #[structopt( + long, + default_value = "./public/404.html", + env = "SERVER_ERROR_PAGE_404" + )] + /// HTML file path for 404 errors. If the path is not specified or simply doesn't exist then the server will use a generic HTML error message. + pub page404: String, + + #[structopt(long, default_value = "", env = "SERVER_FALLBACK_PAGE")] + /// HTML file path that is used for GET requests when the requested path doesn't exist. The fallback page is served with a 200 status code, useful when using client routers. If the path is not specified or simply doesn't exist then this feature will not be active. + pub page_fallback: String, + + #[structopt(long, short = "g", default_value = "error", env = "SERVER_LOG_LEVEL")] + /// Specify a logging level in lower case. Values: error, warn, info, debug or trace + pub log_level: String, + + #[structopt( + long, + short = "c", + default_value = "", + env = "SERVER_CORS_ALLOW_ORIGINS" + )] + /// Specify an optional CORS list of allowed origin hosts separated by comas. Host ports or protocols aren't being checked. Use an asterisk (*) to allow any host. + pub cors_allow_origins: String, + + #[structopt( + long, + short = "j", + default_value = "origin, content-type", + env = "SERVER_CORS_ALLOW_HEADERS" + )] + /// Specify an optional CORS list of allowed headers separated by comas. Default "origin, content-type". It requires `--cors-allow-origins` to be used along with. + pub cors_allow_headers: String, + + #[structopt( + long, + short = "t", + parse(try_from_str), + default_value = "false", + env = "SERVER_HTTP2_TLS" + )] + /// Enable HTTP/2 with TLS support. + pub http2: bool, + + #[structopt( + long, + required_if("http2", "true"), + default_value = "", + env = "SERVER_HTTP2_TLS_CERT" + )] + /// Specify the file path to read the certificate. + pub http2_tls_cert: String, + + #[structopt( + long, + required_if("http2", "true"), + default_value = "", + env = "SERVER_HTTP2_TLS_KEY" + )] + /// Specify the file path to read the private key. + pub http2_tls_key: String, + + #[structopt( + long, + short = "x", + parse(try_from_str), + 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. + pub compression: bool, + + #[structopt( + long, + short = "z", + parse(try_from_str), + default_value = "false", + env = "SERVER_DIRECTORY_LISTING" + )] + /// Enable directory listing for all requests ending with the slash character (‘/’). + pub directory_listing: bool, + + #[structopt( + long, + required_if("directory_listing", "true"), + default_value = "6", + env = "SERVER_DIRECTORY_LISTING_ORDER" + )] + /// Specify a default code number to order directory listing entries per `Name`, `Last modified` or `Size` attributes (columns). Code numbers supported: 0 (Name asc), 1 (Name desc), 2 (Last modified asc), 3 (Last modified desc), 4 (Size asc), 5 (Size desc). Default 6 (unordered) + pub directory_listing_order: u8, + + #[structopt( + long, + parse(try_from_str), + required_if("http2", "true"), + default_value_if("http2", Some("true"), "true"), + default_value = "false", + env = "SERVER_SECURITY_HEADERS" + )] + /// Enable security headers by default when HTTP/2 feature is activated. + /// Headers included: "Strict-Transport-Security: max-age=63072000; includeSubDomains; preload" (2 years max-age), + /// "X-Frame-Options: DENY", "X-XSS-Protection: 1; mode=block" and "Content-Security-Policy: frame-ancestors 'self'". + pub security_headers: bool, + + #[structopt( + long, + short = "e", + parse(try_from_str), + default_value = "true", + env = "SERVER_CACHE_CONTROL_HEADERS" + )] + /// Enable cache control headers for incoming requests based on a set of file types. The file type list can be found on `src/control_headers.rs` file. + pub cache_control_headers: bool, + + /// It provides The "Basic" HTTP Authentication scheme using credentials as "user-id:password" pairs. Password must be encoded using the "BCrypt" password-hashing function. + #[structopt(long, default_value = "", env = "SERVER_BASIC_AUTH")] + pub basic_auth: String, + + #[structopt(long, short = "q", default_value = "0", env = "SERVER_GRACE_PERIOD")] + /// Defines a grace period in seconds after a `SIGTERM` signal is caught which will delay the server before to shut it down gracefully. The maximum value is 255 seconds. + pub grace_period: u8, + + #[structopt(long, short = "w", env = "SERVER_CONFIG_FILE")] + /// Server TOML configuration file path. + pub config_file: Option, +} diff --git a/src/settings/file.rs b/src/settings/file.rs new file mode 100644 index 0000000..bff1623 --- /dev/null +++ b/src/settings/file.rs @@ -0,0 +1,173 @@ +//! The server configuration file options (manifest) + +use headers::HeaderMap; +use serde::Deserialize; +use std::collections::BTreeSet; +use std::path::Path; + +use crate::{helpers, Context, Result}; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum LogLevel { + Error, + Warn, + Info, + Debug, + Trace, +} + +impl LogLevel { + pub fn name(&self) -> &'static str { + match self { + LogLevel::Error => "error", + LogLevel::Warn => "warn", + LogLevel::Info => "info", + LogLevel::Debug => "debug", + LogLevel::Trace => "trace", + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct Headers { + pub source: String, + #[serde(rename(deserialize = "headers"), with = "http_serde::header_map")] + pub headers: HeaderMap, +} + +/// Advanced server options only available in configuration file mode. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct Advanced { + // Headers + pub headers: Option>, +} + +/// General server options available in configuration file mode. +/// Note that the `--config-file` option is excluded from itself. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct General { + // Address & Root dir + pub host: Option, + pub port: Option, + pub root: Option, + + // Logging + pub log_level: Option, + + // Cache Control headers + pub cache_control_headers: Option, + + // Compression + pub compression: Option, + + // Error pages + pub page404: Option, + pub page50x: Option, + + // HTTP/2 + TLS + pub http2: Option, + pub http2_tls_cert: Option, + pub http2_tls_key: Option, + + // Security headers + pub security_headers: Option, + + // CORS + pub cors_allow_origins: Option, + pub cors_allow_headers: Option, + + // Directoy listing + pub directory_listing: Option, + pub directory_listing_order: Option, + + // Basich Authentication + pub basic_auth: Option, + + // File descriptor binding + pub fd: Option, + + // Worker threads + pub threads_multiplier: Option, + + pub grace_period: Option, + + pub page_fallback: Option, +} + +/// Full server configuration +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct Settings { + pub general: Option, + pub advanced: Option, +} + +impl Settings { + /// Read and deserialize the server TOML configuration file by path. + pub fn read(config_file: &Path) -> Result { + // Validate TOML file extension + let ext = config_file.extension(); + if ext.is_none() || ext.unwrap().is_empty() || ext.unwrap().ne("toml") { + bail!("configuration file should be in toml format. E.g `config.toml`"); + } + + // TODO: validate minimal TOML file structure needed + let toml = + read_toml_file(config_file).with_context(|| "error reading toml configuration file")?; + let mut unused = BTreeSet::new(); + let manifest: Settings = serde_ignored::deserialize(toml, |path| { + let mut key = String::new(); + helpers::stringify(&mut key, &path); + unused.insert(key); + }) + .with_context(|| "error during toml configuration file deserialization")?; + + for key in unused { + println!( + "Warning: unused configuration manifest key \"{}\" or unsuported", + key + ); + } + + Ok(manifest) + } +} + +/// Read and parse a TOML file from an specific path. +fn read_toml_file(path: &Path) -> Result { + let toml_str = helpers::read_file(path).with_context(|| { + format!( + "error trying to deserialize toml configuration file at \"{}\"", + path.display() + ) + })?; + + let first_error = match toml_str.parse() { + Ok(res) => return Ok(res), + Err(err) => err, + }; + + let mut second_parser = toml::de::Deserializer::new(&toml_str); + second_parser.set_require_newline_after_table(false); + if let Ok(res) = toml::Value::deserialize(&mut second_parser) { + let msg = format!( + "\ +TOML file found which contains invalid syntax and will soon not parse +at `{}`. +The TOML spec requires newlines after table definitions (e.g., `[a] b = 1` is +invalid), but this file has a table header which does not have a newline after +it. A newline needs to be added and this warning will soon become a hard error +in the future.", + path.display() + ); + println!("{}", &msg); + return Ok(res); + } + + let first_error = anyhow::Error::from(first_error); + Err(first_error.context("could not parse data input as toml format")) +} diff --git a/src/settings/mod.rs b/src/settings/mod.rs new file mode 100644 index 0000000..65ebf9f --- /dev/null +++ b/src/settings/mod.rs @@ -0,0 +1,210 @@ +use globset::{Glob, GlobMatcher}; +use headers::HeaderMap; +use structopt::StructOpt; + +use crate::{Context, Result}; + +mod cli; +pub mod file; + +use cli::General; + +/// The `headers` file options. +pub struct Headers { + /// Source pattern glob matcher + pub source: GlobMatcher, + /// Map of custom HTTP headers + pub headers: HeaderMap, +} + +/// The `advanced` file options. +pub struct Advanced { + pub headers: Option>, +} + +/// The full server CLI and File options. +pub struct Settings { + /// General server options + pub general: General, + /// Advanced server options + pub advanced: Option, +} + +impl Settings { + /// Handles CLI and config file options and converging them into one. + pub fn get() -> Result { + let opts = General::from_args(); + + // Define the general CLI/file options + let mut host = opts.host.to_owned(); + let mut port = opts.port; + let mut root = opts.root.to_owned(); + let mut log_level = opts.log_level.to_owned(); + let mut config_file = opts.config_file.clone(); + let mut cache_control_headers = opts.cache_control_headers; + let mut compression = opts.compression; + let mut page404 = opts.page404.to_owned(); + let mut page50x = opts.page50x.to_owned(); + let mut http2 = opts.http2; + let mut http2_tls_cert = opts.http2_tls_cert.to_owned(); + let mut http2_tls_key = opts.http2_tls_key.to_owned(); + let mut security_headers = opts.security_headers; + let mut cors_allow_origins = opts.cors_allow_origins.to_owned(); + let mut cors_allow_headers = opts.cors_allow_headers.to_owned(); + let mut directory_listing = opts.directory_listing; + let mut directory_listing_order = opts.directory_listing_order; + let mut basic_auth = opts.basic_auth.to_owned(); + let mut fd = opts.fd; + let mut threads_multiplier = opts.threads_multiplier; + let mut grace_period = opts.grace_period; + let mut page_fallback = opts.page_fallback.to_owned(); + + // Define the advanced file options + let mut settings_advanced: Option = None; + + // Handle "config file options" and set them when available + // NOTE: All config file based options shouldn't be mandatory, therefore `Some()` wrapped + if let Some(ref p) = opts.config_file { + if p.is_file() { + let path_resolved = p + .canonicalize() + .with_context(|| "error resolving toml config file path")?; + + let settings = file::Settings::read(&path_resolved) + .with_context(|| { + "can not read toml config file because has invalid or unsupported format/options" + })?; + + config_file = Some(path_resolved); + + // Assign the corresponding file option values + if let Some(general) = settings.general { + if let Some(ref v) = general.host { + host = v.to_owned() + } + if let Some(v) = general.port { + port = v + } + if let Some(ref v) = general.root { + root = v.to_owned() + } + if let Some(ref v) = general.log_level { + log_level = v.name().to_lowercase(); + } + if let Some(v) = general.cache_control_headers { + cache_control_headers = v + } + if let Some(v) = general.compression { + compression = v + } + if let Some(ref v) = general.page404 { + page404 = v.to_owned() + } + if let Some(ref v) = general.page50x { + page50x = v.to_owned() + } + if let Some(v) = general.http2 { + http2 = v + } + if let Some(ref v) = general.http2_tls_cert { + http2_tls_cert = v.to_owned() + } + if let Some(ref v) = general.http2_tls_key { + http2_tls_key = v.to_owned() + } + if let Some(v) = general.security_headers { + security_headers = v + } + if let Some(ref v) = general.cors_allow_origins { + cors_allow_origins = v.to_owned() + } + if let Some(ref v) = general.cors_allow_headers { + cors_allow_headers = v.to_owned() + } + if let Some(v) = general.directory_listing { + directory_listing = v + } + if let Some(v) = general.directory_listing_order { + directory_listing_order = v + } + if let Some(ref v) = general.basic_auth { + basic_auth = v.to_owned() + } + if let Some(v) = general.fd { + fd = Some(v) + } + if let Some(v) = general.threads_multiplier { + threads_multiplier = v + } + if let Some(v) = general.grace_period { + grace_period = v + } + if let Some(ref v) = general.page_fallback { + page_fallback = v.to_owned() + } + } + + // Prepare the "advanced" options + if let Some(advanced) = settings.advanced { + // 1. Custom HTTP headers assignment + let headers_entries = match advanced.headers { + Some(headers_entries) => { + let mut headers_vec: Vec = Vec::new(); + + // Compile a glob pattern for each header sources entry + for headers_entry in headers_entries.iter() { + let source = Glob::new(&headers_entry.source) + .with_context(|| { + format!( + "can not compile glob pattern for header source: {}", + &headers_entry.source + ) + })? + .compile_matcher(); + + headers_vec.push(Headers { + source, + headers: headers_entry.headers.to_owned(), + }); + } + Some(headers_vec) + } + _ => None, + }; + + settings_advanced = Some(Advanced { + headers: headers_entries, + }); + } + } + } + + Ok(Settings { + general: General { + host, + port, + root, + log_level, + config_file, + cache_control_headers, + compression, + page404, + page50x, + http2, + http2_tls_cert, + http2_tls_key, + security_headers, + cors_allow_origins, + cors_allow_headers, + directory_listing, + directory_listing_order, + basic_auth, + fd, + threads_multiplier, + grace_period, + page_fallback, + }, + advanced: settings_advanced, + }) + } +} diff --git a/tests/toml/config.toml b/tests/toml/config.toml new file mode 100644 index 0000000..51de796 --- /dev/null +++ b/tests/toml/config.toml @@ -0,0 +1,68 @@ +[general] + +#### Address & Root dir +host = "::" +port = 8087 +root = "docker/public" + +#### Logging +log-level = "trace" + +#### Cache Control headers +cache-control-headers = true + +#### Auto Compression +compression = true + +#### Error pages +page404 = "docker/public/404.html" +page50x = "docker/public/50x.html" + +#### HTTP/2 + TLS +http2 = false +http2-tls-cert = "" +http2-tls-key = "" + +#### CORS & Security headers +security-headers = true +cors-allow-origins = "" + +#### Directoy listing +directory-listing = false + +#### Basich Authentication +basic-auth = "" + +#### File descriptor binding +# fd = "" + +#### Worker threads +threads-multiplier = 1 + +#### Grace period after a graceful shutdown +grace-period = 0 + +#### Page fallback for 404s +page-fallback = "" + +[advanced] + +#### HTTP Headers customization + +#### a. Oneline version +[[advanced.headers]] +source = "**/*.{js,css}" +headers = { Access-Control-Allow-Origin = "*", X-XSS-PROTECTION = "1; mode=block" } + +# #### b. Multiline version +[[advanced.headers]] +source = "index.html" +[advanced.headers.headers] +Cache-Control = "public, max-age=36000" +Content-Security-Policy = "frame-ancestors 'self'" +Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload" + +#### c. Multiline version with explicit key (dotted) +[[advanced.headers]] +source = "**/*.{jpg,jpeg,png,ico,gif}" +headers.Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload" -- libgit2 1.7.2