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::{Context, Result, helpers};
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    pub compression_static: Option<bool>,
261
262    /// Error 404 pages.
263    pub page404: Option<PathBuf>,
264    /// Error 50x pages.
265    pub page50x: Option<PathBuf>,
266
267    /// HTTP/2 + TLS.
268    #[cfg(feature = "http2")]
269    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
270    pub http2: Option<bool>,
271    /// Http2 tls certificate feature.
272    #[cfg(feature = "http2")]
273    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
274    pub http2_tls_cert: Option<PathBuf>,
275    /// Http2 tls key feature.
276    #[cfg(feature = "http2")]
277    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
278    pub http2_tls_key: Option<PathBuf>,
279
280    /// Redirect all HTTP requests to HTTPS.
281    #[cfg(feature = "http2")]
282    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
283    pub https_redirect: Option<bool>,
284    /// HTTP host port where the redirect server will listen for requests to redirect them to HTTPS.
285    #[cfg(feature = "http2")]
286    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
287    pub https_redirect_host: Option<String>,
288    /// Host port for redirecting HTTP requests to HTTPS.
289    #[cfg(feature = "http2")]
290    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
291    pub https_redirect_from_port: Option<u16>,
292    /// List of host names or IPs allowed to redirect from.
293    #[cfg(feature = "http2")]
294    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
295    pub https_redirect_from_hosts: Option<String>,
296
297    /// Security headers.
298    pub security_headers: Option<bool>,
299
300    /// Cors allow origins feature.
301    pub cors_allow_origins: Option<String>,
302    /// Cors allow headers feature.
303    pub cors_allow_headers: Option<String>,
304    /// Cors expose headers feature.
305    pub cors_expose_headers: Option<String>,
306
307    /// List of files to be used as an index for requests ending with the slash character (‘/’).
308    pub index_files: Option<String>,
309
310    /// Directory listing feature.
311    #[cfg(feature = "directory-listing")]
312    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
313    pub directory_listing: Option<bool>,
314    /// Directory listing order feature.
315    #[cfg(feature = "directory-listing")]
316    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
317    pub directory_listing_order: Option<u8>,
318    /// Directory listing format feature.
319    #[cfg(feature = "directory-listing")]
320    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
321    pub directory_listing_format: Option<DirListFmt>,
322
323    /// Directory listing download feature.
324    #[cfg(feature = "directory-listing-download")]
325    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing-download")))]
326    pub directory_listing_download: Option<Vec<DirDownloadFmt>>,
327
328    /// Basic Authentication feature.
329    #[cfg(feature = "basic-auth")]
330    #[cfg_attr(docsrs, doc(cfg(feature = "basic-auth")))]
331    pub basic_auth: Option<String>,
332
333    /// File descriptor binding feature.
334    pub fd: Option<usize>,
335
336    /// Worker threads.
337    pub threads_multiplier: Option<usize>,
338
339    /// Max blocking threads feature.
340    pub max_blocking_threads: Option<usize>,
341
342    /// Grace period feature.
343    pub grace_period: Option<u8>,
344
345    /// Page fallback feature.
346    #[cfg(feature = "fallback-page")]
347    #[cfg_attr(docsrs, doc(cfg(feature = "fallback-page")))]
348    pub page_fallback: Option<PathBuf>,
349
350    /// Log remote address feature.
351    pub log_remote_address: Option<bool>,
352
353    /// Log the X-Real-IP header.
354    pub log_x_real_ip: Option<bool>,
355
356    /// Log the X-Forwarded-For header.
357    pub log_forwarded_for: Option<bool>,
358
359    /// Trusted IPs for remote addresses.
360    pub trusted_proxies: Option<Vec<IpAddr>>,
361
362    /// Redirect trailing slash feature.
363    pub redirect_trailing_slash: Option<bool>,
364
365    /// Ignore hidden files feature.
366    pub ignore_hidden_files: Option<bool>,
367
368    /// Prevent following symbolic links of files or directories.
369    pub disable_symlinks: Option<bool>,
370
371    /// Health endpoint feature.
372    pub health: Option<bool>,
373
374    /// Accept markdown content negotiation feature.
375    pub accept_markdown: Option<bool>,
376
377    #[cfg(all(unix, feature = "experimental"))]
378    /// Metrics endpoint feature (experimental).
379    pub experimental_metrics: Option<bool>,
380
381    /// Maintenance mode feature.
382    pub maintenance_mode: Option<bool>,
383
384    /// Custom HTTP status for when entering into maintenance mode.
385    pub maintenance_mode_status: Option<u16>,
386
387    /// Custom maintenance mode HTML file.
388    pub maintenance_mode_file: Option<PathBuf>,
389
390    #[cfg(feature = "experimental")]
391    /// In-memory files cache feature.
392    pub memory_cache: Option<bool>,
393
394    #[cfg(windows)]
395    /// windows service feature.
396    pub windows_service: Option<bool>,
397}
398
399/// Full server configuration
400#[derive(Debug, Serialize, Deserialize, Clone)]
401#[serde(rename_all = "kebab-case")]
402pub struct Settings {
403    /// General settings.
404    pub general: Option<General>,
405    /// Advanced settings.
406    pub advanced: Option<Advanced>,
407}
408
409impl Settings {
410    /// Read and deserialize the server TOML configuration file by path.
411    pub fn read(config_file: &Path) -> Result<Settings> {
412        // Validate TOML file extension
413        let ext = config_file.extension();
414        if ext.is_none() || ext.unwrap().is_empty() || ext.unwrap().ne("toml") {
415            bail!("configuration file should be in toml format. E.g `sws.toml`");
416        }
417
418        // TODO: validate minimal TOML file structure needed
419        let toml =
420            read_toml_file(config_file).with_context(|| "error reading toml configuration file")?;
421        let mut unused = BTreeSet::new();
422        let manifest: Settings = serde_ignored::deserialize(toml, |path| {
423            let mut key = String::new();
424            helpers::stringify(&mut key, &path);
425            unused.insert(key);
426        })
427        .with_context(|| "error during toml configuration file deserialization")?;
428
429        for key in unused {
430            println!("Warning: unused configuration manifest key \"{key}\" or unsupported");
431        }
432
433        Ok(manifest)
434    }
435}
436
437/// Read and parse a TOML file from an specific path.
438fn read_toml_file(path: &Path) -> Result<toml::Value> {
439    let toml_str = helpers::read_file(path).with_context(|| {
440        format!(
441            "error trying to deserialize toml configuration file at \"{}\"",
442            path.display()
443        )
444    })?;
445
446    toml::from_str(&toml_str)
447        .map_err(|e| anyhow::Error::from(e).context("could not parse input as TOML"))
448}