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