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