ferron/config/adapters/
kdl.rs

1use std::{
2  collections::{HashMap, HashSet},
3  error::Error,
4  fs,
5  net::{IpAddr, SocketAddr},
6  path::{Path, PathBuf},
7  str::FromStr,
8};
9
10use glob::glob;
11use kdl::{KdlDocument, KdlNode, KdlValue};
12
13use crate::config::{
14  parse_conditional_data, Conditional, ConditionalData, Conditions, ErrorHandlerStatus, ServerConfiguration,
15  ServerConfigurationEntries, ServerConfigurationEntry, ServerConfigurationFilters, ServerConfigurationValue,
16};
17
18use super::ConfigurationAdapter;
19
20fn kdl_node_to_configuration_entry(kdl_node: &KdlNode) -> ServerConfigurationEntry {
21  let mut values = Vec::new();
22  let mut props = HashMap::new();
23  for kdl_entry in kdl_node.iter() {
24    let value = match kdl_entry.value().to_owned() {
25      KdlValue::String(value) => ServerConfigurationValue::String(value),
26      KdlValue::Integer(value) => ServerConfigurationValue::Integer(value),
27      KdlValue::Float(value) => ServerConfigurationValue::Float(value),
28      KdlValue::Bool(value) => ServerConfigurationValue::Bool(value),
29      KdlValue::Null => ServerConfigurationValue::Null,
30    };
31    if let Some(prop_name) = kdl_entry.name() {
32      props.insert(prop_name.value().to_string(), value);
33    } else {
34      values.push(value);
35    }
36  }
37  if values.is_empty() {
38    // If KDL node doesn't have any arguments, add the "#true" KDL value
39    values.push(ServerConfigurationValue::Bool(true));
40  }
41  ServerConfigurationEntry { values, props }
42}
43
44fn load_configuration_inner(
45  path: PathBuf,
46  loaded_paths: &mut HashSet<PathBuf>,
47) -> Result<Vec<ServerConfiguration>, Box<dyn Error + Send + Sync>> {
48  // Canonicalize the path
49  let canonical_pathbuf = fs::canonicalize(&path).unwrap_or_else(|_| path.clone());
50
51  // Check if the path is duplicate. If it's not, add it to loaded paths.
52  if loaded_paths.contains(&canonical_pathbuf) {
53    let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
54
55    Err(anyhow::anyhow!(
56      "Detected the server configuration file include loop while attempting to load \"{}\"",
57      canonical_path
58    ))?
59  } else {
60    loaded_paths.insert(canonical_pathbuf.clone());
61  }
62
63  // Read the configuration file
64  let file_contents = match fs::read_to_string(&path) {
65    Ok(file) => file,
66    Err(err) => {
67      let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
68
69      Err(anyhow::anyhow!(
70        "Failed to read from the server configuration file at \"{}\": {}",
71        canonical_path,
72        err
73      ))?
74    }
75  };
76
77  // Parse the configuration file contents
78  let kdl_document: KdlDocument = match file_contents.parse() {
79    Ok(document) => document,
80    Err(err) => {
81      let err: miette::Error = err.into();
82      Err(anyhow::anyhow!(
83        "Failed to parse the server configuration file: {:?}",
84        err
85      ))?
86    }
87  };
88
89  // Loaded configuration vector
90  let mut configurations = Vec::new();
91
92  // Loaded conditions
93  let mut loaded_conditions: HashMap<String, Vec<ConditionalData>> = HashMap::new();
94
95  // KDL configuration snippets
96  let mut snippets: HashMap<String, KdlDocument> = HashMap::new();
97
98  // Iterate over KDL nodes
99  for kdl_node in kdl_document {
100    let global_name = kdl_node.name().value();
101    let children = kdl_node.children();
102    if global_name == "snippet" {
103      if let Some(snippet_name) = kdl_node.get(0).and_then(|v| v.as_string()) {
104        if let Some(children) = children {
105          snippets.insert(snippet_name.to_string(), children.to_owned());
106        } else {
107          Err(anyhow::anyhow!("Snippet \"{snippet_name}\" is missing children"))?
108        }
109      } else {
110        Err(anyhow::anyhow!("Invalid or missing snippet name"))?
111      }
112    } else if let Some(children) = children {
113      for global_name in global_name.split(",") {
114        let host_filter = if global_name == "globals" {
115          (None, None, None, false)
116        } else if let Ok(socket_addr) = global_name.parse::<SocketAddr>() {
117          (None, Some(socket_addr.ip()), Some(socket_addr.port()), true)
118        } else if let Some((address, port_str)) = global_name.rsplit_once(':') {
119          if let Ok(port) = port_str.parse::<u16>() {
120            if let Ok(ip_address) = address
121              .strip_prefix('[')
122              .and_then(|s| s.strip_suffix(']'))
123              .unwrap_or(address)
124              .parse::<IpAddr>()
125            {
126              (None, Some(ip_address), Some(port), true)
127            } else if address == "*" || address.is_empty() {
128              (None, None, Some(port), true)
129            } else {
130              (Some(address.to_string()), None, Some(port), true)
131            }
132          } else if port_str == "*" {
133            if let Ok(ip_address) = address
134              .strip_prefix('[')
135              .and_then(|s| s.strip_suffix(']'))
136              .unwrap_or(address)
137              .parse::<IpAddr>()
138            {
139              (None, Some(ip_address), None, true)
140            } else if address == "*" || address.is_empty() {
141              (None, None, None, true)
142            } else {
143              (Some(address.to_string()), None, None, true)
144            }
145          } else {
146            let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
147
148            Err(anyhow::anyhow!("Invalid host specifier at \"{}\"", canonical_path))?
149          }
150        } else if let Ok(ip_address) = global_name
151          .strip_prefix('[')
152          .and_then(|s| s.strip_suffix(']'))
153          .unwrap_or(global_name)
154          .parse::<IpAddr>()
155        {
156          (None, Some(ip_address), None, true)
157        } else if global_name == "*" || global_name.is_empty() {
158          (None, None, None, true)
159        } else {
160          (Some(global_name.to_string()), None, None, true)
161        };
162
163        let mut configuration_entries: HashMap<String, ServerConfigurationEntries> = HashMap::new();
164        for kdl_node in children.nodes() {
165          #[allow(clippy::too_many_arguments)]
166          fn kdl_iterate_fn(
167            canonical_pathbuf: &PathBuf,
168            host_filter: &(Option<String>, Option<IpAddr>, Option<u16>, bool),
169            configurations: &mut Vec<ServerConfiguration>,
170            configuration_entries: &mut HashMap<String, ServerConfigurationEntries>,
171            kdl_node: &KdlNode,
172            conditions: &mut Option<&mut Conditions>,
173            is_error_config: bool,
174            loaded_conditions: &mut HashMap<String, Vec<ConditionalData>>,
175            snippets: &HashMap<String, KdlDocument>,
176          ) -> Result<(), Box<dyn Error + Send + Sync>> {
177            let (hostname, ip, port, is_host) = host_filter;
178            let kdl_node_name = kdl_node.name().value();
179            let children = kdl_node.children();
180            if kdl_node_name == "use" {
181              if let Some(snippet_name) = kdl_node.entry(0).and_then(|e| e.value().as_string()) {
182                if let Some(snippet) = snippets.get(snippet_name) {
183                  for kdl_node in snippet.nodes() {
184                    kdl_iterate_fn(
185                      canonical_pathbuf,
186                      host_filter,
187                      configurations,
188                      configuration_entries,
189                      kdl_node,
190                      conditions,
191                      is_error_config,
192                      loaded_conditions,
193                      snippets,
194                    )?;
195                  }
196                } else {
197                  Err(anyhow::anyhow!(
198                    "Snippet not defined: {snippet_name}. You might need to define it before using it"
199                  ))?;
200                }
201              } else {
202                Err(anyhow::anyhow!("Invalid `use` statement"))?;
203              }
204            } else if kdl_node_name == "location" {
205              if is_error_config {
206                Err(anyhow::anyhow!("Locations in error configurations aren't allowed"))?;
207              } else if conditions.is_some() {
208                Err(anyhow::anyhow!(
209                  "Nested locations and locations in conditions aren't allowed"
210                ))?;
211              }
212              let mut configuration_entries: HashMap<String, ServerConfigurationEntries> = HashMap::new();
213              if let Some(children) = children {
214                if let Some(location) = kdl_node.entry(0) {
215                  if let Some(location_str) = location.value().as_string() {
216                    let mut conditions = Conditions {
217                      location_prefix: location_str.to_string(),
218                      conditionals: vec![],
219                    };
220                    let mut loaded_conditions = loaded_conditions.clone();
221                    for kdl_node in children.nodes() {
222                      kdl_iterate_fn(
223                        canonical_pathbuf,
224                        host_filter,
225                        configurations,
226                        &mut configuration_entries,
227                        kdl_node,
228                        &mut Some(&mut conditions),
229                        is_error_config,
230                        &mut loaded_conditions,
231                        snippets,
232                      )?;
233                    }
234                    if kdl_node
235                      .entry("remove_base")
236                      .and_then(|e| e.value().as_bool())
237                      .unwrap_or(false)
238                    {
239                      configuration_entries.insert(
240                        "UNDOCUMENTED_REMOVE_PATH_PREFIX".to_string(),
241                        ServerConfigurationEntries {
242                          inner: vec![ServerConfigurationEntry {
243                            values: vec![ServerConfigurationValue::String(location_str.to_string())],
244                            props: HashMap::new(),
245                          }],
246                        },
247                      );
248                    }
249                    configurations.push(ServerConfiguration {
250                      entries: configuration_entries,
251                      filters: ServerConfigurationFilters {
252                        is_host: *is_host,
253                        hostname: hostname.clone(),
254                        ip: *ip,
255                        port: *port,
256                        condition: Some(conditions),
257                        error_handler_status: None,
258                      },
259                      modules: vec![],
260                    });
261                  } else {
262                    let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
263
264                    Err(anyhow::anyhow!("Invalid location path at \"{}\"", canonical_path))?
265                  }
266                } else {
267                  let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
268
269                  Err(anyhow::anyhow!("Invalid location at \"{}\"", canonical_path))?
270                }
271              } else {
272                let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
273
274                Err(anyhow::anyhow!(
275                  "Locations should have children, but they don't at \"{}\"",
276                  canonical_path
277                ))?
278              }
279            } else if kdl_node_name == "condition" {
280              if is_error_config {
281                Err(anyhow::anyhow!("Conditions in error configurations aren't allowed"))?;
282              }
283              if let Some(children) = children {
284                if let Some(condition_name) = kdl_node.entry(0) {
285                  if let Some(condition_name_str) = condition_name.value().as_string() {
286                    let mut conditions_data = Vec::new();
287
288                    for kdl_node in children.nodes() {
289                      let value = kdl_node_to_configuration_entry(kdl_node);
290                      let name = kdl_node.name().value();
291                      conditions_data.push(match parse_conditional_data(name, value) {
292                        Ok(d) => d,
293                        Err(err) => Err(anyhow::anyhow!(
294                          "Invalid or unsupported subcondition at \"{condition_name_str}\" condition: {err}"
295                        ))?,
296                      });
297                    }
298
299                    loaded_conditions.insert(condition_name_str.to_string(), conditions_data);
300                  } else {
301                    let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
302
303                    Err(anyhow::anyhow!("Invalid location path at \"{}\"", canonical_path))?
304                  }
305                } else {
306                  let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
307
308                  Err(anyhow::anyhow!("Invalid location at \"{}\"", canonical_path))?
309                }
310              } else {
311                let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
312
313                Err(anyhow::anyhow!(
314                  "Locations should have children, but they don't at \"{}\"",
315                  canonical_path
316                ))?
317              }
318            } else if kdl_node_name == "if" {
319              if is_error_config {
320                Err(anyhow::anyhow!("Conditions in error configurations aren't allowed"))?;
321              }
322              let mut configuration_entries: HashMap<String, ServerConfigurationEntries> = HashMap::new();
323              if let Some(children) = children {
324                if let Some(condition_name) = kdl_node.entry(0) {
325                  if let Some(condition_name_str) = condition_name.value().as_string() {
326                    let mut new_conditions = if let Some(conditions) = conditions {
327                      conditions.clone()
328                    } else {
329                      Conditions {
330                        location_prefix: "/".to_string(),
331                        conditionals: vec![],
332                      }
333                    };
334
335                    if let Some(conditionals) = loaded_conditions.get(condition_name_str) {
336                      new_conditions.conditionals.push(Conditional::If(conditionals.clone()));
337                    } else {
338                      Err(anyhow::anyhow!(
339                        "Condition not defined: {condition_name_str}. You might need to define it before using it"
340                      ))?;
341                    }
342
343                    let mut loaded_conditions = loaded_conditions.clone();
344                    for kdl_node in children.nodes() {
345                      kdl_iterate_fn(
346                        canonical_pathbuf,
347                        host_filter,
348                        configurations,
349                        &mut configuration_entries,
350                        kdl_node,
351                        &mut Some(&mut new_conditions),
352                        is_error_config,
353                        &mut loaded_conditions,
354                        snippets,
355                      )?;
356                    }
357
358                    configurations.push(ServerConfiguration {
359                      entries: configuration_entries,
360                      filters: ServerConfigurationFilters {
361                        is_host: *is_host,
362                        hostname: hostname.clone(),
363                        ip: *ip,
364                        port: *port,
365                        condition: Some(new_conditions.to_owned()),
366                        error_handler_status: None,
367                      },
368                      modules: vec![],
369                    });
370                  } else {
371                    let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
372
373                    Err(anyhow::anyhow!("Invalid location path at \"{}\"", canonical_path))?
374                  }
375                } else {
376                  let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
377
378                  Err(anyhow::anyhow!("Invalid location at \"{}\"", canonical_path))?
379                }
380              } else {
381                let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
382
383                Err(anyhow::anyhow!(
384                  "Locations should have children, but they don't at \"{}\"",
385                  canonical_path
386                ))?
387              }
388            } else if kdl_node_name == "if_not" {
389              if is_error_config {
390                Err(anyhow::anyhow!("Conditions in error configurations aren't allowed"))?;
391              }
392              let mut configuration_entries: HashMap<String, ServerConfigurationEntries> = HashMap::new();
393              if let Some(children) = children {
394                if let Some(condition_name) = kdl_node.entry(0) {
395                  if let Some(condition_name_str) = condition_name.value().as_string() {
396                    let mut new_conditions = if let Some(conditions) = conditions {
397                      conditions.clone()
398                    } else {
399                      Conditions {
400                        location_prefix: "/".to_string(),
401                        conditionals: vec![],
402                      }
403                    };
404
405                    if let Some(conditionals) = loaded_conditions.get(condition_name_str) {
406                      new_conditions
407                        .conditionals
408                        .push(Conditional::IfNot(conditionals.clone()));
409                    } else {
410                      Err(anyhow::anyhow!(
411                        "Condition not defined: {condition_name_str}. You might need to define it before using it"
412                      ))?;
413                    }
414
415                    let mut loaded_conditions = loaded_conditions.clone();
416                    for kdl_node in children.nodes() {
417                      kdl_iterate_fn(
418                        canonical_pathbuf,
419                        host_filter,
420                        configurations,
421                        &mut configuration_entries,
422                        kdl_node,
423                        &mut Some(&mut new_conditions),
424                        is_error_config,
425                        &mut loaded_conditions,
426                        snippets,
427                      )?;
428                    }
429
430                    configurations.push(ServerConfiguration {
431                      entries: configuration_entries,
432                      filters: ServerConfigurationFilters {
433                        is_host: *is_host,
434                        hostname: hostname.clone(),
435                        ip: *ip,
436                        port: *port,
437                        condition: Some(new_conditions.to_owned()),
438                        error_handler_status: None,
439                      },
440                      modules: vec![],
441                    });
442                  } else {
443                    let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
444
445                    Err(anyhow::anyhow!("Invalid location path at \"{}\"", canonical_path))?
446                  }
447                } else {
448                  let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
449
450                  Err(anyhow::anyhow!("Invalid location at \"{}\"", canonical_path))?
451                }
452              } else {
453                let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
454
455                Err(anyhow::anyhow!(
456                  "Locations should have children, but they don't at \"{}\"",
457                  canonical_path
458                ))?
459              }
460            } else if kdl_node_name == "error_config" {
461              if is_error_config {
462                Err(anyhow::anyhow!("Nested error configurations aren't allowed"))?;
463              }
464              let mut configuration_entries: HashMap<String, ServerConfigurationEntries> = HashMap::new();
465              if let Some(children) = children {
466                if let Some(error_status_code) = kdl_node.entry(0) {
467                  if let Some(error_status_code) = error_status_code.value().as_integer() {
468                    let mut loaded_conditions = loaded_conditions.clone();
469                    for kdl_node in children.nodes() {
470                      kdl_iterate_fn(
471                        canonical_pathbuf,
472                        host_filter,
473                        configurations,
474                        &mut configuration_entries,
475                        kdl_node,
476                        conditions,
477                        true,
478                        &mut loaded_conditions,
479                        snippets,
480                      )?;
481                    }
482                    configurations.push(ServerConfiguration {
483                      entries: configuration_entries,
484                      filters: ServerConfigurationFilters {
485                        is_host: *is_host,
486                        hostname: hostname.clone(),
487                        ip: *ip,
488                        port: *port,
489                        condition: None,
490                        error_handler_status: Some(ErrorHandlerStatus::Status(error_status_code as u16)),
491                      },
492                      modules: vec![],
493                    });
494                  } else {
495                    let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
496
497                    Err(anyhow::anyhow!(
498                      "Invalid error handler status code at \"{}\"",
499                      canonical_path
500                    ))?
501                  }
502                } else {
503                  for kdl_node in children.nodes() {
504                    let kdl_node_name = kdl_node.name().value();
505                    let value = kdl_node_to_configuration_entry(kdl_node);
506                    if let Some(entries) = configuration_entries.get_mut(kdl_node_name) {
507                      entries.inner.push(value);
508                    } else {
509                      configuration_entries.insert(
510                        kdl_node_name.to_string(),
511                        ServerConfigurationEntries { inner: vec![value] },
512                      );
513                    }
514                  }
515                  configurations.push(ServerConfiguration {
516                    entries: configuration_entries,
517                    filters: ServerConfigurationFilters {
518                      is_host: *is_host,
519                      hostname: hostname.clone(),
520                      ip: *ip,
521                      port: *port,
522                      condition: None,
523                      error_handler_status: Some(ErrorHandlerStatus::Any),
524                    },
525                    modules: vec![],
526                  });
527                }
528              } else {
529                let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
530
531                Err(anyhow::anyhow!(
532                  "Error handler blocks should have children, but they don't at \"{}\"",
533                  canonical_path
534                ))?
535              }
536            } else {
537              let value = kdl_node_to_configuration_entry(kdl_node);
538              if let Some(entries) = configuration_entries.get_mut(kdl_node_name) {
539                entries.inner.push(value);
540              } else {
541                configuration_entries.insert(
542                  kdl_node_name.to_string(),
543                  ServerConfigurationEntries { inner: vec![value] },
544                );
545              }
546            }
547            Ok(())
548          }
549          kdl_iterate_fn(
550            &canonical_pathbuf,
551            &host_filter,
552            &mut configurations,
553            &mut configuration_entries,
554            kdl_node,
555            &mut None,
556            false,
557            &mut loaded_conditions,
558            &snippets,
559          )?;
560        }
561        let (hostname, ip, port, is_host) = host_filter;
562        configurations.push(ServerConfiguration {
563          entries: configuration_entries,
564          filters: ServerConfigurationFilters {
565            is_host,
566            hostname,
567            ip,
568            port,
569            condition: None,
570            error_handler_status: None,
571          },
572          modules: vec![],
573        });
574      }
575    } else if global_name == "include" {
576      // Get the list of included files and include the configurations
577      let mut include_files = Vec::new();
578      for include_one in kdl_node.entries() {
579        if include_one.name().is_some() {
580          continue;
581        }
582        if let Some(include_glob) = include_one.value().as_string() {
583          let include_glob_pathbuf = match PathBuf::from_str(include_glob) {
584            Ok(pathbuf) => pathbuf,
585            Err(err) => {
586              let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
587
588              Err(anyhow::anyhow!(
589                "Failed to determine includes for the server configuration file at \"{}\": {}",
590                canonical_path,
591                err
592              ))?
593            }
594          };
595          let include_glob_pathbuf_canonicalized = if include_glob_pathbuf.is_absolute() {
596            include_glob_pathbuf
597          } else {
598            let mut canonical_dirname = canonical_pathbuf.clone();
599            canonical_dirname.pop();
600            canonical_dirname.join(include_glob_pathbuf)
601          };
602          let files_globbed = match glob(&include_glob_pathbuf_canonicalized.to_string_lossy()) {
603            Ok(files_globbed) => files_globbed,
604            Err(err) => {
605              let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
606
607              Err(anyhow::anyhow!(
608                "Failed to determine includes for the server configuration file at \"{}\": {}",
609                canonical_path,
610                err
611              ))?
612            }
613          };
614
615          for file_globbed_result in files_globbed {
616            let file_globbed = match file_globbed_result {
617              Ok(file_globbed) => file_globbed,
618              Err(err) => {
619                let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
620
621                Err(anyhow::anyhow!(
622                  "Failed to determine includes for the server configuration file at \"{}\": {}",
623                  canonical_path,
624                  err
625                ))?
626              }
627            };
628            include_files.push(fs::canonicalize(&file_globbed).unwrap_or_else(|_| file_globbed.clone()));
629          }
630        }
631      }
632
633      for included_file in include_files {
634        configurations.extend(load_configuration_inner(included_file, loaded_paths)?);
635      }
636    } else {
637      let canonical_path = canonical_pathbuf.to_string_lossy().into_owned();
638
639      Err(anyhow::anyhow!("Invalid top-level directive at \"{}\"", canonical_path))?
640    }
641  }
642
643  Ok(configurations)
644}
645
646/// A KDL configuration adapter
647pub struct KdlConfigurationAdapter;
648
649impl KdlConfigurationAdapter {
650  /// Creates a new configuration adapter
651  pub fn new() -> Self {
652    Self
653  }
654}
655
656impl ConfigurationAdapter for KdlConfigurationAdapter {
657  fn load_configuration(&self, path: &Path) -> Result<Vec<ServerConfiguration>, Box<dyn Error + Send + Sync>> {
658    load_configuration_inner(path.to_path_buf(), &mut HashSet::new())
659  }
660}