Skip to main content

static_web_server/settings/
mod.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//! Module that provides all settings of SWS.
7//!
8
9use aho_corasick::AhoCorasick;
10use clap::Parser;
11use globset::{Glob, GlobBuilder, GlobMatcher};
12use headers::HeaderMap;
13use hyper::StatusCode;
14use regex_lite::Regex;
15use std::path::{Path, PathBuf};
16
17use crate::{Context, Result, helpers, logger};
18
19pub mod cli;
20#[doc(hidden)]
21pub mod cli_output;
22pub mod file;
23
24pub use cli::Commands;
25
26use cli::General;
27
28#[cfg(feature = "experimental")]
29use self::file::MemoryCache;
30
31use self::file::{RedirectsKind, Settings as FileSettings};
32
33#[cfg(any(
34    feature = "compression",
35    feature = "compression-gzip",
36    feature = "compression-brotli",
37    feature = "compression-zstd",
38    feature = "compression-deflate"
39))]
40pub use file::CompressionLevel;
41
42/// The `headers` file options.
43pub struct Headers {
44    /// Source pattern glob matcher
45    pub source: GlobMatcher,
46    /// Map of custom HTTP headers
47    pub headers: HeaderMap,
48}
49
50/// The `Rewrites` file options.
51pub struct Rewrites {
52    /// Source pattern Regex matcher
53    pub source: Regex,
54    /// A local file that must exist
55    pub destination: String,
56    /// Optional redirect type either 301 (Moved Permanently) or 302 (Found).
57    pub redirect: Option<RedirectsKind>,
58    /// Pre-compiled Aho-Corasick automaton for placeholder replacement.
59    pub replacer: AhoCorasick,
60}
61
62/// The `Redirects` file options.
63pub struct Redirects {
64    /// Optional host to match against an incoming URI host if specified
65    pub host: Option<String>,
66    /// Source pattern Regex matcher
67    pub source: Regex,
68    /// A local file that must exist
69    pub destination: String,
70    /// Redirection type either 301 (Moved Permanently) or 302 (Found)
71    pub kind: StatusCode,
72    /// Pre-compiled Aho-Corasick automaton for placeholder replacement.
73    pub replacer: AhoCorasick,
74}
75
76/// The `VirtualHosts` file options.
77pub struct VirtualHosts {
78    /// The value to check for in the "Host" header
79    pub host: String,
80    /// The root directory for this virtual host
81    pub root: PathBuf,
82}
83
84/// The `advanced` file options.
85#[derive(Default)]
86pub struct Advanced {
87    /// Headers list.
88    pub headers: Option<Vec<Headers>>,
89    /// Rewrites list.
90    pub rewrites: Option<Vec<Rewrites>>,
91    /// Redirects list.
92    pub redirects: Option<Vec<Redirects>>,
93    /// Name-based virtual hosting
94    pub virtual_hosts: Option<Vec<VirtualHosts>>,
95    #[cfg(feature = "experimental")]
96    /// In-memory cache feature (experimental).
97    pub memory_cache: Option<MemoryCache>,
98}
99
100/// Build an `AhoCorasick` automaton for the placeholder patterns `$0`, `$1`, ..., `$N`
101/// based on the number of capture groups in the given regex.
102pub fn build_placeholder_replacer(regex: &Regex) -> AhoCorasick {
103    let patterns: Vec<String> = (0..regex.captures_len()).map(|i| format!("${i}")).collect();
104    AhoCorasick::new(&patterns).expect("failed to build Aho-Corasick automaton for placeholders")
105}
106
107/// The full server CLI and File options.
108pub struct Settings {
109    /// General server options
110    pub general: General,
111    /// Advanced server options
112    pub advanced: Option<Advanced>,
113}
114
115impl Settings {
116    /// Reads CLI/Env and config file options returning the server settings.
117    /// It also takes care to initialize the logging system with its level
118    /// once the `general` settings are determined.
119    pub fn get(log_init: bool) -> Result<Settings> {
120        Self::parse_from(log_init, None)
121    }
122
123    /// Reads CLI/Env and config file options returning the server settings
124    /// without parsing arguments useful for testing.
125    pub fn get_unparsed(log_init: bool, args: &[&str]) -> Result<Settings> {
126        Self::parse_from(log_init, Some(args))
127    }
128
129    fn parse_from(log_init: bool, args: Option<&[&str]>) -> Result<Settings> {
130        let opts = match args {
131            Some(v) => General::parse_from(v),
132            None => General::parse(),
133        };
134
135        // Define the general CLI/file options
136        let version = opts.version;
137        let mut host = opts.host;
138        let mut port = opts.port;
139        let mut root = opts.root;
140        let mut log_level = opts.log_level;
141        let mut log_with_ansi = opts.log_with_ansi;
142        let mut config_file = opts.config_file.clone();
143        let mut cache_control_headers = opts.cache_control_headers;
144
145        #[cfg(any(
146            feature = "compression",
147            feature = "compression-gzip",
148            feature = "compression-brotli",
149            feature = "compression-zstd",
150            feature = "compression-deflate"
151        ))]
152        let mut compression = opts.compression;
153        #[cfg(any(
154            feature = "compression",
155            feature = "compression-gzip",
156            feature = "compression-brotli",
157            feature = "compression-zstd",
158            feature = "compression-deflate"
159        ))]
160        let mut compression_level = opts.compression_level;
161
162        let mut compression_static = opts.compression_static;
163
164        let mut page404 = opts.page404;
165        let mut page50x = opts.page50x;
166
167        #[cfg(feature = "http2")]
168        let mut http2 = opts.http2;
169        #[cfg(feature = "http2")]
170        let mut http2_tls_cert = opts.http2_tls_cert;
171        #[cfg(feature = "http2")]
172        let mut http2_tls_key = opts.http2_tls_key;
173        #[cfg(feature = "http2")]
174        let mut https_redirect = opts.https_redirect;
175        #[cfg(feature = "http2")]
176        let mut https_redirect_host = opts.https_redirect_host;
177        #[cfg(feature = "http2")]
178        let mut https_redirect_from_port = opts.https_redirect_from_port;
179        #[cfg(feature = "http2")]
180        let mut https_redirect_from_hosts = opts.https_redirect_from_hosts;
181
182        let mut security_headers = opts.security_headers;
183        let mut cors_allow_origins = opts.cors_allow_origins;
184        let mut cors_allow_headers = opts.cors_allow_headers;
185        let mut cors_expose_headers = opts.cors_expose_headers;
186
187        #[cfg(feature = "directory-listing")]
188        let mut directory_listing = opts.directory_listing;
189        #[cfg(feature = "directory-listing")]
190        let mut directory_listing_order = opts.directory_listing_order;
191        #[cfg(feature = "directory-listing")]
192        let mut directory_listing_format = opts.directory_listing_format;
193
194        #[cfg(feature = "directory-listing-download")]
195        let mut directory_listing_download = opts.directory_listing_download;
196
197        #[cfg(feature = "basic-auth")]
198        let mut basic_auth = opts.basic_auth;
199
200        let mut fd = opts.fd;
201        let mut threads_multiplier = opts.threads_multiplier;
202        let mut max_blocking_threads = opts.max_blocking_threads;
203        let mut grace_period = opts.grace_period;
204
205        #[cfg(feature = "fallback-page")]
206        let mut page_fallback = opts.page_fallback;
207
208        let mut log_remote_address = opts.log_remote_address;
209        let mut log_x_real_ip = opts.log_x_real_ip;
210        let mut log_forwarded_for = opts.log_forwarded_for;
211        let mut trusted_proxies = opts.trusted_proxies;
212        let mut redirect_trailing_slash = opts.redirect_trailing_slash;
213        let mut ignore_hidden_files = opts.ignore_hidden_files;
214        let mut disable_symlinks = opts.disable_symlinks;
215        let mut use_relative_root = opts.use_relative_root;
216        let mut accept_markdown = opts.accept_markdown;
217        let mut default_text_charset = opts.text_charset;
218        let mut index_files = opts.index_files;
219        let mut health = opts.health;
220
221        #[cfg(feature = "metrics")]
222        let mut metrics = opts.metrics;
223
224        let mut maintenance_mode = opts.maintenance_mode;
225        let mut maintenance_mode_status = opts.maintenance_mode_status;
226        let mut maintenance_mode_file = opts.maintenance_mode_file;
227
228        // Windows-only options
229        #[cfg(windows)]
230        let mut windows_service = opts.windows_service;
231
232        // Define the advanced file options
233        let mut settings_advanced: Option<Advanced> = None;
234
235        let to_use_config_file = match Path::new("./config.toml").is_file() {
236            true => {
237                eprintln!(
238                    "Deprecated: 'config.toml' found, rename it to 'sws.toml' to prepare for future releases"
239                );
240                PathBuf::from("./config.toml")
241            }
242            false => opts.config_file.clone(),
243        };
244
245        if let Some((settings, config_file_resolved)) = read_file_settings(&to_use_config_file)? {
246            config_file = config_file_resolved;
247
248            // File-based "general" options
249            let has_general_settings = settings.general.is_some();
250            if let Some(general) = settings.general {
251                if let Some(v) = general.host {
252                    host = v
253                }
254                if let Some(v) = general.port {
255                    port = v
256                }
257                if let Some(v) = general.root {
258                    root = v
259                }
260                if let Some(ref v) = general.log_level {
261                    log_level = v.name().to_lowercase();
262                }
263                if let Some(v) = general.log_with_ansi {
264                    log_with_ansi = v;
265                }
266                if let Some(v) = general.cache_control_headers {
267                    cache_control_headers = v
268                }
269                #[cfg(any(
270                    feature = "compression",
271                    feature = "compression-gzip",
272                    feature = "compression-brotli",
273                    feature = "compression-zstd",
274                    feature = "compression-deflate"
275                ))]
276                if let Some(v) = general.compression {
277                    compression = v
278                }
279                #[cfg(any(
280                    feature = "compression",
281                    feature = "compression-gzip",
282                    feature = "compression-brotli",
283                    feature = "compression-zstd",
284                    feature = "compression-deflate"
285                ))]
286                if let Some(v) = general.compression_level {
287                    compression_level = v
288                }
289                if let Some(v) = general.compression_static {
290                    compression_static = v
291                }
292                if let Some(v) = general.page404 {
293                    page404 = v
294                }
295                if let Some(v) = general.page50x {
296                    page50x = v
297                }
298                #[cfg(feature = "http2")]
299                if let Some(v) = general.http2 {
300                    http2 = v
301                }
302                #[cfg(feature = "http2")]
303                if let Some(v) = general.http2_tls_cert {
304                    http2_tls_cert = Some(v)
305                }
306                #[cfg(feature = "http2")]
307                if let Some(v) = general.http2_tls_key {
308                    http2_tls_key = Some(v)
309                }
310                #[cfg(feature = "http2")]
311                if let Some(v) = general.https_redirect {
312                    https_redirect = v
313                }
314                #[cfg(feature = "http2")]
315                if let Some(v) = general.https_redirect_host {
316                    https_redirect_host = v
317                }
318                #[cfg(feature = "http2")]
319                if let Some(v) = general.https_redirect_from_port {
320                    https_redirect_from_port = v
321                }
322                #[cfg(feature = "http2")]
323                if let Some(v) = general.https_redirect_from_hosts {
324                    https_redirect_from_hosts = v
325                }
326                #[cfg(feature = "http2")]
327                match general.security_headers {
328                    Some(v) => security_headers = v,
329                    _ => {
330                        if http2 {
331                            security_headers = true;
332                        }
333                    }
334                }
335                #[cfg(not(feature = "http2"))]
336                if let Some(v) = general.security_headers {
337                    security_headers = v
338                }
339                if let Some(ref v) = general.cors_allow_origins {
340                    v.clone_into(&mut cors_allow_origins)
341                }
342                if let Some(ref v) = general.cors_allow_headers {
343                    v.clone_into(&mut cors_allow_headers)
344                }
345                if let Some(ref v) = general.cors_expose_headers {
346                    v.clone_into(&mut cors_expose_headers)
347                }
348                #[cfg(feature = "directory-listing")]
349                if let Some(v) = general.directory_listing {
350                    directory_listing = v
351                }
352                #[cfg(feature = "directory-listing")]
353                if let Some(v) = general.directory_listing_order {
354                    directory_listing_order = v
355                }
356                #[cfg(feature = "directory-listing")]
357                if let Some(v) = general.directory_listing_format {
358                    directory_listing_format = v
359                }
360                #[cfg(feature = "directory-listing-download")]
361                if let Some(v) = general.directory_listing_download {
362                    directory_listing_download = v
363                }
364                #[cfg(feature = "basic-auth")]
365                if let Some(ref v) = general.basic_auth {
366                    v.clone_into(&mut basic_auth)
367                }
368                if let Some(v) = general.fd {
369                    fd = Some(v)
370                }
371                if let Some(v) = general.threads_multiplier {
372                    threads_multiplier = v
373                }
374                if let Some(v) = general.max_blocking_threads {
375                    max_blocking_threads = v
376                }
377                if let Some(v) = general.grace_period {
378                    grace_period = v
379                }
380                #[cfg(feature = "fallback-page")]
381                if let Some(v) = general.page_fallback {
382                    page_fallback = v
383                }
384                if let Some(v) = general.log_remote_address {
385                    log_remote_address = v
386                }
387                if let Some(v) = general.log_x_real_ip {
388                    log_x_real_ip = v
389                }
390                if let Some(v) = general.log_forwarded_for {
391                    log_forwarded_for = v
392                }
393                if let Some(v) = general.trusted_proxies {
394                    trusted_proxies = v
395                }
396                if let Some(v) = general.redirect_trailing_slash {
397                    redirect_trailing_slash = v
398                }
399                if let Some(v) = general.ignore_hidden_files {
400                    ignore_hidden_files = v
401                }
402                if let Some(v) = general.disable_symlinks {
403                    disable_symlinks = v
404                }
405                if let Some(v) = general.use_relative_root {
406                    use_relative_root = v
407                }
408                if let Some(v) = general.health {
409                    health = v
410                }
411                if let Some(v) = general.accept_markdown {
412                    accept_markdown = v
413                }
414                if let Some(v) = general.text_charset {
415                    default_text_charset = v
416                }
417                #[cfg(feature = "metrics")]
418                if let Some(v) = general.metrics {
419                    metrics = v
420                }
421                if let Some(v) = general.index_files {
422                    index_files = v
423                }
424                if let Some(v) = general.maintenance_mode {
425                    maintenance_mode = v
426                }
427                if let Some(v) = general.maintenance_mode_status {
428                    maintenance_mode_status =
429                        StatusCode::from_u16(v).with_context(|| "invalid HTTP status code")?
430                }
431                if let Some(v) = general.maintenance_mode_file {
432                    maintenance_mode_file = v
433                }
434
435                // Windows-only options
436                #[cfg(windows)]
437                if let Some(v) = general.windows_service {
438                    windows_service = v
439                }
440            }
441
442            // Logging system initialization in config file context
443            if log_init {
444                logger::init(log_level.as_str(), log_with_ansi)?;
445            }
446
447            tracing::debug!("config file read successfully");
448            tracing::debug!("config file path provided: {}", opts.config_file.display());
449            tracing::debug!("config file path resolved: {}", config_file.display());
450
451            if !has_general_settings {
452                tracing::warn!(
453                    "config file empty or no `general` settings found, using default values"
454                );
455            }
456
457            // File-based "advanced" options
458            if let Some(advanced) = settings.advanced {
459                // 1. Custom HTTP headers assignment
460                let headers_entries = match advanced.headers {
461                    Some(headers_entries) => {
462                        let mut headers_vec: Vec<Headers> = Vec::new();
463
464                        // Compile a glob pattern for each header sources entry
465                        for headers_entry in headers_entries.iter() {
466                            let source = Glob::new(&headers_entry.source)
467                                .with_context(|| {
468                                    format!(
469                                        "can not compile glob pattern for header source: {}",
470                                        headers_entry.source
471                                    )
472                                })?
473                                .compile_matcher();
474
475                            headers_vec.push(Headers {
476                                source,
477                                headers: headers_entry.headers.to_owned(),
478                            });
479                        }
480                        Some(headers_vec)
481                    }
482                    _ => None,
483                };
484
485                // 2. Rewrites assignment
486                let rewrites_entries = match advanced.rewrites {
487                    Some(rewrites_entries) => {
488                        let mut rewrites_vec: Vec<Rewrites> = Vec::new();
489
490                        // Compile a glob pattern for each rewrite sources entry
491                        for rewrites_entry in rewrites_entries.iter() {
492                            let source = GlobBuilder::new(&rewrites_entry.source)
493                                .literal_separator(true)
494                                .build()
495                                .with_context(|| {
496                                    format!(
497                                        "can not compile glob pattern for rewrite source: {}",
498                                        rewrites_entry.source
499                                    )
500                                })?
501                                .compile_matcher();
502
503                            let pattern = source
504                                .glob()
505                                .regex()
506                                .trim_start_matches("(?-u)")
507                                .replace("?:.*", ".*")
508                                .replace("?:", "")
509                                .replace(".*.*", ".*")
510                                .to_owned();
511                            tracing::debug!(
512                                "url rewrites glob pattern: {}",
513                                &rewrites_entry.source
514                            );
515                            tracing::debug!("url rewrites regex equivalent: {}", pattern);
516
517                            let source = Regex::new(&pattern).with_context(|| {
518                                    format!(
519                                        "can not compile regex pattern equivalent for rewrite source: {}",
520                                        pattern
521                                    )
522                                })?;
523
524                            let replacer = build_placeholder_replacer(&source);
525                            rewrites_vec.push(Rewrites {
526                                source,
527                                destination: rewrites_entry.destination.to_owned(),
528                                redirect: rewrites_entry.redirect.to_owned(),
529                                replacer,
530                            });
531                        }
532                        Some(rewrites_vec)
533                    }
534                    _ => None,
535                };
536
537                // 3. Redirects assignment
538                let redirects_entries = match advanced.redirects {
539                    Some(redirects_entries) => {
540                        let mut redirects_vec: Vec<Redirects> = Vec::new();
541
542                        // Compile a glob pattern for each redirect sources entry
543                        for redirects_entry in redirects_entries.iter() {
544                            let source = GlobBuilder::new(&redirects_entry.source)
545                                .literal_separator(true)
546                                .build()
547                                .with_context(|| {
548                                    format!(
549                                        "can not compile glob pattern for redirect source: {}",
550                                        redirects_entry.source
551                                    )
552                                })?
553                                .compile_matcher();
554
555                            let pattern = source
556                                .glob()
557                                .regex()
558                                .trim_start_matches("(?-u)")
559                                .replace("?:.*", ".*")
560                                .replace("?:", "")
561                                .replace(".*.*", ".*")
562                                .to_owned();
563                            tracing::debug!(
564                                "url redirects glob pattern: {}",
565                                &redirects_entry.source
566                            );
567                            tracing::debug!("url redirects regex equivalent: {}", pattern);
568
569                            let source = Regex::new(&pattern).with_context(|| {
570                                    format!(
571                                        "can not compile regex pattern equivalent for redirect source: {}",
572                                        pattern
573                                    )
574                                })?;
575
576                            let status_code = redirects_entry.kind.to_owned() as u16;
577                            let replacer = build_placeholder_replacer(&source);
578                            redirects_vec.push(Redirects {
579                                host: redirects_entry.host.to_owned(),
580                                source,
581                                destination: redirects_entry.destination.to_owned(),
582                                kind: StatusCode::from_u16(status_code).with_context(|| {
583                                    format!("invalid redirect status code: {status_code}")
584                                })?,
585                                replacer,
586                            });
587                        }
588                        Some(redirects_vec)
589                    }
590                    _ => None,
591                };
592
593                // 3. Virtual hosts assignment
594                let vhosts_entries = match advanced.virtual_hosts {
595                    Some(vhosts_entries) => {
596                        let mut vhosts_vec: Vec<VirtualHosts> = Vec::new();
597
598                        for vhosts_entry in vhosts_entries.iter() {
599                            if let Some(root) = vhosts_entry.root.to_owned() {
600                                // Make sure path is valid
601                                let root_dir = helpers::get_valid_dirpath(&root)
602                                    .with_context(|| "root directory for virtual host was not found or inaccessible")?;
603                                // Canonicalize once so the per-request
604                                // containment check can skip a `canonicalize`
605                                // unless `use_relative_root` is enabled.
606                                let root_dir = if use_relative_root {
607                                    root_dir
608                                } else {
609                                    root_dir.canonicalize().unwrap_or(root_dir)
610                                };
611                                tracing::debug!(
612                                    "added virtual host: {} -> {}",
613                                    vhosts_entry.host,
614                                    root_dir.display()
615                                );
616                                vhosts_vec.push(VirtualHosts {
617                                    host: vhosts_entry.host.to_owned(),
618                                    root: root_dir,
619                                });
620                            }
621                        }
622                        Some(vhosts_vec)
623                    }
624                    _ => None,
625                };
626
627                settings_advanced = Some(Advanced {
628                    headers: headers_entries,
629                    rewrites: rewrites_entries,
630                    redirects: redirects_entries,
631                    virtual_hosts: vhosts_entries,
632                    #[cfg(feature = "experimental")]
633                    memory_cache: advanced.memory_cache,
634                });
635            }
636        } else if log_init {
637            // Logging system initialization on demand
638            logger::init(log_level.as_str(), log_with_ansi)?;
639        }
640
641        Ok(Settings {
642            general: General {
643                version,
644                host,
645                port,
646                root,
647                log_level,
648                log_with_ansi,
649                config_file,
650                cache_control_headers,
651                #[cfg(any(
652                    feature = "compression",
653                    feature = "compression-gzip",
654                    feature = "compression-brotli",
655                    feature = "compression-zstd",
656                    feature = "compression-deflate"
657                ))]
658                compression,
659                #[cfg(any(
660                    feature = "compression",
661                    feature = "compression-gzip",
662                    feature = "compression-brotli",
663                    feature = "compression-zstd",
664                    feature = "compression-deflate"
665                ))]
666                compression_level,
667                compression_static,
668                page404,
669                page50x,
670                #[cfg(feature = "http2")]
671                http2,
672                #[cfg(feature = "http2")]
673                http2_tls_cert,
674                #[cfg(feature = "http2")]
675                http2_tls_key,
676                #[cfg(feature = "http2")]
677                https_redirect,
678                #[cfg(feature = "http2")]
679                https_redirect_host,
680                #[cfg(feature = "http2")]
681                https_redirect_from_port,
682                #[cfg(feature = "http2")]
683                https_redirect_from_hosts,
684                security_headers,
685                cors_allow_origins,
686                cors_allow_headers,
687                cors_expose_headers,
688                #[cfg(feature = "directory-listing")]
689                directory_listing,
690                #[cfg(feature = "directory-listing")]
691                directory_listing_order,
692                #[cfg(feature = "directory-listing")]
693                directory_listing_format,
694                #[cfg(feature = "directory-listing-download")]
695                directory_listing_download,
696                #[cfg(feature = "basic-auth")]
697                basic_auth,
698                fd,
699                threads_multiplier,
700                max_blocking_threads,
701                grace_period,
702                #[cfg(feature = "fallback-page")]
703                page_fallback,
704                log_remote_address,
705                log_x_real_ip,
706                log_forwarded_for,
707                trusted_proxies,
708                redirect_trailing_slash,
709                ignore_hidden_files,
710                disable_symlinks,
711                use_relative_root,
712                accept_markdown,
713                text_charset: default_text_charset,
714                index_files,
715                health,
716                #[cfg(feature = "metrics")]
717                metrics,
718                maintenance_mode,
719                maintenance_mode_status,
720                maintenance_mode_file,
721
722                // Windows-only options and commands
723                #[cfg(windows)]
724                windows_service,
725                commands: opts.commands,
726            },
727            advanced: settings_advanced,
728        })
729    }
730}
731
732fn read_file_settings(config_file: &Path) -> Result<Option<(FileSettings, PathBuf)>> {
733    if config_file.is_file() {
734        let file_path_resolved = config_file
735            .canonicalize()
736            .with_context(|| "unable to resolve toml config file path")?;
737
738        let settings = FileSettings::read(&file_path_resolved).with_context(
739            || "unable to read toml config file because has invalid format or unsupported options",
740        )?;
741
742        return Ok(Some((settings, file_path_resolved)));
743    }
744    Ok(None)
745}