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