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;
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    #[arg(
300        long,
301        default_value = "false",
302        default_missing_value("true"),
303        num_args(0..=1),
304        require_equals(false),
305        action = clap::ArgAction::Set,
306        env = "SERVER_COMPRESSION_STATIC",
307    )]
308    /// Look up the pre-compressed file variant (`.gz`, `.br` or `.zst`) on disk of a requested file and serves it directly if available.
309    /// The compression type is determined by the `Accept-Encoding` header.
310    pub compression_static: bool,
311
312    #[cfg(feature = "directory-listing")]
313    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
314    #[arg(
315        long,
316        short = 'z',
317        default_value = "false",
318        default_missing_value("true"),
319        num_args(0..=1),
320        require_equals(false),
321        action = clap::ArgAction::Set,
322        env = "SERVER_DIRECTORY_LISTING",
323    )]
324    /// Enable directory listing for all requests ending with the slash character (‘/’).
325    pub directory_listing: bool,
326
327    #[cfg(feature = "directory-listing")]
328    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
329    #[arg(
330        long,
331        requires_if("true", "directory_listing"),
332        default_value = "6",
333        env = "SERVER_DIRECTORY_LISTING_ORDER"
334    )]
335    /// 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)
336    pub directory_listing_order: u8,
337
338    #[cfg(feature = "directory-listing")]
339    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing")))]
340    #[arg(
341        long,
342        value_enum,
343        requires_if("true", "directory_listing"),
344        default_value = "html",
345        env = "SERVER_DIRECTORY_LISTING_FORMAT",
346        ignore_case(true)
347    )]
348    /// Specify a content format for directory listing entries. Formats supported: "html" or "json". Default "html".
349    pub directory_listing_format: DirListFmt,
350
351    #[cfg(feature = "directory-listing-download")]
352    #[cfg_attr(docsrs, doc(cfg(feature = "directory-listing-download")))]
353    #[arg(
354        long,
355        value_delimiter(','),
356        value_enum,
357        requires_ifs([
358            ("targz", "directory_listing"),
359        ]),
360        require_equals(true),
361        action = clap::ArgAction::Set,
362        env = "SERVER_DIRECTORY_LISTING_DOWNLOAD",
363        ignore_case(true)
364    )]
365    /// Specify list of enabled format(s) for directory download. Format supported: `targz`. Default to empty list (disabled).
366    pub directory_listing_download: Vec<DirDownloadFmt>,
367
368    #[arg(
369        long,
370        default_value = "false",
371        default_value_if("http2", "true", Some("true")),
372        default_missing_value("true"),
373        num_args(0..=1),
374        require_equals(false),
375        action = clap::ArgAction::Set,
376        env = "SERVER_SECURITY_HEADERS",
377    )]
378    /// Enable security headers by default when HTTP/2 feature is activated.
379    /// Headers included: "Strict-Transport-Security: max-age=63072000; includeSubDomains; preload" (2 years max-age),
380    /// "X-Frame-Options: DENY" and "Content-Security-Policy: frame-ancestors 'self'".
381    pub security_headers: bool,
382
383    #[arg(
384        long,
385        short = 'e',
386        default_value = "true",
387        env = "SERVER_CACHE_CONTROL_HEADERS"
388    )]
389    #[arg(
390        long,
391        short = 'e',
392        default_value = "true",
393        default_missing_value("true"),
394        num_args(0..=1),
395        require_equals(false),
396        action = clap::ArgAction::Set,
397        env = "SERVER_CACHE_CONTROL_HEADERS",
398    )]
399    /// 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.
400    pub cache_control_headers: bool,
401
402    #[cfg(feature = "basic-auth")]
403    /// It provides The "Basic" HTTP Authentication scheme using credentials as "user-id:password" pairs. Password must be encoded using the "BCrypt" password-hashing function.
404    #[arg(long, default_value = "", env = "SERVER_BASIC_AUTH")]
405    pub basic_auth: String,
406
407    #[arg(long, short = 'q', default_value = "0", env = "SERVER_GRACE_PERIOD")]
408    /// 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.
409    pub grace_period: u8,
410
411    #[arg(
412        long,
413        short = 'w',
414        default_value = "./sws.toml",
415        value_parser = value_parser_pathbuf,
416        env = "SERVER_CONFIG_FILE"
417    )]
418    /// Server TOML configuration file path.
419    pub config_file: PathBuf,
420
421    #[arg(
422        long,
423        default_value = "false",
424        default_missing_value("true"),
425        num_args(0..=1),
426        require_equals(false),
427        action = clap::ArgAction::Set,
428        env = "SERVER_LOG_REMOTE_ADDRESS",
429    )]
430    /// Log incoming requests information along with its remote address if available using the `info` log level.
431    pub log_remote_address: bool,
432
433    #[arg(
434        long,
435        default_value = "false",
436        default_missing_value("true"),
437        num_args(0..=1),
438        require_equals(false),
439        action = clap::ArgAction::Set,
440        env = "SERVER_LOG_X_REAL_IP",
441    )]
442    /// Log the X-Real-IP header for remote IP information.
443    pub log_x_real_ip: bool,
444
445    #[arg(
446        long,
447        default_value = "false",
448        default_missing_value("true"),
449        num_args(0..=1),
450        require_equals(false),
451        action = clap::ArgAction::Set,
452        env = "SERVER_LOG_FORWARDED_FOR",
453    )]
454    /// Log the X-Forwarded-For header for remote IP information
455    pub log_forwarded_for: bool,
456
457    #[arg(
458        long,
459        require_equals(false),
460        value_delimiter(','),
461        action = clap::ArgAction::Set,
462        env = "SERVER_TRUSTED_PROXIES",
463    )]
464    /// List of IPs to use X-Forwarded-For from. The default is to trust all
465    pub trusted_proxies: Vec<IpAddr>,
466
467    #[arg(
468        long,
469        default_value = "true",
470        default_missing_value("true"),
471        num_args(0..=1),
472        require_equals(false),
473        action = clap::ArgAction::Set,
474        env = "SERVER_REDIRECT_TRAILING_SLASH",
475    )]
476    /// 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.
477    pub redirect_trailing_slash: bool,
478
479    #[arg(
480        long,
481        default_value = "true",
482        default_missing_value("true"),
483        num_args(0..=1),
484        require_equals(false),
485        action = clap::ArgAction::Set,
486        env = "SERVER_IGNORE_HIDDEN_FILES",
487    )]
488    /// Ignore hidden files/directories (dotfiles), preventing them to be served and being included in auto HTML index pages (directory listing).
489    pub ignore_hidden_files: bool,
490
491    #[arg(
492        long,
493        default_value = "true",
494        default_missing_value("true"),
495        num_args(0..=1),
496        require_equals(false),
497        action = clap::ArgAction::Set,
498        env = "SERVER_DISABLE_SYMLINKS",
499    )]
500    /// Prevent following files or directories if any path name component is a symbolic link.
501    pub disable_symlinks: bool,
502
503    #[arg(
504        long,
505        default_value = "false",
506        default_missing_value("true"),
507        num_args(0..=1),
508        require_equals(false),
509        action = clap::ArgAction::Set,
510        env = "SERVER_ACCEPT_MARKDOWN",
511    )]
512    /// Enable markdown content negotiation. When a client sends Accept: text/markdown, serve .md or .html.md files if available.
513    pub accept_markdown: bool,
514
515    #[arg(
516        long,
517        default_value = "false",
518        default_missing_value("true"),
519        num_args(0..=1),
520        require_equals(false),
521        action = clap::ArgAction::Set,
522        env = "SERVER_HEALTH",
523    )]
524    /// Add a /health endpoint that doesn't generate any log entry and returns a 200 status code.
525    /// This is especially useful with Kubernetes liveness and readiness probes.
526    pub health: bool,
527
528    #[cfg(all(unix, feature = "experimental"))]
529    #[arg(
530        long = "experimental-metrics",
531        default_value = "false",
532        default_missing_value("true"),
533        num_args(0..=1),
534        require_equals(false),
535        action = clap::ArgAction::Set,
536        env = "SERVER_EXPERIMENTAL_METRICS",
537    )]
538    /// Add a /metrics endpoint that returns a Prometheus metrics response.
539    pub experimental_metrics: bool,
540
541    #[arg(
542        long,
543        default_value = "false",
544        default_missing_value("true"),
545        num_args(0..=1),
546        require_equals(false),
547        action = clap::ArgAction::Set,
548        env = "SERVER_MAINTENANCE_MODE"
549    )]
550    /// Enable the server's maintenance mode functionality.
551    pub maintenance_mode: bool,
552
553    #[arg(
554        long,
555        default_value = "503",
556        value_parser = value_parser_status_code,
557        requires_if("true", "maintenance_mode"),
558        env = "SERVER_MAINTENANCE_MODE_STATUS"
559    )]
560    /// Provide a custom HTTP status code when entering into maintenance mode. Default 503.
561    pub maintenance_mode_status: StatusCode,
562
563    #[arg(
564        long,
565        default_value = "",
566        value_parser = value_parser_pathbuf,
567        requires_if("true", "maintenance_mode"),
568        env = "SERVER_MAINTENANCE_MODE_FILE"
569    )]
570    /// Provide a custom maintenance mode HTML file. If not provided then a generic message will be displayed.
571    pub maintenance_mode_file: PathBuf,
572
573    //
574    // Windows specific arguments and commands
575    //
576    #[cfg(windows)]
577    #[arg(
578        long,
579        short = 's',
580        default_value = "false",
581        default_missing_value("true"),
582        num_args(0..=1),
583        require_equals(false),
584        action = clap::ArgAction::Set,
585        env = "SERVER_WINDOWS_SERVICE",
586    )]
587    /// Tell the web server to run in a Windows Service context. Note that the `install` subcommand will enable this option automatically.
588    pub windows_service: bool,
589
590    // Subcommands
591    #[command(subcommand)]
592    /// Subcommands for additional maintenance tasks, like installing and uninstalling the SWS Windows Service and generation of completions and man pages
593    pub commands: Option<Commands>,
594
595    #[arg(
596        long,
597        short = 'V',
598        default_value = "false",
599        default_missing_value("true")
600    )]
601    #[doc(hidden)]
602    /// Print version info and exit.
603    pub version: bool,
604}
605
606#[derive(Debug, clap::Subcommand)]
607/// Subcommands for additional maintenance tasks, like installing and uninstalling the SWS Windows Service and generation of completions and man pages
608pub enum Commands {
609    /// Install a Windows Service for the web server.
610    #[cfg(windows)]
611    #[command(name = "install")]
612    Install {},
613
614    /// Uninstall the current Windows Service.
615    #[cfg(windows)]
616    #[command(name = "uninstall")]
617    Uninstall {},
618
619    /// Generate man pages and shell completions
620    #[command(name = "generate")]
621    Generate {
622        /// Generate shell completions
623        #[arg(long)]
624        completions: bool,
625        /// Generate man pages
626        #[arg(long)]
627        man_pages: bool,
628        /// Path to write generated artifacts to
629        out_dir: PathBuf,
630    },
631}
632
633fn value_parser_pathbuf(s: &str) -> Result<PathBuf, String> {
634    Ok(PathBuf::from(s))
635}
636
637fn value_parser_status_code(s: &str) -> Result<StatusCode, String> {
638    match s.parse::<u16>() {
639        Ok(code) => StatusCode::from_u16(code).map_err(|err| err.to_string()),
640        Err(err) => Err(err.to_string()),
641    }
642}