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