ferron/util/
combine_config.rs

1use std::{net::IpAddr, sync::Arc};
2
3use yaml_rust2::{yaml::Hash, Yaml};
4
5use crate::ferron_util::{
6  ip_match::ip_match, match_hostname::match_hostname, match_location::match_location,
7};
8
9pub fn combine_config(
10  config: Arc<Yaml>,
11  hostname: Option<&str>,
12  client_ip: IpAddr,
13  path: &str,
14) -> Option<Yaml> {
15  let global_config = config["global"].as_hash();
16  let combined_config = global_config.cloned();
17
18  if let Some(host_config) = config["hosts"].as_vec() {
19    for host in host_config {
20      if let Some(host_hashtable) = host.as_hash() {
21        let domain_matched = host_hashtable
22          .get(&Yaml::String("domain".to_string()))
23          .and_then(Yaml::as_str)
24          .map(|domain| match_hostname(Some(domain), hostname))
25          .unwrap_or(true);
26
27        let ip_matched = host_hashtable
28          .get(&Yaml::String("ip".to_string()))
29          .and_then(Yaml::as_str)
30          .map(|ip| ip_match(ip, client_ip))
31          .unwrap_or(true);
32
33        if domain_matched && ip_matched {
34          return Some(merge_host_configs(combined_config, host_hashtable, path));
35        }
36      }
37    }
38  }
39
40  combined_config.map(Yaml::Hash)
41}
42
43fn merge_host_configs(global: Option<Hash>, host: &Hash, path: &str) -> Yaml {
44  let mut merged = global.unwrap_or_default();
45  let mut locations = None;
46
47  for (key, value) in host {
48    if let Some(key) = key.as_str() {
49      if key == "locations" {
50        if let Some(obtained_locations) = value.as_vec() {
51          locations = Some(obtained_locations);
52        }
53      } else {
54        match value {
55          Yaml::Array(host_array) => {
56            merged
57              .entry(Yaml::String(key.to_string()))
58              .and_modify(|global_val| {
59                if let Yaml::Array(global_array) = global_val {
60                  global_array.extend(host_array.clone());
61                } else {
62                  *global_val = Yaml::Array(host_array.clone());
63                }
64              })
65              .or_insert_with(|| Yaml::Array(host_array.clone()));
66          }
67          Yaml::Hash(host_hash) => {
68            merged
69              .entry(Yaml::String(key.to_string()))
70              .and_modify(|global_val| {
71                if let Yaml::Hash(global_hash) = global_val {
72                  for (k, v) in host_hash {
73                    global_hash.insert(k.clone(), v.clone());
74                  }
75                } else {
76                  *global_val = Yaml::Hash(host_hash.clone());
77                }
78              })
79              .or_insert_with(|| Yaml::Hash(host_hash.clone()));
80          }
81          _ => {
82            merged.insert(Yaml::String(key.to_string()), value.clone());
83          }
84        }
85      }
86    }
87  }
88
89  if let Some(locations) = locations {
90    if let Ok(decoded_path) = urlencoding::decode(path) {
91      for location in locations {
92        if let Some(location_hashtable) = location.as_hash() {
93          let path_matched = location_hashtable
94            .get(&Yaml::String("path".to_string()))
95            .and_then(Yaml::as_str)
96            .map(|path_match| match_location(path_match, &decoded_path))
97            .unwrap_or(true);
98
99          if path_matched {
100            return merge_location_configs(Some(merged), location_hashtable);
101          }
102        }
103      }
104    }
105  }
106
107  Yaml::Hash(merged)
108}
109
110fn merge_location_configs(global: Option<Hash>, location: &Hash) -> Yaml {
111  let mut merged = global.unwrap_or_default();
112
113  for (key, value) in location {
114    if let Some(key) = key.as_str() {
115      match value {
116        Yaml::Array(host_array) => {
117          merged
118            .entry(Yaml::String(key.to_string()))
119            .and_modify(|global_val| {
120              if let Yaml::Array(global_array) = global_val {
121                global_array.extend(host_array.clone());
122              } else {
123                *global_val = Yaml::Array(host_array.clone());
124              }
125            })
126            .or_insert_with(|| Yaml::Array(host_array.clone()));
127        }
128        Yaml::Hash(host_hash) => {
129          merged
130            .entry(Yaml::String(key.to_string()))
131            .and_modify(|global_val| {
132              if let Yaml::Hash(global_hash) = global_val {
133                for (k, v) in host_hash {
134                  global_hash.insert(k.clone(), v.clone());
135                }
136              } else {
137                *global_val = Yaml::Hash(host_hash.clone());
138              }
139            })
140            .or_insert_with(|| Yaml::Hash(host_hash.clone()));
141        }
142        _ => {
143          merged.insert(Yaml::String(key.to_string()), value.clone());
144        }
145      }
146    }
147  }
148
149  Yaml::Hash(merged)
150}
151
152#[cfg(test)]
153mod tests {
154  use super::*;
155  use std::net::{IpAddr, Ipv4Addr};
156  use yaml_rust2::{Yaml, YamlLoader};
157
158  fn create_test_config() -> Arc<Yaml> {
159    let yaml_str = r#"
160        global:
161          key1:
162            - global_value1
163          key2:
164            - global_value2
165        hosts:
166          - domain: example.com
167            ip: 192.168.1.1
168            key1:
169              - host_value1
170            key2:
171              - host_value2
172          - domain: test.com
173            ip: 192.168.1.2
174            key3:
175              - host_value3
176        "#;
177
178    let docs = YamlLoader::load_from_str(yaml_str).unwrap();
179    Arc::new(docs[0].clone())
180  }
181
182  #[test]
183  fn test_combine_config_with_matching_hostname_and_ip() {
184    let config = create_test_config();
185    let hostname = Some("example.com");
186    let client_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
187
188    let result = combine_config(config, hostname, client_ip, "/");
189    assert!(result.is_some());
190
191    let result_yaml = result.unwrap();
192    let result_hash = result_yaml.as_hash().unwrap();
193
194    assert_eq!(
195      result_hash
196        .get(&Yaml::String("key1".to_string()))
197        .unwrap()
198        .as_vec()
199        .unwrap()
200        .len(),
201      2
202    );
203    assert_eq!(
204      result_hash
205        .get(&Yaml::String("key2".to_string()))
206        .unwrap()
207        .as_vec()
208        .unwrap()
209        .len(),
210      2
211    );
212  }
213
214  #[test]
215  fn test_combine_config_with_non_matching_hostname() {
216    let config = create_test_config();
217    let hostname = Some("nonexistent.com");
218    let client_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
219
220    let result = combine_config(config, hostname, client_ip, "/");
221    assert!(result
222      .unwrap()
223      .as_hash()
224      .unwrap()
225      .get(&Yaml::String(String::from("key3")))
226      .is_none());
227  }
228
229  #[test]
230  fn test_combine_config_with_non_matching_ip() {
231    let config = create_test_config();
232    let hostname = Some("example.com");
233    let client_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2));
234
235    let result = combine_config(config, hostname, client_ip, "/");
236    assert!(result
237      .unwrap()
238      .as_hash()
239      .unwrap()
240      .get(&Yaml::String(String::from("key3")))
241      .is_none());
242  }
243
244  #[test]
245  fn test_combine_config_with_global_only() {
246    let yaml_str = r#"
247        global:
248          key1: value1
249          key2:
250            - global_value2
251        hosts: []
252        "#;
253
254    let docs = YamlLoader::load_from_str(yaml_str).unwrap();
255    let config = Arc::new(docs[0].clone());
256    let hostname = None;
257    let client_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
258
259    let result = combine_config(config, hostname, client_ip, "/");
260    assert!(result.is_some());
261
262    let result_yaml = result.unwrap();
263    let result_hash = result_yaml.as_hash().unwrap();
264
265    assert_eq!(
266      result_hash
267        .get(&Yaml::String("key1".to_string()))
268        .unwrap()
269        .as_str()
270        .unwrap(),
271      "value1"
272    );
273    assert_eq!(
274      result_hash
275        .get(&Yaml::String("key2".to_string()))
276        .unwrap()
277        .as_vec()
278        .unwrap()
279        .len(),
280      1
281    );
282  }
283
284  #[test]
285  fn test_combine_config_with_empty_host_config() {
286    let yaml_str = r#"
287        global:
288          key1: value1
289          key2:
290            - global_value2
291        hosts: []
292        "#;
293
294    let docs = YamlLoader::load_from_str(yaml_str).unwrap();
295    let config_yaml = docs[0].clone();
296
297    let hostname = Some("example.com");
298    let client_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
299
300    let result = combine_config(Arc::new(config_yaml), hostname, client_ip, "/");
301    assert!(result.is_some());
302
303    let result_yaml = result.unwrap();
304
305    assert_eq!(result_yaml["key1"].as_str().unwrap(), "value1");
306    assert_eq!(result_yaml["key2"].as_vec().unwrap().len(), 1);
307  }
308
309  #[test]
310  fn test_combine_config_with_path_match() {
311    let yaml_str = r#"
312        global:
313          key1:
314            - global_value1
315          key2:
316            - global_value2
317        hosts:
318          - domain: example.com
319            ip: 192.168.1.1
320            locations:
321              - path: /test
322                key3:
323                  - location_value
324        "#;
325
326    let docs = YamlLoader::load_from_str(yaml_str).unwrap();
327    let config_yaml = docs[0].clone();
328
329    let hostname = Some("example.com");
330    let client_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
331
332    let result = combine_config(Arc::new(config_yaml), hostname, client_ip, "/test");
333    assert!(result.is_some());
334
335    let result_yaml = result.unwrap();
336
337    assert_eq!(result_yaml["key3"].as_vec().unwrap().len(), 1);
338  }
339}