ferron/config/
mod.rs

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