ferron_common/
config.rs

1use std::fmt::{Debug, Display, Formatter};
2use std::hash::Hasher;
3use std::net::IpAddr;
4use std::sync::Arc;
5use std::{cmp::Ordering, collections::HashMap};
6
7use fancy_regex::Regex;
8
9use crate::modules::Module;
10use crate::observability::ObservabilityBackendChannels;
11use crate::util::IpBlockList;
12
13/// Conditional data
14#[non_exhaustive]
15#[repr(u8)]
16#[derive(Clone, Debug)]
17pub enum ConditionalData {
18  IsRemoteIp(IpBlockList),
19  IsForwardedFor(IpBlockList),
20  IsNotRemoteIp(IpBlockList),
21  IsNotForwardedFor(IpBlockList),
22  IsEqual(String, String),
23  IsNotEqual(String, String),
24  IsRegex(String, Regex),
25  IsNotRegex(String, Regex),
26  IsRego(Arc<regorus::Engine>),
27  SetConstant(String, String),
28  IsLanguage(String),
29}
30
31impl PartialEq for ConditionalData {
32  fn eq(&self, other: &Self) -> bool {
33    match (self, other) {
34      (Self::IsRemoteIp(v1), Self::IsRemoteIp(v2)) => v1 == v2,
35      (Self::IsForwardedFor(v1), Self::IsForwardedFor(v2)) => v1 == v2,
36      (Self::IsNotRemoteIp(v1), Self::IsNotRemoteIp(v2)) => v1 == v2,
37      (Self::IsNotForwardedFor(v1), Self::IsNotForwardedFor(v2)) => v1 == v2,
38      (Self::IsEqual(v1, v2), Self::IsEqual(v3, v4)) => v1 == v3 && v2 == v4,
39      (Self::IsNotEqual(v1, v2), Self::IsNotEqual(v3, v4)) => v1 == v3 && v2 == v4,
40      (Self::IsRegex(v1, v2), Self::IsRegex(v3, v4)) => v1 == v3 && v2.as_str() == v4.as_str(),
41      (Self::IsNotRegex(v1, v2), Self::IsNotRegex(v3, v4)) => v1 == v3 && v2.as_str() == v4.as_str(),
42      (Self::IsRego(v1), Self::IsRego(v2)) => v1.get_policies().ok() == v2.get_policies().ok(),
43      (Self::SetConstant(v1, v2), Self::SetConstant(v3, v4)) => v1 == v3 && v2 == v4,
44      (Self::IsLanguage(v1), Self::IsLanguage(v2)) => v1 == v2,
45      _ => false,
46    }
47  }
48}
49
50impl Eq for ConditionalData {}
51
52impl Ord for ConditionalData {
53  fn cmp(&self, other: &Self) -> Ordering {
54    match (self, other) {
55      (Self::IsRemoteIp(v1), Self::IsRemoteIp(v2)) => v1.cmp(v2),
56      (Self::IsForwardedFor(v1), Self::IsForwardedFor(v2)) => v1.cmp(v2),
57      (Self::IsNotRemoteIp(v1), Self::IsNotRemoteIp(v2)) => v1.cmp(v2),
58      (Self::IsNotForwardedFor(v1), Self::IsNotForwardedFor(v2)) => v1.cmp(v2),
59      (Self::IsEqual(v1, v2), Self::IsEqual(v3, v4)) => v1.cmp(v3).then(v2.cmp(v4)),
60      (Self::IsNotEqual(v1, v2), Self::IsNotEqual(v3, v4)) => v1.cmp(v3).then(v2.cmp(v4)),
61      (Self::IsRegex(v1, v2), Self::IsRegex(v3, v4)) => v1.cmp(v3).then(v2.as_str().cmp(v4.as_str())),
62      (Self::IsNotRegex(v1, v2), Self::IsNotRegex(v3, v4)) => v1.cmp(v3).then(v2.as_str().cmp(v4.as_str())),
63      (Self::IsRego(v1), Self::IsRego(v2)) => v1.get_policies().ok().cmp(&v2.get_policies().ok()),
64      (Self::SetConstant(v1, v2), Self::SetConstant(v3, v4)) => v1.cmp(v3).then(v2.cmp(v4)),
65      _ => {
66        // SAFETY: See https://doc.rust-lang.org/core/mem/fn.discriminant.html
67        let discriminant_self = unsafe { *<*const _>::from(self).cast::<u8>() };
68        let discriminant_other = unsafe { *<*const _>::from(other).cast::<u8>() };
69        discriminant_self.cmp(&discriminant_other)
70      }
71    }
72  }
73}
74
75impl PartialOrd for ConditionalData {
76  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
77    Some(self.cmp(other))
78  }
79}
80
81fn count_logical_slashes(s: &str) -> usize {
82  if s.is_empty() {
83    // Input is empty, zero slashes
84    return 0;
85  }
86  let trimmed = s.trim_end_matches('/');
87  if trimmed.is_empty() {
88    // Trimmed input is empty, but the original wasn't, probably input with only slashes
89    return 0;
90  }
91
92  let mut count = 0;
93  let mut prev_was_slash = false;
94
95  for ch in trimmed.chars() {
96    if ch == '/' {
97      if !prev_was_slash {
98        count += 1;
99        prev_was_slash = true;
100      }
101    } else {
102      prev_was_slash = false;
103    }
104  }
105
106  count
107}
108
109/// The struct containing conditions
110#[derive(Clone, Debug, PartialEq, Eq)]
111pub struct Conditions {
112  /// The location prefix
113  pub location_prefix: String,
114
115  /// The conditionals
116  pub conditionals: Vec<Conditional>,
117}
118
119impl Ord for Conditions {
120  fn cmp(&self, other: &Self) -> Ordering {
121    count_logical_slashes(&self.location_prefix)
122      .cmp(&count_logical_slashes(&other.location_prefix))
123      .then_with(|| self.conditionals.len().cmp(&other.conditionals.len()))
124  }
125}
126
127impl PartialOrd for Conditions {
128  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
129    Some(self.cmp(other))
130  }
131}
132
133/// The enum containing a conditional
134#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
135pub enum Conditional {
136  /// "if" condition
137  If(Vec<ConditionalData>),
138
139  /// "if_not" condition
140  IfNot(Vec<ConditionalData>),
141}
142
143/// A specific Ferron server configuration
144#[derive(Clone)]
145pub struct ServerConfiguration {
146  /// Entries for the configuration
147  pub entries: HashMap<String, ServerConfigurationEntries>,
148
149  /// Configuration filters
150  pub filters: ServerConfigurationFilters,
151
152  /// Loaded modules
153  pub modules: Vec<Arc<dyn Module + Send + Sync>>,
154
155  /// Loaded observability backend channels
156  pub observability: ObservabilityBackendChannels,
157}
158
159impl Debug for ServerConfiguration {
160  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
161    f.debug_struct("ServerConfiguration")
162      .field("entries", &self.entries)
163      .field("filters", &self.filters)
164      .finish()
165  }
166}
167
168/// A error handler status code
169#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
170pub enum ErrorHandlerStatus {
171  /// Any status code
172  Any,
173
174  /// Specific status code
175  Status(u16),
176}
177
178/// A Ferron server configuration filter
179#[derive(Clone, Debug, PartialEq, Eq)]
180pub struct ServerConfigurationFilters {
181  /// Whether the configuration represents a host block
182  pub is_host: bool,
183
184  /// The hostname
185  pub hostname: Option<String>,
186
187  /// The IP address
188  pub ip: Option<IpAddr>,
189
190  /// The port
191  pub port: Option<u16>,
192
193  /// The conditions
194  pub condition: Option<Conditions>,
195
196  /// The error handler status code
197  pub error_handler_status: Option<ErrorHandlerStatus>,
198}
199
200impl ServerConfigurationFilters {
201  /// Checks if the server configuration is global
202  pub fn is_global(&self) -> bool {
203    self.is_host
204      && self.hostname.is_none()
205      && self.ip.is_none()
206      && self.port.is_none()
207      && self.condition.is_none()
208      && self.error_handler_status.is_none()
209  }
210
211  /// Checks if the server configuration is global and doesn't represent a host block
212  pub fn is_global_non_host(&self) -> bool {
213    !self.is_host
214  }
215}
216
217impl Ord for ServerConfigurationFilters {
218  fn cmp(&self, other: &Self) -> Ordering {
219    self
220      .is_host
221      .cmp(&other.is_host)
222      .then_with(|| self.port.is_some().cmp(&other.port.is_some()))
223      .then_with(|| self.ip.is_some().cmp(&other.ip.is_some()))
224      .then_with(|| {
225        self
226          .hostname
227          .as_ref()
228          .map(|h| !h.starts_with("*."))
229          .cmp(&other.hostname.as_ref().map(|h| !h.starts_with("*.")))
230      }) // Take wildcard hostnames into account
231      .then_with(|| {
232        self
233          .hostname
234          .as_ref()
235          .map(|h| h.trim_end_matches('.').chars().filter(|c| *c == '.').count())
236          .cmp(
237            &other
238              .hostname
239              .as_ref()
240              .map(|h| h.trim_end_matches('.').chars().filter(|c| *c == '.').count()),
241          )
242      }) // Take also amount of dots in hostnames (domain level) into account
243      .then_with(|| self.condition.cmp(&other.condition)) // Use `cmp` method for `Ord` trait implemented for `Condition`
244      .then_with(|| {
245        self
246          .error_handler_status
247          .is_some()
248          .cmp(&other.error_handler_status.is_some())
249      })
250  }
251}
252
253impl PartialOrd for ServerConfigurationFilters {
254  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
255    Some(self.cmp(other))
256  }
257}
258
259impl Display for ServerConfigurationFilters {
260  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
261    if !self.is_host {
262      write!(f, "\"globals\" block")
263    } else {
264      let mut blocks = Vec::new();
265      if self.ip.is_some() || self.hostname.is_some() || self.port.is_some() {
266        let mut name = String::new();
267        if let Some(hostname) = &self.hostname {
268          name.push_str(hostname);
269          if let Some(ip) = self.ip {
270            name.push_str(&format!("({ip})"));
271          }
272        } else if let Some(ip) = self.ip {
273          name.push_str(&ip.to_string());
274        }
275        if let Some(port) = self.port {
276          name.push_str(&format!(":{port}"));
277        }
278        if !name.is_empty() {
279          blocks.push(format!("\"{name}\" host block",));
280        }
281      }
282      if let Some(condition) = &self.condition {
283        let mut name = String::new();
284        if !condition.location_prefix.is_empty() {
285          name.push_str(&format!("\"{}\" location", condition.location_prefix));
286        }
287        if !condition.conditionals.is_empty() {
288          if !name.is_empty() {
289            name.push_str(" and ");
290          }
291          name.push_str("some conditional blocks");
292        } else {
293          name.push_str(" block");
294        }
295        blocks.push(name);
296      }
297      if let Some(error_handler_status) = &self.error_handler_status {
298        match error_handler_status {
299          ErrorHandlerStatus::Any => blocks.push("\"error_status\" block".to_string()),
300          ErrorHandlerStatus::Status(status) => blocks.push(format!("\"error_status {status}\" block")),
301        }
302      }
303      write!(f, "{}", blocks.join(" -> "))
304    }
305  }
306}
307
308/// A specific list of Ferron server configuration entries
309#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
310pub struct ServerConfigurationEntries {
311  /// Vector of configuration entries
312  pub inner: Vec<ServerConfigurationEntry>,
313}
314
315impl ServerConfigurationEntries {
316  /// Extracts one value from the entry list
317  pub fn get_value(&self) -> Option<&ServerConfigurationValue> {
318    self.inner.last().and_then(|last_vector| last_vector.values.first())
319  }
320
321  /// Extracts one entry from the entry list
322  pub fn get_entry(&self) -> Option<&ServerConfigurationEntry> {
323    self.inner.last()
324  }
325
326  /// Extracts a vector of values from the entry list
327  pub fn get_values(&self) -> Vec<&ServerConfigurationValue> {
328    let mut iterator: Box<dyn Iterator<Item = &ServerConfigurationValue>> = Box::new(vec![].into_iter());
329    for entry in &self.inner {
330      iterator = Box::new(iterator.chain(entry.values.iter()));
331    }
332    iterator.collect()
333  }
334}
335
336/// A specific Ferron server configuration entry
337#[derive(Debug, Clone, PartialEq, Eq)]
338pub struct ServerConfigurationEntry {
339  /// Values for the entry
340  pub values: Vec<ServerConfigurationValue>,
341
342  /// Props for the entry
343  pub props: HashMap<String, ServerConfigurationValue>,
344}
345
346impl std::hash::Hash for ServerConfigurationEntry {
347  fn hash<H: Hasher>(&self, state: &mut H) {
348    // Hash the values vector
349    self.values.hash(state);
350
351    // For HashMap, we need to hash in a deterministic order
352    // since HashMap iteration order is not guaranteed
353    let mut props_vec: Vec<_> = self.props.iter().collect();
354    props_vec.sort_by(|a, b| a.0.cmp(b.0)); // Sort by key
355
356    // Hash the length first, then each key-value pair
357    props_vec.len().hash(state);
358    for (key, value) in props_vec {
359      key.hash(state);
360      value.hash(state);
361    }
362  }
363}
364
365/// A specific Ferron server configuration value
366#[derive(Debug, Clone, PartialOrd)]
367pub enum ServerConfigurationValue {
368  /// A string
369  String(String),
370
371  /// A non-float number
372  Integer(i128),
373
374  /// A floating point number
375  Float(f64),
376
377  /// A boolean
378  Bool(bool),
379
380  /// The null value
381  Null,
382}
383
384impl std::hash::Hash for ServerConfigurationValue {
385  fn hash<H: Hasher>(&self, state: &mut H) {
386    match self {
387      Self::String(s) => {
388        0u8.hash(state);
389        s.hash(state);
390      }
391      Self::Integer(i) => {
392        1u8.hash(state);
393        i.hash(state);
394      }
395      Self::Float(f) => {
396        2u8.hash(state);
397        // Convert to bits for consistent hashing
398        // Handle NaN by using a consistent bit pattern
399        if f.is_nan() {
400          f64::NAN.to_bits().hash(state);
401        } else {
402          f.to_bits().hash(state);
403        }
404      }
405      Self::Bool(b) => {
406        3u8.hash(state);
407        b.hash(state);
408      }
409      Self::Null => {
410        4u8.hash(state);
411      }
412    }
413  }
414}
415
416impl ServerConfigurationValue {
417  /// Checks if the value is a string
418  pub fn is_string(&self) -> bool {
419    matches!(self, Self::String(..))
420  }
421
422  /// Checks if the value is a non-float number
423  pub fn is_integer(&self) -> bool {
424    matches!(self, Self::Integer(..))
425  }
426
427  /// Checks if the value is a floating point number
428  #[allow(dead_code)]
429  pub fn is_float(&self) -> bool {
430    matches!(self, Self::Float(..))
431  }
432
433  /// Checks if the value is a boolean
434  pub fn is_bool(&self) -> bool {
435    matches!(self, Self::Bool(..))
436  }
437
438  /// Checks if the value is a null value
439  pub fn is_null(&self) -> bool {
440    matches!(self, Self::Null)
441  }
442
443  /// Extracts a `&str` from the value
444  pub fn as_str(&self) -> Option<&str> {
445    use ServerConfigurationValue::*;
446    match self {
447      String(s) => Some(s),
448      _ => None,
449    }
450  }
451
452  /// Extracts a `i128` from the value
453  pub fn as_i128(&self) -> Option<i128> {
454    use ServerConfigurationValue::*;
455    match self {
456      Integer(i) => Some(*i),
457      _ => None,
458    }
459  }
460
461  /// Extracts a `f64` from the value
462  #[allow(dead_code)]
463  pub fn as_f64(&self) -> Option<f64> {
464    match self {
465      Self::Float(i) => Some(*i),
466      _ => None,
467    }
468  }
469
470  /// Extracts a `bool` from the value
471  pub fn as_bool(&self) -> Option<bool> {
472    if let Self::Bool(v) = self {
473      Some(*v)
474    } else {
475      None
476    }
477  }
478}
479
480impl Eq for ServerConfigurationValue {}
481
482impl PartialEq for ServerConfigurationValue {
483  fn eq(&self, other: &Self) -> bool {
484    match (self, other) {
485      (Self::Bool(left), Self::Bool(right)) => left == right,
486      (Self::Integer(left), Self::Integer(right)) => left == right,
487      (Self::Float(left), Self::Float(right)) => {
488        let left = if left == &f64::NEG_INFINITY {
489          -f64::MAX
490        } else if left == &f64::INFINITY {
491          f64::MAX
492        } else if left.is_nan() {
493          0.0
494        } else {
495          *left
496        };
497
498        let right = if right == &f64::NEG_INFINITY {
499          -f64::MAX
500        } else if right == &f64::INFINITY {
501          f64::MAX
502        } else if right.is_nan() {
503          0.0
504        } else {
505          *right
506        };
507
508        left == right
509      }
510      (Self::String(left), Self::String(right)) => left == right,
511      _ => core::mem::discriminant(self) == core::mem::discriminant(other),
512    }
513  }
514}