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