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