index : static-web-server.git

ascending towards madness

author Jose Quintana <joseluisquintana20@gmail.com> 2022-04-21 22:18:10.0 +00:00:00
committer Jose Quintana <joseluisquintana20@gmail.com> 2022-04-21 22:18:10.0 +00:00:00
commit
b9033b71a5c80ed530a9b56991eed705d114efab [patch]
tree
1e46ce4f47890f541aee13eb3ab377a5f311303e
parent
9f4bbd7f034cf9c50f46f7c7a2f7202d1b5a9e9e
download
b9033b71a5c80ed530a9b56991eed705d114efab.tar.gz

chore: preliminary toml manifest file



Diff

 Cargo.lock             |  41 ++++++++++++++-
 Cargo.toml             |   3 +-
 src/config.rs          |  13 ++++-
 src/helpers.rs         |  40 ++++++++++++-
 src/lib.rs             |   4 +-
 src/manifest.rs        | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++-
 tests/toml/config.toml |  66 ++++++++++++++++++++++-
 7 files changed, 315 insertions(+), 1 deletion(-)

diff --git a/Cargo.lock b/Cargo.lock
index 7cd7654..b51976c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -774,6 +774,35 @@ dependencies = [
]

[[package]]
name = "serde"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
dependencies = [
 "serde_derive",
]

[[package]]
name = "serde_derive"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "serde_ignored"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c2c7d39d14f2f2ea82239de71594782f186fd03501ac81f0ce08e674819ff2f"
dependencies = [
 "serde",
]

[[package]]
name = "sha-1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -873,6 +902,8 @@ dependencies = [
 "percent-encoding",
 "pin-project",
 "rustls-pemfile",
 "serde",
 "serde_ignored",
 "signal-hook",
 "signal-hook-tokio",
 "structopt",
@@ -881,6 +912,7 @@ dependencies = [
 "tokio",
 "tokio-rustls",
 "tokio-util",
 "toml",
 "tracing",
 "tracing-subscriber",
]
@@ -1025,6 +1057,15 @@ dependencies = [
]

[[package]]
name = "toml"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
dependencies = [
 "serde",
]

[[package]]
name = "tower-service"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index d88e041..8f936ae 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -51,6 +51,9 @@ 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"

[target.'cfg(all(target_env = "musl", target_pointer_width = "64"))'.dependencies.tikv-jemallocator]
version = "0.4"
diff --git a/src/config.rs b/src/config.rs
index e04d838..e46f497 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,3 +1,7 @@
//! Server CLI Options

use std::path::PathBuf;

use structopt::StructOpt;

#[derive(Debug, StructOpt)]
@@ -171,4 +175,13 @@ 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",
        default_value = "config.toml",
        env = "SEVER_CONFIG_FILE"
    )]
    /// Server TOML configuration file path.
    pub config_file: PathBuf,
}
diff --git a/src/helpers.rs b/src/helpers.rs
index 375efa2..66fc20e 100644
--- a/src/helpers.rs
+++ b/src/helpers.rs
@@ -1,7 +1,7 @@
use std::fs;
use std::path::{Path, PathBuf};

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

/// Validate and return a directory path.
pub fn get_valid_dirpath<P: AsRef<Path>>(path: P) -> Result<PathBuf>
@@ -37,3 +37,41 @@ pub fn read_file_content(p: &str) -> String {
    }
    String::new()
}

/// Read the entire contents of a file into a bytes vector.
pub fn read_bytes(path: &Path) -> Result<Vec<u8>> {
    fs::read(path).with_context(|| format!("failed to read `{}`", path.display()))
}

/// Read an UTF-8 file from a specific path.
pub fn read_file(path: &Path) -> Result<String> {
    match String::from_utf8(read_bytes(path)?) {
        Ok(s) => Ok(s),
        Err(_) => bail!("path at `{}` was not valid utf-8", path.display()),
    }
}

pub fn stringify(dst: &mut String, path: &serde_ignored::Path<'_>) {
    use serde_ignored::Path;

    match *path {
        Path::Root => {}
        Path::Seq { parent, index } => {
            stringify(dst, parent);
            if !dst.is_empty() {
                dst.push('.');
            }
            dst.push_str(&index.to_string());
        }
        Path::Map { parent, ref key } => {
            stringify(dst, parent);
            if !dst.is_empty() {
                dst.push('.');
            }
            dst.push_str(key);
        }
        Path::Some { parent }
        | Path::NewtypeVariant { parent }
        | Path::NewtypeStruct { parent } => stringify(dst, parent),
    }
}
diff --git a/src/lib.rs b/src/lib.rs
index 2cb08e1..569aef6 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -6,6 +6,9 @@
#[macro_use]
extern crate anyhow;

#[macro_use]
extern crate serde;

pub mod basic_auth;
pub mod compression;
pub mod config;
@@ -16,6 +19,7 @@ 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;
diff --git a/src/manifest.rs b/src/manifest.rs
new file mode 100644
index 0000000..2ce9066
--- /dev/null
+++ b/src/manifest.rs
@@ -0,0 +1,149 @@
//! The server configuration file (Manifest)

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, Default)]
#[serde(rename_all = "kebab-case")]
pub struct Header {
    pub key: String,
    pub value: String,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "kebab-case")]
pub struct Headers {
    pub source: String,
    pub headers: Option<Vec<Header>>,
}

#[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: String,

    // Directoy listing
    pub directory_listing: bool,
    pub directory_listing_order: 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/tests/toml/config.toml b/tests/toml/config.toml
new file mode 100644
index 0000000..842d6cf
--- /dev/null
+++ b/tests/toml/config.toml
@@ -0,0 +1,66 @@
# Address & Root dir
host = "::"
port = 80
root = "./public"

# Logging
log-level = "error"

# Cache Control headers
cache-control-headers = true

# Auto Compression
compression = true

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

# HTTP/2 + TLS
http2 = true
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 = 0

page-fallback = ""

[[headers]]
source = "**/*.@(eot|otf|ttf|ttc|woff|font.css)"
  [[headers.headers]]
  key = "Access-Control-Allow-Origin"
  value = "*"
  [[headers.headers]]
  key = "Access-Control-Allow-Origin"
  value = "*"
  [[headers.headers]]
  key = "Access-Control-Allow-Origin"
  value = "*"

[[headers]]
source = "**/*.@(jpg|jpeg|gif|png)"
  [[headers.headers]]
  key = "Cache-Control"
  value = "max-age=7200"

[[headers]]
source = "404.html"
  [[headers.headers]]
  key = "Cache-Control"
  value = "max-age=300"