use headers::HeaderMap;
use serde::Deserialize;
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::path::Path;
use std::{collections::BTreeSet, path::PathBuf};
#[cfg(feature = "directory-listing")]
use crate::directory_listing::DirListFmt;
use crate::{helpers, Context, Result};
#[derive(Debug, Serialize, Deserialize, Clone)]
#[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",
}
}
}
#[cfg(any(
feature = "compression",
feature = "compression-gzip",
feature = "compression-brotli",
feature = "compression-zstd",
feature = "compression-deflate"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "compression",
feature = "compression-gzip",
feature = "compression-brotli",
feature = "compression-zstd",
feature = "compression-deflate"
)))
)]
#[derive(clap::ValueEnum, Debug, Serialize, Deserialize, Copy, Clone)]
#[serde(rename_all = "kebab-case")]
pub enum CompressionLevel {
Fastest,
Best,
Default,
}
#[cfg(any(
feature = "compression",
feature = "compression-gzip",
feature = "compression-brotli",
feature = "compression-zstd",
feature = "compression-deflate"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "compression",
feature = "compression-gzip",
feature = "compression-brotli",
feature = "compression-zstd",
feature = "compression-deflate"
)))
)]
impl CompressionLevel {
pub(crate) fn into_algorithm_level(self, default: i32) -> async_compression::Level {
match self {
Self::Fastest => async_compression::Level::Fastest,
Self::Best => async_compression::Level::Best,
Self::Default => async_compression::Level::Precise(default),
}
}
}
#[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,
}
#[derive(Debug, Serialize_repr, Deserialize_repr, Clone)]
#[repr(u16)]
pub enum RedirectsKind {
Permanent = 301,
Temporary = 302,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Redirects {
pub host: Option<String>,
pub source: String,
pub destination: String,
pub kind: RedirectsKind,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Rewrites {
pub source: String,
pub destination: String,
pub redirect: Option<RedirectsKind>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct VirtualHosts {
pub host: String,
pub root: Option<PathBuf>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Advanced {
pub headers: Option<Vec<Headers>>,
pub rewrites: Option<Vec<Rewrites>>,
pub redirects: Option<Vec<Redirects>>,
pub virtual_hosts: Option<Vec<VirtualHosts>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct General {
pub host: Option<String>,
pub port: Option<u16>,
pub root: Option<PathBuf>,
pub log_level: Option<LogLevel>,
pub cache_control_headers: Option<bool>,
#[cfg(any(
feature = "compression",
feature = "compression-gzip",
feature = "compression-brotli",
feature = "compression-zstd",
feature = "compression-deflate"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "compression",
feature = "compression-gzip",
feature = "compression-brotli",
feature = "compression-zstd",
feature = "compression-deflate"
)))
)]
pub compression: Option<bool>,
#[cfg(any(
feature = "compression",
feature = "compression-gzip",
feature = "compression-brotli",
feature = "compression-zstd",
feature = "compression-deflate"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "compression",
feature = "compression-gzip",
feature = "compression-brotli",
feature = "compression-zstd",
feature = "compression-deflate"
)))
)]
pub compression_level: Option<CompressionLevel>,
#[cfg(any(
feature = "compression",
feature = "compression-gzip",
feature = "compression-brotli",
feature = "compression-zstd",
feature = "compression-deflate"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "compression",
feature = "compression-gzip",
feature = "compression-brotli",
feature = "compression-zstd",
feature = "compression-deflate"
)))
)]
pub compression_static: Option<bool>,
pub page404: Option<PathBuf>,
pub page50x: Option<PathBuf>,
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub http2: Option<bool>,
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub http2_tls_cert: Option<PathBuf>,
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub http2_tls_key: Option<PathBuf>,
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub https_redirect: Option<bool>,
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub https_redirect_host: Option<String>,
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub https_redirect_from_port: Option<u16>,
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub https_redirect_from_hosts: Option<String>,
pub security_headers: Option<bool>,
pub cors_allow_origins: Option<String>,
pub cors_allow_headers: Option<String>,
pub cors_expose_headers: Option<String>,
pub index_files: Option<String>,
#[cfg(feature = "directory-listing")]
#[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
pub directory_listing: Option<bool>,
#[cfg(feature = "directory-listing")]
#[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
pub directory_listing_order: Option<u8>,
#[cfg(feature = "directory-listing")]
#[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
pub directory_listing_format: Option<DirListFmt>,
#[cfg(feature = "basic-auth")]
#[cfg_attr(docsrs, doc(cfg(feature = "basic-auth")))]
pub basic_auth: Option<String>,
pub fd: Option<usize>,
pub threads_multiplier: Option<usize>,
pub max_blocking_threads: Option<usize>,
pub grace_period: Option<u8>,
#[cfg(feature = "fallback-page")]
#[cfg_attr(docsrs, doc(cfg(feature = "fallback-page")))]
pub page_fallback: Option<PathBuf>,
pub log_remote_address: Option<bool>,
pub redirect_trailing_slash: Option<bool>,
pub ignore_hidden_files: Option<bool>,
pub health: Option<bool>,
#[cfg(all(unix, feature = "experimental"))]
pub experimental_metrics: Option<bool>,
pub maintenance_mode: Option<bool>,
pub maintenance_mode_status: Option<u16>,
pub maintenance_mode_file: Option<PathBuf>,
#[cfg(windows)]
pub windows_service: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Settings {
pub general: Option<General>,
pub advanced: Option<Advanced>,
}
impl Settings {
pub fn read(config_file: &Path) -> Result<Settings> {
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`");
}
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 \"{key}\" or unsupported");
}
Ok(manifest)
}
}
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()
)
})?;
toml_str
.parse()
.map_err(|e| anyhow::Error::from(e).context("could not parse input as TOML"))
}