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