Repository: static-web-server/static-web-server (language: rust)

Issue:
Configurable Cache Control
I'm using `static-web-server` as a development server, and I would love the ability to configure the `Cache-Control` headers in some way.

Some sort of flag would be fine, but have you considered configuration beyond flags in general? Do you already have thoughts on the topic?
Configurable Cache Control
I'm using `static-web-server` as a development server, and I would love the ability to configure the `Cache-Control` headers in some way.

Some sort of flag would be fine, but have you considered configuration beyond flags in general? Do you already have thoughts on the topic?


Discussion / hints:
That's razonable, I have no suggestions right now but open to some ideas so feel free to suggest something or send a PR.

Just to share an idea while I came across with the existing cache control feature.
For me the easiest way to kind of workaround part of the current limitation is using two new flags like:

```sh
# To configure the extensions to apply the header 
--cache-control-extensions="atom,json,rss,xml"
# To configure the max-age
--cache-control-expires=2h
```

The problem is that you could cache certain files in one-way only.

I recognize that the existing feature is arbitrary and ad-hoc however the possibility of a config file is also not discarded but of course implies more extra work.

Thoughts?
Just to "unfreeze" this feature request.
In **v2** you can disable or enable the headers via `-e=true|false, --cache-control-headers=true|false`.
However yeah we need something more flexible so I will plan something around it like a proper config file (probably a .toml one) because it can also help for example defining custom routes which I am also interested in.
Config file setup is under proposal on https://github.com/joseluisq/static-web-server/discussions/69. Feedback welcome!
I just hit this (CSP in my case), and was wondering whether it wouldn't be easier to allow string values for cache control headers and security headers, so you could set your own version if the defaults are no good for a specific use case? Maybe just a newline-delimited list of headers...
That's razonable, I have no suggestions right now but open to some ideas so feel free to suggest something or send a PR.

Just to share an idea while I came across with the existing cache control feature.
For me the easiest way to kind of workaround part of the current limitation is using two new flags like:

```sh
# To configure the extensions to apply the header 
--cache-control-extensions="atom,json,rss,xml"
# To configure the max-age
--cache-control-expires=2h
```

The problem is that you could cache certain files in one-way only.

I recognize that the existing feature is arbitrary and ad-hoc however the possibility of a config file is also not discarded but of course implies more extra work.

Thoughts?
Just to "unfreeze" this feature request.
In **v2** you can disable or enable the headers via `-e=true|false, --cache-control-headers=true|false`.
However yeah we need something more flexible so I will plan something around it like a proper config file (probably a .toml one) because it can also help for example defining custom routes which I am also interested in.
Config file setup is under proposal on https://github.com/joseluisq/static-web-server/discussions/69. Feedback welcome!
I just hit this (CSP in my case), and was wondering whether it wouldn't be easier to allow string values for cache control headers and security headers, so you could set your own version if the defaults are no good for a specific use case? Maybe just a newline-delimited list of headers...

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 7cd7654d..57bfade9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8,6 +8,15 @@ version = "1.0.2"
 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"
@@ -122,6 +131,15 @@ dependencies = [
  "alloc-stdlib",
 ]
 
+[[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"
@@ -311,6 +329,20 @@ dependencies = [
  "wasi 0.10.2+wasi-snapshot-preview1",
 ]
 
+[[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"
@@ -400,6 +432,16 @@ dependencies = [
  "pin-project-lite",
 ]
 
+[[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"
@@ -721,6 +763,23 @@ dependencies = [
  "bitflags",
 ]
 
+[[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"
@@ -773,6 +832,35 @@ dependencies = [
  "untrusted",
 ]
 
+[[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"
@@ -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",
 ]
@@ -1024,6 +1117,15 @@ dependencies = [
  "tracing",
 ]
 
+[[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"
diff --git a/Cargo.toml b/Cargo.toml
index d88e0418..400d9a17 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 0e60829a..5b9c9bb4 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 f78b2878..67ee8392 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 00c6e3cf..e1678877 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 00000000..9d0a63c6
--- /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 07f715a5..bab3034b 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 00000000..41cf9311
--- /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 a6ac352e..e9b125b8 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 4d34f643..96e4138e 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 cb90e83d..298f1e31 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 9b74b663..898d8f5a 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 b5ac46b0..05663a6f 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/custom_headers.rs b/src/custom_headers.rs
new file mode 100644
index 00000000..e6854c9e
--- /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 840db08f..61eab62d 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 375efa2a..66fc20ea 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 2cb08e1b..49c9ddc6 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 a46fb667..a39f2981 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/config.rs b/src/settings/cli.rs
similarity index 95%
rename from src/config.rs
rename to src/settings/cli.rs
index e04d838b..4889cef6 100644
--- a/src/config.rs
+++ b/src/settings/cli.rs
@@ -1,8 +1,12 @@
+//! 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 Config {
+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,
@@ -171,4 +175,8 @@ pub struct Config {
     #[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 00000000..bff1623e
--- /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 00000000..65ebf9ff
--- /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,
+        })
+    }
+}


Test patch:
diff --git a/tests/toml/config.toml b/tests/toml/config.toml
new file mode 100644
index 00000000..51de796c
--- /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"