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 accept_markdown = opts.accept_markdown;
216        let mut default_text_charset = opts.text_charset;
217        let mut index_files = opts.index_files;
218        let mut health = opts.health;
219
220        #[cfg(feature = "metrics")]
221        let mut metrics = opts.metrics;
222
223        let mut maintenance_mode = opts.maintenance_mode;
224        let mut maintenance_mode_status = opts.maintenance_mode_status;
225        let mut maintenance_mode_file = opts.maintenance_mode_file;
226
227        // Windows-only options
228        #[cfg(windows)]
229        let mut windows_service = opts.windows_service;
230
231        // Define the advanced file options
232        let mut settings_advanced: Option<Advanced> = None;
233
234        let to_use_config_file = match Path::new("./config.toml").is_file() {
235            true => {
236                eprintln!(
237                    "Deprecated: 'config.toml' found, rename it to 'sws.toml' to prepare for future releases"
238                );
239                PathBuf::from("./config.toml")
240            }
241            false => opts.config_file.clone(),
242        };
243
244        if let Some((settings, config_file_resolved)) = read_file_settings(&to_use_config_file)? {
245            config_file = config_file_resolved;
246
247            // File-based "general" options
248            let has_general_settings = settings.general.is_some();
249            if let Some(general) = settings.general {
250                if let Some(v) = general.host {
251                    host = v
252                }
253                if let Some(v) = general.port {
254                    port = v
255                }
256                if let Some(v) = general.root {
257                    root = v
258                }
259                if let Some(ref v) = general.log_level {
260                    log_level = v.name().to_lowercase();
261                }
262                if let Some(v) = general.log_with_ansi {
263                    log_with_ansi = v;
264                }
265                if let Some(v) = general.cache_control_headers {
266                    cache_control_headers = v
267                }
268                #[cfg(any(
269                    feature = "compression",
270                    feature = "compression-gzip",
271                    feature = "compression-brotli",
272                    feature = "compression-zstd",
273                    feature = "compression-deflate"
274                ))]
275                if let Some(v) = general.compression {
276                    compression = v
277                }
278                #[cfg(any(
279                    feature = "compression",
280                    feature = "compression-gzip",
281                    feature = "compression-brotli",
282                    feature = "compression-zstd",
283                    feature = "compression-deflate"
284                ))]
285                if let Some(v) = general.compression_level {
286                    compression_level = v
287                }
288                if let Some(v) = general.compression_static {
289                    compression_static = v
290                }
291                if let Some(v) = general.page404 {
292                    page404 = v
293                }
294                if let Some(v) = general.page50x {
295                    page50x = v
296                }
297                #[cfg(feature = "http2")]
298                if let Some(v) = general.http2 {
299                    http2 = v
300                }
301                #[cfg(feature = "http2")]
302                if let Some(v) = general.http2_tls_cert {
303                    http2_tls_cert = Some(v)
304                }
305                #[cfg(feature = "http2")]
306                if let Some(v) = general.http2_tls_key {
307                    http2_tls_key = Some(v)
308                }
309                #[cfg(feature = "http2")]
310                if let Some(v) = general.https_redirect {
311                    https_redirect = v
312                }
313                #[cfg(feature = "http2")]
314                if let Some(v) = general.https_redirect_host {
315                    https_redirect_host = v
316                }
317                #[cfg(feature = "http2")]
318                if let Some(v) = general.https_redirect_from_port {
319                    https_redirect_from_port = v
320                }
321                #[cfg(feature = "http2")]
322                if let Some(v) = general.https_redirect_from_hosts {
323                    https_redirect_from_hosts = v
324                }
325                #[cfg(feature = "http2")]
326                match general.security_headers {
327                    Some(v) => security_headers = v,
328                    _ => {
329                        if http2 {
330                            security_headers = true;
331                        }
332                    }
333                }
334                #[cfg(not(feature = "http2"))]
335                if let Some(v) = general.security_headers {
336                    security_headers = v
337                }
338                if let Some(ref v) = general.cors_allow_origins {
339                    v.clone_into(&mut cors_allow_origins)
340                }
341                if let Some(ref v) = general.cors_allow_headers {
342                    v.clone_into(&mut cors_allow_headers)
343                }
344                if let Some(ref v) = general.cors_expose_headers {
345                    v.clone_into(&mut cors_expose_headers)
346                }
347                #[cfg(feature = "directory-listing")]
348                if let Some(v) = general.directory_listing {
349                    directory_listing = v
350                }
351                #[cfg(feature = "directory-listing")]
352                if let Some(v) = general.directory_listing_order {
353                    directory_listing_order = v
354                }
355                #[cfg(feature = "directory-listing")]
356                if let Some(v) = general.directory_listing_format {
357                    directory_listing_format = v
358                }
359                #[cfg(feature = "directory-listing-download")]
360                if let Some(v) = general.directory_listing_download {
361                    directory_listing_download = v
362                }
363                #[cfg(feature = "basic-auth")]
364                if let Some(ref v) = general.basic_auth {
365                    v.clone_into(&mut basic_auth)
366                }
367                if let Some(v) = general.fd {
368                    fd = Some(v)
369                }
370                if let Some(v) = general.threads_multiplier {
371                    threads_multiplier = v
372                }
373                if let Some(v) = general.max_blocking_threads {
374                    max_blocking_threads = v
375                }
376                if let Some(v) = general.grace_period {
377                    grace_period = v
378                }
379                #[cfg(feature = "fallback-page")]
380                if let Some(v) = general.page_fallback {
381                    page_fallback = v
382                }
383                if let Some(v) = general.log_remote_address {
384                    log_remote_address = v
385                }
386                if let Some(v) = general.log_x_real_ip {
387                    log_x_real_ip = v
388                }
389                if let Some(v) = general.log_forwarded_for {
390                    log_forwarded_for = v
391                }
392                if let Some(v) = general.trusted_proxies {
393                    trusted_proxies = v
394                }
395                if let Some(v) = general.redirect_trailing_slash {
396                    redirect_trailing_slash = v
397                }
398                if let Some(v) = general.ignore_hidden_files {
399                    ignore_hidden_files = v
400                }
401                if let Some(v) = general.disable_symlinks {
402                    disable_symlinks = v
403                }
404                if let Some(v) = general.health {
405                    health = v
406                }
407                if let Some(v) = general.accept_markdown {
408                    accept_markdown = v
409                }
410                if let Some(v) = general.text_charset {
411                    default_text_charset = v
412                }
413                #[cfg(feature = "metrics")]
414                if let Some(v) = general.metrics {
415                    metrics = v
416                }
417                if let Some(v) = general.index_files {
418                    index_files = v
419                }
420                if let Some(v) = general.maintenance_mode {
421                    maintenance_mode = v
422                }
423                if let Some(v) = general.maintenance_mode_status {
424                    maintenance_mode_status =
425                        StatusCode::from_u16(v).with_context(|| "invalid HTTP status code")?
426                }
427                if let Some(v) = general.maintenance_mode_file {
428                    maintenance_mode_file = v
429                }
430
431                // Windows-only options
432                #[cfg(windows)]
433                if let Some(v) = general.windows_service {
434                    windows_service = v
435                }
436            }
437
438            // Logging system initialization in config file context
439            if log_init {
440                logger::init(log_level.as_str(), log_with_ansi)?;
441            }
442
443            tracing::debug!("config file read successfully");
444            tracing::debug!("config file path provided: {}", opts.config_file.display());
445            tracing::debug!("config file path resolved: {}", config_file.display());
446
447            if !has_general_settings {
448                tracing::warn!(
449                    "config file empty or no `general` settings found, using default values"
450                );
451            }
452
453            // File-based "advanced" options
454            if let Some(advanced) = settings.advanced {
455                // 1. Custom HTTP headers assignment
456                let headers_entries = match advanced.headers {
457                    Some(headers_entries) => {
458                        let mut headers_vec: Vec<Headers> = Vec::new();
459
460                        // Compile a glob pattern for each header sources entry
461                        for headers_entry in headers_entries.iter() {
462                            let source = Glob::new(&headers_entry.source)
463                                .with_context(|| {
464                                    format!(
465                                        "can not compile glob pattern for header source: {}",
466                                        &headers_entry.source
467                                    )
468                                })?
469                                .compile_matcher();
470
471                            headers_vec.push(Headers {
472                                source,
473                                headers: headers_entry.headers.to_owned(),
474                            });
475                        }
476                        Some(headers_vec)
477                    }
478                    _ => None,
479                };
480
481                // 2. Rewrites assignment
482                let rewrites_entries = match advanced.rewrites {
483                    Some(rewrites_entries) => {
484                        let mut rewrites_vec: Vec<Rewrites> = Vec::new();
485
486                        // Compile a glob pattern for each rewrite sources entry
487                        for rewrites_entry in rewrites_entries.iter() {
488                            let source = GlobBuilder::new(&rewrites_entry.source)
489                                .literal_separator(true)
490                                .build()
491                                .with_context(|| {
492                                    format!(
493                                        "can not compile glob pattern for rewrite source: {}",
494                                        &rewrites_entry.source
495                                    )
496                                })?
497                                .compile_matcher();
498
499                            let pattern = source
500                                .glob()
501                                .regex()
502                                .trim_start_matches("(?-u)")
503                                .replace("?:.*", ".*")
504                                .replace("?:", "")
505                                .replace(".*.*", ".*")
506                                .to_owned();
507                            tracing::debug!(
508                                "url rewrites glob pattern: {}",
509                                &rewrites_entry.source
510                            );
511                            tracing::debug!("url rewrites regex equivalent: {}", pattern);
512
513                            let source = Regex::new(&pattern).with_context(|| {
514                                    format!(
515                                        "can not compile regex pattern equivalent for rewrite source: {}",
516                                        &pattern
517                                    )
518                                })?;
519
520                            let replacer = build_placeholder_replacer(&source);
521                            rewrites_vec.push(Rewrites {
522                                source,
523                                destination: rewrites_entry.destination.to_owned(),
524                                redirect: rewrites_entry.redirect.to_owned(),
525                                replacer,
526                            });
527                        }
528                        Some(rewrites_vec)
529                    }
530                    _ => None,
531                };
532
533                // 3. Redirects assignment
534                let redirects_entries = match advanced.redirects {
535                    Some(redirects_entries) => {
536                        let mut redirects_vec: Vec<Redirects> = Vec::new();
537
538                        // Compile a glob pattern for each redirect sources entry
539                        for redirects_entry in redirects_entries.iter() {
540                            let source = GlobBuilder::new(&redirects_entry.source)
541                                .literal_separator(true)
542                                .build()
543                                .with_context(|| {
544                                    format!(
545                                        "can not compile glob pattern for redirect source: {}",
546                                        &redirects_entry.source
547                                    )
548                                })?
549                                .compile_matcher();
550
551                            let pattern = source
552                                .glob()
553                                .regex()
554                                .trim_start_matches("(?-u)")
555                                .replace("?:.*", ".*")
556                                .replace("?:", "")
557                                .replace(".*.*", ".*")
558                                .to_owned();
559                            tracing::debug!(
560                                "url redirects glob pattern: {}",
561                                &redirects_entry.source
562                            );
563                            tracing::debug!("url redirects regex equivalent: {}", pattern);
564
565                            let source = Regex::new(&pattern).with_context(|| {
566                                    format!(
567                                        "can not compile regex pattern equivalent for redirect source: {}",
568                                        &pattern
569                                    )
570                                })?;
571
572                            let status_code = redirects_entry.kind.to_owned() as u16;
573                            let replacer = build_placeholder_replacer(&source);
574                            redirects_vec.push(Redirects {
575                                host: redirects_entry.host.to_owned(),
576                                source,
577                                destination: redirects_entry.destination.to_owned(),
578                                kind: StatusCode::from_u16(status_code).with_context(|| {
579                                    format!("invalid redirect status code: {status_code}")
580                                })?,
581                                replacer,
582                            });
583                        }
584                        Some(redirects_vec)
585                    }
586                    _ => None,
587                };
588
589                // 3. Virtual hosts assignment
590                let vhosts_entries = match advanced.virtual_hosts {
591                    Some(vhosts_entries) => {
592                        let mut vhosts_vec: Vec<VirtualHosts> = Vec::new();
593
594                        for vhosts_entry in vhosts_entries.iter() {
595                            if let Some(root) = vhosts_entry.root.to_owned() {
596                                // Make sure path is valid
597                                let root_dir = helpers::get_valid_dirpath(&root)
598                                    .with_context(|| "root directory for virtual host was not found or inaccessible")?;
599                                // Canonicalize once so the per-request
600                                // containment check can skip a `canonicalize`
601                                // syscall (see `static_files`).
602                                let root_dir = root_dir.canonicalize().unwrap_or(root_dir);
603                                tracing::debug!(
604                                    "added virtual host: {} -> {}",
605                                    vhosts_entry.host,
606                                    root_dir.display()
607                                );
608                                vhosts_vec.push(VirtualHosts {
609                                    host: vhosts_entry.host.to_owned(),
610                                    root: root_dir,
611                                });
612                            }
613                        }
614                        Some(vhosts_vec)
615                    }
616                    _ => None,
617                };
618
619                settings_advanced = Some(Advanced {
620                    headers: headers_entries,
621                    rewrites: rewrites_entries,
622                    redirects: redirects_entries,
623                    virtual_hosts: vhosts_entries,
624                    #[cfg(feature = "experimental")]
625                    memory_cache: advanced.memory_cache,
626                });
627            }
628        } else if log_init {
629            // Logging system initialization on demand
630            logger::init(log_level.as_str(), log_with_ansi)?;
631        }
632
633        Ok(Settings {
634            general: General {
635                version,
636                host,
637                port,
638                root,
639                log_level,
640                log_with_ansi,
641                config_file,
642                cache_control_headers,
643                #[cfg(any(
644                    feature = "compression",
645                    feature = "compression-gzip",
646                    feature = "compression-brotli",
647                    feature = "compression-zstd",
648                    feature = "compression-deflate"
649                ))]
650                compression,
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_level,
659                compression_static,
660                page404,
661                page50x,
662                #[cfg(feature = "http2")]
663                http2,
664                #[cfg(feature = "http2")]
665                http2_tls_cert,
666                #[cfg(feature = "http2")]
667                http2_tls_key,
668                #[cfg(feature = "http2")]
669                https_redirect,
670                #[cfg(feature = "http2")]
671                https_redirect_host,
672                #[cfg(feature = "http2")]
673                https_redirect_from_port,
674                #[cfg(feature = "http2")]
675                https_redirect_from_hosts,
676                security_headers,
677                cors_allow_origins,
678                cors_allow_headers,
679                cors_expose_headers,
680                #[cfg(feature = "directory-listing")]
681                directory_listing,
682                #[cfg(feature = "directory-listing")]
683                directory_listing_order,
684                #[cfg(feature = "directory-listing")]
685                directory_listing_format,
686                #[cfg(feature = "directory-listing-download")]
687                directory_listing_download,
688                #[cfg(feature = "basic-auth")]
689                basic_auth,
690                fd,
691                threads_multiplier,
692                max_blocking_threads,
693                grace_period,
694                #[cfg(feature = "fallback-page")]
695                page_fallback,
696                log_remote_address,
697                log_x_real_ip,
698                log_forwarded_for,
699                trusted_proxies,
700                redirect_trailing_slash,
701                ignore_hidden_files,
702                disable_symlinks,
703                accept_markdown,
704                text_charset: default_text_charset,
705                index_files,
706                health,
707                #[cfg(feature = "metrics")]
708                metrics,
709                maintenance_mode,
710                maintenance_mode_status,
711                maintenance_mode_file,
712
713                // Windows-only options and commands
714                #[cfg(windows)]
715                windows_service,
716                commands: opts.commands,
717            },
718            advanced: settings_advanced,
719        })
720    }
721}
722
723fn read_file_settings(config_file: &Path) -> Result<Option<(FileSettings, PathBuf)>> {
724    if config_file.is_file() {
725        let file_path_resolved = config_file
726            .canonicalize()
727            .with_context(|| "unable to resolve toml config file path")?;
728
729        let settings = FileSettings::read(&file_path_resolved).with_context(
730            || "unable to read toml config file because has invalid format or unsupported options",
731        )?;
732
733        return Ok(Some((settings, file_path_resolved)));
734    }
735    Ok(None)
736}