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::Regex;
14use std::path::{Path, PathBuf};
15
16use crate::{helpers, logger, Context, Result};
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::read(log_init, true)
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) -> Result<Settings> {
114        Self::read(log_init, false)
115    }
116
117    fn read(log_init: bool, parse_args: bool) -> Result<Settings> {
118        let opts = if parse_args {
119            General::parse()
120        } else {
121            General::parse_from([""])
122        };
123
124        // Define the general CLI/file options
125        let version = opts.version;
126        let mut host = opts.host;
127        let mut port = opts.port;
128        let mut root = opts.root;
129        let mut log_level = opts.log_level;
130        let mut log_with_ansi = opts.log_with_ansi;
131        let mut config_file = opts.config_file.clone();
132        let mut cache_control_headers = opts.cache_control_headers;
133
134        #[cfg(any(
135            feature = "compression",
136            feature = "compression-gzip",
137            feature = "compression-brotli",
138            feature = "compression-zstd",
139            feature = "compression-deflate"
140        ))]
141        let mut compression = opts.compression;
142        #[cfg(any(
143            feature = "compression",
144            feature = "compression-gzip",
145            feature = "compression-brotli",
146            feature = "compression-zstd",
147            feature = "compression-deflate"
148        ))]
149        let mut compression_level = opts.compression_level;
150        #[cfg(any(
151            feature = "compression",
152            feature = "compression-gzip",
153            feature = "compression-brotli",
154            feature = "compression-zstd",
155            feature = "compression-deflate"
156        ))]
157        let mut compression_static = opts.compression_static;
158
159        let mut page404 = opts.page404;
160        let mut page50x = opts.page50x;
161
162        #[cfg(feature = "http2")]
163        let mut http2 = opts.http2;
164        #[cfg(feature = "http2")]
165        let mut http2_tls_cert = opts.http2_tls_cert;
166        #[cfg(feature = "http2")]
167        let mut http2_tls_key = opts.http2_tls_key;
168        #[cfg(feature = "http2")]
169        let mut https_redirect = opts.https_redirect;
170        #[cfg(feature = "http2")]
171        let mut https_redirect_host = opts.https_redirect_host;
172        #[cfg(feature = "http2")]
173        let mut https_redirect_from_port = opts.https_redirect_from_port;
174        #[cfg(feature = "http2")]
175        let mut https_redirect_from_hosts = opts.https_redirect_from_hosts;
176
177        let mut security_headers = opts.security_headers;
178        let mut cors_allow_origins = opts.cors_allow_origins;
179        let mut cors_allow_headers = opts.cors_allow_headers;
180        let mut cors_expose_headers = opts.cors_expose_headers;
181
182        #[cfg(feature = "directory-listing")]
183        let mut directory_listing = opts.directory_listing;
184        #[cfg(feature = "directory-listing")]
185        let mut directory_listing_order = opts.directory_listing_order;
186        #[cfg(feature = "directory-listing")]
187        let mut directory_listing_format = opts.directory_listing_format;
188
189        #[cfg(feature = "directory-listing-download")]
190        let mut directory_listing_download = opts.directory_listing_download;
191
192        #[cfg(feature = "basic-auth")]
193        let mut basic_auth = opts.basic_auth;
194
195        let mut fd = opts.fd;
196        let mut threads_multiplier = opts.threads_multiplier;
197        let mut max_blocking_threads = opts.max_blocking_threads;
198        let mut grace_period = opts.grace_period;
199
200        #[cfg(feature = "fallback-page")]
201        let mut page_fallback = opts.page_fallback;
202
203        let mut log_remote_address = opts.log_remote_address;
204        let mut log_x_real_ip = opts.log_x_real_ip;
205        let mut log_forwarded_for = opts.log_forwarded_for;
206        let mut trusted_proxies = opts.trusted_proxies;
207        let mut redirect_trailing_slash = opts.redirect_trailing_slash;
208        let mut ignore_hidden_files = opts.ignore_hidden_files;
209        let mut disable_symlinks = opts.disable_symlinks;
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!("Deprecated: 'config.toml' found, rename it to 'sws.toml' to prepare for future releases");
230                PathBuf::from("./config.toml")
231            }
232            false => opts.config_file.clone(),
233        };
234
235        if let Some((settings, config_file_resolved)) = read_file_settings(&to_use_config_file)? {
236            config_file = config_file_resolved;
237
238            // File-based "general" options
239            let has_general_settings = settings.general.is_some();
240            if has_general_settings {
241                let general = settings.general.unwrap();
242
243                if let Some(v) = general.host {
244                    host = v
245                }
246                if let Some(v) = general.port {
247                    port = v
248                }
249                if let Some(v) = general.root {
250                    root = v
251                }
252                if let Some(ref v) = general.log_level {
253                    log_level = v.name().to_lowercase();
254                }
255                if let Some(v) = general.log_with_ansi {
256                    log_with_ansi = v;
257                }
258                if let Some(v) = general.cache_control_headers {
259                    cache_control_headers = v
260                }
261                #[cfg(any(
262                    feature = "compression",
263                    feature = "compression-gzip",
264                    feature = "compression-brotli",
265                    feature = "compression-zstd",
266                    feature = "compression-deflate"
267                ))]
268                if let Some(v) = general.compression {
269                    compression = v
270                }
271                #[cfg(any(
272                    feature = "compression",
273                    feature = "compression-gzip",
274                    feature = "compression-brotli",
275                    feature = "compression-zstd",
276                    feature = "compression-deflate"
277                ))]
278                if let Some(v) = general.compression_level {
279                    compression_level = v
280                }
281                #[cfg(any(
282                    feature = "compression",
283                    feature = "compression-gzip",
284                    feature = "compression-brotli",
285                    feature = "compression-zstd",
286                    feature = "compression-deflate"
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                #[cfg(all(unix, feature = "experimental"))]
408                if let Some(v) = general.experimental_metrics {
409                    experimental_metrics = v
410                }
411                if let Some(v) = general.index_files {
412                    index_files = v
413                }
414                if let Some(v) = general.maintenance_mode {
415                    maintenance_mode = v
416                }
417                if let Some(v) = general.maintenance_mode_status {
418                    maintenance_mode_status =
419                        StatusCode::from_u16(v).with_context(|| "invalid HTTP status code")?
420                }
421                if let Some(v) = general.maintenance_mode_file {
422                    maintenance_mode_file = v
423                }
424
425                // Windows-only options
426                #[cfg(windows)]
427                if let Some(v) = general.windows_service {
428                    windows_service = v
429                }
430            }
431
432            // Logging system initialization in config file context
433            if log_init {
434                logger::init(log_level.as_str(), log_with_ansi)?;
435            }
436
437            tracing::debug!("config file read successfully");
438            tracing::debug!("config file path provided: {}", opts.config_file.display());
439            tracing::debug!("config file path resolved: {}", config_file.display());
440
441            if !has_general_settings {
442                tracing::warn!(
443                    "config file empty or no `general` settings found, using default values"
444                );
445            }
446
447            // File-based "advanced" options
448            if let Some(advanced) = settings.advanced {
449                // 1. Custom HTTP headers assignment
450                let headers_entries = match advanced.headers {
451                    Some(headers_entries) => {
452                        let mut headers_vec: Vec<Headers> = Vec::new();
453
454                        // Compile a glob pattern for each header sources entry
455                        for headers_entry in headers_entries.iter() {
456                            let source = Glob::new(&headers_entry.source)
457                                .with_context(|| {
458                                    format!(
459                                        "can not compile glob pattern for header source: {}",
460                                        &headers_entry.source
461                                    )
462                                })?
463                                .compile_matcher();
464
465                            headers_vec.push(Headers {
466                                source,
467                                headers: headers_entry.headers.to_owned(),
468                            });
469                        }
470                        Some(headers_vec)
471                    }
472                    _ => None,
473                };
474
475                // 2. Rewrites assignment
476                let rewrites_entries = match advanced.rewrites {
477                    Some(rewrites_entries) => {
478                        let mut rewrites_vec: Vec<Rewrites> = Vec::new();
479
480                        // Compile a glob pattern for each rewrite sources entry
481                        for rewrites_entry in rewrites_entries.iter() {
482                            let source = GlobBuilder::new(&rewrites_entry.source)
483                                .literal_separator(true)
484                                .build()
485                                .with_context(|| {
486                                    format!(
487                                        "can not compile glob pattern for rewrite source: {}",
488                                        &rewrites_entry.source
489                                    )
490                                })?
491                                .compile_matcher();
492
493                            let pattern = source
494                                .glob()
495                                .regex()
496                                .trim_start_matches("(?-u)")
497                                .replace("?:.*", ".*")
498                                .replace("?:", "")
499                                .replace(".*.*", ".*")
500                                .to_owned();
501                            tracing::debug!(
502                                "url rewrites glob pattern: {}",
503                                &rewrites_entry.source
504                            );
505                            tracing::debug!("url rewrites regex equivalent: {}", pattern);
506
507                            let source = Regex::new(&pattern).with_context(|| {
508                                    format!(
509                                        "can not compile regex pattern equivalent for rewrite source: {}",
510                                        &pattern
511                                    )
512                                })?;
513
514                            rewrites_vec.push(Rewrites {
515                                source,
516                                destination: rewrites_entry.destination.to_owned(),
517                                redirect: rewrites_entry.redirect.to_owned(),
518                            });
519                        }
520                        Some(rewrites_vec)
521                    }
522                    _ => None,
523                };
524
525                // 3. Redirects assignment
526                let redirects_entries = match advanced.redirects {
527                    Some(redirects_entries) => {
528                        let mut redirects_vec: Vec<Redirects> = Vec::new();
529
530                        // Compile a glob pattern for each redirect sources entry
531                        for redirects_entry in redirects_entries.iter() {
532                            let source = GlobBuilder::new(&redirects_entry.source)
533                                .literal_separator(true)
534                                .build()
535                                .with_context(|| {
536                                    format!(
537                                        "can not compile glob pattern for redirect source: {}",
538                                        &redirects_entry.source
539                                    )
540                                })?
541                                .compile_matcher();
542
543                            let pattern = source
544                                .glob()
545                                .regex()
546                                .trim_start_matches("(?-u)")
547                                .replace("?:.*", ".*")
548                                .replace("?:", "")
549                                .replace(".*.*", ".*")
550                                .to_owned();
551                            tracing::debug!(
552                                "url redirects glob pattern: {}",
553                                &redirects_entry.source
554                            );
555                            tracing::debug!("url redirects regex equivalent: {}", pattern);
556
557                            let source = Regex::new(&pattern).with_context(|| {
558                                    format!(
559                                        "can not compile regex pattern equivalent for redirect source: {}",
560                                        &pattern
561                                    )
562                                })?;
563
564                            let status_code = redirects_entry.kind.to_owned() as u16;
565                            redirects_vec.push(Redirects {
566                                host: redirects_entry.host.to_owned(),
567                                source,
568                                destination: redirects_entry.destination.to_owned(),
569                                kind: StatusCode::from_u16(status_code).with_context(|| {
570                                    format!("invalid redirect status code: {status_code}")
571                                })?,
572                            });
573                        }
574                        Some(redirects_vec)
575                    }
576                    _ => None,
577                };
578
579                // 3. Virtual hosts assignment
580                let vhosts_entries = match advanced.virtual_hosts {
581                    Some(vhosts_entries) => {
582                        let mut vhosts_vec: Vec<VirtualHosts> = Vec::new();
583
584                        for vhosts_entry in vhosts_entries.iter() {
585                            if let Some(root) = vhosts_entry.root.to_owned() {
586                                // Make sure path is valid
587                                let root_dir = helpers::get_valid_dirpath(&root)
588                                    .with_context(|| "root directory for virtual host was not found or inaccessible")?;
589                                tracing::debug!(
590                                    "added virtual host: {} -> {}",
591                                    vhosts_entry.host,
592                                    root_dir.display()
593                                );
594                                vhosts_vec.push(VirtualHosts {
595                                    host: vhosts_entry.host.to_owned(),
596                                    root: root_dir,
597                                });
598                            }
599                        }
600                        Some(vhosts_vec)
601                    }
602                    _ => None,
603                };
604
605                settings_advanced = Some(Advanced {
606                    headers: headers_entries,
607                    rewrites: rewrites_entries,
608                    redirects: redirects_entries,
609                    virtual_hosts: vhosts_entries,
610                    #[cfg(feature = "experimental")]
611                    memory_cache: advanced.memory_cache,
612                });
613            }
614        } else if log_init {
615            // Logging system initialization on demand
616            logger::init(log_level.as_str(), log_with_ansi)?;
617        }
618
619        Ok(Settings {
620            general: General {
621                version,
622                host,
623                port,
624                root,
625                log_level,
626                log_with_ansi,
627                config_file,
628                cache_control_headers,
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,
637                #[cfg(any(
638                    feature = "compression",
639                    feature = "compression-gzip",
640                    feature = "compression-brotli",
641                    feature = "compression-zstd",
642                    feature = "compression-deflate"
643                ))]
644                compression_level,
645                #[cfg(any(
646                    feature = "compression",
647                    feature = "compression-gzip",
648                    feature = "compression-brotli",
649                    feature = "compression-zstd",
650                    feature = "compression-deflate"
651                ))]
652                compression_static,
653                page404,
654                page50x,
655                #[cfg(feature = "http2")]
656                http2,
657                #[cfg(feature = "http2")]
658                http2_tls_cert,
659                #[cfg(feature = "http2")]
660                http2_tls_key,
661                #[cfg(feature = "http2")]
662                https_redirect,
663                #[cfg(feature = "http2")]
664                https_redirect_host,
665                #[cfg(feature = "http2")]
666                https_redirect_from_port,
667                #[cfg(feature = "http2")]
668                https_redirect_from_hosts,
669                security_headers,
670                cors_allow_origins,
671                cors_allow_headers,
672                cors_expose_headers,
673                #[cfg(feature = "directory-listing")]
674                directory_listing,
675                #[cfg(feature = "directory-listing")]
676                directory_listing_order,
677                #[cfg(feature = "directory-listing")]
678                directory_listing_format,
679                #[cfg(feature = "directory-listing-download")]
680                directory_listing_download,
681                #[cfg(feature = "basic-auth")]
682                basic_auth,
683                fd,
684                threads_multiplier,
685                max_blocking_threads,
686                grace_period,
687                #[cfg(feature = "fallback-page")]
688                page_fallback,
689                log_remote_address,
690                log_x_real_ip,
691                log_forwarded_for,
692                trusted_proxies,
693                redirect_trailing_slash,
694                ignore_hidden_files,
695                disable_symlinks,
696                index_files,
697                health,
698                #[cfg(all(unix, feature = "experimental"))]
699                experimental_metrics,
700                maintenance_mode,
701                maintenance_mode_status,
702                maintenance_mode_file,
703
704                // Windows-only options and commands
705                #[cfg(windows)]
706                windows_service,
707                commands: opts.commands,
708            },
709            advanced: settings_advanced,
710        })
711    }
712}
713
714fn read_file_settings(config_file: &Path) -> Result<Option<(FileSettings, PathBuf)>> {
715    if config_file.is_file() {
716        let file_path_resolved = config_file
717            .canonicalize()
718            .with_context(|| "unable to resolve toml config file path")?;
719
720        let settings = FileSettings::read(&file_path_resolved).with_context(|| {
721            "unable to read toml config file because has invalid format or unsupported options"
722        })?;
723
724        return Ok(Some((settings, file_path_resolved)));
725    }
726    Ok(None)
727}