ferron/util/
env_config.rs

1use std::env;
2use yaml_rust2::Yaml;
3
4/// Apply environment variable overrides with prefix FERRON_ to the provided YAML configuration.
5/// Maps directly to the fields in the global section of ferron.yaml.
6pub fn apply_env_vars_to_config(yaml_config: &mut Yaml) {
7  let yaml_config_hash = match yaml_config.as_mut_hash() {
8    Some(h) => h,
9    None => return,
10  };
11
12  let global_yaml = match yaml_config_hash.get_mut(&Yaml::String("global".to_string())) {
13    Some(y) => y,
14    None => {
15      yaml_config_hash.insert(
16        Yaml::String("global".to_owned()),
17        Yaml::Hash(yaml_rust2::yaml::Hash::new()),
18      );
19      match yaml_config_hash.get_mut(&Yaml::String("global".to_string())) {
20        Some(y) => y,
21        None => return,
22      }
23    }
24  };
25
26  let global_hash = match global_yaml.as_mut_hash() {
27    Some(h) => h,
28    None => return,
29  };
30
31  // Port settings
32  if let Ok(port_val) = env::var("FERRON_PORT") {
33    if let Ok(port) = port_val.parse::<i64>() {
34      global_hash.insert(Yaml::String("port".into()), Yaml::Integer(port));
35    } else {
36      // Handle port as string for address:port format
37      global_hash.insert(Yaml::String("port".into()), Yaml::String(port_val));
38    }
39  }
40
41  if let Ok(sport_val) = env::var("FERRON_SPORT") {
42    if let Ok(sport) = sport_val.parse::<i64>() {
43      global_hash.insert(Yaml::String("sport".into()), Yaml::Integer(sport));
44    } else {
45      // Handle sport as string for address:port format
46      global_hash.insert(Yaml::String("sport".into()), Yaml::String(sport_val));
47    }
48  }
49
50  // HTTP/2 settings - only add if at least one HTTP/2 variable is set
51  let http2_initial_window = env::var("FERRON_HTTP2_INITIAL_WINDOW_SIZE")
52    .ok()
53    .and_then(|val| val.parse::<i64>().ok());
54  let http2_max_frame = env::var("FERRON_HTTP2_MAX_FRAME_SIZE")
55    .ok()
56    .and_then(|val| val.parse::<i64>().ok());
57  let http2_max_streams = env::var("FERRON_HTTP2_MAX_CONCURRENT_STREAMS")
58    .ok()
59    .and_then(|val| val.parse::<i64>().ok());
60  let http2_max_header = env::var("FERRON_HTTP2_MAX_HEADER_LIST_SIZE")
61    .ok()
62    .and_then(|val| val.parse::<i64>().ok());
63  let http2_enable_connect = env::var("FERRON_HTTP2_ENABLE_CONNECT_PROTOCOL")
64    .ok()
65    .map(|val| matches!(val.to_ascii_lowercase().as_str(), "1" | "true" | "yes"));
66
67  // Only create the http2Settings hash if at least one setting is present
68  if http2_initial_window.is_some()
69    || http2_max_frame.is_some()
70    || http2_max_streams.is_some()
71    || http2_max_header.is_some()
72    || http2_enable_connect.is_some()
73  {
74    let mut http2_hash = yaml_rust2::yaml::Hash::new();
75
76    // Add settings if they exist
77    if let Some(size) = http2_initial_window {
78      http2_hash.insert(
79        Yaml::String("initialWindowSize".into()),
80        Yaml::Integer(size),
81      );
82    }
83
84    if let Some(size) = http2_max_frame {
85      http2_hash.insert(Yaml::String("maxFrameSize".into()), Yaml::Integer(size));
86    }
87
88    if let Some(streams) = http2_max_streams {
89      http2_hash.insert(
90        Yaml::String("maxConcurrentStreams".into()),
91        Yaml::Integer(streams),
92      );
93    }
94
95    if let Some(size) = http2_max_header {
96      http2_hash.insert(
97        Yaml::String("maxHeaderListSize".into()),
98        Yaml::Integer(size),
99      );
100    }
101
102    if let Some(enable) = http2_enable_connect {
103      http2_hash.insert(
104        Yaml::String("enableConnectProtocol".into()),
105        Yaml::Boolean(enable),
106      );
107    }
108
109    // Only add the http2Settings to global if we have settings
110    if !http2_hash.is_empty() {
111      global_hash.insert(Yaml::String("http2Settings".into()), Yaml::Hash(http2_hash));
112    }
113  }
114
115  // Log paths
116  if let Ok(path) = env::var("FERRON_LOG_FILE_PATH") {
117    global_hash.insert(Yaml::String("logFilePath".into()), Yaml::String(path));
118  }
119
120  if let Ok(path) = env::var("FERRON_ERROR_LOG_FILE_PATH") {
121    global_hash.insert(Yaml::String("errorLogFilePath".into()), Yaml::String(path));
122  }
123
124  // TLS/HTTPS settings
125  if let Ok(cert) = env::var("FERRON_CERT") {
126    global_hash.insert(Yaml::String("cert".into()), Yaml::String(cert));
127  }
128
129  if let Ok(key) = env::var("FERRON_KEY") {
130    global_hash.insert(Yaml::String("key".into()), Yaml::String(key));
131  }
132
133  if let Ok(min_ver) = env::var("FERRON_TLS_MIN_VERSION") {
134    global_hash.insert(Yaml::String("tlsMinVersion".into()), Yaml::String(min_ver));
135  }
136
137  if let Ok(max_ver) = env::var("FERRON_TLS_MAX_VERSION") {
138    global_hash.insert(Yaml::String("tlsMaxVersion".into()), Yaml::String(max_ver));
139  }
140
141  // Boolean settings
142  if let Ok(v) = env::var("FERRON_SECURE") {
143    let enable = matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes");
144    global_hash.insert(Yaml::String("secure".into()), Yaml::Boolean(enable));
145  }
146
147  if let Ok(v) = env::var("FERRON_ENABLE_HTTP2") {
148    let enable = matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes");
149    global_hash.insert(Yaml::String("enableHTTP2".into()), Yaml::Boolean(enable));
150  }
151
152  if let Ok(v) = env::var("FERRON_ENABLE_HTTP3") {
153    let enable = matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes");
154    global_hash.insert(Yaml::String("enableHTTP3".into()), Yaml::Boolean(enable));
155  }
156
157  if let Ok(v) = env::var("FERRON_DISABLE_NON_ENCRYPTED_SERVER") {
158    let enable = matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes");
159    global_hash.insert(
160      Yaml::String("disableNonEncryptedServer".into()),
161      Yaml::Boolean(enable),
162    );
163  }
164
165  if let Ok(v) = env::var("FERRON_ENABLE_OCSP_STAPLING") {
166    let enable = matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes");
167    global_hash.insert(
168      Yaml::String("enableOCSPStapling".into()),
169      Yaml::Boolean(enable),
170    );
171  }
172
173  if let Ok(v) = env::var("FERRON_ENABLE_DIRECTORY_LISTING") {
174    let enable = matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes");
175    global_hash.insert(
176      Yaml::String("enableDirectoryListing".into()),
177      Yaml::Boolean(enable),
178    );
179  }
180
181  if let Ok(v) = env::var("FERRON_ENABLE_COMPRESSION") {
182    let enable = matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes");
183    global_hash.insert(
184      Yaml::String("enableCompression".into()),
185      Yaml::Boolean(enable),
186    );
187  }
188
189  // Module loading
190  if let Ok(list) = env::var("FERRON_LOAD_MODULES") {
191    let arr: Vec<Yaml> = list
192      .split(',')
193      .filter_map(|s| {
194        let t = s.trim();
195        if t.is_empty() {
196          None
197        } else {
198          Some(Yaml::String(t.to_string()))
199        }
200      })
201      .collect();
202    if !arr.is_empty() {
203      global_hash.insert(Yaml::String("loadModules".into()), Yaml::Array(arr));
204    }
205  }
206
207  // IP blocklist
208  if let Ok(list) = env::var("FERRON_BLOCKLIST") {
209    let arr: Vec<Yaml> = list
210      .split(',')
211      .filter_map(|s| {
212        let t = s.trim();
213        if t.is_empty() {
214          None
215        } else {
216          Some(Yaml::String(t.to_string()))
217        }
218      })
219      .collect();
220    if !arr.is_empty() {
221      global_hash.insert(Yaml::String("blocklist".into()), Yaml::Array(arr));
222    }
223  }
224
225  // SNI configuration
226  if let Ok(sni_hosts) = env::var("FERRON_SNI_HOSTS") {
227    let hosts: Vec<&str> = sni_hosts
228      .split(',')
229      .map(|s| s.trim())
230      .filter(|s| !s.is_empty())
231      .collect();
232
233    if !hosts.is_empty() {
234      let mut sni_hash = yaml_rust2::yaml::Hash::new();
235
236      for host in hosts {
237        let cert_env_var = format!(
238          "FERRON_SNI_{}_CERT",
239          host
240            .replace('.', "_")
241            .replace('*', "WILDCARD")
242            .to_uppercase()
243        );
244        let key_env_var = format!(
245          "FERRON_SNI_{}_KEY",
246          host
247            .replace('.', "_")
248            .replace('*', "WILDCARD")
249            .to_uppercase()
250        );
251
252        if let (Ok(cert), Ok(key)) = (env::var(&cert_env_var), env::var(&key_env_var)) {
253          let mut host_hash = yaml_rust2::yaml::Hash::new();
254          host_hash.insert(Yaml::String("cert".into()), Yaml::String(cert));
255          host_hash.insert(Yaml::String("key".into()), Yaml::String(key));
256          sni_hash.insert(Yaml::String(host.to_string()), Yaml::Hash(host_hash));
257        }
258      }
259
260      if !sni_hash.is_empty() {
261        global_hash.insert(Yaml::String("sni".into()), Yaml::Hash(sni_hash));
262      }
263    }
264  }
265
266  // Environment variables for processes
267  if let Ok(env_list) = env::var("FERRON_ENV_VARS") {
268    let vars: Vec<&str> = env_list
269      .split(',')
270      .map(|s| s.trim())
271      .filter(|s| !s.is_empty())
272      .collect();
273
274    if !vars.is_empty() {
275      let mut env_hash = yaml_rust2::yaml::Hash::new();
276
277      for var_name in vars {
278        let env_var = format!("FERRON_ENV_{}", var_name.to_uppercase());
279
280        if let Ok(value) = env::var(&env_var) {
281          env_hash.insert(Yaml::String(var_name.to_string()), Yaml::String(value));
282        }
283      }
284
285      if !env_hash.is_empty() {
286        global_hash.insert(
287          Yaml::String("environmentVariables".into()),
288          Yaml::Hash(env_hash),
289        );
290      }
291    }
292  }
293}
294
295/// Return messages describing which env vars starting with FERRON_ are set (for logging).
296pub fn log_env_var_overrides() -> Vec<String> {
297  env::vars()
298    .filter(|(k, _)| k.starts_with("FERRON_"))
299    .map(|(k, v)| format!("Environment override: {k}={v}"))
300    .collect()
301}