1use std::net::SocketAddr;
9use std::path::PathBuf;
10
11use crate::error::Error;
12
13pub trait EnvReader {
17 fn get(&self, key: &str) -> Option<String>;
18}
19
20pub struct ProcessEnv;
22
23impl EnvReader for ProcessEnv {
24 fn get(&self, key: &str) -> Option<String> {
25 std::env::var(key).ok()
26 }
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct Env {
34 pub data_dir: PathBuf,
36 pub config_dir: PathBuf,
38 pub log_level: String,
41 pub bind_ipv4: bool,
43 pub bind_ipv6: bool,
45 pub sec_max_header_bytes: u32,
47 pub sec_max_headers_count: u32,
49 pub sec_header_timeout_secs: u32,
51 pub sec_max_conn_per_ip: u32,
53 pub sec_max_total_conns: u32,
55 pub bind_max_attempts: u32,
57 pub bind_backoff_initial_ms: u32,
59 pub bind_backoff_max_ms: u32,
61 pub force_cancel_grace_secs: u32,
64 pub drain_timeout_secs: u32,
67 pub boot_health_timeout_secs: u32,
70 pub mgmt_unix: PathBuf,
72 pub mgmt_http_bind: Option<SocketAddr>,
75 pub mgmt_http_token: Option<String>,
78}
79
80impl Env {
81 pub fn from_process_env() -> Result<Self, Error> {
87 Self::from_reader(&ProcessEnv)
88 }
89
90 pub fn from_reader<R: EnvReader>(r: &R) -> Result<Self, Error> {
95 Ok(Self {
96 data_dir: r
97 .get("VANE_DATA_DIR")
98 .map_or_else(|| PathBuf::from("/var/lib/vaned"), PathBuf::from),
99 config_dir: r
100 .get("VANE_CONFIG_DIR")
101 .map_or_else(|| PathBuf::from("/etc/vaned"), PathBuf::from),
102 log_level: r.get("VANE_LOG_LEVEL").unwrap_or_else(|| "info".to_string()),
103 bind_ipv4: parse_bool_default_true(r, "VANE_BIND_IPV4")?,
104 bind_ipv6: parse_bool_default_true(r, "VANE_BIND_IPV6")?,
105 sec_max_header_bytes: parse_u32_default(r, "VANE_SEC_MAX_HEADER_BYTES", 65_536)?,
106 sec_max_headers_count: parse_u32_default(r, "VANE_SEC_MAX_HEADERS_COUNT", 100)?,
107 sec_header_timeout_secs: parse_u32_default(r, "VANE_SEC_HEADER_TIMEOUT", 30)?,
108 sec_max_conn_per_ip: parse_u32_default(r, "VANE_SEC_MAX_CONN_PER_IP", 100)?,
109 sec_max_total_conns: parse_u32_default(r, "VANE_SEC_MAX_TOTAL_CONNS", 65_536)?,
110 bind_max_attempts: parse_u32_default(r, "VANE_BIND_MAX_ATTEMPTS", 10)?,
111 bind_backoff_initial_ms: parse_u32_default(r, "VANE_BIND_BACKOFF_INITIAL_MS", 100)?,
112 bind_backoff_max_ms: parse_u32_default(r, "VANE_BIND_BACKOFF_MAX_MS", 5_000)?,
113 force_cancel_grace_secs: parse_u32_default(r, "VANE_FORCE_CANCEL_GRACE_SECS", 5)?,
114 drain_timeout_secs: parse_u32_default(r, "VANE_DRAIN_TIMEOUT_SECS", 30)?,
115 boot_health_timeout_secs: parse_u32_default(r, "VANE_BOOT_HEALTH_TIMEOUT_SECS", 60)?,
116 mgmt_unix: r
117 .get("VANE_MGMT_UNIX")
118 .map_or_else(|| PathBuf::from("/var/run/vaned.sock"), PathBuf::from),
119 mgmt_http_bind: parse_socket_addr_optional(r, "VANE_MGMT_HTTP_BIND")?,
120 mgmt_http_token: r.get("VANE_MGMT_HTTP_TOKEN").filter(|s| !s.is_empty()),
121 })
122 }
123}
124
125fn parse_bool_default_true<R: EnvReader>(r: &R, key: &str) -> Result<bool, Error> {
126 match r.get(key).as_deref() {
127 None | Some("" | "1") => Ok(true),
128 Some("0") => Ok(false),
129 Some(other) => Err(Error::compile(format!("{key} must be \"0\" or \"1\", got {other:?}"))),
130 }
131}
132
133fn parse_u32_default<R: EnvReader>(r: &R, key: &str, default: u32) -> Result<u32, Error> {
134 match r.get(key).filter(|s| !s.is_empty()) {
135 None => Ok(default),
136 Some(s) => s.parse::<u32>().map_err(|e| Error::compile(format!("{key}: {e} ({s:?})"))),
137 }
138}
139
140fn parse_socket_addr_optional<R: EnvReader>(r: &R, key: &str) -> Result<Option<SocketAddr>, Error> {
141 match r.get(key).filter(|s| !s.is_empty()) {
142 None => Ok(None),
143 Some(s) => s
144 .parse::<SocketAddr>()
145 .map(Some)
146 .map_err(|e| Error::compile(format!("{key}: invalid SocketAddr {s:?}: {e}"))),
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use std::collections::HashMap;
153
154 use super::*;
155
156 struct FakeEnv(HashMap<&'static str, &'static str>);
157
158 impl FakeEnv {
159 fn empty() -> Self {
160 Self(HashMap::new())
161 }
162
163 fn with(pairs: &[(&'static str, &'static str)]) -> Self {
164 Self(pairs.iter().copied().collect())
165 }
166 }
167
168 impl EnvReader for FakeEnv {
169 fn get(&self, key: &str) -> Option<String> {
170 self.0.get(key).map(|s| (*s).to_string())
171 }
172 }
173
174 #[test]
175 fn env_defaults_when_all_unset() {
176 let env = Env::from_reader(&FakeEnv::empty()).expect("defaults");
177 assert_eq!(env.data_dir, PathBuf::from("/var/lib/vaned"));
178 assert_eq!(env.config_dir, PathBuf::from("/etc/vaned"));
179 assert_eq!(env.log_level, "info");
180 assert!(env.bind_ipv4);
181 assert!(env.bind_ipv6);
182 assert_eq!(env.sec_max_header_bytes, 65_536);
183 assert_eq!(env.sec_max_headers_count, 100);
184 assert_eq!(env.sec_header_timeout_secs, 30);
185 assert_eq!(env.sec_max_conn_per_ip, 100);
186 assert_eq!(env.sec_max_total_conns, 65_536);
187 assert_eq!(env.mgmt_unix, PathBuf::from("/var/run/vaned.sock"));
188 assert!(env.mgmt_http_bind.is_none());
189 assert!(env.mgmt_http_token.is_none());
190 }
191
192 #[test]
193 fn env_bind_ipv4_zero_yields_false() {
194 let env = Env::from_reader(&FakeEnv::with(&[("VANE_BIND_IPV4", "0")])).expect("ok");
195 assert!(!env.bind_ipv4);
196 }
197
198 #[test]
199 fn env_bind_ipv4_one_yields_true() {
200 let env = Env::from_reader(&FakeEnv::with(&[("VANE_BIND_IPV4", "1")])).expect("ok");
201 assert!(env.bind_ipv4);
202 }
203
204 #[test]
205 fn env_bind_ipv4_empty_string_falls_back_to_default() {
206 let env = Env::from_reader(&FakeEnv::with(&[("VANE_BIND_IPV4", "")])).expect("ok");
209 assert!(env.bind_ipv4, "empty string falls back to default true");
210 }
211
212 #[test]
213 fn env_bind_ipv4_invalid_returns_compile_error_naming_var() {
214 let err = Env::from_reader(&FakeEnv::with(&[("VANE_BIND_IPV4", "yes")])).expect_err("invalid");
215 let msg = err.to_string();
216 assert!(msg.contains("VANE_BIND_IPV4"), "error names the var: {msg}");
217 assert!(msg.contains("\"yes\""), "error quotes the offending value: {msg}");
218 }
219
220 #[test]
221 fn env_sec_integers_parse() {
222 let env = Env::from_reader(&FakeEnv::with(&[
223 ("VANE_SEC_MAX_HEADER_BYTES", "32768"),
224 ("VANE_SEC_MAX_HEADERS_COUNT", "64"),
225 ("VANE_SEC_HEADER_TIMEOUT", "10"),
226 ("VANE_SEC_MAX_CONN_PER_IP", "500"),
227 ]))
228 .expect("ok");
229 assert_eq!(env.sec_max_header_bytes, 32_768);
230 assert_eq!(env.sec_max_headers_count, 64);
231 assert_eq!(env.sec_header_timeout_secs, 10);
232 assert_eq!(env.sec_max_conn_per_ip, 500);
233 }
234
235 #[test]
236 fn env_sec_invalid_integer_errors() {
237 let err = Env::from_reader(&FakeEnv::with(&[("VANE_SEC_MAX_HEADER_BYTES", "huge")]))
238 .expect_err("non-int rejected");
239 let msg = err.to_string();
240 assert!(msg.contains("VANE_SEC_MAX_HEADER_BYTES"), "{msg}");
241 }
242
243 #[test]
244 fn env_sec_negative_integer_errors() {
245 let err = Env::from_reader(&FakeEnv::with(&[("VANE_SEC_MAX_CONN_PER_IP", "-1")]))
247 .expect_err("negative rejected");
248 assert!(err.to_string().contains("VANE_SEC_MAX_CONN_PER_IP"));
249 }
250
251 #[test]
252 fn env_mgmt_http_bind_empty_string_yields_none() {
253 let env = Env::from_reader(&FakeEnv::with(&[("VANE_MGMT_HTTP_BIND", "")])).expect("ok");
254 assert!(env.mgmt_http_bind.is_none());
255 }
256
257 #[test]
258 fn env_mgmt_http_bind_valid_socketaddr_parses() {
259 let env =
260 Env::from_reader(&FakeEnv::with(&[("VANE_MGMT_HTTP_BIND", "127.0.0.1:9000")])).expect("ok");
261 let addr = env.mgmt_http_bind.expect("set");
262 assert_eq!(addr.port(), 9000);
263 }
264
265 #[test]
266 fn env_mgmt_http_bind_invalid_errors() {
267 let err = Env::from_reader(&FakeEnv::with(&[("VANE_MGMT_HTTP_BIND", "not-an-addr")]))
268 .expect_err("bad addr");
269 let msg = err.to_string();
270 assert!(msg.contains("VANE_MGMT_HTTP_BIND"), "{msg}");
271 assert!(msg.contains("\"not-an-addr\""), "error quotes offending value: {msg}");
272 }
273
274 #[test]
275 fn env_mgmt_http_token_empty_string_yields_none() {
276 let env = Env::from_reader(&FakeEnv::with(&[("VANE_MGMT_HTTP_TOKEN", "")])).expect("ok");
277 assert!(env.mgmt_http_token.is_none());
278 }
279
280 #[test]
281 fn env_log_level_passes_through_verbatim() {
282 for level in ["debug", "warn", "trace", "vane=info,hyper=warn"] {
283 let env = Env::from_reader(&FakeEnv::with(&[("VANE_LOG_LEVEL", level)])).expect("ok");
284 assert_eq!(env.log_level, level);
285 }
286 }
287
288 #[test]
289 fn env_data_and_config_dirs_passed_through_as_pathbuf() {
290 let env = Env::from_reader(&FakeEnv::with(&[
291 ("VANE_DATA_DIR", "/srv/vane/data"),
292 ("VANE_CONFIG_DIR", "/srv/vane/etc"),
293 ]))
294 .expect("ok");
295 assert_eq!(env.data_dir, PathBuf::from("/srv/vane/data"));
296 assert_eq!(env.config_dir, PathBuf::from("/srv/vane/etc"));
297 }
298}