1use aho_corasick::AhoCorasick;
10use clap::Parser;
11use globset::{Glob, GlobBuilder, GlobMatcher};
12use headers::HeaderMap;
13use hyper::StatusCode;
14use regex_lite::Regex;
15use std::path::{Path, PathBuf};
16
17use crate::{Context, Result, helpers, logger};
18
19pub mod cli;
20#[doc(hidden)]
21pub mod cli_output;
22pub mod file;
23
24pub use cli::Commands;
25
26use cli::General;
27
28#[cfg(feature = "experimental")]
29use self::file::MemoryCache;
30
31use self::file::{RedirectsKind, Settings as FileSettings};
32
33#[cfg(any(
34 feature = "compression",
35 feature = "compression-gzip",
36 feature = "compression-brotli",
37 feature = "compression-zstd",
38 feature = "compression-deflate"
39))]
40pub use file::CompressionLevel;
41
42pub struct Headers {
44 pub source: GlobMatcher,
46 pub headers: HeaderMap,
48}
49
50pub struct Rewrites {
52 pub source: Regex,
54 pub destination: String,
56 pub redirect: Option<RedirectsKind>,
58 pub replacer: AhoCorasick,
60}
61
62pub struct Redirects {
64 pub host: Option<String>,
66 pub source: Regex,
68 pub destination: String,
70 pub kind: StatusCode,
72 pub replacer: AhoCorasick,
74}
75
76pub struct VirtualHosts {
78 pub host: String,
80 pub root: PathBuf,
82}
83
84#[derive(Default)]
86pub struct Advanced {
87 pub headers: Option<Vec<Headers>>,
89 pub rewrites: Option<Vec<Rewrites>>,
91 pub redirects: Option<Vec<Redirects>>,
93 pub virtual_hosts: Option<Vec<VirtualHosts>>,
95 #[cfg(feature = "experimental")]
96 pub memory_cache: Option<MemoryCache>,
98}
99
100pub fn build_placeholder_replacer(regex: &Regex) -> AhoCorasick {
103 let patterns: Vec<String> = (0..regex.captures_len()).map(|i| format!("${i}")).collect();
104 AhoCorasick::new(&patterns).expect("failed to build Aho-Corasick automaton for placeholders")
105}
106
107pub struct Settings {
109 pub general: General,
111 pub advanced: Option<Advanced>,
113}
114
115impl Settings {
116 pub fn get(log_init: bool) -> Result<Settings> {
120 Self::parse_from(log_init, None)
121 }
122
123 pub fn get_unparsed(log_init: bool, args: &[&str]) -> Result<Settings> {
126 Self::parse_from(log_init, Some(args))
127 }
128
129 fn parse_from(log_init: bool, args: Option<&[&str]>) -> Result<Settings> {
130 let opts = match args {
131 Some(v) => General::parse_from(v),
132 None => General::parse(),
133 };
134
135 let version = opts.version;
137 let mut host = opts.host;
138 let mut port = opts.port;
139 let mut root = opts.root;
140 let mut log_level = opts.log_level;
141 let mut log_with_ansi = opts.log_with_ansi;
142 let mut config_file = opts.config_file.clone();
143 let mut cache_control_headers = opts.cache_control_headers;
144
145 #[cfg(any(
146 feature = "compression",
147 feature = "compression-gzip",
148 feature = "compression-brotli",
149 feature = "compression-zstd",
150 feature = "compression-deflate"
151 ))]
152 let mut compression = opts.compression;
153 #[cfg(any(
154 feature = "compression",
155 feature = "compression-gzip",
156 feature = "compression-brotli",
157 feature = "compression-zstd",
158 feature = "compression-deflate"
159 ))]
160 let mut compression_level = opts.compression_level;
161
162 let mut compression_static = opts.compression_static;
163
164 let mut page404 = opts.page404;
165 let mut page50x = opts.page50x;
166
167 #[cfg(feature = "http2")]
168 let mut http2 = opts.http2;
169 #[cfg(feature = "http2")]
170 let mut http2_tls_cert = opts.http2_tls_cert;
171 #[cfg(feature = "http2")]
172 let mut http2_tls_key = opts.http2_tls_key;
173 #[cfg(feature = "http2")]
174 let mut https_redirect = opts.https_redirect;
175 #[cfg(feature = "http2")]
176 let mut https_redirect_host = opts.https_redirect_host;
177 #[cfg(feature = "http2")]
178 let mut https_redirect_from_port = opts.https_redirect_from_port;
179 #[cfg(feature = "http2")]
180 let mut https_redirect_from_hosts = opts.https_redirect_from_hosts;
181
182 let mut security_headers = opts.security_headers;
183 let mut cors_allow_origins = opts.cors_allow_origins;
184 let mut cors_allow_headers = opts.cors_allow_headers;
185 let mut cors_expose_headers = opts.cors_expose_headers;
186
187 #[cfg(feature = "directory-listing")]
188 let mut directory_listing = opts.directory_listing;
189 #[cfg(feature = "directory-listing")]
190 let mut directory_listing_order = opts.directory_listing_order;
191 #[cfg(feature = "directory-listing")]
192 let mut directory_listing_format = opts.directory_listing_format;
193
194 #[cfg(feature = "directory-listing-download")]
195 let mut directory_listing_download = opts.directory_listing_download;
196
197 #[cfg(feature = "basic-auth")]
198 let mut basic_auth = opts.basic_auth;
199
200 let mut fd = opts.fd;
201 let mut threads_multiplier = opts.threads_multiplier;
202 let mut max_blocking_threads = opts.max_blocking_threads;
203 let mut grace_period = opts.grace_period;
204
205 #[cfg(feature = "fallback-page")]
206 let mut page_fallback = opts.page_fallback;
207
208 let mut log_remote_address = opts.log_remote_address;
209 let mut log_x_real_ip = opts.log_x_real_ip;
210 let mut log_forwarded_for = opts.log_forwarded_for;
211 let mut trusted_proxies = opts.trusted_proxies;
212 let mut redirect_trailing_slash = opts.redirect_trailing_slash;
213 let mut ignore_hidden_files = opts.ignore_hidden_files;
214 let mut disable_symlinks = opts.disable_symlinks;
215 let mut use_relative_root = opts.use_relative_root;
216 let mut accept_markdown = opts.accept_markdown;
217 let mut default_text_charset = opts.text_charset;
218 let mut index_files = opts.index_files;
219 let mut health = opts.health;
220
221 #[cfg(feature = "metrics")]
222 let mut metrics = opts.metrics;
223
224 let mut maintenance_mode = opts.maintenance_mode;
225 let mut maintenance_mode_status = opts.maintenance_mode_status;
226 let mut maintenance_mode_file = opts.maintenance_mode_file;
227
228 #[cfg(windows)]
230 let mut windows_service = opts.windows_service;
231
232 let mut settings_advanced: Option<Advanced> = None;
234
235 let to_use_config_file = match Path::new("./config.toml").is_file() {
236 true => {
237 eprintln!(
238 "Deprecated: 'config.toml' found, rename it to 'sws.toml' to prepare for future releases"
239 );
240 PathBuf::from("./config.toml")
241 }
242 false => opts.config_file.clone(),
243 };
244
245 if let Some((settings, config_file_resolved)) = read_file_settings(&to_use_config_file)? {
246 config_file = config_file_resolved;
247
248 let has_general_settings = settings.general.is_some();
250 if let Some(general) = settings.general {
251 if let Some(v) = general.host {
252 host = v
253 }
254 if let Some(v) = general.port {
255 port = v
256 }
257 if let Some(v) = general.root {
258 root = v
259 }
260 if let Some(ref v) = general.log_level {
261 log_level = v.name().to_lowercase();
262 }
263 if let Some(v) = general.log_with_ansi {
264 log_with_ansi = v;
265 }
266 if let Some(v) = general.cache_control_headers {
267 cache_control_headers = v
268 }
269 #[cfg(any(
270 feature = "compression",
271 feature = "compression-gzip",
272 feature = "compression-brotli",
273 feature = "compression-zstd",
274 feature = "compression-deflate"
275 ))]
276 if let Some(v) = general.compression {
277 compression = v
278 }
279 #[cfg(any(
280 feature = "compression",
281 feature = "compression-gzip",
282 feature = "compression-brotli",
283 feature = "compression-zstd",
284 feature = "compression-deflate"
285 ))]
286 if let Some(v) = general.compression_level {
287 compression_level = v
288 }
289 if let Some(v) = general.compression_static {
290 compression_static = v
291 }
292 if let Some(v) = general.page404 {
293 page404 = v
294 }
295 if let Some(v) = general.page50x {
296 page50x = v
297 }
298 #[cfg(feature = "http2")]
299 if let Some(v) = general.http2 {
300 http2 = v
301 }
302 #[cfg(feature = "http2")]
303 if let Some(v) = general.http2_tls_cert {
304 http2_tls_cert = Some(v)
305 }
306 #[cfg(feature = "http2")]
307 if let Some(v) = general.http2_tls_key {
308 http2_tls_key = Some(v)
309 }
310 #[cfg(feature = "http2")]
311 if let Some(v) = general.https_redirect {
312 https_redirect = v
313 }
314 #[cfg(feature = "http2")]
315 if let Some(v) = general.https_redirect_host {
316 https_redirect_host = v
317 }
318 #[cfg(feature = "http2")]
319 if let Some(v) = general.https_redirect_from_port {
320 https_redirect_from_port = v
321 }
322 #[cfg(feature = "http2")]
323 if let Some(v) = general.https_redirect_from_hosts {
324 https_redirect_from_hosts = v
325 }
326 #[cfg(feature = "http2")]
327 match general.security_headers {
328 Some(v) => security_headers = v,
329 _ => {
330 if http2 {
331 security_headers = true;
332 }
333 }
334 }
335 #[cfg(not(feature = "http2"))]
336 if let Some(v) = general.security_headers {
337 security_headers = v
338 }
339 if let Some(ref v) = general.cors_allow_origins {
340 v.clone_into(&mut cors_allow_origins)
341 }
342 if let Some(ref v) = general.cors_allow_headers {
343 v.clone_into(&mut cors_allow_headers)
344 }
345 if let Some(ref v) = general.cors_expose_headers {
346 v.clone_into(&mut cors_expose_headers)
347 }
348 #[cfg(feature = "directory-listing")]
349 if let Some(v) = general.directory_listing {
350 directory_listing = v
351 }
352 #[cfg(feature = "directory-listing")]
353 if let Some(v) = general.directory_listing_order {
354 directory_listing_order = v
355 }
356 #[cfg(feature = "directory-listing")]
357 if let Some(v) = general.directory_listing_format {
358 directory_listing_format = v
359 }
360 #[cfg(feature = "directory-listing-download")]
361 if let Some(v) = general.directory_listing_download {
362 directory_listing_download = v
363 }
364 #[cfg(feature = "basic-auth")]
365 if let Some(ref v) = general.basic_auth {
366 v.clone_into(&mut basic_auth)
367 }
368 if let Some(v) = general.fd {
369 fd = Some(v)
370 }
371 if let Some(v) = general.threads_multiplier {
372 threads_multiplier = v
373 }
374 if let Some(v) = general.max_blocking_threads {
375 max_blocking_threads = v
376 }
377 if let Some(v) = general.grace_period {
378 grace_period = v
379 }
380 #[cfg(feature = "fallback-page")]
381 if let Some(v) = general.page_fallback {
382 page_fallback = v
383 }
384 if let Some(v) = general.log_remote_address {
385 log_remote_address = v
386 }
387 if let Some(v) = general.log_x_real_ip {
388 log_x_real_ip = v
389 }
390 if let Some(v) = general.log_forwarded_for {
391 log_forwarded_for = v
392 }
393 if let Some(v) = general.trusted_proxies {
394 trusted_proxies = v
395 }
396 if let Some(v) = general.redirect_trailing_slash {
397 redirect_trailing_slash = v
398 }
399 if let Some(v) = general.ignore_hidden_files {
400 ignore_hidden_files = v
401 }
402 if let Some(v) = general.disable_symlinks {
403 disable_symlinks = v
404 }
405 if let Some(v) = general.use_relative_root {
406 use_relative_root = v
407 }
408 if let Some(v) = general.health {
409 health = v
410 }
411 if let Some(v) = general.accept_markdown {
412 accept_markdown = v
413 }
414 if let Some(v) = general.text_charset {
415 default_text_charset = v
416 }
417 #[cfg(feature = "metrics")]
418 if let Some(v) = general.metrics {
419 metrics = v
420 }
421 if let Some(v) = general.index_files {
422 index_files = v
423 }
424 if let Some(v) = general.maintenance_mode {
425 maintenance_mode = v
426 }
427 if let Some(v) = general.maintenance_mode_status {
428 maintenance_mode_status =
429 StatusCode::from_u16(v).with_context(|| "invalid HTTP status code")?
430 }
431 if let Some(v) = general.maintenance_mode_file {
432 maintenance_mode_file = v
433 }
434
435 #[cfg(windows)]
437 if let Some(v) = general.windows_service {
438 windows_service = v
439 }
440 }
441
442 if log_init {
444 logger::init(log_level.as_str(), log_with_ansi)?;
445 }
446
447 tracing::debug!("config file read successfully");
448 tracing::debug!("config file path provided: {}", opts.config_file.display());
449 tracing::debug!("config file path resolved: {}", config_file.display());
450
451 if !has_general_settings {
452 tracing::warn!(
453 "config file empty or no `general` settings found, using default values"
454 );
455 }
456
457 if let Some(advanced) = settings.advanced {
459 let headers_entries = match advanced.headers {
461 Some(headers_entries) => {
462 let mut headers_vec: Vec<Headers> = Vec::new();
463
464 for headers_entry in headers_entries.iter() {
466 let source = Glob::new(&headers_entry.source)
467 .with_context(|| {
468 format!(
469 "can not compile glob pattern for header source: {}",
470 headers_entry.source
471 )
472 })?
473 .compile_matcher();
474
475 headers_vec.push(Headers {
476 source,
477 headers: headers_entry.headers.to_owned(),
478 });
479 }
480 Some(headers_vec)
481 }
482 _ => None,
483 };
484
485 let rewrites_entries = match advanced.rewrites {
487 Some(rewrites_entries) => {
488 let mut rewrites_vec: Vec<Rewrites> = Vec::new();
489
490 for rewrites_entry in rewrites_entries.iter() {
492 let source = GlobBuilder::new(&rewrites_entry.source)
493 .literal_separator(true)
494 .build()
495 .with_context(|| {
496 format!(
497 "can not compile glob pattern for rewrite source: {}",
498 rewrites_entry.source
499 )
500 })?
501 .compile_matcher();
502
503 let pattern = source
504 .glob()
505 .regex()
506 .trim_start_matches("(?-u)")
507 .replace("?:.*", ".*")
508 .replace("?:", "")
509 .replace(".*.*", ".*")
510 .to_owned();
511 tracing::debug!(
512 "url rewrites glob pattern: {}",
513 &rewrites_entry.source
514 );
515 tracing::debug!("url rewrites regex equivalent: {}", pattern);
516
517 let source = Regex::new(&pattern).with_context(|| {
518 format!(
519 "can not compile regex pattern equivalent for rewrite source: {}",
520 pattern
521 )
522 })?;
523
524 let replacer = build_placeholder_replacer(&source);
525 rewrites_vec.push(Rewrites {
526 source,
527 destination: rewrites_entry.destination.to_owned(),
528 redirect: rewrites_entry.redirect.to_owned(),
529 replacer,
530 });
531 }
532 Some(rewrites_vec)
533 }
534 _ => None,
535 };
536
537 let redirects_entries = match advanced.redirects {
539 Some(redirects_entries) => {
540 let mut redirects_vec: Vec<Redirects> = Vec::new();
541
542 for redirects_entry in redirects_entries.iter() {
544 let source = GlobBuilder::new(&redirects_entry.source)
545 .literal_separator(true)
546 .build()
547 .with_context(|| {
548 format!(
549 "can not compile glob pattern for redirect source: {}",
550 redirects_entry.source
551 )
552 })?
553 .compile_matcher();
554
555 let pattern = source
556 .glob()
557 .regex()
558 .trim_start_matches("(?-u)")
559 .replace("?:.*", ".*")
560 .replace("?:", "")
561 .replace(".*.*", ".*")
562 .to_owned();
563 tracing::debug!(
564 "url redirects glob pattern: {}",
565 &redirects_entry.source
566 );
567 tracing::debug!("url redirects regex equivalent: {}", pattern);
568
569 let source = Regex::new(&pattern).with_context(|| {
570 format!(
571 "can not compile regex pattern equivalent for redirect source: {}",
572 pattern
573 )
574 })?;
575
576 let status_code = redirects_entry.kind.to_owned() as u16;
577 let replacer = build_placeholder_replacer(&source);
578 redirects_vec.push(Redirects {
579 host: redirects_entry.host.to_owned(),
580 source,
581 destination: redirects_entry.destination.to_owned(),
582 kind: StatusCode::from_u16(status_code).with_context(|| {
583 format!("invalid redirect status code: {status_code}")
584 })?,
585 replacer,
586 });
587 }
588 Some(redirects_vec)
589 }
590 _ => None,
591 };
592
593 let vhosts_entries = match advanced.virtual_hosts {
595 Some(vhosts_entries) => {
596 let mut vhosts_vec: Vec<VirtualHosts> = Vec::new();
597
598 for vhosts_entry in vhosts_entries.iter() {
599 if let Some(root) = vhosts_entry.root.to_owned() {
600 let root_dir = helpers::get_valid_dirpath(&root)
602 .with_context(|| "root directory for virtual host was not found or inaccessible")?;
603 let root_dir = if use_relative_root {
607 root_dir
608 } else {
609 root_dir.canonicalize().unwrap_or(root_dir)
610 };
611 tracing::debug!(
612 "added virtual host: {} -> {}",
613 vhosts_entry.host,
614 root_dir.display()
615 );
616 vhosts_vec.push(VirtualHosts {
617 host: vhosts_entry.host.to_owned(),
618 root: root_dir,
619 });
620 }
621 }
622 Some(vhosts_vec)
623 }
624 _ => None,
625 };
626
627 settings_advanced = Some(Advanced {
628 headers: headers_entries,
629 rewrites: rewrites_entries,
630 redirects: redirects_entries,
631 virtual_hosts: vhosts_entries,
632 #[cfg(feature = "experimental")]
633 memory_cache: advanced.memory_cache,
634 });
635 }
636 } else if log_init {
637 logger::init(log_level.as_str(), log_with_ansi)?;
639 }
640
641 Ok(Settings {
642 general: General {
643 version,
644 host,
645 port,
646 root,
647 log_level,
648 log_with_ansi,
649 config_file,
650 cache_control_headers,
651 #[cfg(any(
652 feature = "compression",
653 feature = "compression-gzip",
654 feature = "compression-brotli",
655 feature = "compression-zstd",
656 feature = "compression-deflate"
657 ))]
658 compression,
659 #[cfg(any(
660 feature = "compression",
661 feature = "compression-gzip",
662 feature = "compression-brotli",
663 feature = "compression-zstd",
664 feature = "compression-deflate"
665 ))]
666 compression_level,
667 compression_static,
668 page404,
669 page50x,
670 #[cfg(feature = "http2")]
671 http2,
672 #[cfg(feature = "http2")]
673 http2_tls_cert,
674 #[cfg(feature = "http2")]
675 http2_tls_key,
676 #[cfg(feature = "http2")]
677 https_redirect,
678 #[cfg(feature = "http2")]
679 https_redirect_host,
680 #[cfg(feature = "http2")]
681 https_redirect_from_port,
682 #[cfg(feature = "http2")]
683 https_redirect_from_hosts,
684 security_headers,
685 cors_allow_origins,
686 cors_allow_headers,
687 cors_expose_headers,
688 #[cfg(feature = "directory-listing")]
689 directory_listing,
690 #[cfg(feature = "directory-listing")]
691 directory_listing_order,
692 #[cfg(feature = "directory-listing")]
693 directory_listing_format,
694 #[cfg(feature = "directory-listing-download")]
695 directory_listing_download,
696 #[cfg(feature = "basic-auth")]
697 basic_auth,
698 fd,
699 threads_multiplier,
700 max_blocking_threads,
701 grace_period,
702 #[cfg(feature = "fallback-page")]
703 page_fallback,
704 log_remote_address,
705 log_x_real_ip,
706 log_forwarded_for,
707 trusted_proxies,
708 redirect_trailing_slash,
709 ignore_hidden_files,
710 disable_symlinks,
711 use_relative_root,
712 accept_markdown,
713 text_charset: default_text_charset,
714 index_files,
715 health,
716 #[cfg(feature = "metrics")]
717 metrics,
718 maintenance_mode,
719 maintenance_mode_status,
720 maintenance_mode_file,
721
722 #[cfg(windows)]
724 windows_service,
725 commands: opts.commands,
726 },
727 advanced: settings_advanced,
728 })
729 }
730}
731
732fn read_file_settings(config_file: &Path) -> Result<Option<(FileSettings, PathBuf)>> {
733 if config_file.is_file() {
734 let file_path_resolved = config_file
735 .canonicalize()
736 .with_context(|| "unable to resolve toml config file path")?;
737
738 let settings = FileSettings::read(&file_path_resolved).with_context(
739 || "unable to read toml config file because has invalid format or unsupported options",
740 )?;
741
742 return Ok(Some((settings, file_path_resolved)));
743 }
744 Ok(None)
745}