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