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
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 pub replacer: AhoCorasick,
59}
60
61pub struct Redirects {
63 pub host: Option<String>,
65 pub source: Regex,
67 pub destination: String,
69 pub kind: StatusCode,
71 pub replacer: AhoCorasick,
73}
74
75pub struct VirtualHosts {
77 pub host: String,
79 pub root: PathBuf,
81}
82
83#[derive(Default)]
85pub struct Advanced {
86 pub headers: Option<Vec<Headers>>,
88 pub rewrites: Option<Vec<Rewrites>>,
90 pub redirects: Option<Vec<Redirects>>,
92 pub virtual_hosts: Option<Vec<VirtualHosts>>,
94 pub memory_cache: Option<MemoryCache>,
96}
97
98pub fn build_placeholder_replacer(regex: &Regex) -> AhoCorasick {
101 let patterns: Vec<String> = (0..regex.captures_len()).map(|i| format!("${i}")).collect();
102 AhoCorasick::new(&patterns).expect("failed to build Aho-Corasick automaton for placeholders")
103}
104
105pub struct Settings {
107 pub general: General,
109 pub advanced: Option<Advanced>,
111}
112
113impl Settings {
114 pub fn get(log_init: bool) -> Result<Settings> {
118 Self::parse_from(log_init, None)
119 }
120
121 pub fn get_unparsed(log_init: bool, args: &[&str]) -> Result<Settings> {
124 Self::parse_from(log_init, Some(args))
125 }
126
127 fn parse_from(log_init: bool, args: Option<&[&str]>) -> Result<Settings> {
128 let opts = match args {
129 Some(v) => General::parse_from(v),
130 None => General::parse(),
131 };
132
133 let version = opts.version;
135 let mut host = opts.host;
136 let mut port = opts.port;
137 let mut root = opts.root;
138 let mut log_level = opts.log_level;
139 let mut log_with_ansi = opts.log_with_ansi;
140 let mut log_format = opts.log_format;
141 let mut log_file = opts.log_file.clone();
142 let mut config_file = opts.config_file.clone();
143 let mut cache_control_headers = opts.cache_control_headers;
144 let mut etag = opts.etag;
145
146 #[cfg(any(
147 feature = "compression",
148 feature = "compression-gzip",
149 feature = "compression-brotli",
150 feature = "compression-zstd",
151 feature = "compression-deflate"
152 ))]
153 let mut compression = opts.compression;
154 #[cfg(any(
155 feature = "compression",
156 feature = "compression-gzip",
157 feature = "compression-brotli",
158 feature = "compression-zstd",
159 feature = "compression-deflate"
160 ))]
161 let mut compression_level = opts.compression_level;
162
163 let mut compression_static = opts.compression_static;
164
165 let mut page404 = opts.page404;
166 let mut page50x = opts.page50x;
167
168 #[cfg(feature = "tls")]
169 let mut tls = opts.tls;
170 #[cfg(feature = "tls")]
171 let mut tls_cert = opts.tls_cert;
172 #[cfg(feature = "tls")]
173 let mut tls_key = opts.tls_key;
174 #[cfg(feature = "tls")]
175 let mut https_redirect = opts.https_redirect;
176 #[cfg(feature = "tls")]
177 let mut https_redirect_host = opts.https_redirect_host;
178 #[cfg(feature = "tls")]
179 let mut https_redirect_from_port = opts.https_redirect_from_port;
180 #[cfg(feature = "tls")]
181 let mut https_redirect_from_hosts = opts.https_redirect_from_hosts;
182 #[cfg(feature = "http2")]
183 let mut http2 = opts.http2;
184
185 let mut security_headers = opts.security_headers;
186 let mut cors_allow_origins = opts.cors_allow_origins;
187 let mut cors_allow_headers = opts.cors_allow_headers;
188 let mut cors_expose_headers = opts.cors_expose_headers;
189
190 #[cfg(feature = "directory-listing")]
191 let mut directory_listing = opts.directory_listing;
192 #[cfg(feature = "directory-listing")]
193 let mut directory_listing_order = opts.directory_listing_order;
194 #[cfg(feature = "directory-listing")]
195 let mut directory_listing_format = opts.directory_listing_format;
196
197 #[cfg(feature = "directory-listing-download")]
198 let mut directory_listing_download = opts.directory_listing_download;
199
200 #[cfg(feature = "basic-auth")]
201 let mut basic_auth = opts.basic_auth;
202
203 let mut fd = opts.fd;
204 #[cfg(unix)]
205 let mut unix_socket = opts.unix_socket.clone();
206 #[cfg(unix)]
207 let mut unix_socket_mode = opts.unix_socket_mode;
208 #[cfg(unix)]
209 let mut unix_socket_force = opts.unix_socket_force;
210 let mut threads_multiplier = opts.threads_multiplier;
211 let mut max_blocking_threads = opts.max_blocking_threads;
212 let mut grace_period = opts.grace_period;
213
214 #[cfg(feature = "fallback-page")]
215 let mut page_fallback = opts.page_fallback;
216
217 let mut log_remote_address = opts.log_remote_address;
218 let mut log_x_real_ip = opts.log_x_real_ip;
219 let mut log_forwarded_for = opts.log_forwarded_for;
220 let mut trusted_proxies = opts.trusted_proxies;
221 let mut redirect_trailing_slash = opts.redirect_trailing_slash;
222 let mut include_hidden = opts.include_hidden;
223 let mut follow_symlinks = opts.follow_symlinks;
224 let mut use_relative_root = opts.use_relative_root;
225 let mut accept_markdown = opts.accept_markdown;
226 let mut text_charset = opts.text_charset;
227 let mut index_files = opts.index_files;
228 let mut health = opts.health;
229
230 #[cfg(feature = "metrics")]
231 let mut metrics = opts.metrics;
232
233 let mut maintenance_mode = opts.maintenance_mode;
234 let mut maintenance_mode_status = opts.maintenance_mode_status;
235 let mut maintenance_mode_file = opts.maintenance_mode_file;
236
237 #[cfg(windows)]
239 let mut windows_service = opts.windows_service;
240
241 let mut settings_advanced: Option<Advanced> = None;
243
244 let to_use_config_file = match Path::new("./config.toml").is_file() {
245 true => {
246 eprintln!(
247 "Deprecated: 'config.toml' found, rename it to 'sws.toml' to prepare for future releases"
248 );
249 PathBuf::from("./config.toml")
250 }
251 false => opts.config_file.clone(),
252 };
253
254 if let Some((settings, config_file_resolved)) = read_file_settings(&to_use_config_file)? {
255 config_file = config_file_resolved;
256
257 let has_general_settings = settings.general.is_some();
259 if let Some(general) = settings.general {
260 if let Some(v) = general.host {
261 host = v
262 }
263 if let Some(v) = general.port {
264 port = v
265 }
266 if let Some(v) = general.root {
267 root = v
268 }
269 if let Some(ref v) = general.log_level {
270 log_level = v.name().to_lowercase();
271 }
272 if let Some(v) = general.log_with_ansi {
273 log_with_ansi = v;
274 }
275 if let Some(v) = general.log_format {
276 log_format = v;
277 }
278 if let Some(v) = general.log_file {
279 log_file = Some(v);
280 }
281 if let Some(v) = general.cache_control_headers {
282 cache_control_headers = v
283 }
284 if let Some(v) = general.etag {
285 etag = v
286 }
287 #[cfg(any(
288 feature = "compression",
289 feature = "compression-gzip",
290 feature = "compression-brotli",
291 feature = "compression-zstd",
292 feature = "compression-deflate"
293 ))]
294 if let Some(v) = general.compression {
295 compression = v
296 }
297 #[cfg(any(
298 feature = "compression",
299 feature = "compression-gzip",
300 feature = "compression-brotli",
301 feature = "compression-zstd",
302 feature = "compression-deflate"
303 ))]
304 if let Some(v) = general.compression_level {
305 compression_level = v
306 }
307 if let Some(v) = general.compression_static {
308 compression_static = v
309 }
310 if let Some(v) = general.page404 {
311 page404 = v
312 }
313 if let Some(v) = general.page50x {
314 page50x = v
315 }
316 #[cfg(feature = "tls")]
317 if let Some(v) = general.tls {
318 tls = v
319 }
320 #[cfg(feature = "tls")]
321 if let Some(v) = general.tls_cert {
322 tls_cert = Some(v)
323 }
324 #[cfg(feature = "tls")]
325 if let Some(v) = general.tls_key {
326 tls_key = Some(v)
327 }
328 #[cfg(feature = "http2")]
329 if let Some(v) = general.http2 {
330 http2 = v
331 }
332 #[cfg(feature = "tls")]
333 if let Some(v) = general.https_redirect {
334 https_redirect = v
335 }
336 #[cfg(feature = "tls")]
337 if let Some(v) = general.https_redirect_host {
338 https_redirect_host = v
339 }
340 #[cfg(feature = "tls")]
341 if let Some(v) = general.https_redirect_from_port {
342 https_redirect_from_port = v
343 }
344 #[cfg(feature = "tls")]
345 if let Some(v) = general.https_redirect_from_hosts {
346 https_redirect_from_hosts = v
347 }
348 #[cfg(feature = "tls")]
349 match general.security_headers {
350 Some(v) => security_headers = v,
351 _ => {
352 if tls {
353 security_headers = true;
354 }
355 }
356 }
357 #[cfg(not(feature = "tls"))]
358 if let Some(v) = general.security_headers {
359 security_headers = v
360 }
361 if let Some(ref v) = general.cors_allow_origins {
362 v.clone_into(&mut cors_allow_origins)
363 }
364 if let Some(ref v) = general.cors_allow_headers {
365 v.clone_into(&mut cors_allow_headers)
366 }
367 if let Some(ref v) = general.cors_expose_headers {
368 v.clone_into(&mut cors_expose_headers)
369 }
370 #[cfg(feature = "directory-listing")]
371 if let Some(v) = general.directory_listing {
372 directory_listing = v
373 }
374 #[cfg(feature = "directory-listing")]
375 if let Some(v) = general.directory_listing_order {
376 directory_listing_order = v
377 }
378 #[cfg(feature = "directory-listing")]
379 if let Some(v) = general.directory_listing_format {
380 directory_listing_format = v
381 }
382 #[cfg(feature = "directory-listing-download")]
383 if let Some(v) = general.directory_listing_download {
384 directory_listing_download = v
385 }
386 #[cfg(feature = "basic-auth")]
387 if let Some(ref v) = general.basic_auth {
388 v.clone_into(&mut basic_auth)
389 }
390 if let Some(v) = general.fd {
391 fd = Some(v)
392 }
393 #[cfg(unix)]
394 if let Some(v) = general.unix_socket {
395 unix_socket = Some(v)
396 }
397 #[cfg(unix)]
398 if let Some(v) = general.unix_socket_mode {
399 unix_socket_mode = Some(v)
400 }
401 #[cfg(unix)]
402 if let Some(v) = general.unix_socket_force {
403 unix_socket_force = v
404 }
405 if let Some(v) = general.threads_multiplier {
406 threads_multiplier = v
407 }
408 if let Some(v) = general.max_blocking_threads {
409 max_blocking_threads = v
410 }
411 if let Some(v) = general.grace_period {
412 grace_period = v
413 }
414 #[cfg(feature = "fallback-page")]
415 if let Some(v) = general.page_fallback {
416 page_fallback = v
417 }
418 if let Some(v) = general.log_remote_address {
419 log_remote_address = v
420 }
421 if let Some(v) = general.log_x_real_ip {
422 log_x_real_ip = v
423 }
424 if let Some(v) = general.log_forwarded_for {
425 log_forwarded_for = v
426 }
427 if let Some(v) = general.trusted_proxies {
428 trusted_proxies = v
429 }
430 if let Some(v) = general.redirect_trailing_slash {
431 redirect_trailing_slash = v
432 }
433 if let Some(v) = general.include_hidden {
434 include_hidden = v
435 }
436 if let Some(v) = general.follow_symlinks {
437 follow_symlinks = v
438 }
439 if let Some(v) = general.use_relative_root {
440 use_relative_root = v
441 }
442 if let Some(v) = general.health {
443 health = v
444 }
445 if let Some(v) = general.accept_markdown {
446 accept_markdown = v
447 }
448 if let Some(v) = general.text_charset {
449 text_charset = v
450 }
451 #[cfg(feature = "metrics")]
452 if let Some(v) = general.metrics {
453 metrics = v
454 }
455 if let Some(v) = general.index_files {
456 index_files = v
457 }
458 if let Some(v) = general.maintenance_mode {
459 maintenance_mode = v
460 }
461 if let Some(v) = general.maintenance_mode_status {
462 maintenance_mode_status =
463 StatusCode::from_u16(v).with_context(|| "invalid HTTP status code")?
464 }
465 if let Some(v) = general.maintenance_mode_file {
466 maintenance_mode_file = v
467 }
468
469 #[cfg(windows)]
471 if let Some(v) = general.windows_service {
472 windows_service = v
473 }
474 }
475
476 if log_init {
478 logger::init(
479 log_level.as_str(),
480 &log_format,
481 log_with_ansi,
482 log_file.as_deref(),
483 )?;
484 }
485
486 tracing::debug!("config file read successfully");
487 tracing::debug!("config file path provided: {}", opts.config_file.display());
488 tracing::debug!("config file path resolved: {}", config_file.display());
489
490 if !has_general_settings {
491 tracing::warn!(
492 "config file empty or no `general` settings found, using default values"
493 );
494 }
495
496 if let Some(advanced) = settings.advanced {
498 let headers_entries = match advanced.headers {
500 Some(headers_entries) => {
501 let mut headers_vec: Vec<Headers> = Vec::new();
502
503 for headers_entry in headers_entries.iter() {
505 let source = Glob::new(&headers_entry.source)
506 .with_context(|| {
507 format!(
508 "can not compile glob pattern for header source: {}",
509 headers_entry.source
510 )
511 })?
512 .compile_matcher();
513
514 headers_vec.push(Headers {
515 source,
516 headers: headers_entry.headers.to_owned(),
517 });
518 }
519 Some(headers_vec)
520 }
521 _ => None,
522 };
523
524 let rewrites_entries = match advanced.rewrites {
526 Some(rewrites_entries) => {
527 let mut rewrites_vec: Vec<Rewrites> = Vec::new();
528
529 for rewrites_entry in rewrites_entries.iter() {
531 let source = GlobBuilder::new(&rewrites_entry.source)
532 .literal_separator(true)
533 .build()
534 .with_context(|| {
535 format!(
536 "can not compile glob pattern for rewrite source: {}",
537 rewrites_entry.source
538 )
539 })?
540 .compile_matcher();
541
542 let pattern = source
543 .glob()
544 .regex()
545 .trim_start_matches("(?-u)")
546 .replace("?:.*", ".*")
547 .replace("?:", "")
548 .replace(".*.*", ".*")
549 .to_owned();
550 tracing::debug!(
551 "url rewrites glob pattern: {}",
552 &rewrites_entry.source
553 );
554 tracing::debug!("url rewrites regex equivalent: {}", pattern);
555
556 let source = Regex::new(&pattern).with_context(|| {
557 format!(
558 "can not compile regex pattern equivalent for rewrite source: {}",
559 pattern
560 )
561 })?;
562
563 let replacer = build_placeholder_replacer(&source);
564 rewrites_vec.push(Rewrites {
565 source,
566 destination: rewrites_entry.destination.to_owned(),
567 redirect: rewrites_entry.redirect.to_owned(),
568 replacer,
569 });
570 }
571 Some(rewrites_vec)
572 }
573 _ => None,
574 };
575
576 let redirects_entries = match advanced.redirects {
578 Some(redirects_entries) => {
579 let mut redirects_vec: Vec<Redirects> = Vec::new();
580
581 for redirects_entry in redirects_entries.iter() {
583 let source = GlobBuilder::new(&redirects_entry.source)
584 .literal_separator(true)
585 .build()
586 .with_context(|| {
587 format!(
588 "can not compile glob pattern for redirect source: {}",
589 redirects_entry.source
590 )
591 })?
592 .compile_matcher();
593
594 let pattern = source
595 .glob()
596 .regex()
597 .trim_start_matches("(?-u)")
598 .replace("?:.*", ".*")
599 .replace("?:", "")
600 .replace(".*.*", ".*")
601 .to_owned();
602 tracing::debug!(
603 "url redirects glob pattern: {}",
604 &redirects_entry.source
605 );
606 tracing::debug!("url redirects regex equivalent: {}", pattern);
607
608 let source = Regex::new(&pattern).with_context(|| {
609 format!(
610 "can not compile regex pattern equivalent for redirect source: {}",
611 pattern
612 )
613 })?;
614
615 let status_code = redirects_entry.kind.to_owned() as u16;
616 let replacer = build_placeholder_replacer(&source);
617 redirects_vec.push(Redirects {
618 host: redirects_entry.host.to_owned(),
619 source,
620 destination: redirects_entry.destination.to_owned(),
621 kind: StatusCode::from_u16(status_code).with_context(|| {
622 format!("invalid redirect status code: {status_code}")
623 })?,
624 replacer,
625 });
626 }
627 Some(redirects_vec)
628 }
629 _ => None,
630 };
631
632 let vhosts_entries = match advanced.virtual_hosts {
634 Some(vhosts_entries) => {
635 let mut vhosts_vec: Vec<VirtualHosts> = Vec::new();
636
637 for vhosts_entry in vhosts_entries.iter() {
638 if let Some(root) = vhosts_entry.root.to_owned() {
639 let root_dir = helpers::get_valid_dirpath(&root)
641 .with_context(|| "root directory for virtual host was not found or inaccessible")?;
642 let root_dir = if use_relative_root {
647 root_dir
648 } else {
649 root_dir.canonicalize().unwrap_or(root_dir)
650 };
651 tracing::debug!(
652 "added virtual host: {} -> {}",
653 vhosts_entry.host,
654 root_dir.display()
655 );
656 vhosts_vec.push(VirtualHosts {
657 host: vhosts_entry.host.to_owned(),
658 root: root_dir,
659 });
660 }
661 }
662 Some(vhosts_vec)
663 }
664 _ => None,
665 };
666
667 settings_advanced = Some(Advanced {
668 headers: headers_entries,
669 rewrites: rewrites_entries,
670 redirects: redirects_entries,
671 virtual_hosts: vhosts_entries,
672 memory_cache: advanced.memory_cache,
673 });
674 }
675 } else if log_init {
676 logger::init(
678 log_level.as_str(),
679 &log_format,
680 log_with_ansi,
681 log_file.as_deref(),
682 )?;
683 }
684
685 #[cfg(all(feature = "http2", feature = "tls"))]
687 if http2 && !tls {
688 bail!("HTTP/2 requires TLS; enable --tls along with --tls-cert and --tls-key");
689 }
690
691 if log_with_ansi && log_format != logger::LogFormat::Pretty {
693 bail!("--log-with-ansi requires --log-format=pretty");
694 }
695
696 #[cfg(feature = "tls")]
698 if https_redirect && !tls {
699 bail!("--https-redirect requires TLS to be enabled (--tls)");
700 }
701
702 #[cfg(feature = "tls")]
704 if tls && !security_headers {
705 security_headers = true;
706 }
707
708 Ok(Settings {
709 general: General {
710 version,
711 host,
712 port,
713 root,
714 log_level,
715 log_with_ansi,
716 log_format,
717 log_file,
718 config_file,
719 cache_control_headers,
720 etag,
721 #[cfg(any(
722 feature = "compression",
723 feature = "compression-gzip",
724 feature = "compression-brotli",
725 feature = "compression-zstd",
726 feature = "compression-deflate"
727 ))]
728 compression,
729 #[cfg(any(
730 feature = "compression",
731 feature = "compression-gzip",
732 feature = "compression-brotli",
733 feature = "compression-zstd",
734 feature = "compression-deflate"
735 ))]
736 compression_level,
737 compression_static,
738 page404,
739 page50x,
740 #[cfg(feature = "http2")]
741 http2,
742 #[cfg(feature = "tls")]
743 tls,
744 #[cfg(feature = "tls")]
745 tls_cert,
746 #[cfg(feature = "tls")]
747 tls_key,
748 #[cfg(feature = "tls")]
749 https_redirect,
750 #[cfg(feature = "tls")]
751 https_redirect_host,
752 #[cfg(feature = "tls")]
753 https_redirect_from_port,
754 #[cfg(feature = "tls")]
755 https_redirect_from_hosts,
756 security_headers,
757 cors_allow_origins,
758 cors_allow_headers,
759 cors_expose_headers,
760 #[cfg(feature = "directory-listing")]
761 directory_listing,
762 #[cfg(feature = "directory-listing")]
763 directory_listing_order,
764 #[cfg(feature = "directory-listing")]
765 directory_listing_format,
766 #[cfg(feature = "directory-listing-download")]
767 directory_listing_download,
768 #[cfg(feature = "basic-auth")]
769 basic_auth,
770 fd,
771 #[cfg(unix)]
772 unix_socket,
773 #[cfg(unix)]
774 unix_socket_mode,
775 #[cfg(unix)]
776 unix_socket_force,
777 threads_multiplier,
778 max_blocking_threads,
779 grace_period,
780 #[cfg(feature = "fallback-page")]
781 page_fallback,
782 log_remote_address,
783 log_x_real_ip,
784 log_forwarded_for,
785 trusted_proxies,
786 redirect_trailing_slash,
787 include_hidden,
788 follow_symlinks,
789 use_relative_root,
790 accept_markdown,
791 text_charset,
792 index_files,
793 health,
794 #[cfg(feature = "metrics")]
795 metrics,
796 maintenance_mode,
797 maintenance_mode_status,
798 maintenance_mode_file,
799
800 #[cfg(windows)]
802 windows_service,
803 commands: opts.commands,
804 },
805 advanced: settings_advanced,
806 })
807 }
808}
809
810fn read_file_settings(config_file: &Path) -> Result<Option<(FileSettings, PathBuf)>> {
811 if config_file.is_file() {
812 let file_path_resolved = config_file
813 .canonicalize()
814 .with_context(|| "unable to resolve toml config file path")?;
815
816 let settings = FileSettings::read(&file_path_resolved).with_context(
817 || "unable to read toml config file because has invalid format or unsupported options",
818 )?;
819
820 return Ok(Some((settings, file_path_resolved)));
821 }
822 Ok(None)
823}