index : static-web-server.git

ascending towards madness

author Jose Quintana <joseluisquintana20@gmail.com> 2022-04-25 14:53:43.0 +00:00:00
committer Jose Quintana <joseluisquintana20@gmail.com> 2022-04-25 14:53:43.0 +00:00:00
commit
31bb70a13c818939aa51b079a9312acf5876767d [patch]
tree
0a798ac225e31cf17151213c621104f299f49058
parent
3b2d191846104a274456e7bd100eb86651acd339
download
31bb70a13c818939aa51b079a9312acf5876767d.tar.gz

refactor: general and advanced settings



Diff

 src/bin/server.rs      |   2 +-
 src/config.rs          | 182 +--------------------------------------------------
 src/lib.rs             |   5 +-
 src/manifest.rs        | 144 +----------------------------------------
 src/server.rs          | 112 ++++++++++++-------------------
 src/settings/cli.rs    | 182 ++++++++++++++++++++++++++++++++++++++++++++++++++-
 src/settings/file.rs   | 173 ++++++++++++++++++++++++++++++++++++++++++++++++-
 src/settings/mod.rs    | 160 ++++++++++++++++++++++++++++++++++++++++++++-
 tests/toml/config.toml |  72 ++++++++++----------
 9 files changed, 602 insertions(+), 430 deletions(-)

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 3d12d16..0000000
--- a/src/config.rs
+++ /dev/null
@@ -1,182 +0,0 @@
//! Server CLI Options

use std::path::PathBuf;

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,

    #[structopt(long, short = "w", env = "SERVER_CONFIG_FILE")]
    /// Server TOML configuration file path.
    pub config_file: Option<PathBuf>,
}
diff --git a/src/lib.rs b/src/lib.rs
index 569aef6..7061658 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -11,7 +11,6 @@ extern crate serde;

pub mod basic_auth;
pub mod compression;
pub mod config;
pub mod control_headers;
pub mod cors;
pub mod error_page;
@@ -19,10 +18,10 @@ pub mod fallback_page;
pub mod handler;
pub mod helpers;
pub mod logger;
pub mod manifest;
pub mod security_headers;
pub mod server;
pub mod service;
pub mod settings;
pub mod signals;
pub mod static_files;
pub mod tls;
@@ -31,6 +30,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/manifest.rs b/src/manifest.rs
deleted file mode 100644
index 8daad27..0000000
--- a/src/manifest.rs
+++ /dev/null
@@ -1,144 +0,0 @@
//! The server configuration file (Manifest)

use headers::HeaderMap;
use serde::Deserialize;
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};

use crate::{helpers, Context, Result};

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum LogLevel {
    Error,
    Warn,
    Info,
    Debug,
    Trace,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Headers {
    pub source: String,
    #[serde(with = "http_serde::header_map")]
    pub headers: HeaderMap,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Manifest {
    // General
    pub host: Option<String>,
    pub port: Option<u8>,
    pub root: Option<String>,

    // Logging
    pub log_level: Option<LogLevel>,

    // Cache Control headers
    pub cache_control_headers: bool,

    // Compression
    pub compression: bool,

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

    // HTTP/2 + TLS
    pub http2: bool,
    pub http2_tls_cert: PathBuf,
    pub http2_tls_key: PathBuf,

    // Security headers
    pub security_headers: bool,

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

    // Directoy listing
    pub directory_listing: 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: usize,

    pub grace_period: u8,

    pub page_fallback: String,

    // Headers
    #[serde(rename(deserialize = "headers"))]
    pub headers: Option<Vec<Headers>>,
}

/// Read a TOML file from path.
fn read_file(path: &Path) -> Result<toml::Value> {
    let toml_str = helpers::read_file(path).with_context(|| {
        format!(
            "error trying to deserialize configuration \"{}\" file toml.",
            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 input as TOML format"))
}

/// Detect and read the configuration manifest file by path.
pub fn read_manifest(config_file: &Path) -> Result<Option<Manifest>> {
    // Validate TOML file extension
    let ext = config_file.extension();
    if ext.is_none() || ext.unwrap().is_empty() || ext.unwrap().ne("toml") {
        return Ok(None);
    }

    // TODO: validate minimal TOML file structure needed
    let toml = read_file(config_file).with_context(|| "error reading configuration toml file.")?;
    let mut unused = BTreeSet::new();
    let manifest: Manifest = serde_ignored::deserialize(toml, |path| {
        let mut key = String::new();
        helpers::stringify(&mut key, &path);
        unused.insert(key);
    })
    .with_context(|| "error during configuration toml file deserialization.")?;

    for key in unused {
        println!(
            "Warning: unused configuration manifest key \"{}\" or unsuported.",
            key
        );
    }

    Ok(Some(manifest))
}
diff --git a/src/server.rs b/src/server.rs
index 45ef981..6e60c67 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, manifest, 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,52 +52,40 @@ 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;

        // Initialize logging system
        logger::init(&opts.log_level)
            .with_context(|| "failed to initialize logging".to_string())?;

        // TODO:
        // Check for a config file
        if let Some(config_file) = &opts.config_file {
            if config_file.is_file() {
                let path_resolved = config_file
                    .canonicalize()
                    .with_context(|| "error resolving config file path.")?;

                let manifest = manifest::read_manifest(&path_resolved).with_context(|| {
                    format!(
                        "can not get \"{}\" config file because has invalid format or inaccessible",
                        path_resolved.display()
                    )
                })?;
        let general = self.opts.general;

                println!("{:?}", manifest.unwrap().headers);
            }
        // TODO: handle advaced options
        // let advanced = 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();
@@ -107,55 +94,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
@@ -177,31 +164,30 @@ impl Server {

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

@@ -231,7 +217,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();

@@ -265,9 +251,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..27d0f6e
--- /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(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
    #[serde(rename(deserialize = "headers"))]
    pub headers: Option<Vec<Headers>>,
}

/// General server options available configuration file mode.
#[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: General,
    pub advanced: 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..13944b9
--- /dev/null
+++ b/src/settings/mod.rs
@@ -0,0 +1,160 @@
use structopt::StructOpt;

use crate::{Context, Result};

mod cli;
mod file;

use cli::General;
use file::Advanced;

/// The Server CLI and File settings.
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 file::Settings{ general, advanced } = 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);

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

                settings_advanced = Some(advanced)
            }
        }

        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
index 4479753..3eb3915 100644
--- a/tests/toml/config.toml
+++ b/tests/toml/config.toml
@@ -1,63 +1,67 @@
# Address & Root dir
host = "::"
port = 80
root = "./public"
[general]

# Logging
log-level = "error"
#### Address & Root dir
# host = "::"
# port = 80
# root = "./public"

#### Logging
log-level = "debug"

# Cache Control headers
cache-control-headers = true
# cache-control-headers = true

# Auto Compression
compression = true
#### Auto Compression
# compression = true

# Error pages
page404 = "404.html"
page50x = "50x.html"
#### Error pages
# page404 = "404.html"
# page50x = "50x.html"

# HTTP/2 + TLS
http2 = true
http2-tls-cert = ""
http2-tls-key = ""
#### HTTP/2 + TLS
# http2 = true
# http2-tls-cert = ""
# http2-tls-key = ""

# CORS & Security headers
security-headers = true
cors-allow-origins = ""
#### CORS & Security headers
# security-headers = true
# cors-allow-origins = ""

# Directoy listing
directory-listing = false
#### Directoy listing
# directory-listing = false

# Basich Authentication
basic-auth = ""
#### Basich Authentication
# basic-auth = ""

# File descriptor binding
#### File descriptor binding
# fd = ""

# Worker threads
threads-multiplier = 1
#### Worker threads
# threads-multiplier = 1

#### Grace period after a graceful shutdown
# grace-period = 0

# Grace period after a graceful shutdown
grace-period = 0
#### Page fallback for 404s
# page-fallback = ""

# Page fallback for 404s
page-fallback = ""
[advanced]

# HTTP Headers customization
#### HTTP Headers customization

# a. Online version
#### a. Oneline version
[[headers]]
source = "**/*.@(eot|otf|ttf|ttc|woff|font.css)"
headers = { Access-Control-Allow-Origin = "*", X-XSS-PROTECTION = "1; mode=block" }

# b. Multiline version
#### b. Multiline version
[[headers]]
  source = "**/*.@(jpg|jpeg|gif|png)"
[headers.headers]
  Cache-Control = "max-age=7200"
  Content-Security-Policy = "frame-ancestors 'self'"

# c. Multiline version with explicit key (dotted)
#### c. Multiline version with explicit key (dotted)
[[headers]]
source = "404.html"
headers.Cache-Control = "max-age=300"