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;
17
18use crate::Result;
19
20/// General server configuration available in CLI and config file options.
21#[derive(Parser, Debug)]
22#[command(author, about, long_about)]
23pub struct General {
24    #[arg(long, short = 'a', default_value = "::", env = "SERVER_HOST")]
25    /// Host address (E.g 127.0.0.1 or ::1)
26    pub host: String,
27
28    #[arg(long, short = 'p', default_value = "80", env = "SERVER_PORT")]
29    /// Host port
30    pub port: u16,
31
32    #[cfg_attr(
33        feature = "http2",
34        arg(
35            long,
36            short = 'f',
37            env = "SERVER_LISTEN_FD",
38            conflicts_with_all(&["host", "port", "https_redirect"])
39        )
40    )]
41    #[cfg_attr(
42        not(feature = "http2"),
43        arg(
44            long,
45            short = 'f',
46            env = "SERVER_LISTEN_FD",
47            conflicts_with_all(&["host", "port"])
48        )
49    )]
50    /// Instead of binding to a TCP port, accept incoming connections to an already-bound TCP
51    /// socket listener on the specified file descriptor number (usually zero). Requires that the
52    /// parent process (e.g. inetd, launchd, or systemd) binds an address and port on behalf of
53    /// static-web-server, before arranging for the resulting file descriptor to be inherited by
54    /// static-web-server. Cannot be used in conjunction with the port and host arguments. The
55    /// included systemd unit file utilises this feature to increase security by allowing the
56    /// static-web-server to be sandboxed more completely.
57    pub fd: Option<usize>,
58
59    #[cfg_attr(
60        not(target_family = "wasm"),
61        arg(
62            long,
63            short = 'n',
64            default_value = "1",
65            env = "SERVER_THREADS_MULTIPLIER"
66        )
67    )]
68    #[cfg_attr(
69        target_family = "wasm",
70        arg(
71            long,
72            short = 'n',
73            default_value = "2",
74            env = "SERVER_THREADS_MULTIPLIER"
75        )
76    )] // We use 2 as the threads multiplier in Wasm, 1 in Native
77    /// Number of worker threads multiplier that'll be multiplied by the number of system CPUs
78    /// using the formula: `worker threads = number of CPUs * n` where `n` is the value that changes here.
79    /// When multiplier value is 0 or 1 then one thread per core is used.
80    /// 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.
81    pub threads_multiplier: usize,
82
83    #[cfg_attr(
84        not(target_family = "wasm"),
85        arg(
86            long,
87            short = 'b',
88            default_value = "512",
89            env = "SERVER_MAX_BLOCKING_THREADS"
90        )
91    )]
92    #[cfg_attr(
93        target_family = "wasm",
94        arg(
95            long,
96            short = 'b',
97            default_value = "20",
98            env = "SERVER_MAX_BLOCKING_THREADS"
99        )
100    )] // We use 20 in Wasm, 512 in Native (default for tokio)
101    /// Maximum number of blocking threads
102    pub max_blocking_threads: usize,
103
104    #[arg(long, short = 'd', default_value = "./public", env = "SERVER_ROOT")]
105    /// Root directory path of static files.
106    pub root: PathBuf,
107
108    #[arg(long, default_value = "./50x.html", env = "SERVER_ERROR_PAGE_50X")]
109    /// HTML file path for 50x errors. If the path is not specified or simply doesn't exist
110    /// then the server will use a generic HTML error message.
111    /// If a relative path is used then it will be resolved under the root directory.
112    pub page50x: PathBuf,
113
114    #[arg(long, default_value = "./404.html", env = "SERVER_ERROR_PAGE_404")]
115    /// HTML file path for 404 errors. If the path is not specified or simply doesn't exist
116    /// then the server will use a generic HTML error message.
117    /// If a relative path is used then it will be resolved under the root directory.
118    pub page404: PathBuf,
119
120    #[cfg(feature = "fallback-page")]
121    #[cfg_attr(docsrs, doc(cfg(feature = "fallback-page")))]
122    #[arg(long, default_value = "", value_parser = value_parser_pathbuf, env = "SERVER_FALLBACK_PAGE")]
123    /// 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.
124    pub page_fallback: PathBuf,
125
126    #[arg(long, short = 'g', default_value = "error", env = "SERVER_LOG_LEVEL")]
127    /// Specify a logging level in lower case. Values: error, warn, info, debug or trace
128    pub log_level: String,
129
130    #[arg(
131        long,
132        default_value = "false",
133        default_missing_value("true"),
134        num_args(0..=1),
135        require_equals(false),
136        action = clap::ArgAction::Set,
137        env = "SERVER_LOG_WITH_ANSI",
138    )]
139    /// Enable or disable ANSI escape codes for colors and other text formatting of the log output.
140    pub log_with_ansi: bool,
141
142    #[arg(
143        long,
144        short = 'c',
145        default_value = "",
146        env = "SERVER_CORS_ALLOW_ORIGINS"
147    )]
148    /// 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.
149    pub cors_allow_origins: String,
150
151    #[arg(
152        long,
153        short = 'j',
154        default_value = "origin, content-type, authorization",
155        env = "SERVER_CORS_ALLOW_HEADERS"
156    )]
157    /// 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.
158    pub cors_allow_headers: String,
159
160    #[arg(
161        long,
162        default_value = "origin, content-type",
163        env = "SERVER_CORS_EXPOSE_HEADERS"
164    )]
165    /// 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.
166    pub cors_expose_headers: String,
167
168    #[arg(
169        long,
170        short = 't',
171        default_value = "false",
172        default_missing_value("true"),
173        num_args(0..=1),
174        require_equals(false),
175        action = clap::ArgAction::Set,
176        env = "SERVER_HTTP2_TLS",
177    )]
178    #[cfg(feature = "http2")]
179    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
180    /// Enable HTTP/2 with TLS support.
181    pub http2: bool,
182
183    #[arg(long, required_if_eq("http2", "true"), env = "SERVER_HTTP2_TLS_CERT")]
184    #[cfg(feature = "http2")]
185    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
186    /// Specify the file path to read the certificate.
187    pub http2_tls_cert: Option<PathBuf>,
188
189    #[arg(long, required_if_eq("http2", "true"), env = "SERVER_HTTP2_TLS_KEY")]
190    #[cfg(feature = "http2")]
191    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
192    /// Specify the file path to read the private key.
193    pub http2_tls_key: Option<PathBuf>,
194
195    #[arg(
196        long,
197        default_value = "false",
198        default_missing_value("true"),
199        num_args(0..=1),
200        require_equals(false),
201        action = clap::ArgAction::Set,
202        requires_if("true", "http2"),
203        env = "SERVER_HTTPS_REDIRECT"
204    )]
205    #[cfg(feature = "http2")]
206    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
207    /// Redirect all requests with scheme "http" to "https" for the current server instance. It depends on "http2" to be enabled.
208    pub https_redirect: bool,
209
210    #[arg(
211        long,
212        requires_if("true", "https_redirect"),
213        default_value = "localhost",
214        env = "SERVER_HTTPS_REDIRECT_HOST"
215    )]
216    #[cfg(feature = "http2")]
217    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
218    /// Canonical host name or IP of the HTTPS (HTTPS/2) server. It depends on "https_redirect" to be enabled.
219    pub https_redirect_host: String,
220
221    #[arg(
222        long,
223        requires_if("true", "https_redirect"),
224        default_value = "80",
225        env = "SERVER_HTTPS_REDIRECT_FROM_PORT"
226    )]
227    #[cfg(feature = "http2")]
228    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
229    /// HTTP host port where the redirect server will listen for requests to redirect them to HTTPS. It depends on "https_redirect" to be enabled.
230    pub https_redirect_from_port: u16,
231
232    #[arg(
233        long,
234        requires_if("true", "https_redirect"),
235        default_value = "localhost",
236        env = "SERVER_HTTPS_REDIRECT_FROM_HOSTS"
237    )]
238    #[cfg(feature = "http2")]
239    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
240    /// 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.
241    pub https_redirect_from_hosts: String,
242
243    #[arg(long, default_value = "index.html", env = "SERVER_INDEX_FILES")]
244    /// List of files that will be used as an index for requests ending with the slash character (‘/’).
245    /// Files are checked in the specified order.
246    pub index_files: String,
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    #[cfg_attr(
256        docsrs,
257        doc(cfg(any(
258            feature = "compression",
259            feature = "compression-gzip",
260            feature = "compression-brotli",
261            feature = "compression-zstd",
262            feature = "compression-deflate"
263        )))
264    )]
265    #[arg(
266        long,
267        short = 'x',
268        default_value = "true",
269        default_missing_value("true"),
270        num_args(0..=1),
271        require_equals(false),
272        action = clap::ArgAction::Set,
273        env = "SERVER_COMPRESSION",
274    )]
275    /// Gzip, Deflate, Brotli or Zstd compression on demand determined by the Accept-Encoding header and applied to text-based web file types only.
276    pub compression: bool,
277
278    #[cfg(any(
279        feature = "compression",
280        feature = "compression-gzip",
281        feature = "compression-brotli",
282        feature = "compression-zstd",
283        feature = "compression-deflate"
284    ))]
285    #[cfg_attr(
286        docsrs,
287        doc(cfg(any(
288            feature = "compression",
289            feature = "compression-gzip",
290            feature = "compression-brotli",
291            feature = "compression-zstd",
292            feature = "compression-deflate"
293        )))
294    )]
295    #[arg(long, default_value = "default", env = "SERVER_COMPRESSION_LEVEL")]
296    /// Compression level to apply for Gzip, Deflate, Brotli or Zstd compression.
297    pub compression_level: super::CompressionLevel,
298
299    #[cfg(any(
300        feature = "compression",
301        feature = "compression-gzip",
302        feature = "compression-brotli",
303        feature = "compression-zstd",
304        feature = "compression-deflate"
305    ))]
306    #[cfg_attr(
307        docsrs,
308        doc(cfg(any(
309            feature = "compression",
310            feature = "compression-gzip",
311            feature = "compression-brotli",
312            feature = "compression-zstd",
313            feature = "compression-deflate"
314        )))
315    )]
316    #[arg(
317        long,
318        default_value = "false",
319        default_missing_value("true"),
320        num_args(0..=1),
321        require_equals(false),
322        action = clap::ArgAction::Set,
323        env = "SERVER_COMPRESSION_STATIC",
324    )]
325    /// Look up the pre-compressed file variant (`.gz`, `.br` or `.zst`) on disk of a requested file and serves it directly if available.
326    /// The compression type is determined by the `Accept-Encoding` header.
327    pub compression_static: bool,
328
329    #[cfg(feature = "directory-listing")]
330    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
331    #[arg(
332        long,
333        short = 'z',
334        default_value = "false",
335        default_missing_value("true"),
336        num_args(0..=1),
337        require_equals(false),
338        action = clap::ArgAction::Set,
339        env = "SERVER_DIRECTORY_LISTING",
340    )]
341    /// Enable directory listing for all requests ending with the slash character (‘/’).
342    pub directory_listing: bool,
343
344    #[cfg(feature = "directory-listing")]
345    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
346    #[arg(
347        long,
348        requires_if("true", "directory_listing"),
349        default_value = "6",
350        env = "SERVER_DIRECTORY_LISTING_ORDER"
351    )]
352    /// 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)
353    pub directory_listing_order: u8,
354
355    #[cfg(feature = "directory-listing")]
356    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
357    #[arg(
358        long,
359        value_enum,
360        requires_if("true", "directory_listing"),
361        default_value = "html",
362        env = "SERVER_DIRECTORY_LISTING_FORMAT",
363        ignore_case(true)
364    )]
365    /// Specify a content format for directory listing entries. Formats supported: "html" or "json". Default "html".
366    pub directory_listing_format: DirListFmt,
367
368    #[cfg(feature = "directory-listing-download")]
369    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing-download")))]
370    #[arg(
371        long,
372        value_delimiter(','),
373        value_enum,
374        requires_ifs([
375            ("targz", "directory_listing"),
376        ]),
377        require_equals(true),
378        action = clap::ArgAction::Set,
379        env = "SERVER_DIRECTORY_LISTING_DOWNLOAD",
380        ignore_case(true)
381    )]
382    /// Specify list of enabled format(s) for directory download. Format supported: `targz`. Default to empty list (disabled).
383    pub directory_listing_download: Vec<DirDownloadFmt>,
384
385    #[arg(
386        long,
387        default_value = "false",
388        default_value_if("http2", "true", Some("true")),
389        default_missing_value("true"),
390        num_args(0..=1),
391        require_equals(false),
392        action = clap::ArgAction::Set,
393        env = "SERVER_SECURITY_HEADERS",
394    )]
395    /// Enable security headers by default when HTTP/2 feature is activated.
396    /// Headers included: "Strict-Transport-Security: max-age=63072000; includeSubDomains; preload" (2 years max-age),
397    /// "X-Frame-Options: DENY" and "Content-Security-Policy: frame-ancestors 'self'".
398    pub security_headers: bool,
399
400    #[arg(
401        long,
402        short = 'e',
403        default_value = "true",
404        env = "SERVER_CACHE_CONTROL_HEADERS"
405    )]
406    #[arg(
407        long,
408        short = 'e',
409        default_value = "true",
410        default_missing_value("true"),
411        num_args(0..=1),
412        require_equals(false),
413        action = clap::ArgAction::Set,
414        env = "SERVER_CACHE_CONTROL_HEADERS",
415    )]
416    /// 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.
417    pub cache_control_headers: bool,
418
419    #[cfg(feature = "basic-auth")]
420    /// It provides The "Basic" HTTP Authentication scheme using credentials as "user-id:password" pairs. Password must be encoded using the "BCrypt" password-hashing function.
421    #[arg(long, default_value = "", env = "SERVER_BASIC_AUTH")]
422    pub basic_auth: String,
423
424    #[arg(long, short = 'q', default_value = "0", env = "SERVER_GRACE_PERIOD")]
425    /// 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.
426    pub grace_period: u8,
427
428    #[arg(
429        long,
430        short = 'w',
431        default_value = "./config.toml",
432        value_parser = value_parser_pathbuf,
433        env = "SERVER_CONFIG_FILE"
434    )]
435    /// Server TOML configuration file path.
436    pub config_file: PathBuf,
437
438    #[arg(
439        long,
440        default_value = "false",
441        default_missing_value("true"),
442        num_args(0..=1),
443        require_equals(false),
444        action = clap::ArgAction::Set,
445        env = "SERVER_LOG_REMOTE_ADDRESS",
446    )]
447    /// Log incoming requests information along with its remote address if available using the `info` log level.
448    pub log_remote_address: bool,
449
450    #[arg(
451        long,
452        default_value = "false",
453        default_missing_value("true"),
454        num_args(0..=1),
455        require_equals(false),
456        action = clap::ArgAction::Set,
457        env = "SERVER_LOG_X_REAL_IP",
458    )]
459    /// Log the X-Real-IP header for remote IP information.
460    pub log_x_real_ip: bool,
461
462    #[arg(
463        long,
464        default_value = "false",
465        default_missing_value("true"),
466        num_args(0..=1),
467        require_equals(false),
468        action = clap::ArgAction::Set,
469        env = "SERVER_LOG_FORWARDED_FOR",
470    )]
471    /// Log the X-Forwarded-For header for remote IP information
472    pub log_forwarded_for: bool,
473
474    #[arg(
475        long,
476        require_equals(false),
477        value_delimiter(','),
478        action = clap::ArgAction::Set,
479        env = "SERVER_TRUSTED_PROXIES",
480    )]
481    /// List of IPs to use X-Forwarded-For from. The default is to trust all
482    pub trusted_proxies: Vec<IpAddr>,
483
484    #[arg(
485        long,
486        default_value = "true",
487        default_missing_value("true"),
488        num_args(0..=1),
489        require_equals(false),
490        action = clap::ArgAction::Set,
491        env = "SERVER_REDIRECT_TRAILING_SLASH",
492    )]
493    /// 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.
494    pub redirect_trailing_slash: bool,
495
496    #[arg(
497        long,
498        default_value = "false",
499        default_missing_value("true"),
500        num_args(0..=1),
501        require_equals(false),
502        action = clap::ArgAction::Set,
503        env = "SERVER_IGNORE_HIDDEN_FILES",
504    )]
505    /// Ignore hidden files/directories (dotfiles), preventing them to be served and being included in auto HTML index pages (directory listing).
506    pub ignore_hidden_files: bool,
507
508    #[arg(
509        long,
510        default_value = "false",
511        default_missing_value("true"),
512        num_args(0..=1),
513        require_equals(false),
514        action = clap::ArgAction::Set,
515        env = "SERVER_DISABLE_SYMLINKS",
516    )]
517    /// Prevent following files or directories if any path name component is a symbolic link.
518    pub disable_symlinks: bool,
519
520    #[arg(
521        long,
522        default_value = "false",
523        default_missing_value("true"),
524        num_args(0..=1),
525        require_equals(false),
526        action = clap::ArgAction::Set,
527        env = "SERVER_HEALTH",
528    )]
529    /// Add a /health endpoint that doesn't generate any log entry and returns a 200 status code.
530    /// This is especially useful with Kubernetes liveness and readiness probes.
531    pub health: bool,
532
533    #[cfg(all(unix, feature = "experimental"))]
534    #[arg(
535        long = "experimental-metrics",
536        default_value = "false",
537        default_missing_value("true"),
538        num_args(0..=1),
539        require_equals(false),
540        action = clap::ArgAction::Set,
541        env = "SERVER_EXPERIMENTAL_METRICS",
542    )]
543    /// Add a /metrics endpoint that returns a Prometheus metrics response.
544    pub experimental_metrics: bool,
545
546    #[arg(
547        long,
548        default_value = "false",
549        default_missing_value("true"),
550        num_args(0..=1),
551        require_equals(false),
552        action = clap::ArgAction::Set,
553        env = "SERVER_MAINTENANCE_MODE"
554    )]
555    /// Enable the server's maintenance mode functionality.
556    pub maintenance_mode: bool,
557
558    #[arg(
559        long,
560        default_value = "503",
561        value_parser = value_parser_status_code,
562        requires_if("true", "maintenance_mode"),
563        env = "SERVER_MAINTENANCE_MODE_STATUS"
564    )]
565    /// Provide a custom HTTP status code when entering into maintenance mode. Default 503.
566    pub maintenance_mode_status: StatusCode,
567
568    #[arg(
569        long,
570        default_value = "",
571        value_parser = value_parser_pathbuf,
572        requires_if("true", "maintenance_mode"),
573        env = "SERVER_MAINTENANCE_MODE_FILE"
574    )]
575    /// Provide a custom maintenance mode HTML file. If not provided then a generic message will be displayed.
576    pub maintenance_mode_file: PathBuf,
577
578    //
579    // Windows specific arguments and commands
580    //
581    #[cfg(windows)]
582    #[arg(
583        long,
584        short = 's',
585        default_value = "false",
586        default_missing_value("true"),
587        num_args(0..=1),
588        require_equals(false),
589        action = clap::ArgAction::Set,
590        env = "SERVER_WINDOWS_SERVICE",
591    )]
592    /// Tell the web server to run in a Windows Service context. Note that the `install` subcommand will enable this option automatically.
593    pub windows_service: bool,
594
595    // Subcommands
596    #[command(subcommand)]
597    /// Subcommands for additional maintenance tasks, like installing and uninstalling the SWS Windows Service and generation of completions and man pages
598    pub commands: Option<Commands>,
599
600    #[arg(
601        long,
602        short = 'V',
603        default_value = "false",
604        default_missing_value("true")
605    )]
606    #[doc(hidden)]
607    /// Print version info and exit.
608    pub version: bool,
609}
610
611#[derive(Debug, clap::Subcommand)]
612/// Subcommands for additional maintenance tasks, like installing and uninstalling the SWS Windows Service and generation of completions and man pages
613pub enum Commands {
614    /// Install a Windows Service for the web server.
615    #[cfg(windows)]
616    #[command(name = "install")]
617    Install {},
618
619    /// Uninstall the current Windows Service.
620    #[cfg(windows)]
621    #[command(name = "uninstall")]
622    Uninstall {},
623
624    /// Generate man pages and shell completions
625    #[command(name = "generate")]
626    Generate {
627        /// Generate shell completions
628        #[arg(long)]
629        completions: bool,
630        /// Generate man pages
631        #[arg(long)]
632        man_pages: bool,
633        /// Path to write generated artifacts to
634        out_dir: PathBuf,
635    },
636}
637
638fn value_parser_pathbuf(s: &str) -> Result<PathBuf, String> {
639    Ok(PathBuf::from(s))
640}
641
642fn value_parser_status_code(s: &str) -> Result<StatusCode, String> {
643    match s.parse::<u16>() {
644        Ok(code) => StatusCode::from_u16(code).map_err(|err| err.to_string()),
645        Err(err) => Err(err.to_string()),
646    }
647}