1use clap::Parser;
10use globset::{Glob, GlobBuilder, GlobMatcher};
11use headers::HeaderMap;
12use hyper::StatusCode;
13use regex::Regex;
14use std::path::{Path, PathBuf};
15
16use crate::{helpers, logger, Context, Result};
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::read(log_init, true)
109 }
110
111 pub fn get_unparsed(log_init: bool) -> Result<Settings> {
114 Self::read(log_init, false)
115 }
116
117 fn read(log_init: bool, parse_args: bool) -> Result<Settings> {
118 let opts = if parse_args {
119 General::parse()
120 } else {
121 General::parse_from([""])
122 };
123
124 let version = opts.version;
126 let mut host = opts.host;
127 let mut port = opts.port;
128 let mut root = opts.root;
129 let mut log_level = opts.log_level;
130 let mut log_with_ansi = opts.log_with_ansi;
131 let mut config_file = opts.config_file.clone();
132 let mut cache_control_headers = opts.cache_control_headers;
133
134 #[cfg(any(
135 feature = "compression",
136 feature = "compression-gzip",
137 feature = "compression-brotli",
138 feature = "compression-zstd",
139 feature = "compression-deflate"
140 ))]
141 let mut compression = opts.compression;
142 #[cfg(any(
143 feature = "compression",
144 feature = "compression-gzip",
145 feature = "compression-brotli",
146 feature = "compression-zstd",
147 feature = "compression-deflate"
148 ))]
149 let mut compression_level = opts.compression_level;
150 #[cfg(any(
151 feature = "compression",
152 feature = "compression-gzip",
153 feature = "compression-brotli",
154 feature = "compression-zstd",
155 feature = "compression-deflate"
156 ))]
157 let mut compression_static = opts.compression_static;
158
159 let mut page404 = opts.page404;
160 let mut page50x = opts.page50x;
161
162 #[cfg(feature = "http2")]
163 let mut http2 = opts.http2;
164 #[cfg(feature = "http2")]
165 let mut http2_tls_cert = opts.http2_tls_cert;
166 #[cfg(feature = "http2")]
167 let mut http2_tls_key = opts.http2_tls_key;
168 #[cfg(feature = "http2")]
169 let mut https_redirect = opts.https_redirect;
170 #[cfg(feature = "http2")]
171 let mut https_redirect_host = opts.https_redirect_host;
172 #[cfg(feature = "http2")]
173 let mut https_redirect_from_port = opts.https_redirect_from_port;
174 #[cfg(feature = "http2")]
175 let mut https_redirect_from_hosts = opts.https_redirect_from_hosts;
176
177 let mut security_headers = opts.security_headers;
178 let mut cors_allow_origins = opts.cors_allow_origins;
179 let mut cors_allow_headers = opts.cors_allow_headers;
180 let mut cors_expose_headers = opts.cors_expose_headers;
181
182 #[cfg(feature = "directory-listing")]
183 let mut directory_listing = opts.directory_listing;
184 #[cfg(feature = "directory-listing")]
185 let mut directory_listing_order = opts.directory_listing_order;
186 #[cfg(feature = "directory-listing")]
187 let mut directory_listing_format = opts.directory_listing_format;
188
189 #[cfg(feature = "directory-listing-download")]
190 let mut directory_listing_download = opts.directory_listing_download;
191
192 #[cfg(feature = "basic-auth")]
193 let mut basic_auth = opts.basic_auth;
194
195 let mut fd = opts.fd;
196 let mut threads_multiplier = opts.threads_multiplier;
197 let mut max_blocking_threads = opts.max_blocking_threads;
198 let mut grace_period = opts.grace_period;
199
200 #[cfg(feature = "fallback-page")]
201 let mut page_fallback = opts.page_fallback;
202
203 let mut log_remote_address = opts.log_remote_address;
204 let mut log_x_real_ip = opts.log_x_real_ip;
205 let mut log_forwarded_for = opts.log_forwarded_for;
206 let mut trusted_proxies = opts.trusted_proxies;
207 let mut redirect_trailing_slash = opts.redirect_trailing_slash;
208 let mut ignore_hidden_files = opts.ignore_hidden_files;
209 let mut disable_symlinks = opts.disable_symlinks;
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!("Deprecated: 'config.toml' found, rename it to 'sws.toml' to prepare for future releases");
230 PathBuf::from("./config.toml")
231 }
232 false => opts.config_file.clone(),
233 };
234
235 if let Some((settings, config_file_resolved)) = read_file_settings(&to_use_config_file)? {
236 config_file = config_file_resolved;
237
238 let has_general_settings = settings.general.is_some();
240 if has_general_settings {
241 let general = settings.general.unwrap();
242
243 if let Some(v) = general.host {
244 host = v
245 }
246 if let Some(v) = general.port {
247 port = v
248 }
249 if let Some(v) = general.root {
250 root = v
251 }
252 if let Some(ref v) = general.log_level {
253 log_level = v.name().to_lowercase();
254 }
255 if let Some(v) = general.log_with_ansi {
256 log_with_ansi = v;
257 }
258 if let Some(v) = general.cache_control_headers {
259 cache_control_headers = v
260 }
261 #[cfg(any(
262 feature = "compression",
263 feature = "compression-gzip",
264 feature = "compression-brotli",
265 feature = "compression-zstd",
266 feature = "compression-deflate"
267 ))]
268 if let Some(v) = general.compression {
269 compression = v
270 }
271 #[cfg(any(
272 feature = "compression",
273 feature = "compression-gzip",
274 feature = "compression-brotli",
275 feature = "compression-zstd",
276 feature = "compression-deflate"
277 ))]
278 if let Some(v) = general.compression_level {
279 compression_level = v
280 }
281 #[cfg(any(
282 feature = "compression",
283 feature = "compression-gzip",
284 feature = "compression-brotli",
285 feature = "compression-zstd",
286 feature = "compression-deflate"
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 #[cfg(all(unix, feature = "experimental"))]
408 if let Some(v) = general.experimental_metrics {
409 experimental_metrics = v
410 }
411 if let Some(v) = general.index_files {
412 index_files = v
413 }
414 if let Some(v) = general.maintenance_mode {
415 maintenance_mode = v
416 }
417 if let Some(v) = general.maintenance_mode_status {
418 maintenance_mode_status =
419 StatusCode::from_u16(v).with_context(|| "invalid HTTP status code")?
420 }
421 if let Some(v) = general.maintenance_mode_file {
422 maintenance_mode_file = v
423 }
424
425 #[cfg(windows)]
427 if let Some(v) = general.windows_service {
428 windows_service = v
429 }
430 }
431
432 if log_init {
434 logger::init(log_level.as_str(), log_with_ansi)?;
435 }
436
437 tracing::debug!("config file read successfully");
438 tracing::debug!("config file path provided: {}", opts.config_file.display());
439 tracing::debug!("config file path resolved: {}", config_file.display());
440
441 if !has_general_settings {
442 tracing::warn!(
443 "config file empty or no `general` settings found, using default values"
444 );
445 }
446
447 if let Some(advanced) = settings.advanced {
449 let headers_entries = match advanced.headers {
451 Some(headers_entries) => {
452 let mut headers_vec: Vec<Headers> = Vec::new();
453
454 for headers_entry in headers_entries.iter() {
456 let source = Glob::new(&headers_entry.source)
457 .with_context(|| {
458 format!(
459 "can not compile glob pattern for header source: {}",
460 &headers_entry.source
461 )
462 })?
463 .compile_matcher();
464
465 headers_vec.push(Headers {
466 source,
467 headers: headers_entry.headers.to_owned(),
468 });
469 }
470 Some(headers_vec)
471 }
472 _ => None,
473 };
474
475 let rewrites_entries = match advanced.rewrites {
477 Some(rewrites_entries) => {
478 let mut rewrites_vec: Vec<Rewrites> = Vec::new();
479
480 for rewrites_entry in rewrites_entries.iter() {
482 let source = GlobBuilder::new(&rewrites_entry.source)
483 .literal_separator(true)
484 .build()
485 .with_context(|| {
486 format!(
487 "can not compile glob pattern for rewrite source: {}",
488 &rewrites_entry.source
489 )
490 })?
491 .compile_matcher();
492
493 let pattern = source
494 .glob()
495 .regex()
496 .trim_start_matches("(?-u)")
497 .replace("?:.*", ".*")
498 .replace("?:", "")
499 .replace(".*.*", ".*")
500 .to_owned();
501 tracing::debug!(
502 "url rewrites glob pattern: {}",
503 &rewrites_entry.source
504 );
505 tracing::debug!("url rewrites regex equivalent: {}", pattern);
506
507 let source = Regex::new(&pattern).with_context(|| {
508 format!(
509 "can not compile regex pattern equivalent for rewrite source: {}",
510 &pattern
511 )
512 })?;
513
514 rewrites_vec.push(Rewrites {
515 source,
516 destination: rewrites_entry.destination.to_owned(),
517 redirect: rewrites_entry.redirect.to_owned(),
518 });
519 }
520 Some(rewrites_vec)
521 }
522 _ => None,
523 };
524
525 let redirects_entries = match advanced.redirects {
527 Some(redirects_entries) => {
528 let mut redirects_vec: Vec<Redirects> = Vec::new();
529
530 for redirects_entry in redirects_entries.iter() {
532 let source = GlobBuilder::new(&redirects_entry.source)
533 .literal_separator(true)
534 .build()
535 .with_context(|| {
536 format!(
537 "can not compile glob pattern for redirect source: {}",
538 &redirects_entry.source
539 )
540 })?
541 .compile_matcher();
542
543 let pattern = source
544 .glob()
545 .regex()
546 .trim_start_matches("(?-u)")
547 .replace("?:.*", ".*")
548 .replace("?:", "")
549 .replace(".*.*", ".*")
550 .to_owned();
551 tracing::debug!(
552 "url redirects glob pattern: {}",
553 &redirects_entry.source
554 );
555 tracing::debug!("url redirects regex equivalent: {}", pattern);
556
557 let source = Regex::new(&pattern).with_context(|| {
558 format!(
559 "can not compile regex pattern equivalent for redirect source: {}",
560 &pattern
561 )
562 })?;
563
564 let status_code = redirects_entry.kind.to_owned() as u16;
565 redirects_vec.push(Redirects {
566 host: redirects_entry.host.to_owned(),
567 source,
568 destination: redirects_entry.destination.to_owned(),
569 kind: StatusCode::from_u16(status_code).with_context(|| {
570 format!("invalid redirect status code: {status_code}")
571 })?,
572 });
573 }
574 Some(redirects_vec)
575 }
576 _ => None,
577 };
578
579 let vhosts_entries = match advanced.virtual_hosts {
581 Some(vhosts_entries) => {
582 let mut vhosts_vec: Vec<VirtualHosts> = Vec::new();
583
584 for vhosts_entry in vhosts_entries.iter() {
585 if let Some(root) = vhosts_entry.root.to_owned() {
586 let root_dir = helpers::get_valid_dirpath(&root)
588 .with_context(|| "root directory for virtual host was not found or inaccessible")?;
589 tracing::debug!(
590 "added virtual host: {} -> {}",
591 vhosts_entry.host,
592 root_dir.display()
593 );
594 vhosts_vec.push(VirtualHosts {
595 host: vhosts_entry.host.to_owned(),
596 root: root_dir,
597 });
598 }
599 }
600 Some(vhosts_vec)
601 }
602 _ => None,
603 };
604
605 settings_advanced = Some(Advanced {
606 headers: headers_entries,
607 rewrites: rewrites_entries,
608 redirects: redirects_entries,
609 virtual_hosts: vhosts_entries,
610 #[cfg(feature = "experimental")]
611 memory_cache: advanced.memory_cache,
612 });
613 }
614 } else if log_init {
615 logger::init(log_level.as_str(), log_with_ansi)?;
617 }
618
619 Ok(Settings {
620 general: General {
621 version,
622 host,
623 port,
624 root,
625 log_level,
626 log_with_ansi,
627 config_file,
628 cache_control_headers,
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,
637 #[cfg(any(
638 feature = "compression",
639 feature = "compression-gzip",
640 feature = "compression-brotli",
641 feature = "compression-zstd",
642 feature = "compression-deflate"
643 ))]
644 compression_level,
645 #[cfg(any(
646 feature = "compression",
647 feature = "compression-gzip",
648 feature = "compression-brotli",
649 feature = "compression-zstd",
650 feature = "compression-deflate"
651 ))]
652 compression_static,
653 page404,
654 page50x,
655 #[cfg(feature = "http2")]
656 http2,
657 #[cfg(feature = "http2")]
658 http2_tls_cert,
659 #[cfg(feature = "http2")]
660 http2_tls_key,
661 #[cfg(feature = "http2")]
662 https_redirect,
663 #[cfg(feature = "http2")]
664 https_redirect_host,
665 #[cfg(feature = "http2")]
666 https_redirect_from_port,
667 #[cfg(feature = "http2")]
668 https_redirect_from_hosts,
669 security_headers,
670 cors_allow_origins,
671 cors_allow_headers,
672 cors_expose_headers,
673 #[cfg(feature = "directory-listing")]
674 directory_listing,
675 #[cfg(feature = "directory-listing")]
676 directory_listing_order,
677 #[cfg(feature = "directory-listing")]
678 directory_listing_format,
679 #[cfg(feature = "directory-listing-download")]
680 directory_listing_download,
681 #[cfg(feature = "basic-auth")]
682 basic_auth,
683 fd,
684 threads_multiplier,
685 max_blocking_threads,
686 grace_period,
687 #[cfg(feature = "fallback-page")]
688 page_fallback,
689 log_remote_address,
690 log_x_real_ip,
691 log_forwarded_for,
692 trusted_proxies,
693 redirect_trailing_slash,
694 ignore_hidden_files,
695 disable_symlinks,
696 index_files,
697 health,
698 #[cfg(all(unix, feature = "experimental"))]
699 experimental_metrics,
700 maintenance_mode,
701 maintenance_mode_status,
702 maintenance_mode_file,
703
704 #[cfg(windows)]
706 windows_service,
707 commands: opts.commands,
708 },
709 advanced: settings_advanced,
710 })
711 }
712}
713
714fn read_file_settings(config_file: &Path) -> Result<Option<(FileSettings, PathBuf)>> {
715 if config_file.is_file() {
716 let file_path_resolved = config_file
717 .canonicalize()
718 .with_context(|| "unable to resolve toml config file path")?;
719
720 let settings = FileSettings::read(&file_path_resolved).with_context(|| {
721 "unable to read toml config file because has invalid format or unsupported options"
722 })?;
723
724 return Ok(Some((settings, file_path_resolved)));
725 }
726 Ok(None)
727}