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