index : static-web-server.git

ascending towards madness

author Jose Quintana <1700322+joseluisq@users.noreply.github.com> 2022-05-03 13:33:25.0 +00:00:00
committer GitHub <noreply@github.com> 2022-05-03 13:33:25.0 +00:00:00
commit
1fd3e483a99d57dcbbdc48d31eaac87ed2c102f7 [patch]
tree
5488403cd993b9adfde3cade518886b4e2aa43f3
parent
b9ca0de8f5feb31159ddc0fa139f1c6f27c01706
parent
7dda2ea54c43bd2ee5de4f86385df655d179e3eb
download
1fd3e483a99d57dcbbdc48d31eaac87ed2c102f7.tar.gz

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.

Diff

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

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 <https://git.io/joseluisq>"]
authors = ["Jose Quintana <https://joseluisq.net>"]
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 <https://git.io/joseluisq>
Copyright (c) 2019-present Jose Quintana <https://joseluisq.net>

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 <https://git.io/joseluisq>
static-web-server 2.8.0
Jose Quintana <https://joseluisq.net>
A blazing fast and asynchronous web server for static files-serving.

USAGE:
@@ -31,6 +31,9 @@ OPTIONS:
    -x, --compression <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 <config-file>
            Server TOML configuration file path [env: SERVER_CONFIG_FILE=]

    -j, --cors-allow-headers <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<usize>,

    #[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<Vec<Headers>>,
    resp: &mut Response<Body>,
) {
    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<Advanced>,
}

/// 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<P: AsRef<Path>>(path: P) -> Result<PathBuf>
@@ -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<Vec<u8>> {
    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<String> {
    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<Server> {
        // 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::<IpAddr>()
                    .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<usize>,

    #[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<PathBuf>,
}
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<Vec<Headers>>,
}

/// 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<String>,
    pub port: Option<u16>,
    pub root: Option<String>,

    // Logging
    pub log_level: Option<LogLevel>,

    // Cache Control headers
    pub cache_control_headers: Option<bool>,

    // Compression
    pub compression: Option<bool>,

    // Error pages
    pub page404: Option<String>,
    pub page50x: Option<String>,

    // HTTP/2 + TLS
    pub http2: Option<bool>,
    pub http2_tls_cert: Option<String>,
    pub http2_tls_key: Option<String>,

    // Security headers
    pub security_headers: Option<bool>,

    // CORS
    pub cors_allow_origins: Option<String>,
    pub cors_allow_headers: Option<String>,

    // Directoy listing
    pub directory_listing: Option<bool>,
    pub directory_listing_order: Option<u8>,

    // Basich Authentication
    pub basic_auth: Option<String>,

    // File descriptor binding
    pub fd: Option<usize>,

    // Worker threads
    pub threads_multiplier: Option<usize>,

    pub grace_period: Option<u8>,

    pub page_fallback: Option<String>,
}

/// Full server configuration
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Settings {
    pub general: Option<General>,
    pub advanced: Option<Advanced>,
}

impl Settings {
    /// Read and deserialize the server TOML configuration file by path.
    pub fn read(config_file: &Path) -> Result<Settings> {
        // 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<toml::Value> {
    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<Vec<Headers>>,
}

/// The full server CLI and File options.
pub struct Settings {
    /// General server options
    pub general: General,
    /// Advanced server options
    pub advanced: Option<Advanced>,
}

impl Settings {
    /// Handles CLI and config file options and converging them into one.
    pub fn get() -> Result<Settings> {
        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<Advanced> = 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<Headers> = 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"