Skip to main content

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