Skip to main content

rws_http/entry_point/command_line_args/
mod.rs

1use std::env;
2use crate::entry_point::Config;
3
4#[cfg(test)]
5mod tests;
6
7pub fn override_environment_variables_from_command_line_args() {
8    println!("\n  Start of Reading Command Line Arguments Section");
9
10    let args = env::args().collect();
11    let params = CommandLineArgument::get_command_line_arg_list();
12    CommandLineArgument::_parse(args, params);
13
14    println!("  End of Reading Command Line Arguments\n");
15}
16
17/// Returns the text to print for an informational flag — `--version` / `-V`
18/// or `--help` — if `args` (as from `env::args()`, program name first)
19/// contains one, so the binary can print it and exit before starting a
20/// server. `None` means start normally. `-h` is not a help alias: it's
21/// already `--cors-allow-headers`'s short form.
22pub fn informational_flag_output(args: &[String]) -> Option<String> {
23    let flags = args.iter().skip(1);
24    for arg in flags {
25        match arg.as_str() {
26            "--version" | "-V" => {
27                // A FIPS build says so (18.23.0); others print just the version.
28                return Some(format!("rws {}{}\n", env!("CARGO_PKG_VERSION"), if cfg!(feature = "fips") { " (FIPS)" } else { "" }));
29            }
30            "--help" => return Some(help_text()),
31            _ => {}
32        }
33    }
34    None
35}
36
37fn help_text() -> String {
38    let mut text = format!(
39        "rws {} — HTTP/1.1, HTTP/2 and HTTP/3 web server and reverse proxy\n\n\
40         Usage: rws [OPTIONS]\n\n\
41         Serves the current directory on http://0.0.0.0:7878 by default. Drop an\n\
42         rws.config.toml with [[route]]/[[upstream]] sections in the working\n\
43         directory to run as a config-driven reverse proxy instead.\n\n\
44         Options (each also settable via the environment variable shown):\n",
45        env!("CARGO_PKG_VERSION")
46    );
47    for argument in CommandLineArgument::get_command_line_arg_list() {
48        let flag = if argument.short_form.is_empty() {
49            format!("    --{}=<value>", argument.long_form)
50        } else {
51            format!("-{}, --{}=<value>", argument.short_form, argument.long_form)
52        };
53        let hint = argument._hint.unwrap_or_default();
54        text.push_str(&format!("  {:<44} {}\n  {:<44} [env: {}]\n", flag, hint, "", argument.environment_variable));
55    }
56    text.push_str("      --check[=<file>]                         Validate rws.config.toml (or <file>) and exit:\n");
57    text.push_str("                                               0 if it has no errors, 1 otherwise\n");
58    text.push_str("      trust [--print] | untrust                Put rws's local CA (tls = \"internal\") in the\n");
59    text.push_str("                                               system trust store, or take it out\n");
60    text.push_str("      ctl <command>                            Drive a running server's admin API\n");
61    text.push_str("                                               (rws ctl help; also installed as rwsctl)\n");
62    text.push_str("      gen model|route|migration|test <name>    Generate code into the project in the working\n");
63    text.push_str("                                               directory (rws gen --help)\n");
64    text.push_str("  -V, --version                                Print version and exit\n");
65    text.push_str("      --help                                   Print this help and exit\n");
66    text.push_str("      --windows-service=<dir>                  (Windows) Run as a service serving <dir>\n\n");
67    text.push_str("Docs: https://rws8.tech\n");
68    text
69}
70
71pub struct CommandLineArgument {
72    pub short_form: String,
73    pub long_form: String,
74    pub environment_variable: String,
75    pub _hint: Option<String>,
76}
77
78impl CommandLineArgument {
79    pub fn get_command_line_arg_list() -> Vec<CommandLineArgument> {
80        let mut argument_list : Vec<CommandLineArgument> = vec![];
81
82        let argument = CommandLineArgument {
83            short_form: "p".to_string(),
84            long_form: "port".to_string(),
85            environment_variable: Config::RWS_CONFIG_PORT.to_string(),
86            _hint: Some("Port".to_string())
87        };
88        argument_list.push(argument);
89
90        let argument = CommandLineArgument {
91            short_form: "i".to_string(),
92            long_form: "ip".to_string(),
93            environment_variable: Config::RWS_CONFIG_IP.to_string(),
94            _hint: Some("IP or domain".to_string())
95        };
96        argument_list.push(argument);
97
98        let argument = CommandLineArgument {
99            short_form: "t".to_string(),
100            long_form: "thread-count".to_string(),
101            environment_variable: Config::RWS_CONFIG_THREAD_COUNT.to_string(),
102            _hint: Some("Number of threads".to_string())
103        };
104        argument_list.push(argument);
105
106
107        let argument = CommandLineArgument {
108            short_form: "a".to_string(),
109            long_form: "cors-allow-all".to_string(),
110            environment_variable: Config::RWS_CONFIG_CORS_ALLOW_ALL.to_string(),
111            _hint: Some("If set to true, will allow all CORS requests, other CORS properties will be ignored".to_string())
112        };
113        argument_list.push(argument);
114
115        let argument = CommandLineArgument {
116            short_form: "o".to_string(),
117            long_form: "cors-allow-origins".to_string(),
118            environment_variable: Config::RWS_CONFIG_CORS_ALLOW_ORIGINS.to_string(),
119            _hint: Some("Comma separated list of allowed origins, example: https://foo.example,https://bar.example".to_string())
120        };
121        argument_list.push(argument);
122
123        let argument = CommandLineArgument {
124            short_form: "m".to_string(),
125            long_form: "cors-allow-methods".to_string(),
126            environment_variable: Config::RWS_CONFIG_CORS_ALLOW_METHODS.to_string(),
127            _hint: Some("Comma separated list of allowed methods, example: POST,PUT".to_string())
128        };
129        argument_list.push(argument);
130
131        let argument = CommandLineArgument {
132            short_form: "h".to_string(),
133            long_form: "cors-allow-headers".to_string(),
134            environment_variable: Config::RWS_CONFIG_CORS_ALLOW_HEADERS.to_string(),
135            _hint: Some("Comma separated list of allowed request headers, in lowercase, example: content-type,x-custom-header".to_string())
136        };
137        argument_list.push(argument);
138
139        let argument = CommandLineArgument {
140            short_form: "c".to_string(),
141            long_form: "cors-allow-credentials".to_string(),
142            environment_variable: Config::RWS_CONFIG_CORS_ALLOW_CREDENTIALS.to_string(),
143            _hint: Some("If set to true, will allow to transmit credentials via CORS requests".to_string())
144        };
145        argument_list.push(argument);
146
147        let argument = CommandLineArgument {
148            short_form: "e".to_string(),
149            long_form: "cors-expose-headers".to_string(),
150            environment_variable: Config::RWS_CONFIG_CORS_EXPOSE_HEADERS.to_string(),
151            _hint: Some("Comma separated list of allowed response headers, in lowercase, example: content-type,x-custom-header".to_string())
152        };
153        argument_list.push(argument);
154
155        let argument = CommandLineArgument {
156            short_form: "g".to_string(),
157            long_form: "cors-max-age".to_string(),
158            environment_variable: Config::RWS_CONFIG_CORS_MAX_AGE.to_string(),
159            _hint: Some("How long results of preflight requests can be cached (in seconds)".to_string())
160        };
161        argument_list.push(argument);
162
163        let argument = CommandLineArgument {
164            short_form: "r".to_string(),
165            long_form: "request-allocation-size-in-bytes".to_string(),
166            environment_variable: Config::RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES.to_string(),
167            _hint: Some("In bytes, how much memory to allocate for each request".to_string())
168        };
169        argument_list.push(argument);
170
171        let argument = CommandLineArgument {
172            short_form: "s".to_string(),
173            long_form: "tls-cert-file".to_string(),
174            environment_variable: Config::RWS_CONFIG_TLS_CERT_FILE.to_string(),
175            _hint: Some("Path to TLS certificate PEM file (enables HTTPS and HTTP/2)".to_string())
176        };
177        argument_list.push(argument);
178
179        let argument = CommandLineArgument {
180            short_form: "k".to_string(),
181            long_form: "tls-key-file".to_string(),
182            environment_variable: Config::RWS_CONFIG_TLS_KEY_FILE.to_string(),
183            _hint: Some("Path to TLS private key PEM file (enables HTTPS and HTTP/2)".to_string())
184        };
185        argument_list.push(argument);
186
187        // Long-form only (empty short_form): every single letter that reads
188        // naturally is taken. In rws.config.toml these are `http_redirect_port`
189        // at the top level and the keys of an `[acme]` section (`domains`,
190        // `email`, ...), which the config-file reader prefixes to `acme-*`.
191        let long_only = [
192            ("http-redirect-port", Config::RWS_CONFIG_HTTP_REDIRECT_PORT,
193                "Plain-HTTP port that 301-redirects to HTTPS (and answers ACME HTTP-01 challenges), e.g. 80"),
194            ("acme-domains", Config::RWS_CONFIG_ACME_DOMAINS,
195                "Comma separated domains to get a Let's Encrypt certificate for; enables automatic HTTPS"),
196            ("acme-email", Config::RWS_CONFIG_ACME_EMAIL,
197                "Contact email for the ACME account"),
198            ("acme-staging", Config::RWS_CONFIG_ACME_STAGING,
199                "If set to true, uses the Let's Encrypt staging environment (untrusted test certificates)"),
200            ("acme-directory", Config::RWS_CONFIG_ACME_DIRECTORY,
201                "ACME directory URL of a CA other than Let's Encrypt"),
202            ("acme-ca-file", Config::RWS_CONFIG_ACME_CA_FILE,
203                "PEM file of extra root CAs to trust for the ACME directory (private ACME CAs)"),
204            ("acme-challenge-port", Config::RWS_CONFIG_ACME_CHALLENGE_PORT,
205                "Port the CA connects to for HTTP-01 challenges (default 80)"),
206            ("acme-cert-path", Config::RWS_CONFIG_ACME_CERT_PATH,
207                "Where to store the issued certificate (default: tls-cert-file, else .rws/acme/cert.pem)"),
208            ("acme-key-path", Config::RWS_CONFIG_ACME_KEY_PATH,
209                "Where to store the certificate's private key (default: tls-key-file, else .rws/acme/key.pem)"),
210            ("acme-profile", Config::RWS_CONFIG_ACME_PROFILE,
211                "Certificate profile to order, as the CA offers them (e.g. classic, tlsserver, shortlived)"),
212            ("acme-challenge", Config::RWS_CONFIG_ACME_CHALLENGE,
213                "How to prove control of the domains: http-01 (default), tls-alpn-01, dns-01 (wildcards) or dns-persist-01"),
214            ("acme-dns-provider", Config::RWS_CONFIG_ACME_DNS_PROVIDER,
215                "DNS-01: rfc2136 (DNS UPDATE), cloudflare, route53 or exec (a program)"),
216            ("acme-dns-server", Config::RWS_CONFIG_ACME_DNS_SERVER,
217                "DNS-01 rfc2136: the primary DNS server, host[:port]"),
218            ("acme-dns-zone", Config::RWS_CONFIG_ACME_DNS_ZONE,
219                "DNS-01: the zone of the records (found by SOA lookup when not set)"),
220            ("acme-dns-tsig-key", Config::RWS_CONFIG_ACME_DNS_TSIG_KEY,
221                "DNS-01 rfc2136: TSIG key name (the secret goes in RWS_CONFIG_ACME_DNS_TSIG_SECRET)"),
222            ("acme-dns-tsig-algorithm", Config::RWS_CONFIG_ACME_DNS_TSIG_ALGORITHM,
223                "DNS-01 rfc2136: hmac-sha256 (default) or hmac-sha512"),
224            ("acme-dns-exec", Config::RWS_CONFIG_ACME_DNS_EXEC,
225                "DNS-01 exec: program run as <program> present|cleanup <fqdn> <value>"),
226            ("acme-dns-propagation-secs", Config::RWS_CONFIG_ACME_DNS_PROPAGATION_SECS,
227                "DNS-01: seconds to wait for the record to be visible (default 120)"),
228            ("acme-dns-api-url", Config::RWS_CONFIG_ACME_DNS_API_URL,
229                "DNS-01 cloudflare/route53: API base URL instead of the provider's (the token goes in RWS_CONFIG_ACME_DNS_API_TOKEN)"),
230            ("acme-dns-zone-id", Config::RWS_CONFIG_ACME_DNS_ZONE_ID,
231                "DNS-01 cloudflare/route53: the zone's id, instead of looking it up"),
232            ("acme-redis", Config::RWS_CONFIG_ACME_REDIS,
233                "Redis host:port shared by replicas: one orders each certificate, all install it"),
234            ("acme-redis-prefix", Config::RWS_CONFIG_ACME_REDIS_PREFIX,
235                "Key prefix in that Redis (default rws:acme)"),
236            ("acme-on-demand", Config::RWS_CONFIG_ACME_ON_DEMAND,
237                "true: order a certificate at the first handshake for a name the ask endpoint allows"),
238            ("acme-on-demand-ask", Config::RWS_CONFIG_ACME_ON_DEMAND_ASK,
239                "On-demand TLS: URL asked ?domain=<name> before each order; 2xx allows it"),
240        ];
241        for (long_form, environment_variable, hint) in long_only {
242            argument_list.push(CommandLineArgument {
243                short_form: "".to_string(),
244                long_form: long_form.to_string(),
245                environment_variable: environment_variable.to_string(),
246                _hint: Some(hint.to_string()),
247            });
248        }
249
250        let argument = CommandLineArgument {
251            short_form: "".to_string(),
252            long_form: "tls".to_string(),
253            environment_variable: "RWS_CONFIG_TLS".to_string(),
254            _hint: Some("internal: HTTPS from rws's own CA (local development; see rws trust)".to_string())
255        };
256        argument_list.push(argument);
257
258        argument_list
259    }
260
261    pub fn _parse(args: Vec<String>, argument_list : Vec<CommandLineArgument>) -> Vec<CommandLineArgument> {
262        for unparsed_argument in args.iter() {
263
264            let boxed_split = unparsed_argument.split_once('=');
265            if boxed_split.is_some() {
266
267                let (parameter, value) = boxed_split.unwrap();
268                println!("\n    {}={}", parameter, crate::entry_point::shown(parameter, value));
269                let boxed_predefined_argument =
270                    argument_list
271                        .iter()
272                        .find(
273                            |predefined_argument| {
274                                let short_form = predefined_argument.short_form.to_string();
275                                let long_form = predefined_argument.long_form.to_string();
276                                let _param_short_form = ["-", &short_form].join("");
277                                let _param_long_form = ["--", &long_form].join("");
278
279                                (!short_form.is_empty() && parameter.eq(_param_short_form.as_str())) ||
280                                    parameter.eq(_param_long_form.as_str())
281                });
282                if boxed_predefined_argument.is_some() {
283                    let predefined_argument = boxed_predefined_argument.unwrap();
284                    CommandLineArgument::set_environment_variable(predefined_argument, value.to_string());
285                }
286
287            }
288        }
289        argument_list
290    }
291
292    pub fn set_environment_variable(argument: &CommandLineArgument, value: String) {
293        if value.contains('\0') {
294            // env::set_var panics on NUL; reachable from rws.config.toml values.
295            eprintln!("    Warning: value for --{} contains a NUL byte — ignored", argument.long_form);
296            return;
297        }
298        env::set_var(&argument.environment_variable, &value);
299        crate::server_config::invalidate();
300        println!("    Set env variable '{}' to value '{}'", argument.environment_variable, crate::entry_point::shown(&argument.environment_variable, &value));
301    }
302}