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