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