1use clap::Parser;
9use hyper::StatusCode;
10use std::{net::IpAddr, path::PathBuf};
11
12#[cfg(feature = "directory-listing")]
13use crate::directory_listing::DirListFmt;
14
15#[cfg(feature = "directory-listing-download")]
16use crate::directory_listing::download::DirDownloadFmt;
17use crate::logger::LogFormat;
18
19use crate::Result;
20
21#[derive(Parser, Debug)]
23#[command(author, about, long_about)]
24pub struct General {
25 #[arg(long, short = 'a', default_value = "::", env = "SERVER_HOST")]
26 pub host: String,
28
29 #[arg(long, short = 'p', default_value = "8787", env = "SERVER_PORT")]
30 pub port: u16,
32
33 #[cfg_attr(
34 feature = "tls",
35 arg(
36 long,
37 short = 'f',
38 env = "SERVER_LISTEN_FD",
39 conflicts_with_all(&["host", "port", "https_redirect"])
40 )
41 )]
42 #[cfg_attr(
43 not(feature = "tls"),
44 arg(
45 long,
46 short = 'f',
47 env = "SERVER_LISTEN_FD",
48 conflicts_with_all(&["host", "port"])
49 )
50 )]
51 pub fd: Option<usize>,
59
60 #[cfg(unix)]
64 #[cfg_attr(
65 feature = "tls",
66 arg(
67 long,
68 env = "SERVER_UNIX_SOCKET",
69 conflicts_with_all(&["host", "port", "fd", "tls", "https_redirect"]),
70 )
71 )]
72 #[cfg_attr(
73 not(feature = "tls"),
74 arg(
75 long,
76 env = "SERVER_UNIX_SOCKET",
77 conflicts_with_all(&["host", "port", "fd"]),
78 )
79 )]
80 pub unix_socket: Option<PathBuf>,
86
87 #[cfg(unix)]
88 #[arg(
89 long,
90 env = "SERVER_UNIX_SOCKET_MODE",
91 value_parser = parse_octal_mode,
92 requires = "unix_socket",
93 )]
94 pub unix_socket_mode: Option<u32>,
98
99 #[cfg(unix)]
100 #[arg(
101 long,
102 default_value = "false",
103 default_missing_value("true"),
104 num_args(0..=1),
105 require_equals(false),
106 action = clap::ArgAction::Set,
107 env = "SERVER_UNIX_SOCKET_FORCE",
108 requires = "unix_socket",
109 )]
110 pub unix_socket_force: bool,
114
115 #[cfg_attr(
116 not(target_family = "wasm"),
117 arg(
118 long,
119 short = 'n',
120 default_value = "1",
121 env = "SERVER_THREADS_MULTIPLIER"
122 )
123 )]
124 #[cfg_attr(
125 target_family = "wasm",
126 arg(
127 long,
128 short = 'n',
129 default_value = "2",
130 env = "SERVER_THREADS_MULTIPLIER"
131 )
132 )] pub threads_multiplier: usize,
138
139 #[cfg_attr(
140 not(target_family = "wasm"),
141 arg(
142 long,
143 short = 'b',
144 default_value = "512",
145 env = "SERVER_MAX_BLOCKING_THREADS"
146 )
147 )]
148 #[cfg_attr(
149 target_family = "wasm",
150 arg(
151 long,
152 short = 'b',
153 default_value = "20",
154 env = "SERVER_MAX_BLOCKING_THREADS"
155 )
156 )] pub max_blocking_threads: usize,
159
160 #[arg(long, short = 'd', default_value = "./public", env = "SERVER_ROOT")]
161 pub root: PathBuf,
163
164 #[arg(
165 long,
166 default_value = "false",
167 default_missing_value("true"),
168 num_args(0..=1),
169 require_equals(false),
170 action = clap::ArgAction::Set,
171 env = "SERVER_USE_RELATIVE_ROOT",
172 )]
173 pub use_relative_root: bool,
176
177 #[arg(long, default_value = "./50x.html", env = "SERVER_ERROR_PAGE_50X")]
178 pub page50x: PathBuf,
182
183 #[arg(long, default_value = "./404.html", env = "SERVER_ERROR_PAGE_404")]
184 pub page404: PathBuf,
188
189 #[cfg(feature = "fallback-page")]
190 #[cfg_attr(docsrs, doc(cfg(feature = "fallback-page")))]
191 #[arg(long, default_value = "", value_parser = value_parser_pathbuf, env = "SERVER_FALLBACK_PAGE")]
192 pub page_fallback: PathBuf,
194
195 #[arg(long, short = 'g', default_value = "error", env = "SERVER_LOG_LEVEL")]
196 pub log_level: String,
198
199 #[arg(
200 long,
201 value_enum,
202 default_value = "json",
203 env = "SERVER_LOG_FORMAT",
204 ignore_case(true)
205 )]
206 pub log_format: LogFormat,
208
209 #[arg(
210 long,
211 default_value = "false",
212 default_missing_value("true"),
213 num_args(0..=1),
214 action = clap::ArgAction::Set,
215 env = "SERVER_LOG_WITH_ANSI",
216 )]
217 pub log_with_ansi: bool,
219
220 #[arg(
221 long,
222 env = "SERVER_LOG_FILE",
223 value_parser = value_parser_pathbuf,
224 )]
225 pub log_file: Option<PathBuf>,
235
236 #[arg(
237 long,
238 short = 'c',
239 default_value = "",
240 env = "SERVER_CORS_ALLOW_ORIGINS"
241 )]
242 pub cors_allow_origins: String,
244
245 #[arg(
246 long,
247 short = 'j',
248 default_value = "origin, content-type, authorization",
249 env = "SERVER_CORS_ALLOW_HEADERS"
250 )]
251 pub cors_allow_headers: String,
253
254 #[arg(
255 long,
256 default_value = "origin, content-type",
257 env = "SERVER_CORS_EXPOSE_HEADERS"
258 )]
259 pub cors_expose_headers: String,
261
262 #[arg(
263 long,
264 short = 't',
265 default_value = "false",
266 default_missing_value("true"),
267 num_args(0..=1),
268 require_equals(false),
269 action = clap::ArgAction::Set,
270 env = "SERVER_TLS",
271 )]
272 #[cfg(feature = "tls")]
273 #[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
274 pub tls: bool,
276
277 #[arg(long, required_if_eq("tls", "true"), env = "SERVER_TLS_CERT")]
278 #[cfg(feature = "tls")]
279 #[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
280 pub tls_cert: Option<PathBuf>,
282
283 #[arg(long, required_if_eq("tls", "true"), env = "SERVER_TLS_KEY")]
284 #[cfg(feature = "tls")]
285 #[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
286 pub tls_key: Option<PathBuf>,
288
289 #[arg(
290 long,
291 default_value = "false",
292 default_missing_value("true"),
293 num_args(0..=1),
294 require_equals(false),
295 action = clap::ArgAction::Set,
296 env = "SERVER_HTTP2",
297 )]
298 #[cfg(feature = "http2")]
299 #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
300 pub http2: bool,
302
303 #[arg(
304 long,
305 default_value = "false",
306 default_missing_value("true"),
307 num_args(0..=1),
308 require_equals(false),
309 action = clap::ArgAction::Set,
310 env = "SERVER_HTTPS_REDIRECT"
311 )]
312 #[cfg(feature = "tls")]
313 #[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
314 pub https_redirect: bool,
316
317 #[arg(long, default_value = "localhost", env = "SERVER_HTTPS_REDIRECT_HOST")]
318 #[cfg(feature = "tls")]
319 #[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
320 pub https_redirect_host: String,
322
323 #[arg(long, default_value = "8787", env = "SERVER_HTTPS_REDIRECT_FROM_PORT")]
324 #[cfg(feature = "tls")]
325 #[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
326 pub https_redirect_from_port: u16,
328
329 #[arg(
330 long,
331 default_value = "localhost",
332 env = "SERVER_HTTPS_REDIRECT_FROM_HOSTS"
333 )]
334 #[cfg(feature = "tls")]
335 #[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
336 pub https_redirect_from_hosts: String,
338
339 #[arg(long, default_value = "index.html", env = "SERVER_INDEX_FILES")]
340 pub index_files: String,
343
344 #[cfg(any(
345 feature = "compression",
346 feature = "compression-gzip",
347 feature = "compression-brotli",
348 feature = "compression-zstd",
349 feature = "compression-deflate"
350 ))]
351 #[cfg_attr(
352 docsrs,
353 doc(cfg(any(
354 feature = "compression",
355 feature = "compression-gzip",
356 feature = "compression-brotli",
357 feature = "compression-zstd",
358 feature = "compression-deflate"
359 )))
360 )]
361 #[arg(
362 long,
363 short = 'x',
364 default_value = "true",
365 default_missing_value("true"),
366 num_args(0..=1),
367 require_equals(false),
368 action = clap::ArgAction::Set,
369 env = "SERVER_COMPRESSION",
370 )]
371 pub compression: bool,
373
374 #[cfg(any(
375 feature = "compression",
376 feature = "compression-gzip",
377 feature = "compression-brotli",
378 feature = "compression-zstd",
379 feature = "compression-deflate"
380 ))]
381 #[cfg_attr(
382 docsrs,
383 doc(cfg(any(
384 feature = "compression",
385 feature = "compression-gzip",
386 feature = "compression-brotli",
387 feature = "compression-zstd",
388 feature = "compression-deflate"
389 )))
390 )]
391 #[arg(long, default_value = "default", env = "SERVER_COMPRESSION_LEVEL")]
392 pub compression_level: super::CompressionLevel,
394
395 #[arg(
396 long,
397 default_value = "true",
398 default_missing_value("true"),
399 num_args(0..=1),
400 require_equals(false),
401 action = clap::ArgAction::Set,
402 env = "SERVER_COMPRESSION_STATIC",
403 )]
404 pub compression_static: bool,
407
408 #[cfg(feature = "directory-listing")]
409 #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
410 #[arg(
411 long,
412 short = 'z',
413 default_value = "false",
414 default_missing_value("true"),
415 num_args(0..=1),
416 require_equals(false),
417 action = clap::ArgAction::Set,
418 env = "SERVER_DIRECTORY_LISTING",
419 )]
420 pub directory_listing: bool,
422
423 #[cfg(feature = "directory-listing")]
424 #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
425 #[arg(
426 long,
427 requires_if("true", "directory_listing"),
428 default_value = "6",
429 env = "SERVER_DIRECTORY_LISTING_ORDER"
430 )]
431 pub directory_listing_order: u8,
433
434 #[cfg(feature = "directory-listing")]
435 #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
436 #[arg(
437 long,
438 value_enum,
439 requires_if("true", "directory_listing"),
440 default_value = "html",
441 env = "SERVER_DIRECTORY_LISTING_FORMAT",
442 ignore_case(true)
443 )]
444 pub directory_listing_format: DirListFmt,
446
447 #[cfg(feature = "directory-listing-download")]
448 #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing-download")))]
449 #[arg(
450 long,
451 value_delimiter(','),
452 value_enum,
453 requires_ifs([
454 ("targz", "directory_listing"),
455 ]),
456 require_equals(true),
457 action = clap::ArgAction::Set,
458 env = "SERVER_DIRECTORY_LISTING_DOWNLOAD",
459 ignore_case(true)
460 )]
461 pub directory_listing_download: Vec<DirDownloadFmt>,
463
464 #[cfg_attr(
465 feature = "tls",
466 arg(
467 long,
468 default_value = "true",
469 default_missing_value("true"),
470 num_args(0..=1),
471 require_equals(false),
472 action = clap::ArgAction::Set,
473 default_value_if("tls", "true", Some("true")),
474 env = "SERVER_SECURITY_HEADERS",
475 )
476 )]
477 #[cfg_attr(
478 not(feature = "tls"),
479 arg(
480 long,
481 default_value = "false",
482 default_missing_value("true"),
483 num_args(0..=1),
484 require_equals(false),
485 action = clap::ArgAction::Set,
486 env = "SERVER_SECURITY_HEADERS",
487 )
488 )]
489 pub security_headers: bool,
493
494 #[arg(
495 long,
496 short = 'e',
497 default_value = "true",
498 env = "SERVER_CACHE_CONTROL_HEADERS"
499 )]
500 #[arg(
501 long,
502 short = 'e',
503 default_value = "true",
504 default_missing_value("true"),
505 num_args(0..=1),
506 require_equals(false),
507 action = clap::ArgAction::Set,
508 env = "SERVER_CACHE_CONTROL_HEADERS",
509 )]
510 pub cache_control_headers: bool,
512
513 #[arg(
514 long,
515 default_value = "true",
516 default_missing_value("true"),
517 num_args(0..=1),
518 require_equals(false),
519 action = clap::ArgAction::Set,
520 env = "SERVER_ETAG",
521 )]
522 pub etag: bool,
524
525 #[cfg(feature = "basic-auth")]
526 #[arg(long, default_value = "", env = "SERVER_BASIC_AUTH")]
528 pub basic_auth: String,
529
530 #[arg(long, short = 'q', default_value = "0", env = "SERVER_GRACE_PERIOD")]
531 pub grace_period: u8,
533
534 #[arg(
535 long,
536 short = 'w',
537 default_value = "./sws.toml",
538 value_parser = value_parser_pathbuf,
539 env = "SERVER_CONFIG_FILE"
540 )]
541 pub config_file: PathBuf,
543
544 #[arg(
545 long,
546 default_value = "false",
547 default_missing_value("true"),
548 num_args(0..=1),
549 require_equals(false),
550 action = clap::ArgAction::Set,
551 env = "SERVER_LOG_REMOTE_ADDRESS",
552 )]
553 pub log_remote_address: bool,
555
556 #[arg(
557 long,
558 default_value = "false",
559 default_missing_value("true"),
560 num_args(0..=1),
561 require_equals(false),
562 action = clap::ArgAction::Set,
563 env = "SERVER_LOG_X_REAL_IP",
564 )]
565 pub log_x_real_ip: bool,
567
568 #[arg(
569 long,
570 default_value = "false",
571 default_missing_value("true"),
572 num_args(0..=1),
573 require_equals(false),
574 action = clap::ArgAction::Set,
575 env = "SERVER_LOG_FORWARDED_FOR",
576 )]
577 pub log_forwarded_for: bool,
579
580 #[arg(
581 long,
582 require_equals(false),
583 value_delimiter(','),
584 action = clap::ArgAction::Set,
585 env = "SERVER_TRUSTED_PROXIES",
586 )]
587 pub trusted_proxies: Vec<IpAddr>,
589
590 #[arg(
591 long,
592 default_value = "true",
593 default_missing_value("true"),
594 num_args(0..=1),
595 require_equals(false),
596 action = clap::ArgAction::Set,
597 env = "SERVER_REDIRECT_TRAILING_SLASH",
598 )]
599 pub redirect_trailing_slash: bool,
601
602 #[arg(
603 long,
604 default_value = "false",
605 default_missing_value("true"),
606 num_args(0..=1),
607 require_equals(false),
608 action = clap::ArgAction::Set,
609 env = "SERVER_INCLUDE_HIDDEN",
610 )]
611 pub include_hidden: bool,
613
614 #[arg(
615 long,
616 default_value = "false",
617 default_missing_value("true"),
618 num_args(0..=1),
619 require_equals(false),
620 action = clap::ArgAction::Set,
621 env = "SERVER_FOLLOW_SYMLINKS",
622 )]
623 pub follow_symlinks: bool,
625
626 #[arg(
627 long,
628 default_value = "false",
629 default_missing_value("true"),
630 num_args(0..=1),
631 require_equals(false),
632 action = clap::ArgAction::Set,
633 env = "SERVER_ACCEPT_MARKDOWN",
634 )]
635 pub accept_markdown: bool,
637
638 #[arg(
639 long,
640 default_value = "true",
641 default_missing_value("true"),
642 num_args(0..=1),
643 require_equals(false),
644 action = clap::ArgAction::Set,
645 env = "SERVER_TEXT_CHARSET",
646 )]
647 pub text_charset: bool,
649
650 #[arg(
651 long,
652 default_value = "false",
653 default_missing_value("true"),
654 num_args(0..=1),
655 require_equals(false),
656 action = clap::ArgAction::Set,
657 env = "SERVER_HEALTH",
658 )]
659 pub health: bool,
662
663 #[cfg(feature = "metrics")]
664 #[arg(
665 long,
666 default_value = "false",
667 default_missing_value("true"),
668 num_args(0..=1),
669 require_equals(false),
670 action = clap::ArgAction::Set,
671 env = "SERVER_METRICS",
672 )]
673 pub metrics: bool,
675
676 #[arg(
677 long,
678 default_value = "false",
679 default_missing_value("true"),
680 num_args(0..=1),
681 require_equals(false),
682 action = clap::ArgAction::Set,
683 env = "SERVER_MAINTENANCE_MODE"
684 )]
685 pub maintenance_mode: bool,
687
688 #[arg(
689 long,
690 default_value = "503",
691 value_parser = value_parser_status_code,
692 requires_if("true", "maintenance_mode"),
693 env = "SERVER_MAINTENANCE_MODE_STATUS"
694 )]
695 pub maintenance_mode_status: StatusCode,
697
698 #[arg(
699 long,
700 default_value = "",
701 value_parser = value_parser_pathbuf,
702 requires_if("true", "maintenance_mode"),
703 env = "SERVER_MAINTENANCE_MODE_FILE"
704 )]
705 pub maintenance_mode_file: PathBuf,
707
708 #[cfg(windows)]
712 #[arg(
713 long,
714 short = 's',
715 default_value = "false",
716 default_missing_value("true"),
717 num_args(0..=1),
718 require_equals(false),
719 action = clap::ArgAction::Set,
720 env = "SERVER_WINDOWS_SERVICE",
721 )]
722 pub windows_service: bool,
724
725 #[command(subcommand)]
727 pub commands: Option<Commands>,
729
730 #[arg(
731 long,
732 short = 'V',
733 default_value = "false",
734 default_missing_value("true")
735 )]
736 #[doc(hidden)]
737 pub version: bool,
739}
740
741#[derive(Debug, clap::Subcommand)]
742pub enum Commands {
744 #[cfg(windows)]
746 #[command(name = "install")]
747 Install {},
748
749 #[cfg(windows)]
751 #[command(name = "uninstall")]
752 Uninstall {},
753
754 #[command(name = "generate")]
756 Generate {
757 #[arg(long)]
759 completions: bool,
760 #[arg(long)]
762 man_pages: bool,
763 out_dir: PathBuf,
765 },
766}
767
768fn value_parser_pathbuf(s: &str) -> Result<PathBuf, String> {
769 Ok(PathBuf::from(s))
770}
771
772fn value_parser_status_code(s: &str) -> Result<StatusCode, String> {
773 match s.parse::<u16>() {
774 Ok(code) => StatusCode::from_u16(code).map_err(|err| err.to_string()),
775 Err(err) => Err(err.to_string()),
776 }
777}
778
779#[cfg(unix)]
785fn parse_octal_mode(s: &str) -> Result<u32, String> {
786 let trimmed = s.trim();
787 if trimmed.is_empty() {
788 return Err("unix socket mode cannot be empty".to_owned());
789 }
790 let digits = trimmed
791 .strip_prefix("0o")
792 .or_else(|| trimmed.strip_prefix("0O"))
793 .unwrap_or(trimmed);
794 let mode = u32::from_str_radix(digits, 8)
795 .map_err(|e| format!("invalid octal unix socket mode '{s}': {e}"))?;
796 if mode > 0o7777 {
799 return Err(format!(
800 "unix socket mode '{s}' exceeds maximum 07777 (12-bit permission mask)"
801 ));
802 }
803 Ok(mode)
804}
805
806#[cfg(all(test, unix))]
807mod tests {
808 use super::parse_octal_mode;
809
810 #[test]
811 fn parses_bare_octal_digits() {
812 assert_eq!(parse_octal_mode("660").unwrap(), 0o660);
815 }
816
817 #[test]
818 fn parses_leading_zero_and_0o_prefix() {
819 assert_eq!(parse_octal_mode("0660").unwrap(), 0o660);
822 assert_eq!(parse_octal_mode("0o660").unwrap(), 0o660);
823 assert_eq!(parse_octal_mode("0O660").unwrap(), 0o660);
824 }
825
826 #[test]
827 fn rejects_non_octal_digits() {
828 assert!(parse_octal_mode("789").is_err());
831 }
832
833 #[test]
834 fn rejects_empty_and_whitespace() {
835 assert!(parse_octal_mode("").is_err());
836 assert!(parse_octal_mode(" ").is_err());
837 }
838
839 #[test]
840 fn rejects_values_above_07777() {
841 assert!(parse_octal_mode("10000").is_err());
844 }
845}