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