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