Skip to main content

static_web_server/settings/
cli.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// This file is part of Static Web Server.
3// See https://static-web-server.net/ for more information
4// Copyright (C) 2019-present Jose Quintana <joseluisq.net>
5
6//! The server CLI options.
7
8use 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/// General server configuration available in CLI and config file options.
22#[derive(Parser, Debug)]
23#[command(author, about, long_about)]
24pub struct General {
25    #[arg(long, short = 'a', default_value = "::", env = "SERVER_HOST")]
26    /// Host address (E.g 127.0.0.1 or ::1)
27    pub host: String,
28
29    #[arg(long, short = 'p', default_value = "8787", env = "SERVER_PORT")]
30    /// Host port
31    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    /// Instead of binding to a TCP port, accept incoming connections to an already-bound TCP
52    /// socket listener on the specified file descriptor number (usually zero). Requires that the
53    /// parent process (e.g. inetd, launchd, or systemd) binds an address and port on behalf of
54    /// static-web-server, before arranging for the resulting file descriptor to be inherited by
55    /// static-web-server. Cannot be used in conjunction with the port and host arguments. The
56    /// included systemd unit file utilises this feature to increase security by allowing the
57    /// static-web-server to be sandboxed more completely.
58    pub fd: Option<usize>,
59
60    // Unix Domain Socket (UDS) options
61    // Mutually exclusive with TCP-based options (host/port/fd) and TLS.
62    // Gated to Unix targets; on Windows these flags are not exposed.
63    #[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    /// Bind the server to a Unix Domain Socket (UDS) at the given filesystem path
81    /// instead of a TCP host/port. Useful for reverse-proxy setups (e.g. nginx) on the
82    /// same host where TCP/IP overhead is undesirable and filesystem-based access
83    /// control is preferred. Cannot be combined with `--host`, `--port`, `--fd`, or
84    /// TLS-related options. The socket file is removed on a graceful shutdown.
85    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    /// Filesystem permission bits applied to the Unix socket file after binding,
95    /// expressed in octal (e.g. `660`, `0660`, or `0o660`). When omitted the socket
96    /// is created with the process umask. Only meaningful together with `--unix-socket`.
97    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    /// When `true`, remove an existing socket file at `--unix-socket` before binding.
111    /// This is useful when the server was previously killed abruptly and left a stale
112    /// socket behind. Defaults to `false` to avoid clobbering an unrelated file.
113    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    )] // We use 2 as the threads multiplier in Wasm, 1 in Native
133    /// Number of worker threads multiplier that'll be multiplied by the number of system CPUs
134    /// using the formula: `worker threads = number of CPUs * n` where `n` is the value that changes here.
135    /// When multiplier value is 0 or 1 then one thread per core is used.
136    /// Number of worker threads result should be a number between 1 and 32,768 though it is advised to keep this value on the smaller side.
137    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    )] // We use 20 in Wasm, 512 in Native (default for tokio)
157    /// Maximum number of blocking threads
158    pub max_blocking_threads: usize,
159
160    #[arg(long, short = 'd', default_value = "./public", env = "SERVER_ROOT")]
161    /// Root directory path of static files.
162    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    /// Resolve the web root directory at request time rather than at startup,
174    /// allowing symlinked root directories to be swapped at runtime.
175    pub use_relative_root: bool,
176
177    #[arg(long, default_value = "./50x.html", env = "SERVER_ERROR_PAGE_50X")]
178    /// HTML file path for 50x errors. If the path is not specified or simply doesn't exist
179    /// then the server will use a generic HTML error message.
180    /// If a relative path is used then it will be resolved under the root directory.
181    pub page50x: PathBuf,
182
183    #[arg(long, default_value = "./404.html", env = "SERVER_ERROR_PAGE_404")]
184    /// HTML file path for 404 errors. If the path is not specified or simply doesn't exist
185    /// then the server will use a generic HTML error message.
186    /// If a relative path is used then it will be resolved under the root directory.
187    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    /// A HTML file path (not relative to the root) used for GET requests when the requested path doesn't exist. The fallback page is served with a 200 status code, useful when using client routers. If the path doesn't exist then the feature is not activated.
193    pub page_fallback: PathBuf,
194
195    #[arg(long, short = 'g', default_value = "error", env = "SERVER_LOG_LEVEL")]
196    /// Specify a logging level in lower case. Values: error, warn, info, debug or trace
197    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    /// Specify the logging output format. Values: json (structured single-line JSON for production) or pretty (human-readable text for development)
207    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    /// Enable or disable ANSI escape codes for colors and other text formatting of the log output. Only effective when `--log-format pretty` is used.
218    pub log_with_ansi: bool,
219
220    #[arg(
221        long,
222        env = "SERVER_LOG_FILE",
223        value_parser = value_parser_pathbuf,
224    )]
225    /// Optional filesystem path to stream log records to in addition to stderr.
226    /// When set, logs are written asynchronously through a background worker
227    /// thread (non-blocking I/O), so the request path is never delayed by disk
228    /// writes. Missing parent directories are created on startup. ANSI escape
229    /// codes are always disabled for file output regardless of
230    /// `--log-with-ansi`. The file uses the format selected by
231    /// `--log-format` (JSON by default). The file is opened in append mode and
232    /// is not rotated by SWS, use an external tool (e.g. `logrotate`) for
233    /// rotation.
234    pub log_file: Option<PathBuf>,
235
236    #[arg(
237        long,
238        short = 'c',
239        default_value = "",
240        env = "SERVER_CORS_ALLOW_ORIGINS"
241    )]
242    /// Specify an optional CORS list of allowed origin hosts separated by commas. Host ports or protocols aren't being checked. Use an asterisk (*) to allow any host.
243    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    /// Specify an optional CORS list of allowed headers separated by commas. Default "origin, content-type". It requires `--cors-allow-origins` to be used along with.
252    pub cors_allow_headers: String,
253
254    #[arg(
255        long,
256        default_value = "origin, content-type",
257        env = "SERVER_CORS_EXPOSE_HEADERS"
258    )]
259    /// Specify an optional CORS list of exposed headers separated by commas. Default "origin, content-type". It requires `--cors-expose-origins` to be used along with.
260    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    /// Enable TLS/HTTPS support. Requires --tls-cert and --tls-key.
275    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    /// Specify the file path to the TLS certificate.
281    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    /// Specify the file path to the TLS private key.
287    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    /// Enable HTTP/2 protocol support. Requires TLS to be enabled (--tls).
301    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    /// Redirect all requests with scheme "http" to "https" for the current server instance. Requires TLS to be enabled (--tls).
315    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    /// Canonical host name or IP of the HTTPS server. It depends on "https_redirect" to be enabled.
321    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    /// HTTP host port where the redirect server will listen for requests to redirect them to HTTPS. It depends on "https_redirect" to be enabled.
327    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    /// List of host names or IPs allowed to redirect from. HTTP requests must contain the HTTP 'Host' header and match against this list. It depends on "https_redirect" to be enabled.
337    pub https_redirect_from_hosts: String,
338
339    #[arg(long, default_value = "index.html", env = "SERVER_INDEX_FILES")]
340    /// List of files that will be used as an index for requests ending with the slash character (‘/’).
341    /// Files are checked in the specified order.
342    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    /// Gzip, Deflate, Brotli or Zstd compression on demand determined by the Accept-Encoding header and applied to text-based web file types only.
372    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    /// Compression level to apply for Gzip, Deflate, Brotli or Zstd compression.
393    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    /// Look up the pre-compressed file variant (`.gz`, `.br` or `.zst`) on disk of a requested file and serves it directly if available.
405    /// The compression type is determined by the `Accept-Encoding` header.
406    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    /// Enable directory listing for all requests ending with the slash character (‘/’).
421    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    /// Specify a default code number to order directory listing entries per `Name`, `Last modified` or `Size` attributes (columns). Code numbers supported: 0 (Name asc), 1 (Name desc), 2 (Last modified asc), 3 (Last modified desc), 4 (Size asc), 5 (Size desc). Default 6 (unordered)
432    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    /// Specify a content format for directory listing entries. Formats supported: "html" or "json". Default "html".
445    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    /// Specify list of enabled format(s) for directory download. Format supported: `targz`. Default to empty list (disabled).
462    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    /// Enable security headers by default when TLS feature is activated.
490    /// Headers included: "Strict-Transport-Security: max-age=63072000; includeSubDomains; preload" (2 years max-age),
491    /// "X-Frame-Options: DENY" and "Content-Security-Policy: frame-ancestors 'self'".
492    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    /// Enable cache control headers for incoming requests based on a set of file types. The file type list can be found on `src/control_headers.rs` file.
511    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    /// Enable weak `ETag` headers (`W/"<mtime>-<size>"`) and full conditional request handling (`If-None-Match`, `If-Match`, `If-Range`). Composes with `--cache-control-headers`; emits validators on every static-file response so clients can revalidate hot HTML even when long `max-age` is configured elsewhere.
523    pub etag: bool,
524
525    #[cfg(feature = "basic-auth")]
526    /// It provides The "Basic" HTTP Authentication scheme using credentials as "user-id:password" pairs. Password must be encoded using the "BCrypt" password-hashing function.
527    #[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    /// Defines a grace period in seconds after a `SIGTERM` signal is caught which will delay the server before to shut it down gracefully. The maximum value is 255 seconds.
532    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    /// Server TOML configuration file path.
542    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    /// Log incoming requests information along with its remote address if available using the `info` log level.
554    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    /// Log the X-Real-IP header for remote IP information.
566    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    /// Log the X-Forwarded-For header for remote IP information
578    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    /// List of IPs to use X-Forwarded-For from. The default is to trust all
588    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    /// Check for a trailing slash in the requested directory URI and redirect permanently (308) to the same path with a trailing slash suffix if it is missing.
600    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    /// Include hidden files/directories (dotfiles), allowing them to be served and listed in auto HTML index pages (directory listing). Disabled by default; hidden files return `404 Not Found`.
612    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    /// Follow symbolic links when serving files or directories. Disabled by default; requests whose path contains any symlink component return `403 Forbidden`.
624    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    /// Enable markdown content negotiation. When a client sends Accept: text/markdown, serve .md or .html.md files if available.
636    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    /// Set a default `charset=utf-8` parameter on limited set of `text` responses that don't already have one.
648    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    /// Add a /health endpoint that doesn't generate any log entry and returns a 200 status code.
660    /// This is especially useful with Kubernetes liveness and readiness probes.
661    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    /// Enable the /metrics endpoint that exposes Prometheus metrics for HTTP requests, connections, and latency.
674    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    /// Enable the server's maintenance mode functionality.
686    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    /// Provide a custom HTTP status code when entering into maintenance mode. Default 503.
696    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    /// Provide a custom maintenance mode HTML file. If not provided then a generic message will be displayed.
706    pub maintenance_mode_file: PathBuf,
707
708    //
709    // Windows specific arguments and commands
710    //
711    #[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    /// Tell the web server to run in a Windows Service context. Note that the `install` subcommand will enable this option automatically.
723    pub windows_service: bool,
724
725    // Subcommands
726    #[command(subcommand)]
727    /// Subcommands for additional maintenance tasks, like installing and uninstalling the SWS Windows Service and generation of completions and man pages
728    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    /// Print version info and exit.
738    pub version: bool,
739}
740
741#[derive(Debug, clap::Subcommand)]
742/// Subcommands for additional maintenance tasks, like installing and uninstalling the SWS Windows Service and generation of completions and man pages
743pub enum Commands {
744    /// Install a Windows Service for the web server.
745    #[cfg(windows)]
746    #[command(name = "install")]
747    Install {},
748
749    /// Uninstall the current Windows Service.
750    #[cfg(windows)]
751    #[command(name = "uninstall")]
752    Uninstall {},
753
754    /// Generate man pages and shell completions
755    #[command(name = "generate")]
756    Generate {
757        /// Generate shell completions
758        #[arg(long)]
759        completions: bool,
760        /// Generate man pages
761        #[arg(long)]
762        man_pages: bool,
763        /// Path to write generated artifacts to
764        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/// Parse a Unix file mode given in octal (e.g. `660`, `0660`, `0o660`).
780///
781/// The parser intentionally rejects decimal/hex values: file permission bits
782/// are universally expressed in octal, and accepting other bases would silently
783/// produce surprising masks (e.g. `660` parsed as decimal is `0o1224`).
784#[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    // Reject values that would set bits outside the standard 12-bit Unix mode
797    // space (setuid/setgid/sticky + rwx for u/g/o).
798    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        // `660` is the canonical "rw-rw----" mask used for socket files in
813        // most reverse-proxy setups; ensure the most common input is accepted.
814        assert_eq!(parse_octal_mode("660").unwrap(), 0o660);
815    }
816
817    #[test]
818    fn parses_leading_zero_and_0o_prefix() {
819        // Both Unix-style (`0660`) and Rust-style (`0o660`) are accepted; they
820        // must produce identical numeric masks.
821        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        // `8` and `9` are not octal digits; accepting them silently would
829        // produce wrong masks.
830        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        // Anything beyond the 12-bit permission space is almost certainly a
842        // typo (e.g. typed in decimal).
843        assert!(parse_octal_mode("10000").is_err());
844    }
845}