Skip to main content

rust_web_server/entry_point/
mod.rs

1#[cfg(test)]
2mod tests;
3pub mod command_line_args;
4pub mod config_file;
5pub mod environment_variables;
6
7
8use std::{env};
9use crate::virtual_host::VirtualHostConfig;
10
11use crate::entry_point::command_line_args::{override_environment_variables_from_command_line_args};
12use crate::entry_point::config_file::override_environment_variables_from_config;
13use crate::entry_point::environment_variables::read_system_environment_variables;
14
15#[derive(PartialEq, Eq, Clone, Debug)]
16pub struct Config {}
17
18impl Config {
19    pub const RWS_CONFIG_IP: &'static str = "RWS_CONFIG_IP";
20    /// Default is `0.0.0.0` so the server is reachable inside containers and K8s pods.
21    /// For local development you can override to `127.0.0.1` via env var or config file.
22    pub const RWS_CONFIG_IP_DEFAULT_VALUE: &'static str = "0.0.0.0";
23
24    /// Log format: `"combined"` (default, Combined Log Format) or `"json"` (structured JSON).
25    pub const RWS_CONFIG_LOG_FORMAT: &'static str = "RWS_CONFIG_LOG_FORMAT";
26    pub const RWS_CONFIG_LOG_FORMAT_DEFAULT_VALUE: &'static str = "json";
27
28    pub const RWS_CONFIG_PORT: &'static str = "RWS_CONFIG_PORT";
29    pub const RWS_CONFIG_PORT_DEFAULT_VALUE: &'static str = "7878";
30
31    pub const RWS_CONFIG_THREAD_COUNT: &'static str = "RWS_CONFIG_THREAD_COUNT";
32    pub const RWS_CONFIG_THREAD_COUNT_DEFAULT_VALUE: &'static str = "200";
33
34    pub const RWS_CONFIG_CORS_ALLOW_ALL: &'static str = "RWS_CONFIG_CORS_ALLOW_ALL";
35    pub const RWS_CONFIG_CORS_ALLOW_ALL_DEFAULT_VALUE: &'static str = "false";
36
37    pub const RWS_CONFIG_CORS_ALLOW_ORIGINS: &'static str = "RWS_CONFIG_CORS_ALLOW_ORIGINS";
38    pub const RWS_CONFIG_CORS_ALLOW_ORIGINS_DEFAULT_VALUE: &'static str = "";
39
40    pub const RWS_CONFIG_CORS_ALLOW_CREDENTIALS: &'static str = "RWS_CONFIG_CORS_ALLOW_CREDENTIALS";
41    pub const RWS_CONFIG_CORS_ALLOW_CREDENTIALS_DEFAULT_VALUE: &'static str = "";
42
43    pub const RWS_CONFIG_CORS_ALLOW_HEADERS: &'static str = "RWS_CONFIG_CORS_ALLOW_HEADERS";
44    pub const RWS_CONFIG_CORS_ALLOW_HEADERS_DEFAULT_VALUE: &'static str = "";
45
46    pub const RWS_CONFIG_CORS_ALLOW_METHODS: &'static str = "RWS_CONFIG_CORS_ALLOW_METHODS";
47    pub const RWS_CONFIG_CORS_ALLOW_METHODS_DEFAULT_VALUE: &'static str = "";
48
49    pub const RWS_CONFIG_CORS_EXPOSE_HEADERS: &'static str = "RWS_CONFIG_CORS_EXPOSE_HEADERS";
50    pub const RWS_CONFIG_CORS_EXPOSE_HEADERS_DEFAULT_VALUE: &'static str = "";
51
52    pub const RWS_CONFIG_CORS_MAX_AGE: &'static str = "RWS_CONFIG_CORS_MAX_AGE";
53    pub const RWS_CONFIG_CORS_MAX_AGE_DEFAULT_VALUE: &'static str = "86400";
54
55    pub const RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES: &'static str = "RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES";
56    pub const RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES_DEFAULT_VALUE: &'static str = "10000";
57
58    /// Maximum accepted request body size in bytes, checked against `Content-Length`
59    /// before any body bytes are read off the socket. `0` (the default) means
60    /// unlimited — this is opt-in, not a default-on behavior change. Requests whose
61    /// declared body exceeds this get `413 Payload Too Large` and the connection is
62    /// closed (not kept alive), since bytes the server chose not to read are still
63    /// queued on the client's side of the socket.
64    pub const RWS_CONFIG_MAX_BODY_SIZE_IN_BYTES: &'static str = "RWS_CONFIG_MAX_BODY_SIZE_IN_BYTES";
65    pub const RWS_CONFIG_MAX_BODY_SIZE_IN_BYTES_DEFAULT_VALUE: &'static str = "0";
66
67    /// Serve this file (e.g. `"index.html"`) for a `GET`/`HEAD` request that doesn't
68    /// match any real file/directory, instead of `404` — the standard client-side-router
69    /// ("SPA") fallback pattern needed for deep links in a React Router / Vue Router /
70    /// etc. app. Empty (default) disables it entirely. See
71    /// `crate::app::controller::static_resource` for the matching heuristic (skips
72    /// paths that look like a missed static asset, i.e. the last segment has a `.`).
73    pub const RWS_CONFIG_SPA_FALLBACK: &'static str = "RWS_CONFIG_SPA_FALLBACK";
74    pub const RWS_CONFIG_SPA_FALLBACK_DEFAULT_VALUE: &'static str = "";
75
76    /// Comma-separated path prefixes (e.g. `"/api,/healthz"`) that never receive the
77    /// SPA fallback even when `RWS_CONFIG_SPA_FALLBACK` is set — so a real API `404`
78    /// isn't silently rewritten into a `200`. Empty (default) excludes nothing.
79    pub const RWS_CONFIG_SPA_FALLBACK_EXCLUDE_PREFIXES: &'static str = "RWS_CONFIG_SPA_FALLBACK_EXCLUDE_PREFIXES";
80    pub const RWS_CONFIG_SPA_FALLBACK_EXCLUDE_PREFIXES_DEFAULT_VALUE: &'static str = "";
81
82    pub const RWS_CONFIG_TLS_CERT_FILE: &'static str = "RWS_CONFIG_TLS_CERT_FILE";
83    pub const RWS_CONFIG_TLS_CERT_FILE_DEFAULT_VALUE: &'static str = "";
84
85    pub const RWS_CONFIG_TLS_KEY_FILE: &'static str = "RWS_CONFIG_TLS_KEY_FILE";
86    pub const RWS_CONFIG_TLS_KEY_FILE_DEFAULT_VALUE: &'static str = "";
87
88    /// Path to a PEM-encoded CA certificate used to verify client certificates (mTLS).
89    /// When set, the TLS handshake requires a valid client certificate signed by this CA.
90    /// Connections without a valid cert are rejected at the TLS layer (before any HTTP processing).
91    pub const RWS_CONFIG_TLS_CLIENT_CA_FILE: &'static str = "RWS_CONFIG_TLS_CLIENT_CA_FILE";
92    pub const RWS_CONFIG_TLS_CLIENT_CA_FILE_DEFAULT_VALUE: &'static str = "";
93
94    /// Directory containing Tera HTML templates (default: `"templates"`). Requires `tera` feature.
95    pub const RWS_CONFIG_TEMPLATE_DIR: &'static str = "RWS_CONFIG_TEMPLATE_DIR";
96    pub const RWS_CONFIG_TEMPLATE_DIR_DEFAULT_VALUE: &'static str = "templates";
97
98    /// When non-empty, a plain-HTTP listener on this port redirects all requests to HTTPS.
99    /// Set to e.g. `"80"` when running on standard ports. Requires TLS to be configured.
100    pub const RWS_CONFIG_HTTP_REDIRECT_PORT: &'static str = "RWS_CONFIG_HTTP_REDIRECT_PORT";
101    pub const RWS_CONFIG_HTTP_REDIRECT_PORT_DEFAULT_VALUE: &'static str = "";
102
103    // ── ACME (Automatic Certificate Management Environment) ───────────────────
104
105    /// Comma-separated list of domain names to obtain a certificate for.
106    /// Setting this activates ACME at startup. Example: `"example.com,www.example.com"`
107    pub const RWS_CONFIG_ACME_DOMAINS: &'static str = "RWS_CONFIG_ACME_DOMAINS";
108    pub const RWS_CONFIG_ACME_DOMAINS_DEFAULT_VALUE: &'static str = "";
109
110    /// Contact email sent to the CA. Recommended but not required.
111    pub const RWS_CONFIG_ACME_EMAIL: &'static str = "RWS_CONFIG_ACME_EMAIL";
112    pub const RWS_CONFIG_ACME_EMAIL_DEFAULT_VALUE: &'static str = "";
113
114    /// Set to `"true"` to use the Let's Encrypt staging environment (for testing).
115    pub const RWS_CONFIG_ACME_STAGING: &'static str = "RWS_CONFIG_ACME_STAGING";
116    pub const RWS_CONFIG_ACME_STAGING_DEFAULT_VALUE: &'static str = "false";
117
118    /// Custom ACME directory URL. Defaults to Let's Encrypt production.
119    pub const RWS_CONFIG_ACME_DIRECTORY: &'static str = "RWS_CONFIG_ACME_DIRECTORY";
120    pub const RWS_CONFIG_ACME_DIRECTORY_DEFAULT_VALUE: &'static str = "";
121
122    /// Where to write the provisioned certificate chain (PEM). Defaults to `RWS_CONFIG_TLS_CERT_FILE`,
123    /// or `cert.pem` in the working directory when that is empty too.
124    pub const RWS_CONFIG_ACME_CERT_PATH: &'static str = "RWS_CONFIG_ACME_CERT_PATH";
125    pub const RWS_CONFIG_ACME_CERT_PATH_DEFAULT_VALUE: &'static str = "";
126
127    /// Where to write the certificate's private key (PEM). Defaults to `RWS_CONFIG_TLS_KEY_FILE`,
128    /// or `key.pem` in the working directory when that is empty too.
129    pub const RWS_CONFIG_ACME_KEY_PATH: &'static str = "RWS_CONFIG_ACME_KEY_PATH";
130    pub const RWS_CONFIG_ACME_KEY_PATH_DEFAULT_VALUE: &'static str = "";
131
132    /// Port for the temporary HTTP-01 challenge server (default 80).
133    /// Must be reachable from the internet on port 80. Not used with DNS-01.
134    pub const RWS_CONFIG_ACME_CHALLENGE_PORT: &'static str = "RWS_CONFIG_ACME_CHALLENGE_PORT";
135    pub const RWS_CONFIG_ACME_CHALLENGE_PORT_DEFAULT_VALUE: &'static str = "80";
136
137    /// Renew when fewer than this many days remain on the certificate (default 30).
138    pub const RWS_CONFIG_ACME_RENEW_BEFORE_DAYS: &'static str = "RWS_CONFIG_ACME_RENEW_BEFORE_DAYS";
139    /// Empty: renew at two-thirds of the certificate's lifetime (or the CA's
140    /// renewal window), not a fixed number of days before expiry.
141    pub const RWS_CONFIG_ACME_RENEW_BEFORE_DAYS_DEFAULT_VALUE: &'static str = "";
142    pub const RWS_CONFIG_ACME_PROFILE: &'static str = "RWS_CONFIG_ACME_PROFILE";
143
144    /// Path to persist the ACME account key between restarts (default `acme_account.key`).
145    pub const RWS_CONFIG_ACME_ACCOUNT_KEY_PATH: &'static str = "RWS_CONFIG_ACME_ACCOUNT_KEY_PATH";
146    pub const RWS_CONFIG_ACME_ACCOUNT_KEY_PATH_DEFAULT_VALUE: &'static str = "acme_account.key";
147
148    /// PEM file of extra root CAs to trust when talking to the ACME directory,
149    /// on top of the public Web PKI roots — for a private ACME CA (step-ca,
150    /// Pebble, an internal Boulder). Empty means public roots only.
151    pub const RWS_CONFIG_ACME_CA_FILE: &'static str = "RWS_CONFIG_ACME_CA_FILE";
152    pub const RWS_CONFIG_ACME_CA_FILE_DEFAULT_VALUE: &'static str = "";
153
154    /// Seconds between certificate-expiry checks in the renewal loop (default 12h).
155    /// After a failed renewal the next attempt comes after at most one hour.
156    pub const RWS_CONFIG_ACME_RENEW_CHECK_INTERVAL_SECS: &'static str = "RWS_CONFIG_ACME_RENEW_CHECK_INTERVAL_SECS";
157    pub const RWS_CONFIG_ACME_RENEW_CHECK_INTERVAL_SECS_DEFAULT_VALUE: &'static str = "43200";
158
159
160    pub const RWS_DEFAULT_IP: &'static str = "127.0.0.1";
161    pub const RWS_DEFAULT_PORT: &'static i32 = &7878;
162    pub const RWS_DEFAULT_THREAD_COUNT: &'static i32 = &200;
163    pub const RWS_DEFAULT_REQUEST_ALLOCATION_SIZE_IN_BYTES: &'static i64 = &10000;
164
165
166}
167
168pub fn bootstrap() {
169    read_system_environment_variables();
170    override_environment_variables_from_config(None);
171    override_environment_variables_from_command_line_args();
172    crate::server_config::invalidate();
173}
174
175pub fn set_default_values() {
176    println!("  Initializing default values");
177
178    let is_var_set = env::var(Config::RWS_CONFIG_IP).is_ok();
179    if !is_var_set {
180        env::set_var(Config::RWS_CONFIG_IP, Config::RWS_CONFIG_IP_DEFAULT_VALUE);
181        println!("    Default value  for '{}' is '{}'", Config::RWS_CONFIG_IP, Config::RWS_CONFIG_IP_DEFAULT_VALUE);
182    } else {
183        println!("    There is an environment variable  for '{}', default value won't be set", Config::RWS_CONFIG_IP);
184    }
185
186
187    let is_var_set = env::var(Config::RWS_CONFIG_PORT).is_ok();
188    if !is_var_set {
189        env::set_var(Config::RWS_CONFIG_PORT, Config::RWS_CONFIG_PORT_DEFAULT_VALUE);
190        println!("    Default value  for '{}' is '{}'", Config::RWS_CONFIG_PORT, Config::RWS_CONFIG_PORT_DEFAULT_VALUE);
191    } else {
192        println!("    There is an environment variable  for '{}', default value won't be set", Config::RWS_CONFIG_PORT);
193    }
194
195    let is_var_set = env::var(Config::RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES).is_ok();
196    if !is_var_set {
197        env::set_var(Config::RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES, Config::RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES_DEFAULT_VALUE);
198        println!("    Default value  for '{}' is '{}'", Config::RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES, Config::RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES_DEFAULT_VALUE);
199    } else {
200        println!("    There is an environment variable  for '{}', default value won't be set", Config::RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES);
201    }
202
203
204    let is_var_set = env::var(Config::RWS_CONFIG_THREAD_COUNT).is_ok();
205    if !is_var_set {
206        env::set_var(Config::RWS_CONFIG_THREAD_COUNT, Config::RWS_CONFIG_THREAD_COUNT_DEFAULT_VALUE);
207        println!("    Default value  for '{}' is '{}'", Config::RWS_CONFIG_THREAD_COUNT, Config::RWS_CONFIG_THREAD_COUNT_DEFAULT_VALUE);
208    } else {
209        println!("    There is an environment variable  for '{}', default value won't be set", Config::RWS_CONFIG_THREAD_COUNT);
210    }
211
212    let is_var_set = env::var(Config::RWS_CONFIG_CORS_ALLOW_ALL).is_ok();
213    if !is_var_set {
214        env::set_var(Config::RWS_CONFIG_CORS_ALLOW_ALL, Config::RWS_CONFIG_CORS_ALLOW_ALL_DEFAULT_VALUE);
215        println!("    Default value  for '{}' is '{}'", Config::RWS_CONFIG_CORS_ALLOW_ALL, Config::RWS_CONFIG_CORS_ALLOW_ALL_DEFAULT_VALUE);
216    } else {
217        println!("    There is an environment variable  for '{}', default value won't be set", Config::RWS_CONFIG_CORS_ALLOW_ALL);
218    }
219
220
221    let is_var_set = env::var(Config::RWS_CONFIG_CORS_ALLOW_ORIGINS).is_ok();
222    if !is_var_set {
223        env::set_var(Config::RWS_CONFIG_CORS_ALLOW_ORIGINS, Config::RWS_CONFIG_CORS_ALLOW_ORIGINS_DEFAULT_VALUE);
224        println!("    Default value  for '{}' is '{}'", Config::RWS_CONFIG_CORS_ALLOW_ORIGINS, Config::RWS_CONFIG_CORS_ALLOW_ORIGINS_DEFAULT_VALUE);
225    } else {
226        println!("    There is an environment variable  for '{}', default value won't be set", Config::RWS_CONFIG_CORS_ALLOW_ORIGINS);
227    }
228
229    let is_var_set = env::var(Config::RWS_CONFIG_CORS_ALLOW_CREDENTIALS).is_ok();
230    if !is_var_set {
231        env::set_var(Config::RWS_CONFIG_CORS_ALLOW_CREDENTIALS, Config::RWS_CONFIG_CORS_ALLOW_CREDENTIALS_DEFAULT_VALUE);
232        println!("    Default value  for '{}' is '{}'", Config::RWS_CONFIG_CORS_ALLOW_CREDENTIALS, Config::RWS_CONFIG_CORS_ALLOW_CREDENTIALS_DEFAULT_VALUE);
233    } else {
234        println!("    There is an environment variable  for '{}', default value won't be set", Config::RWS_CONFIG_CORS_ALLOW_CREDENTIALS);
235    }
236
237    let is_var_set = env::var(Config::RWS_CONFIG_CORS_ALLOW_HEADERS).is_ok();
238    if !is_var_set {
239        env::set_var(Config::RWS_CONFIG_CORS_ALLOW_HEADERS, Config::RWS_CONFIG_CORS_ALLOW_HEADERS_DEFAULT_VALUE);
240        println!("    Default value  for '{}' is '{}'", Config::RWS_CONFIG_CORS_ALLOW_HEADERS, Config::RWS_CONFIG_CORS_ALLOW_HEADERS_DEFAULT_VALUE);
241    } else {
242        println!("    There is an environment variable  for '{}', default value won't be set", Config::RWS_CONFIG_CORS_ALLOW_HEADERS);
243    }
244
245
246    let is_var_set = env::var(Config::RWS_CONFIG_CORS_ALLOW_METHODS).is_ok();
247    if !is_var_set {
248        env::set_var(Config::RWS_CONFIG_CORS_ALLOW_METHODS, Config::RWS_CONFIG_CORS_ALLOW_METHODS_DEFAULT_VALUE);
249        println!("    Default value  for '{}' is '{}'", Config::RWS_CONFIG_CORS_ALLOW_METHODS, Config::RWS_CONFIG_CORS_ALLOW_METHODS_DEFAULT_VALUE);
250    } else {
251        println!("    There is an environment variable  for '{}', default value won't be set", Config::RWS_CONFIG_CORS_ALLOW_METHODS);
252    }
253
254    let is_var_set = env::var(Config::RWS_CONFIG_CORS_EXPOSE_HEADERS).is_ok();
255    if !is_var_set {
256        env::set_var(Config::RWS_CONFIG_CORS_EXPOSE_HEADERS, Config::RWS_CONFIG_CORS_EXPOSE_HEADERS_DEFAULT_VALUE);
257        println!("    Default value  for '{}' is '{}'", Config::RWS_CONFIG_CORS_EXPOSE_HEADERS, Config::RWS_CONFIG_CORS_EXPOSE_HEADERS_DEFAULT_VALUE);
258    } else {
259        println!("    There is an environment variable  for '{}', default value won't be set", Config::RWS_CONFIG_CORS_EXPOSE_HEADERS);
260    }
261
262    let is_var_set = env::var(Config::RWS_CONFIG_CORS_MAX_AGE).is_ok();
263    if !is_var_set {
264        env::set_var(Config::RWS_CONFIG_CORS_MAX_AGE, Config::RWS_CONFIG_CORS_MAX_AGE_DEFAULT_VALUE);
265        println!("    Default value  for '{}' is '{}'", Config::RWS_CONFIG_CORS_MAX_AGE, Config::RWS_CONFIG_CORS_MAX_AGE_DEFAULT_VALUE);
266    } else {
267        println!("    There is an environment variable  for '{}', default value won't be set", Config::RWS_CONFIG_CORS_MAX_AGE);
268    }
269
270
271    let is_var_set = env::var(Config::RWS_CONFIG_TLS_CERT_FILE).is_ok();
272    if !is_var_set {
273        env::set_var(Config::RWS_CONFIG_TLS_CERT_FILE, Config::RWS_CONFIG_TLS_CERT_FILE_DEFAULT_VALUE);
274        println!("    Default value  for '{}' is '{}'", Config::RWS_CONFIG_TLS_CERT_FILE, Config::RWS_CONFIG_TLS_CERT_FILE_DEFAULT_VALUE);
275    } else {
276        println!("    There is an environment variable  for '{}', default value won't be set", Config::RWS_CONFIG_TLS_CERT_FILE);
277    }
278
279    let is_var_set = env::var(Config::RWS_CONFIG_TLS_KEY_FILE).is_ok();
280    if !is_var_set {
281        env::set_var(Config::RWS_CONFIG_TLS_KEY_FILE, Config::RWS_CONFIG_TLS_KEY_FILE_DEFAULT_VALUE);
282        println!("    Default value  for '{}' is '{}'", Config::RWS_CONFIG_TLS_KEY_FILE, Config::RWS_CONFIG_TLS_KEY_FILE_DEFAULT_VALUE);
283    } else {
284        println!("    There is an environment variable  for '{}', default value won't be set", Config::RWS_CONFIG_TLS_KEY_FILE);
285    }
286
287    let is_var_set = env::var(Config::RWS_CONFIG_HTTP_REDIRECT_PORT).is_ok();
288    if !is_var_set {
289        env::set_var(Config::RWS_CONFIG_HTTP_REDIRECT_PORT, Config::RWS_CONFIG_HTTP_REDIRECT_PORT_DEFAULT_VALUE);
290        println!("    Default value  for '{}' is '{}'", Config::RWS_CONFIG_HTTP_REDIRECT_PORT, Config::RWS_CONFIG_HTTP_REDIRECT_PORT_DEFAULT_VALUE);
291    } else {
292        println!("    There is an environment variable  for '{}', default value won't be set", Config::RWS_CONFIG_HTTP_REDIRECT_PORT);
293    }
294
295    let is_var_set = env::var(Config::RWS_CONFIG_LOG_FORMAT).is_ok();
296    if !is_var_set {
297        env::set_var(Config::RWS_CONFIG_LOG_FORMAT, Config::RWS_CONFIG_LOG_FORMAT_DEFAULT_VALUE);
298        println!("    Default value  for '{}' is '{}'", Config::RWS_CONFIG_LOG_FORMAT, Config::RWS_CONFIG_LOG_FORMAT_DEFAULT_VALUE);
299    } else {
300        println!("    There is an environment variable  for '{}', default value won't be set", Config::RWS_CONFIG_LOG_FORMAT);
301    }
302
303    crate::server_config::invalidate();
304    println!("  End of initializing default values\n");
305}
306
307
308pub fn get_ip_port_thread_count() -> (String, i32, i32) {
309    let mut ip : String = Config::RWS_CONFIG_IP_DEFAULT_VALUE.to_string();
310    let mut port: i32 = *Config::RWS_DEFAULT_PORT;
311    let mut thread_count: i32 = *Config::RWS_DEFAULT_THREAD_COUNT;
312
313    let boxed_ip = env::var(Config::RWS_CONFIG_IP);
314    if boxed_ip.is_ok() {
315        ip = boxed_ip.unwrap()
316    }
317
318    let boxed_port = env::var(Config::RWS_CONFIG_PORT);
319    if boxed_port.is_ok() {
320        let _port = boxed_port.unwrap();
321        let boxed_parse = _port.parse::<i32>();
322        if boxed_parse.is_ok() {
323            port = boxed_parse.unwrap();
324        } else {
325            eprintln!("unable to parse port value, expected number, got {}, variable: {}",
326                      _port, Config::RWS_CONFIG_PORT);
327        }
328    } else {
329        eprintln!("unable to parse port value, variable: {}", Config::RWS_CONFIG_PORT);
330    }
331
332    let boxed_thread_count = env::var(Config::RWS_CONFIG_THREAD_COUNT);
333    if boxed_thread_count.is_ok() {
334        let _thread_count = boxed_thread_count.unwrap();
335        let boxed_parse = _thread_count.parse();
336        if boxed_parse.is_ok() {
337            thread_count = boxed_parse.unwrap()
338        } else {
339            eprintln!("unable to parse thread count value, expected number, got {}, variable: {}",
340                      thread_count, Config::RWS_CONFIG_THREAD_COUNT);
341        }
342
343    } else {
344        eprintln!("unable to parse thread count value, variable: {}", Config::RWS_CONFIG_THREAD_COUNT);
345    }
346
347    (ip, port, thread_count)
348}
349
350/// Read all `[[virtual_host]]` entries from config / env vars.
351///
352/// Each entry must have `RWS_CONFIG_VIRTUAL_HOST_{N}_DOMAIN` set; reading
353/// stops at the first missing index.  `cert_file` and `key_file` default to
354/// empty string if omitted.
355pub fn get_virtual_hosts() -> Vec<VirtualHostConfig> {
356    let mut hosts = Vec::new();
357    let mut i = 0usize;
358    loop {
359        match env::var(format!("RWS_CONFIG_VIRTUAL_HOST_{}_DOMAIN", i)) {
360            Err(_) => break,
361            Ok(domain) => {
362                let cert_file = env::var(format!("RWS_CONFIG_VIRTUAL_HOST_{}_CERT_FILE", i))
363                    .unwrap_or_default();
364                let key_file = env::var(format!("RWS_CONFIG_VIRTUAL_HOST_{}_KEY_FILE", i))
365                    .unwrap_or_default();
366                hosts.push(VirtualHostConfig { domain, cert_file, key_file });
367                i += 1;
368            }
369        }
370    }
371    hosts
372}
373
374pub fn get_request_allocation_size() -> i64 {
375    let mut request_allocation_size: i64 = *Config::RWS_DEFAULT_REQUEST_ALLOCATION_SIZE_IN_BYTES;
376
377    let boxed_port = env::var(Config::RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES);
378    if boxed_port.is_ok() {
379        let _request_allocation_size = boxed_port.unwrap();
380        let boxed_parse = _request_allocation_size.parse::<i64>();
381        if boxed_parse.is_ok() {
382            request_allocation_size = boxed_parse.unwrap();
383        } else {
384            eprintln!("unable to parse port value, expected number, got {}, variable: {}",
385                      _request_allocation_size, Config::RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES);
386        }
387    } else {
388        eprintln!("unable to parse request allocation size value, variable: {}", Config::RWS_CONFIG_REQUEST_ALLOCATION_SIZE_IN_BYTES);
389    }
390
391
392    request_allocation_size
393}
394
395/// Maximum accepted request body size in bytes. `0` means unlimited (the default —
396/// enforcing a cap is opt-in). Read fresh on every call, so `RWS_CONFIG_MAX_BODY_SIZE_IN_BYTES`
397/// takes effect immediately, the same as [`get_request_allocation_size`].
398pub fn get_max_body_size() -> u64 {
399    env::var(Config::RWS_CONFIG_MAX_BODY_SIZE_IN_BYTES)
400        .ok()
401        .and_then(|v| v.parse::<u64>().ok())
402        .unwrap_or(0)
403}
404
405/// The configured SPA-fallback file (e.g. `"index.html"`), or `None` if
406/// `RWS_CONFIG_SPA_FALLBACK` is unset/empty (the default — disabled). Read
407/// fresh on every call, so it can be toggled via `rws.config.toml` + `SIGHUP`
408/// without a restart, the same as [`get_max_body_size`].
409pub fn get_spa_fallback() -> Option<String> {
410    env::var(Config::RWS_CONFIG_SPA_FALLBACK).ok().filter(|v| !v.is_empty())
411}
412
413/// Path prefixes that never receive the SPA fallback (`RWS_CONFIG_SPA_FALLBACK_EXCLUDE_PREFIXES`,
414/// comma-separated), even when the fallback is configured. Empty (default) excludes nothing.
415pub fn get_spa_fallback_exclude_prefixes() -> Vec<String> {
416    env::var(Config::RWS_CONFIG_SPA_FALLBACK_EXCLUDE_PREFIXES)
417        .ok()
418        .map(|v| v.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect())
419        .unwrap_or_default()
420}
421