static_web_server/settings/
file.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// This file is part of Static Web Server.
3// See https://static-web-server.net/ for more information
4// Copyright (C) 2019-present Jose Quintana <joseluisq.net>
5
6//! The server configuration file options (manifest)
7
8use headers::HeaderMap;
9use serde::Deserialize;
10use serde_repr::{Deserialize_repr, Serialize_repr};
11use std::net::IpAddr;
12use std::path::Path;
13use std::{collections::BTreeSet, path::PathBuf};
14
15#[cfg(feature = "directory-listing")]
16use crate::directory_listing::DirListFmt;
17
18use crate::{helpers, Context, Result};
19
20#[derive(Debug, Serialize, Deserialize, Clone)]
21#[serde(rename_all = "kebab-case")]
22/// Log level variants.
23pub enum LogLevel {
24    /// Error log level.
25    Error,
26    /// Warn log level.
27    Warn,
28    /// Info log level.
29    Info,
30    /// Debug log level.
31    Debug,
32    /// Trace log level.
33    Trace,
34}
35
36impl LogLevel {
37    /// Get log level name.
38    pub fn name(&self) -> &'static str {
39        match self {
40            LogLevel::Error => "error",
41            LogLevel::Warn => "warn",
42            LogLevel::Info => "info",
43            LogLevel::Debug => "debug",
44            LogLevel::Trace => "trace",
45        }
46    }
47}
48
49#[cfg(any(
50    feature = "compression",
51    feature = "compression-gzip",
52    feature = "compression-brotli",
53    feature = "compression-zstd",
54    feature = "compression-deflate"
55))]
56#[cfg_attr(
57    docsrs,
58    doc(cfg(any(
59        feature = "compression",
60        feature = "compression-gzip",
61        feature = "compression-brotli",
62        feature = "compression-zstd",
63        feature = "compression-deflate"
64    )))
65)]
66#[derive(clap::ValueEnum, Debug, Serialize, Deserialize, Copy, Clone)]
67#[serde(rename_all = "kebab-case")]
68/// Compression level settings.
69pub enum CompressionLevel {
70    /// Fastest execution at the expense of larger file sizes.
71    Fastest,
72    /// Smallest file size but potentially slow.
73    Best,
74    /// Algorithm-specific default compression level setting.
75    Default,
76}
77
78#[cfg(any(
79    feature = "compression",
80    feature = "compression-gzip",
81    feature = "compression-brotli",
82    feature = "compression-zstd",
83    feature = "compression-deflate"
84))]
85#[cfg_attr(
86    docsrs,
87    doc(cfg(any(
88        feature = "compression",
89        feature = "compression-gzip",
90        feature = "compression-brotli",
91        feature = "compression-zstd",
92        feature = "compression-deflate"
93    )))
94)]
95impl CompressionLevel {
96    /// Converts to a library-specific compression level specification, using
97    /// given numeric level as default.
98    pub(crate) fn into_algorithm_level(self, default: i32) -> async_compression::Level {
99        match self {
100            Self::Fastest => async_compression::Level::Fastest,
101            Self::Best => async_compression::Level::Best,
102            Self::Default => async_compression::Level::Precise(default),
103        }
104    }
105}
106
107#[derive(Debug, Serialize, Deserialize, Clone)]
108#[serde(rename_all = "kebab-case")]
109/// Represents an HTTP headers map.
110pub struct Headers {
111    /// Header source.
112    pub source: String,
113    #[serde(rename(deserialize = "headers"), with = "http_serde::header_map")]
114    /// headers list.
115    pub headers: HeaderMap,
116}
117
118#[derive(Debug, Serialize_repr, Deserialize_repr, Clone)]
119#[repr(u16)]
120/// Represents redirects types.
121pub enum RedirectsKind {
122    /// Moved Permanently
123    Permanent = 301,
124    /// Found
125    Temporary = 302,
126}
127
128#[derive(Debug, Serialize, Deserialize, Clone)]
129#[serde(rename_all = "kebab-case")]
130/// Represents redirects types.
131pub struct Redirects {
132    /// Optional host to match against an incoming URI host if specified
133    pub host: Option<String>,
134    /// Source of the redirect.
135    pub source: String,
136    /// Redirect destination.
137    pub destination: String,
138    /// Redirect type either 301 (Moved Permanently) or 302 (Found).
139    pub kind: RedirectsKind,
140}
141
142#[derive(Debug, Serialize, Deserialize, Clone)]
143#[serde(rename_all = "kebab-case")]
144/// Represents rewrites types.
145pub struct Rewrites {
146    /// Source of the rewrite.
147    pub source: String,
148    /// Rewrite destination.
149    pub destination: String,
150    /// Optional redirect type either 301 (Moved Permanently) or 302 (Found).
151    pub redirect: Option<RedirectsKind>,
152}
153
154#[derive(Debug, Serialize, Deserialize, Clone)]
155#[serde(rename_all = "kebab-case")]
156/// Represents virtual hosts with different root directories
157pub struct VirtualHosts {
158    /// The value to check for in the "Host" header
159    pub host: String,
160    /// The root directory for this virtual host
161    pub root: Option<PathBuf>,
162}
163
164#[cfg(feature = "experimental")]
165#[derive(Debug, Serialize, Deserialize, Clone)]
166#[serde(rename_all = "kebab-case")]
167/// Represents the in-memory file cache feature.
168pub struct MemoryCache {
169    /// Maximum capacity entries of the memory cache store.
170    pub capacity: Option<u64>,
171    /// Time to live in seconds of a cached file entry.
172    pub ttl: Option<u64>,
173    /// Time to idle in seconds of a cached file entry.
174    pub tti: Option<u64>,
175    /// Maximum size in bytes for a file entry to be cached.
176    pub max_file_size: Option<u64>,
177}
178
179/// Advanced server options only available in configuration file mode.
180#[derive(Debug, Serialize, Deserialize, Clone)]
181#[serde(rename_all = "kebab-case")]
182pub struct Advanced {
183    /// Headers
184    pub headers: Option<Vec<Headers>>,
185    /// Rewrites
186    pub rewrites: Option<Vec<Rewrites>>,
187    /// Redirects
188    pub redirects: Option<Vec<Redirects>>,
189    /// Name-based virtual hosting
190    pub virtual_hosts: Option<Vec<VirtualHosts>>,
191    #[cfg(feature = "experimental")]
192    /// In-memory cache feature (experimental).
193    pub memory_cache: Option<MemoryCache>,
194}
195
196/// General server options available in configuration file mode.
197/// Note that the `--config-file` option is excluded from itself.
198#[derive(Debug, Serialize, Deserialize, Clone)]
199#[serde(rename_all = "kebab-case")]
200pub struct General {
201    /// Server address.
202    pub host: Option<String>,
203    /// Server port.
204    pub port: Option<u16>,
205    /// Root directory path.
206    pub root: Option<PathBuf>,
207
208    /// Logging.
209    pub log_level: Option<LogLevel>,
210
211    /// Cache Control headers.
212    pub cache_control_headers: Option<bool>,
213
214    /// Compression.
215    #[cfg(any(
216        feature = "compression",
217        feature = "compression-gzip",
218        feature = "compression-brotli",
219        feature = "compression-zstd",
220        feature = "compression-deflate"
221    ))]
222    #[cfg_attr(
223        docsrs,
224        doc(cfg(any(
225            feature = "compression",
226            feature = "compression-gzip",
227            feature = "compression-brotli",
228            feature = "compression-zstd",
229            feature = "compression-deflate"
230        )))
231    )]
232    pub compression: Option<bool>,
233
234    /// Compression level.
235    #[cfg(any(
236        feature = "compression",
237        feature = "compression-gzip",
238        feature = "compression-brotli",
239        feature = "compression-zstd",
240        feature = "compression-deflate"
241    ))]
242    #[cfg_attr(
243        docsrs,
244        doc(cfg(any(
245            feature = "compression",
246            feature = "compression-gzip",
247            feature = "compression-brotli",
248            feature = "compression-zstd",
249            feature = "compression-deflate"
250        )))
251    )]
252    pub compression_level: Option<CompressionLevel>,
253
254    /// Check for a pre-compressed file on disk.
255    #[cfg(any(
256        feature = "compression",
257        feature = "compression-gzip",
258        feature = "compression-brotli",
259        feature = "compression-zstd",
260        feature = "compression-deflate"
261    ))]
262    #[cfg_attr(
263        docsrs,
264        doc(cfg(any(
265            feature = "compression",
266            feature = "compression-gzip",
267            feature = "compression-brotli",
268            feature = "compression-zstd",
269            feature = "compression-deflate"
270        )))
271    )]
272    pub compression_static: Option<bool>,
273
274    /// Error 404 pages.
275    pub page404: Option<PathBuf>,
276    /// Error 50x pages.
277    pub page50x: Option<PathBuf>,
278
279    /// HTTP/2 + TLS.
280    #[cfg(feature = "http2")]
281    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
282    pub http2: Option<bool>,
283    /// Http2 tls certificate feature.
284    #[cfg(feature = "http2")]
285    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
286    pub http2_tls_cert: Option<PathBuf>,
287    /// Http2 tls key feature.
288    #[cfg(feature = "http2")]
289    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
290    pub http2_tls_key: Option<PathBuf>,
291
292    /// Redirect all HTTP requests to HTTPS.
293    #[cfg(feature = "http2")]
294    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
295    pub https_redirect: Option<bool>,
296    /// HTTP host port where the redirect server will listen for requests to redirect them to HTTPS.
297    #[cfg(feature = "http2")]
298    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
299    pub https_redirect_host: Option<String>,
300    /// Host port for redirecting HTTP requests to HTTPS.
301    #[cfg(feature = "http2")]
302    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
303    pub https_redirect_from_port: Option<u16>,
304    /// List of host names or IPs allowed to redirect from.
305    #[cfg(feature = "http2")]
306    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
307    pub https_redirect_from_hosts: Option<String>,
308
309    /// Security headers.
310    pub security_headers: Option<bool>,
311
312    /// Cors allow origins feature.
313    pub cors_allow_origins: Option<String>,
314    /// Cors allow headers feature.
315    pub cors_allow_headers: Option<String>,
316    /// Cors expose headers feature.
317    pub cors_expose_headers: Option<String>,
318
319    /// List of files to be used as an index for requests ending with the slash character (‘/’).
320    pub index_files: Option<String>,
321
322    /// Directory listing feature.
323    #[cfg(feature = "directory-listing")]
324    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
325    pub directory_listing: Option<bool>,
326    /// Directory listing order feature.
327    #[cfg(feature = "directory-listing")]
328    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
329    pub directory_listing_order: Option<u8>,
330    /// Directory listing format feature.
331    #[cfg(feature = "directory-listing")]
332    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
333    pub directory_listing_format: Option<DirListFmt>,
334
335    /// Basic Authentication feature.
336    #[cfg(feature = "basic-auth")]
337    #[cfg_attr(docsrs, doc(cfg(feature = "basic-auth")))]
338    pub basic_auth: Option<String>,
339
340    /// File descriptor binding feature.
341    pub fd: Option<usize>,
342
343    /// Worker threads.
344    pub threads_multiplier: Option<usize>,
345
346    /// Max blocking threads feature.
347    pub max_blocking_threads: Option<usize>,
348
349    /// Grace period feature.
350    pub grace_period: Option<u8>,
351
352    /// Page fallback feature.
353    #[cfg(feature = "fallback-page")]
354    #[cfg_attr(docsrs, doc(cfg(feature = "fallback-page")))]
355    pub page_fallback: Option<PathBuf>,
356
357    /// Log remote address feature.
358    pub log_remote_address: Option<bool>,
359
360    /// Log the X-Real-IP header.
361    pub log_x_real_ip: Option<bool>,
362
363    /// Log the X-Forwarded-For header.
364    pub log_forwarded_for: Option<bool>,
365
366    /// Trusted IPs for remote addresses.
367    pub trusted_proxies: Option<Vec<IpAddr>>,
368
369    /// Redirect trailing slash feature.
370    pub redirect_trailing_slash: Option<bool>,
371
372    /// Ignore hidden files feature.
373    pub ignore_hidden_files: Option<bool>,
374
375    /// Prevent following symbolic links of files or directories.
376    pub disable_symlinks: Option<bool>,
377
378    /// Health endpoint feature.
379    pub health: Option<bool>,
380
381    #[cfg(all(unix, feature = "experimental"))]
382    /// Metrics endpoint feature (experimental).
383    pub experimental_metrics: Option<bool>,
384
385    /// Maintenance mode feature.
386    pub maintenance_mode: Option<bool>,
387
388    /// Custom HTTP status for when entering into maintenance mode.
389    pub maintenance_mode_status: Option<u16>,
390
391    /// Custom maintenance mode HTML file.
392    pub maintenance_mode_file: Option<PathBuf>,
393
394    #[cfg(feature = "experimental")]
395    /// In-memory files cache feature.
396    pub memory_cache: Option<bool>,
397
398    #[cfg(windows)]
399    /// windows service feature.
400    pub windows_service: Option<bool>,
401}
402
403/// Full server configuration
404#[derive(Debug, Serialize, Deserialize, Clone)]
405#[serde(rename_all = "kebab-case")]
406pub struct Settings {
407    /// General settings.
408    pub general: Option<General>,
409    /// Advanced settings.
410    pub advanced: Option<Advanced>,
411}
412
413impl Settings {
414    /// Read and deserialize the server TOML configuration file by path.
415    pub fn read(config_file: &Path) -> Result<Settings> {
416        // Validate TOML file extension
417        let ext = config_file.extension();
418        if ext.is_none() || ext.unwrap().is_empty() || ext.unwrap().ne("toml") {
419            bail!("configuration file should be in toml format. E.g `config.toml`");
420        }
421
422        // TODO: validate minimal TOML file structure needed
423        let toml =
424            read_toml_file(config_file).with_context(|| "error reading toml configuration file")?;
425        let mut unused = BTreeSet::new();
426        let manifest: Settings = serde_ignored::deserialize(toml, |path| {
427            let mut key = String::new();
428            helpers::stringify(&mut key, &path);
429            unused.insert(key);
430        })
431        .with_context(|| "error during toml configuration file deserialization")?;
432
433        for key in unused {
434            println!("Warning: unused configuration manifest key \"{key}\" or unsupported");
435        }
436
437        Ok(manifest)
438    }
439}
440
441/// Read and parse a TOML file from an specific path.
442fn read_toml_file(path: &Path) -> Result<toml::Value> {
443    let toml_str = helpers::read_file(path).with_context(|| {
444        format!(
445            "error trying to deserialize toml configuration file at \"{}\"",
446            path.display()
447        )
448    })?;
449    toml_str
450        .parse()
451        .map_err(|e| anyhow::Error::from(e).context("could not parse input as TOML"))
452}