From b9033b71a5c80ed530a9b56991eed705d114efab Mon Sep 17 00:00:00 2001 From: Jose Quintana Date: Fri, 22 Apr 2022 00:18:10 +0200 Subject: [PATCH] chore: preliminary toml manifest file --- 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(-) create mode 100644 src/manifest.rs create mode 100644 tests/toml/config.toml 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>(path: P) -> Result @@ -37,3 +37,41 @@ pub fn read_file_content(p: &str) -> String { } String::new() } + +/// Read the entire contents of a file into a bytes vector. +pub fn read_bytes(path: &Path) -> Result> { + fs::read(path).with_context(|| format!("failed to read `{}`", path.display())) +} + +/// Read an UTF-8 file from a specific path. +pub fn read_file(path: &Path) -> Result { + match String::from_utf8(read_bytes(path)?) { + Ok(s) => Ok(s), + Err(_) => bail!("path at `{}` was not valid utf-8", path.display()), + } +} + +pub fn stringify(dst: &mut String, path: &serde_ignored::Path<'_>) { + use serde_ignored::Path; + + match *path { + Path::Root => {} + Path::Seq { parent, index } => { + stringify(dst, parent); + if !dst.is_empty() { + dst.push('.'); + } + dst.push_str(&index.to_string()); + } + Path::Map { parent, ref key } => { + stringify(dst, parent); + if !dst.is_empty() { + dst.push('.'); + } + dst.push_str(key); + } + Path::Some { parent } + | Path::NewtypeVariant { parent } + | Path::NewtypeStruct { parent } => stringify(dst, parent), + } +} diff --git a/src/lib.rs b/src/lib.rs index 2cb08e1..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>, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct Manifest { + // General + pub host: Option, + pub port: Option, + pub root: Option, + + // Logging + pub log_level: Option, + + // Cache Control headers + pub cache_control_headers: bool, + + // Compression + pub compression: bool, + + // Error pages + pub page404: Option, + pub page50x: Option, + + // 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, + + // File descriptor binding + pub fd: Option, + + // Worker threads + pub threads_multiplier: usize, + + pub grace_period: u8, + + pub page_fallback: String, + + // Headers + #[serde(rename(deserialize = "headers"))] + pub headers: Option>, +} + +/// Read a TOML file from path. +fn read_file(path: &Path) -> Result { + 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> { + // 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" -- libgit2 1.7.2