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