ferron/config/
mod.rs

1pub mod adapters;
2pub mod processing;
3
4pub use ferron_common::config::*;
5use ferron_common::util::parse_q_value_header;
6
7use std::collections::{HashMap, HashSet};
8use std::error::Error;
9use std::net::IpAddr;
10use std::sync::Arc;
11use std::{collections::BTreeMap, fmt::Debug};
12
13use fancy_regex::RegexBuilder;
14
15use crate::util::{is_localhost, match_hostname, match_location, replace_header_placeholders, IpBlockList};
16use ferron_common::modules::SocketData;
17
18/// Parses conditional data
19pub fn parse_conditional_data(
20  name: &str,
21  value: ServerConfigurationEntry,
22) -> Result<ConditionalData, Box<dyn Error + Send + Sync>> {
23  Ok(match name {
24    "is_remote_ip" => {
25      let mut list = IpBlockList::new();
26      list.load_from_vec(value.values.iter().filter_map(|v| v.as_str()).collect());
27      ConditionalData::IsRemoteIp(list)
28    }
29    "is_forwarded_for" => {
30      let mut list = IpBlockList::new();
31      list.load_from_vec(value.values.iter().filter_map(|v| v.as_str()).collect());
32      ConditionalData::IsForwardedFor(list)
33    }
34    "is_not_remote_ip" => {
35      let mut list = IpBlockList::new();
36      list.load_from_vec(value.values.iter().filter_map(|v| v.as_str()).collect());
37      ConditionalData::IsNotRemoteIp(list)
38    }
39    "is_not_forwarded_for" => {
40      let mut list = IpBlockList::new();
41      list.load_from_vec(value.values.iter().filter_map(|v| v.as_str()).collect());
42      ConditionalData::IsNotForwardedFor(list)
43    }
44    "is_equal" => ConditionalData::IsEqual(
45      value
46        .values
47        .first()
48        .and_then(|v| v.as_str())
49        .ok_or(anyhow::anyhow!(
50          "Missing or invalid left side of a \"is_equal\" subcondition"
51        ))?
52        .to_string(),
53      value
54        .values
55        .get(1)
56        .and_then(|v| v.as_str())
57        .ok_or(anyhow::anyhow!(
58          "Missing or invalid right side of a \"is_equal\" subcondition"
59        ))?
60        .to_string(),
61    ),
62    "is_not_equal" => ConditionalData::IsNotEqual(
63      value
64        .values
65        .first()
66        .and_then(|v| v.as_str())
67        .ok_or(anyhow::anyhow!(
68          "Missing or invalid left side of a \"is_not_equal\" subcondition"
69        ))?
70        .to_string(),
71      value
72        .values
73        .get(1)
74        .and_then(|v| v.as_str())
75        .ok_or(anyhow::anyhow!(
76          "Missing or invalid right side of a \"is_not_equal\" subcondition"
77        ))?
78        .to_string(),
79    ),
80    "is_regex" => {
81      let left_side = value.values.first().and_then(|v| v.as_str()).ok_or(anyhow::anyhow!(
82        "Missing or invalid left side of a \"is_regex\" subcondition"
83      ))?;
84      let right_side = value.values.get(1).and_then(|v| v.as_str()).ok_or(anyhow::anyhow!(
85        "Missing or invalid right side of a \"is_regex\" subcondition"
86      ))?;
87      ConditionalData::IsRegex(
88        left_side.to_string(),
89        RegexBuilder::new(right_side)
90          .case_insensitive(
91            value
92              .props
93              .get("case_insensitive")
94              .and_then(|p| p.as_bool())
95              .unwrap_or(false),
96          )
97          .build()?,
98      )
99    }
100    "is_not_regex" => {
101      let left_side = value.values.first().and_then(|v| v.as_str()).ok_or(anyhow::anyhow!(
102        "Missing or invalid left side of a \"is_not_regex\" subcondition"
103      ))?;
104      let right_side = value.values.get(1).and_then(|v| v.as_str()).ok_or(anyhow::anyhow!(
105        "Missing or invalid right side of a \"is_not_regex\" subcondition"
106      ))?;
107      ConditionalData::IsNotRegex(
108        left_side.to_string(),
109        RegexBuilder::new(right_side)
110          .case_insensitive(
111            value
112              .props
113              .get("case_insensitive")
114              .and_then(|p| p.as_bool())
115              .unwrap_or(false),
116          )
117          .build()?,
118      )
119    }
120    "is_rego" => {
121      let rego_policy = value
122        .values
123        .first()
124        .and_then(|v| v.as_str())
125        .ok_or(anyhow::anyhow!("Missing or invalid Rego policy"))?;
126      let mut rego_engine = regorus::Engine::new();
127      rego_engine.add_policy("ferron.rego".to_string(), rego_policy.to_string())?;
128      ConditionalData::IsRego(Arc::new(rego_engine))
129    }
130    "set_constant" => ConditionalData::SetConstant(
131      value
132        .values
133        .first()
134        .and_then(|v| v.as_str())
135        .ok_or(anyhow::anyhow!(
136          "Missing or invalid constant name in a \"set_constant\" subcondition"
137        ))?
138        .to_string(),
139      value
140        .values
141        .get(1)
142        .and_then(|v| v.as_str())
143        .ok_or(anyhow::anyhow!(
144          "Missing or invalid constant value in a \"set_constant\" subcondition"
145        ))?
146        .to_string(),
147    ),
148    "is_language" => ConditionalData::IsLanguage(
149      value
150        .values
151        .first()
152        .and_then(|v| v.as_str())
153        .ok_or(anyhow::anyhow!(
154          "Missing or invalid desired language in a \"is_language\" subcondition"
155        ))?
156        .to_string(),
157    ),
158    _ => Err(anyhow::anyhow!("Unrecognized subcondition: {name}"))?,
159  })
160}
161
162/// Matches conditions
163fn match_conditions(
164  conditions: &Conditions,
165  request: &hyper::http::request::Parts,
166  socket_data: &SocketData,
167) -> Result<bool, Box<dyn Error + Send + Sync>> {
168  if !match_location(&conditions.location_prefix, request.uri.path()) {
169    return Ok(false);
170  }
171  for cond in &conditions.conditionals {
172    if !(match cond {
173      Conditional::If(data) => {
174        let mut matches = true;
175        let mut constants = HashMap::new();
176        for d in data {
177          if !match_condition(d, request, socket_data, &mut constants)? {
178            matches = false;
179            break;
180          }
181        }
182        matches
183      }
184      Conditional::IfNot(data) => {
185        let mut matches = true;
186        let mut constants = HashMap::new();
187        for d in data {
188          if !match_condition(d, request, socket_data, &mut constants)? {
189            matches = false;
190            break;
191          }
192        }
193        !matches
194      }
195    }) {
196      return Ok(false);
197    }
198  }
199  Ok(true)
200}
201
202/// Matches a condition
203fn match_condition(
204  condition: &ConditionalData,
205  request: &hyper::http::request::Parts,
206  socket_data: &SocketData,
207  constants: &mut HashMap<String, String>,
208) -> Result<bool, Box<dyn Error + Send + Sync>> {
209  match condition {
210    ConditionalData::IsRemoteIp(list) => Ok(list.is_blocked(socket_data.remote_addr.ip())),
211    ConditionalData::IsForwardedFor(list) => {
212      let client_ip =
213        if let Some(x_forwarded_for) = request.headers.get("x-forwarded-for").and_then(|v| v.to_str().ok()) {
214          let prepared_remote_ip_str = match x_forwarded_for.split(",").next() {
215            Some(ip_address_str) => ip_address_str.replace(" ", ""),
216            None => return Ok(false),
217          };
218
219          let prepared_remote_ip: IpAddr = match prepared_remote_ip_str.parse() {
220            Ok(ip_address) => ip_address,
221            Err(_) => return Ok(false),
222          };
223
224          prepared_remote_ip
225        } else {
226          socket_data.remote_addr.ip()
227        };
228
229      Ok(list.is_blocked(client_ip))
230    }
231    ConditionalData::IsNotRemoteIp(list) => Ok(!list.is_blocked(socket_data.remote_addr.ip())),
232    ConditionalData::IsNotForwardedFor(list) => {
233      let client_ip =
234        if let Some(x_forwarded_for) = request.headers.get("x-forwarded-for").and_then(|v| v.to_str().ok()) {
235          let prepared_remote_ip_str = match x_forwarded_for.split(",").next() {
236            Some(ip_address_str) => ip_address_str.replace(" ", ""),
237            None => return Ok(false),
238          };
239
240          let prepared_remote_ip: IpAddr = match prepared_remote_ip_str.parse() {
241            Ok(ip_address) => ip_address,
242            Err(_) => return Ok(false),
243          };
244
245          prepared_remote_ip
246        } else {
247          socket_data.remote_addr.ip()
248        };
249
250      Ok(!list.is_blocked(client_ip))
251    }
252    ConditionalData::IsEqual(v1, v2) => Ok(
253      replace_header_placeholders(v1, request, Some(socket_data))
254        == replace_header_placeholders(v2, request, Some(socket_data)),
255    ),
256    ConditionalData::IsNotEqual(v1, v2) => Ok(
257      replace_header_placeholders(v1, request, Some(socket_data))
258        != replace_header_placeholders(v2, request, Some(socket_data)),
259    ),
260    ConditionalData::IsRegex(v1, regex) => {
261      Ok(regex.is_match(&replace_header_placeholders(v1, request, Some(socket_data)))?)
262    }
263    ConditionalData::IsNotRegex(v1, regex) => {
264      Ok(!(regex.is_match(&replace_header_placeholders(v1, request, Some(socket_data)))?))
265    }
266    ConditionalData::IsRego(rego_engine) => {
267      let mut cloned_engine = (*rego_engine.clone()).clone();
268      let mut rego_input_object = BTreeMap::new();
269      rego_input_object.insert("method".into(), request.method.as_str().into());
270      rego_input_object.insert(
271        "protocol".into(),
272        match request.version {
273          hyper::Version::HTTP_09 => "HTTP/0.9".into(),
274          hyper::Version::HTTP_10 => "HTTP/1.0".into(),
275          hyper::Version::HTTP_11 => "HTTP/1.1".into(),
276          hyper::Version::HTTP_2 => "HTTP/2.0".into(),
277          hyper::Version::HTTP_3 => "HTTP/3.0".into(),
278          _ => "HTTP/Unknown".into(),
279        },
280      );
281      rego_input_object.insert("uri".into(), request.uri.to_string().into());
282      let mut headers_hashmap_initial: HashMap<String, Vec<regorus::Value>> =
283        HashMap::with_capacity(request.headers.keys_len());
284      for (key, value) in request.headers.iter() {
285        let key_string = key.as_str().to_lowercase();
286        if let Some(header_list) = headers_hashmap_initial.get_mut(&key_string) {
287          header_list.push(value.to_str().unwrap_or("").into());
288        } else {
289          headers_hashmap_initial.insert(key_string, vec![value.to_str().unwrap_or("").into()]);
290        }
291      }
292      let mut headers_btreemap = BTreeMap::new();
293      for (key, value) in headers_hashmap_initial.into_iter() {
294        headers_btreemap.insert(key.into(), value.into());
295      }
296      let headers_rego = regorus::Value::Object(Arc::new(headers_btreemap));
297      rego_input_object.insert("headers".into(), headers_rego);
298      let mut socket_data_btreemap = BTreeMap::new();
299      socket_data_btreemap.insert("client_ip".into(), socket_data.remote_addr.ip().to_string().into());
300      socket_data_btreemap.insert("client_port".into(), (socket_data.remote_addr.port() as u32).into());
301      socket_data_btreemap.insert("server_ip".into(), socket_data.local_addr.ip().to_string().into());
302      socket_data_btreemap.insert("server_port".into(), (socket_data.local_addr.port() as u32).into());
303      socket_data_btreemap.insert("encrypted".into(), socket_data.encrypted.into());
304      let socket_data_rego = regorus::Value::Object(Arc::new(socket_data_btreemap));
305      rego_input_object.insert("socket_data".into(), socket_data_rego);
306      let mut constants_btreemap = BTreeMap::new();
307      for (key, value) in constants.iter_mut() {
308        constants_btreemap.insert(key.to_owned().into(), value.to_owned().into());
309      }
310      let constants_rego = regorus::Value::Object(Arc::new(constants_btreemap));
311      rego_input_object.insert("constants".into(), constants_rego);
312      let rego_input = regorus::Value::Object(Arc::new(rego_input_object));
313      cloned_engine.set_input(rego_input);
314      Ok(*cloned_engine.eval_rule("data.ferron.pass".to_string())?.as_bool()?)
315    }
316    ConditionalData::SetConstant(name, value) => {
317      constants.insert(name.to_owned(), value.to_owned());
318      Ok(true)
319    }
320    ConditionalData::IsLanguage(language) => {
321      let accepted_languages = parse_q_value_header(
322        request
323          .headers
324          .get(hyper::header::ACCEPT_LANGUAGE)
325          .and_then(|v| v.to_str().ok())
326          .unwrap_or("*"),
327      );
328      let supported_languages = constants
329        .get("LANGUAGES")
330        .and_then(|v| {
331          let mut hash_set: HashSet<&str> = HashSet::new();
332          for lang in v.split(",") {
333            hash_set.insert(lang);
334            if let Some((lang2, _)) = lang.split_once('-') {
335              hash_set.insert(lang2);
336            }
337          }
338          if hash_set.is_empty() {
339            None
340          } else {
341            Some(hash_set)
342          }
343        })
344        .unwrap_or(HashSet::from_iter(vec![language.as_str()]));
345      Ok(
346        accepted_languages
347          .iter()
348          .find(|l| {
349            *l == "*"
350              || supported_languages.contains(l.as_str())
351              || l.split_once('-').is_some_and(|(v, _)| supported_languages.contains(v))
352          })
353          .is_some_and(|l| l == language || language.split_once('-').is_some_and(|(v, _)| v == l)),
354      )
355    }
356    _ => Ok(false),
357  }
358}
359
360/// The struct containing all the Ferron server configurations
361#[derive(Debug)]
362pub struct ServerConfigurations {
363  /// Vector of configurations
364  pub inner: Vec<Arc<ServerConfiguration>>,
365}
366
367impl ServerConfigurations {
368  /// Creates the server configurations struct
369  pub fn new(mut inner: Vec<ServerConfiguration>) -> Self {
370    // Reverse the inner vector to ensure the location configurations are checked in the correct order
371    inner.reverse();
372
373    // Sort the configurations array by the specifity of configurations, so that it will be possible to find the configuration
374    inner.sort_by(|a, b| a.filters.cmp(&b.filters));
375    Self {
376      inner: inner.into_iter().map(Arc::new).collect(),
377    }
378  }
379
380  /// Finds a specific server configuration based on request parameters
381  pub fn find_configuration(
382    &self,
383    request: &hyper::http::request::Parts,
384    hostname: Option<&str>,
385    socket_data: &SocketData,
386  ) -> Result<Option<Arc<ServerConfiguration>>, Box<dyn Error + Send + Sync>> {
387    // The inner array is sorted by specifity, so it's easier to find the configurations.
388    // If it was not sorted, we would need to implement the specifity...
389    // Also, the approach mentioned in the line above might be slower...
390    // But there is one thing we're wondering: so many logical operators???
391    for server_configuration in self.inner.iter().rev() {
392      if match_hostname(server_configuration.filters.hostname.as_deref(), hostname)
393        && ((server_configuration.filters.ip.is_none() && (!is_localhost(server_configuration.filters.ip.as_ref(), server_configuration.filters.hostname.as_deref())
394            || socket_data.local_addr.ip().to_canonical().is_loopback()))  // With special `localhost` check
395            || server_configuration.filters.ip == Some(socket_data.local_addr.ip()))
396        && (server_configuration.filters.port.is_none()
397          || server_configuration.filters.port == Some(socket_data.local_addr.port()))
398        && server_configuration
399          .filters
400          .condition
401          .as_ref()
402          .map(|c| match_conditions(c, request, socket_data))
403          .unwrap_or(Ok(true))?
404        && server_configuration.filters.error_handler_status.is_none()
405      {
406        return Ok(Some(server_configuration.clone()));
407      }
408    }
409
410    Ok(None)
411  }
412
413  /// Finds the server error configuration based on configuration filters
414  pub fn find_error_configuration(
415    &self,
416    filters: &ServerConfigurationFilters,
417    status_code: u16,
418  ) -> Option<Arc<ServerConfiguration>> {
419    self
420      .inner
421      .iter()
422      .rev()
423      .find(|c| {
424        c.filters.is_host
425          && c.filters.hostname == filters.hostname
426          && c.filters.ip == filters.ip
427          && c.filters.port == filters.port
428          && (c.filters.condition.is_none() || c.filters.condition == filters.condition)
429          && !c.filters.error_handler_status.as_ref().is_none_or(|s| {
430            !(matches!(s, ErrorHandlerStatus::Any) || matches!(s, ErrorHandlerStatus::Status(x) if *x == status_code))
431          })
432      })
433      .cloned()
434  }
435
436  /// Finds the global server configuration (host or non-host)
437  pub fn find_global_configuration(&self) -> Option<Arc<ServerConfiguration>> {
438    // The server configurations are pre-merged, so we can simply return the found global configuration
439    let mut iterator = self.inner.iter();
440    let first_found = iterator.find(|server_configuration| {
441      server_configuration.filters.is_global() || server_configuration.filters.is_global_non_host()
442    });
443    if let Some(first_found) = first_found {
444      if first_found.filters.is_global() {
445        return Some(first_found.clone());
446      }
447      for server_configuration in iterator {
448        if server_configuration.filters.is_global() {
449          return Some(server_configuration.clone());
450        } else if !server_configuration.filters.is_global_non_host() {
451          return Some(first_found.clone());
452        }
453      }
454    }
455    None
456  }
457}