ferron/util/
validate_config.rs1use crate::ferron_common::ServerConfig;
2use hyper::header::{HeaderName, HeaderValue};
3use std::collections::HashSet;
4use std::error::Error;
5use std::net::IpAddr;
6use std::str::FromStr;
7use yaml_rust2::{yaml, Yaml};
8
9struct UsedProperties<'a> {
11 config: &'a ServerConfig,
12 properties: HashSet<String>,
13}
14
15impl<'a> UsedProperties<'a> {
16 fn new(config: &'a ServerConfig) -> Self {
17 UsedProperties {
18 config,
19 properties: HashSet::new(),
20 }
21 }
22
23 fn contains(&mut self, property: &str) -> bool {
24 self.properties.insert(property.to_string());
25 !self.config[property].is_badvalue()
26 }
27
28 fn unused(&self) -> Vec<String> {
29 let empty_hashmap = yaml::Hash::new();
30 let all_properties = self
31 .config
32 .as_hash()
33 .unwrap_or(&empty_hashmap)
34 .keys()
35 .filter_map(|a| a.as_str().map(|a| a.to_string()));
36 all_properties
37 .filter(|item| !self.properties.contains(item))
38 .collect()
39 }
40}
41
42fn validate_ip(ip: &str) -> bool {
43 let _: IpAddr = match ip.parse() {
44 Ok(addr) => addr,
45 Err(_) => return false,
46 };
47 true
48}
49
50pub fn validate_config(
52 config: ServerConfig,
53 is_global: bool,
54 is_location: bool,
55 is_error_config: bool,
56 modules_optional_builtin: &[String],
57) -> Result<Vec<String>, Box<dyn Error + Send + Sync>> {
58 let mut used_properties = UsedProperties::new(&config);
59
60 let domain_badvalue = !used_properties.contains("domain");
61 let ip_badvalue = !used_properties.contains("ip");
62
63 if !domain_badvalue && config["domain"].as_str().is_none() {
64 Err(anyhow::anyhow!("Invalid domain name"))?
65 }
66
67 if !ip_badvalue {
68 match config["ip"].as_str() {
69 Some(ip) => {
70 if !validate_ip(ip) {
71 Err(anyhow::anyhow!("Invalid IP address"))?;
72 }
73 }
74 None => {
75 Err(anyhow::anyhow!("Invalid IP address"))?;
76 }
77 }
78 }
79
80 if domain_badvalue && ip_badvalue && !is_global && !is_location && !is_error_config {
81 Err(anyhow::anyhow!(
82 "A host must either have IP address or domain name specified"
83 ))?;
84 }
85
86 if used_properties.contains("scode") {
87 if !is_error_config {
88 Err(anyhow::anyhow!(
89 "Status code configuration is only allowed in error configuration"
90 ))?;
91 }
92 if config["scode"].as_i64().is_none() {
93 Err(anyhow::anyhow!("Invalid status code"))?;
94 }
95 }
96
97 if used_properties.contains("locations") {
98 if is_location {
99 Err(anyhow::anyhow!("Nested locations are not allowed"))?;
100 } else if is_error_config {
101 Err(anyhow::anyhow!(
102 "The location configuration is not allowed in the error configuration"
103 ))?;
104 } else if is_global {
105 Err(anyhow::anyhow!(
106 "The location configuration is not allowed in the global configuration"
107 ))?;
108 }
109 }
110
111 if used_properties.contains("errorConfig") {
112 if is_error_config {
113 Err(anyhow::anyhow!("Nested error configuration is not allowed"))?;
114 } else if is_location {
115 Err(anyhow::anyhow!(
116 "The error configuration is not allowed in the location configuration"
117 ))?;
118 } else if is_global {
119 Err(anyhow::anyhow!(
120 "The error configuration is not allowed in the global configuration"
121 ))?;
122 }
123 }
124
125 if used_properties.contains("loadModules") {
126 if !is_global {
127 Err(anyhow::anyhow!(
128 "Module configuration is not allowed in host configuration"
129 ))?
130 }
131 if let Some(modules) = config["loadModules"].as_vec() {
132 let modules_iter = modules.iter();
133 for module_name_yaml in modules_iter {
134 if module_name_yaml.as_str().is_none() {
135 Err(anyhow::anyhow!("Invalid module name"))?
136 }
137 }
138 } else {
139 Err(anyhow::anyhow!("Invalid module configuration"))?
140 }
141 }
142
143 if used_properties.contains("port") {
144 if !is_global {
145 Err(anyhow::anyhow!(
146 "HTTP port configuration is not allowed in host configuration"
147 ))?
148 }
149 if let Some(port) = config["port"].as_i64() {
150 if !(0..=65535).contains(&port) {
151 Err(anyhow::anyhow!("Invalid HTTP port"))?
152 }
153 } else if config["port"].as_str().is_none() {
154 Err(anyhow::anyhow!("Invalid HTTP port"))?
155 }
156 }
157
158 if used_properties.contains("sport") {
159 if !is_global {
160 Err(anyhow::anyhow!(
161 "HTTPS port configuration is not allowed in host configuration"
162 ))?
163 }
164 if let Some(port) = config["sport"].as_i64() {
165 if !(0..=65535).contains(&port) {
166 Err(anyhow::anyhow!("Invalid HTTPS port"))?
167 }
168 } else if config["sport"].as_str().is_none() {
169 Err(anyhow::anyhow!("Invalid HTTPS port"))?
170 }
171 }
172
173 if used_properties.contains("secure") {
174 if !is_global {
175 Err(anyhow::anyhow!(
176 "HTTPS enabling configuration is not allowed in host configuration"
177 ))?
178 }
179 if config["secure"].as_bool().is_none() {
180 Err(anyhow::anyhow!("Invalid HTTPS enabling option value"))?
181 }
182 }
183
184 if used_properties.contains("enableHTTP2") {
185 if !is_global {
186 Err(anyhow::anyhow!(
187 "HTTP/2 enabling configuration is not allowed in host configuration"
188 ))?
189 }
190 if config["enableHTTP2"].as_bool().is_none() {
191 Err(anyhow::anyhow!("Invalid HTTP/2 enabling option value"))?
192 }
193 }
194
195 if used_properties.contains("enableHTTP3") {
196 if !is_global {
197 Err(anyhow::anyhow!(
198 "HTTP/3 enabling configuration is not allowed in host configuration"
199 ))?
200 }
201 if config["enableHTTP3"].as_bool().is_none() {
202 Err(anyhow::anyhow!("Invalid HTTP/3 enabling option value"))?
203 }
204 }
205
206 if used_properties.contains("logFilePath") {
207 if !is_global {
208 Err(anyhow::anyhow!(
209 "Log file configuration is not allowed in host configuration"
210 ))?
211 }
212 if config["logFilePath"].as_str().is_none() {
213 Err(anyhow::anyhow!("Invalid log file path"))?
214 }
215 }
216
217 if used_properties.contains("errorLogFilePath") {
218 if !is_global {
219 Err(anyhow::anyhow!(
220 "Error log file configuration is not allowed in host configuration"
221 ))?
222 }
223 if config["errorLogFilePath"].as_str().is_none() {
224 Err(anyhow::anyhow!("Invalid error log file path"))?
225 }
226 }
227
228 if used_properties.contains("cert") {
229 if !is_global {
230 Err(anyhow::anyhow!(
231 "TLS certificate configuration is not allowed in host configuration"
232 ))?
233 }
234 if config["cert"].as_str().is_none() {
235 Err(anyhow::anyhow!("Invalid TLS certificate path"))?
236 }
237 }
238
239 if used_properties.contains("key") {
240 if !is_global {
241 Err(anyhow::anyhow!(
242 "Private key configuration is not allowed in host configuration"
243 ))?
244 }
245 if config["key"].as_str().is_none() {
246 Err(anyhow::anyhow!("Invalid private key path"))?
247 }
248 }
249
250 if used_properties.contains("sni") {
251 if !is_global {
252 Err(anyhow::anyhow!(
253 "SNI configuration is not allowed in host configuration"
254 ))?
255 }
256 if let Some(sni) = config["sni"].as_hash() {
257 let sni_hostnames = sni.keys();
258 for sni_hostname_unknown in sni_hostnames {
259 if let Some(sni_hostname) = sni_hostname_unknown.as_str() {
260 if sni[sni_hostname_unknown]["cert"].as_str().is_none() {
261 Err(anyhow::anyhow!(
262 "Invalid SNI TLS certificate path for \"{}\"",
263 sni_hostname
264 ))?
265 }
266 if sni[sni_hostname_unknown]["key"].as_str().is_none() {
267 Err(anyhow::anyhow!(
268 "Invalid SNI private key certificate path for \"{}\"",
269 sni_hostname
270 ))?
271 }
272 } else {
273 Err(anyhow::anyhow!("Invalid SNI hostname"))?
274 }
275 }
276 } else {
277 Err(anyhow::anyhow!("Invalid SNI certificate list"))?
278 }
279 }
280
281 if used_properties.contains("http2Settings") {
282 if !is_global {
283 Err(anyhow::anyhow!(
284 "HTTP/2 configuration is not allowed in host configuration"
285 ))?
286 }
287 if config["http2Settings"].as_hash().is_some() {
288 if let Some(initial_window_size) = config["http2Settings"]["initialWindowSize"].as_i64() {
289 if !(0..=2_147_483_647).contains(&initial_window_size) {
290 Err(anyhow::anyhow!("Invalid HTTP/2 initial window size"))?
291 }
292 }
293
294 if let Some(max_frame_size) = config["http2Settings"]["maxFrameSize"].as_i64() {
295 if !(16_384..=16_777_215).contains(&max_frame_size) {
296 Err(anyhow::anyhow!("Invalid HTTP/2 max frame size"))?
297 }
298 }
299
300 if let Some(max_concurrent_streams) = config["http2Settings"]["maxConcurrentStreams"].as_i64()
301 {
302 if max_concurrent_streams < 0 {
303 Err(anyhow::anyhow!("Invalid HTTP/2 max concurrent streams"))?
304 }
305 }
306
307 if let Some(max_header_list_size) = config["http2Settings"]["maxHeaderListSize"].as_i64() {
308 if max_header_list_size < 0 {
309 Err(anyhow::anyhow!("Invalid HTTP/2 max header list size"))?
310 }
311 }
312
313 if !config["http2Settings"]["enableConnectProtocol"].is_badvalue()
314 && config["http2Settings"]["enableConnectProtocol"]
315 .as_bool()
316 .is_none()
317 {
318 Err(anyhow::anyhow!(
319 "Invalid HTTP/2 enable connect protocol option"
320 ))?
321 }
322 } else {
323 Err(anyhow::anyhow!("Invalid HTTP/2 options"))?
324 }
325 }
326
327 if used_properties.contains("useClientCertificate") {
328 if !is_global {
329 Err(anyhow::anyhow!(
330 "Client certificate verfication enabling option is not allowed in host configuration"
331 ))?
332 }
333 if config["useClientCertificate"].as_bool().is_none() {
334 Err(anyhow::anyhow!(
335 "Invalid client certificate verification enabling option value"
336 ))?
337 }
338 }
339
340 if used_properties.contains("cipherSuite") {
341 if !is_global {
342 Err(anyhow::anyhow!(
343 "Cipher suite configuration is not allowed in host configuration"
344 ))?
345 }
346 if let Some(cipher_suites) = config["cipherSuite"].as_vec() {
347 let cipher_suites_iter = cipher_suites.iter();
348 for cipher_suite_name_yaml in cipher_suites_iter {
349 if cipher_suite_name_yaml.as_str().is_none() {
350 Err(anyhow::anyhow!("Invalid cipher suite"))?
351 }
352 }
353 } else {
354 Err(anyhow::anyhow!("Invalid cipher suite configuration"))?
355 }
356 }
357
358 if used_properties.contains("ecdhCurve") {
359 if !is_global {
360 Err(anyhow::anyhow!(
361 "ECDH curve configuration is not allowed in host configuration"
362 ))?
363 }
364 if let Some(ecdh_curves) = config["ecdhCurve"].as_vec() {
365 let ecdh_curves_iter = ecdh_curves.iter();
366 for ecdh_curve_name_yaml in ecdh_curves_iter {
367 if ecdh_curve_name_yaml.as_str().is_none() {
368 Err(anyhow::anyhow!("Invalid ECDH curve"))?
369 }
370 }
371 } else {
372 Err(anyhow::anyhow!("Invalid ECDH curve configuration"))?
373 }
374 }
375
376 if used_properties.contains("tlsMinVersion") {
377 if !is_global {
378 Err(anyhow::anyhow!(
379 "Minimum TLS version is not allowed in host configuration"
380 ))?
381 }
382 if config["tlsMinVersion"].as_str().is_none() {
383 Err(anyhow::anyhow!("Invalid minimum TLS version"))?
384 }
385 }
386
387 if used_properties.contains("tlsMaxVersion") {
388 if !is_global {
389 Err(anyhow::anyhow!(
390 "Maximum TLS version is not allowed in host configuration"
391 ))?
392 }
393 if config["tlsMaxVersion"].as_str().is_none() {
394 Err(anyhow::anyhow!("Invalid maximum TLS version"))?
395 }
396 }
397
398 if used_properties.contains("enableOCSPStapling") {
399 if !is_global {
400 Err(anyhow::anyhow!(
401 "OCSP stapling enabling option is not allowed in host configuration"
402 ))?
403 }
404 if config["enableOCSPStapling"].as_bool().is_none() {
405 Err(anyhow::anyhow!(
406 "Invalid OCSP stapling enabling option value"
407 ))?
408 }
409 }
410
411 if used_properties.contains("serverAdministratorEmail")
412 && config["serverAdministratorEmail"].as_str().is_none()
413 {
414 Err(anyhow::anyhow!(
415 "Invalid server administrator email address"
416 ))?
417 }
418
419 if used_properties.contains("enableIPSpoofing") && config["enableIPSpoofing"].as_bool().is_none()
420 {
421 Err(anyhow::anyhow!(
422 "Invalid X-Forwarded-For enabling option value"
423 ))?
424 }
425
426 if used_properties.contains("disableNonEncryptedServer") {
427 if !is_global {
428 Err(anyhow::anyhow!(
429 "Non-encrypted server disabling option is not allowed in host configuration"
430 ))?
431 }
432 if config["disableNonEncryptedServer"].as_bool().is_none() {
433 Err(anyhow::anyhow!(
434 "Invalid non-encrypted server disabling option value"
435 ))?
436 }
437 }
438
439 if used_properties.contains("blocklist") {
440 if !is_global {
441 Err(anyhow::anyhow!(
442 "Block list configuration is not allowed in host configuration"
443 ))?
444 }
445 if let Some(blocklist) = config["blocklist"].as_vec() {
446 let blocklist_iter = blocklist.iter();
447 for blocklist_entry_yaml in blocklist_iter {
448 match blocklist_entry_yaml.as_str() {
449 Some(blocklist_entry) => {
450 if !validate_ip(blocklist_entry) {
451 Err(anyhow::anyhow!("Invalid block list entry"))?
452 }
453 }
454 None => Err(anyhow::anyhow!("Invalid block list entry"))?,
455 }
456 }
457 } else {
458 Err(anyhow::anyhow!("Invalid block list configuration"))?
459 }
460 }
461
462 if used_properties.contains("environmentVariables") {
463 if !is_global {
464 Err(anyhow::anyhow!(
465 "Environment variable configuration is not allowed in host configuration"
466 ))?
467 }
468 if let Some(environment_variables_hash) = config["environmentVariables"].as_hash() {
469 let environment_variables_hash_iter = environment_variables_hash.iter();
470 for (var_name, var_value) in environment_variables_hash_iter {
471 if var_name.as_str().is_none() || var_value.as_str().is_none() {
472 Err(anyhow::anyhow!("Invalid environment variables"))?
473 }
474 }
475 } else {
476 Err(anyhow::anyhow!("Invalid environment variables"))?
477 }
478 }
479
480 if used_properties.contains("disableToHTTPSRedirect")
481 && config["disableToHTTPSRedirect"].as_bool().is_none()
482 {
483 Err(anyhow::anyhow!(
484 "Invalid HTTP to HTTPS redirect disabling option value"
485 ))?
486 }
487
488 if used_properties.contains("wwwredirect") && config["wwwredirect"].as_bool().is_none() {
489 Err(anyhow::anyhow!(
490 "Invalid to \"www.\" URL redirect disabling option value"
491 ))?
492 }
493
494 if used_properties.contains("customHeaders") {
495 if let Some(custom_headers_hash) = config["customHeaders"].as_hash() {
496 let custom_headers_hash_iter = custom_headers_hash.iter();
497 for (header_name, header_value) in custom_headers_hash_iter {
498 if let Some(header_name) = header_name.as_str() {
499 if let Some(header_value) = header_value.as_str() {
500 if HeaderValue::from_str(header_value).is_err()
501 || HeaderName::from_str(header_name).is_err()
502 {
503 Err(anyhow::anyhow!("Invalid custom headers"))?
504 }
505 } else {
506 Err(anyhow::anyhow!("Invalid custom headers"))?
507 }
508 } else {
509 Err(anyhow::anyhow!("Invalid custom headers"))?
510 }
511 }
512 } else {
513 Err(anyhow::anyhow!("Invalid custom headers"))?
514 }
515 }
516
517 if used_properties.contains("rewriteMap") {
518 if let Some(rewrite_map) = config["rewriteMap"].as_vec() {
519 let rewrite_map_iter = rewrite_map.iter();
520 for rewrite_map_entry_yaml in rewrite_map_iter {
521 if !rewrite_map_entry_yaml.is_hash() {
522 Err(anyhow::anyhow!("Invalid URL rewrite map"))?
523 }
524 if rewrite_map_entry_yaml["regex"].as_str().is_none() {
525 Err(anyhow::anyhow!("Invalid URL rewrite map"))?
526 }
527 if rewrite_map_entry_yaml["replacement"].as_str().is_none() {
528 Err(anyhow::anyhow!("Invalid URL rewrite map"))?
529 }
530 if !rewrite_map_entry_yaml["isNotFile"].is_badvalue()
531 && rewrite_map_entry_yaml["isNotFile"].as_bool().is_none()
532 {
533 Err(anyhow::anyhow!("Invalid URL rewrite map"))?
534 }
535 if !rewrite_map_entry_yaml["isNotDirectory"].is_badvalue()
536 && rewrite_map_entry_yaml["isNotDirectory"].as_bool().is_none()
537 {
538 Err(anyhow::anyhow!("Invalid URL rewrite map"))?
539 }
540 if !rewrite_map_entry_yaml["allowDoubleSlashes"].is_badvalue()
541 && rewrite_map_entry_yaml["allowDoubleSlashes"]
542 .as_bool()
543 .is_none()
544 {
545 Err(anyhow::anyhow!("Invalid URL rewrite map"))?
546 }
547 if !rewrite_map_entry_yaml["last"].is_badvalue()
548 && rewrite_map_entry_yaml["last"].as_bool().is_none()
549 {
550 Err(anyhow::anyhow!("Invalid URL rewrite map"))?
551 }
552 }
553 } else {
554 Err(anyhow::anyhow!("Invalid URL rewrite map"))?
555 }
556 }
557
558 if used_properties.contains("enableRewriteLogging")
559 && config["enableRewriteLogging"].as_bool().is_none()
560 {
561 Err(anyhow::anyhow!(
562 "Invalid URL rewrite logging enabling option value"
563 ))?
564 }
565
566 if used_properties.contains("disableTrailingSlashRedirects")
567 && config["disableTrailingSlashRedirects"].as_bool().is_none()
568 {
569 Err(anyhow::anyhow!(
570 "Invalid trailing slash redirect disabling option value"
571 ))?
572 }
573
574 if used_properties.contains("users") {
575 if let Some(users) = config["users"].as_vec() {
576 let users_iter = users.iter();
577 for user_yaml in users_iter {
578 if !user_yaml.is_hash() {
579 Err(anyhow::anyhow!("Invalid user configuration"))?
580 }
581 if user_yaml["name"].as_str().is_none() {
582 Err(anyhow::anyhow!("Invalid user configuration"))?
583 }
584 if user_yaml["pass"].as_str().is_none() {
585 Err(anyhow::anyhow!("Invalid user configuration"))?
586 }
587 }
588 } else {
589 Err(anyhow::anyhow!("Invalid user configuration"))?
590 }
591 }
592
593 if used_properties.contains("nonStandardCodes") {
594 if let Some(non_standard_codes) = config["nonStandardCodes"].as_vec() {
595 let non_standard_codes_iter = non_standard_codes.iter();
596 for non_standard_code_yaml in non_standard_codes_iter {
597 if !non_standard_code_yaml.is_hash() {
598 Err(anyhow::anyhow!(
599 "Invalid non-standard status code configuration"
600 ))?
601 }
602 if non_standard_code_yaml["scode"].as_i64().is_none() {
603 Err(anyhow::anyhow!(
604 "Invalid non-standard status code configuration"
605 ))?
606 }
607 if !non_standard_code_yaml["regex"].is_badvalue()
608 && non_standard_code_yaml["regex"].as_str().is_none()
609 {
610 Err(anyhow::anyhow!(
611 "Invalid non-standard status code configuration"
612 ))?
613 }
614 if !non_standard_code_yaml["url"].is_badvalue()
615 && non_standard_code_yaml["url"].as_str().is_none()
616 {
617 Err(anyhow::anyhow!(
618 "Invalid non-standard status code configuration"
619 ))?
620 }
621 if non_standard_code_yaml["regex"].is_badvalue()
622 && non_standard_code_yaml["url"].is_badvalue()
623 {
624 Err(anyhow::anyhow!(
625 "Invalid non-standard status code configuration"
626 ))?
627 }
628 if !non_standard_code_yaml["realm"].is_badvalue()
629 && non_standard_code_yaml["realm"].as_str().is_none()
630 {
631 Err(anyhow::anyhow!(
632 "Invalid non-standard status code configuration"
633 ))?
634 }
635 if !non_standard_code_yaml["disableBruteProtection"].is_badvalue()
636 && non_standard_code_yaml["disableBruteProtection"]
637 .as_bool()
638 .is_none()
639 {
640 Err(anyhow::anyhow!(
641 "Invalid non-standard status code configuration"
642 ))?
643 }
644 if !non_standard_code_yaml["userList"].is_badvalue() {
645 if let Some(users) = non_standard_code_yaml["userList"].as_vec() {
646 let users_iter = users.iter();
647 for user_yaml in users_iter {
648 if user_yaml.as_str().is_none() {
649 Err(anyhow::anyhow!(
650 "Invalid non-standard status code configuration"
651 ))?
652 }
653 }
654 } else {
655 Err(anyhow::anyhow!(
656 "Invalid non-standard status code configuration"
657 ))?
658 }
659 }
660 if !non_standard_code_yaml["users"].is_badvalue() {
661 if let Some(users) = non_standard_code_yaml["users"].as_vec() {
662 let users_iter = users.iter();
663 for user_yaml in users_iter {
664 match user_yaml.as_str() {
665 Some(user) => {
666 if !validate_ip(user) {
667 Err(anyhow::anyhow!(
668 "Invalid non-standard status code configuration"
669 ))?
670 }
671 }
672 None => Err(anyhow::anyhow!(
673 "Invalid non-standard status code configuration"
674 ))?,
675 }
676 }
677 } else {
678 Err(anyhow::anyhow!(
679 "Invalid non-standard status code configuration"
680 ))?
681 }
682 }
683 }
684 } else {
685 Err(anyhow::anyhow!(
686 "Invalid non-standard status code configuration"
687 ))?
688 }
689 }
690
691 if used_properties.contains("errorPages") {
692 if let Some(error_pages) = config["errorPages"].as_vec() {
693 let error_pages_iter = error_pages.iter();
694 for error_page_yaml in error_pages_iter {
695 if !error_page_yaml.is_hash() {
696 Err(anyhow::anyhow!("Invalid custom error page configuration"))?
697 }
698 if error_page_yaml["scode"].as_i64().is_none() {
699 Err(anyhow::anyhow!("Invalid custom error page configuration"))?
700 }
701 if error_page_yaml["path"].as_str().is_none() {
702 Err(anyhow::anyhow!("Invalid custom error page configuration"))?
703 }
704 }
705 } else {
706 Err(anyhow::anyhow!("Invalid custom error page configuration"))?
707 }
708 }
709
710 if used_properties.contains("wwwroot") && config["wwwroot"].as_str().is_none() {
711 Err(anyhow::anyhow!("Invalid webroot"))?
712 }
713
714 if used_properties.contains("enableETag") && config["enableETag"].as_bool().is_none() {
715 Err(anyhow::anyhow!("Invalid ETag enabling option"))?
716 }
717
718 if used_properties.contains("enableCompression")
719 && config["enableCompression"].as_bool().is_none()
720 {
721 Err(anyhow::anyhow!("Invalid HTTP compression enabling option"))?
722 }
723
724 if used_properties.contains("enableDirectoryListing")
725 && config["enableDirectoryListing"].as_bool().is_none()
726 {
727 Err(anyhow::anyhow!("Invalid directory listing enabling option"))?
728 }
729
730 if used_properties.contains("enableAutomaticTLS") {
731 if !is_global {
732 Err(anyhow::anyhow!(
733 "Automatic TLS enabling configuration is not allowed in host configuration"
734 ))?
735 }
736 if config["enableAutomaticTLS"].as_bool().is_none() {
737 Err(anyhow::anyhow!(
738 "Invalid automatic TLS enabling option value"
739 ))?
740 }
741 }
742
743 if used_properties.contains("useAutomaticTLSHTTPChallenge") {
744 if !is_global {
745 Err(anyhow::anyhow!(
746 "Automatic TLS HTTP challenge enabling configuration is not allowed in host configuration"
747 ))?
748 }
749 if config["useAutomaticTLSHTTPChallenge"].as_bool().is_none() {
750 Err(anyhow::anyhow!(
751 "Invalid automatic TLS HTTP challenge enabling option value"
752 ))?
753 }
754 }
755
756 if used_properties.contains("automaticTLSContactEmail") {
757 if !is_global {
758 Err(anyhow::anyhow!(
759 "Automatic TLS contact email address configuration is not allowed in host configuration"
760 ))?
761 }
762 if config["automaticTLSContactEmail"].as_str().is_none() {
763 Err(anyhow::anyhow!(
764 "Invalid automatic TLS contact email address"
765 ))?
766 }
767 }
768
769 if used_properties.contains("automaticTLSContactCacheDirectory") {
770 if !is_global {
771 Err(anyhow::anyhow!(
772 "Automatic TLS cache directory configuration is not allowed in host configuration"
773 ))?
774 }
775 if config["automaticTLSContactCacheDirectory"]
776 .as_str()
777 .is_none()
778 {
779 Err(anyhow::anyhow!(
780 "Invalid automatic TLS cache directory path"
781 ))?
782 }
783 }
784
785 if used_properties.contains("automaticTLSLetsEncryptProduction") {
786 if !is_global {
787 Err(anyhow::anyhow!(
788 "Let's Encrypt production endpoint for automatic TLS enabling configuration is not allowed in host configuration"
789 ))?
790 }
791 if config["automaticTLSLetsEncryptProduction"]
792 .as_bool()
793 .is_none()
794 {
795 Err(anyhow::anyhow!(
796 "Invalid Let's Encrypt production endpoint for automatic TLS enabling option value"
797 ))?
798 }
799 }
800
801 if used_properties.contains("timeout") {
802 if !is_global {
803 Err(anyhow::anyhow!(
804 "Server timeout configuration is not allowed in host configuration"
805 ))?
806 }
807 if !config["timeout"].is_null() {
808 if let Some(maximum_cache_response_size) = config["timeout"].as_i64() {
809 if maximum_cache_response_size < 0 {
810 Err(anyhow::anyhow!("Invalid server timeout"))?
811 }
812 } else {
813 Err(anyhow::anyhow!("Invalid server timeout"))?
814 }
815 }
816 }
817
818 for module_optional_builtin in modules_optional_builtin.iter() {
819 match module_optional_builtin as &str {
820 #[cfg(feature = "rproxy")]
821 "rproxy" => {
822 if used_properties.contains("proxyTo") {
823 if let Some(proxy_urls) = config["proxyTo"].as_vec() {
824 let proxy_urls_iter = proxy_urls.iter();
825 for proxy_url_yaml in proxy_urls_iter {
826 if proxy_url_yaml.as_str().is_none() {
827 Err(anyhow::anyhow!("Invalid reverse proxy target URL value"))?
828 }
829 }
830 } else if config["proxyTo"].as_str().is_none() {
831 Err(anyhow::anyhow!("Invalid reverse proxy target URL value"))?
832 }
833 }
834
835 if used_properties.contains("secureProxyTo") {
836 if let Some(proxy_urls) = config["secureProxyTo"].as_vec() {
837 let proxy_urls_iter = proxy_urls.iter();
838 for proxy_url_yaml in proxy_urls_iter {
839 if proxy_url_yaml.as_str().is_none() {
840 Err(anyhow::anyhow!(
841 "Invalid secure reverse proxy target URL value"
842 ))?
843 }
844 }
845 } else if config["secureProxyTo"].as_str().is_none() {
846 Err(anyhow::anyhow!(
847 "Invalid secure reverse proxy target URL value"
848 ))?
849 }
850 }
851
852 if used_properties.contains("enableLoadBalancerHealthCheck")
853 && config["enableLoadBalancerHealthCheck"].as_bool().is_none()
854 {
855 Err(anyhow::anyhow!(
856 "Invalid load balancer health check enabling option value"
857 ))?
858 }
859
860 if used_properties.contains("loadBalancerHealthCheckMaximumFails") {
861 if let Some(window) = config["loadBalancerHealthCheckMaximumFails"].as_i64() {
862 if window < 0 {
863 Err(anyhow::anyhow!(
864 "Invalid load balancer health check maximum fails value"
865 ))?
866 }
867 } else {
868 Err(anyhow::anyhow!(
869 "Invalid load balancer health check maximum fails value"
870 ))?
871 }
872 }
873
874 if used_properties.contains("loadBalancerHealthCheckWindow") {
875 if !is_global {
876 Err(anyhow::anyhow!(
877 "Load balancer health check window configuration is not allowed in host configuration"
878 ))?
879 }
880 if let Some(window) = config["loadBalancerHealthCheckWindow"].as_i64() {
881 if window < 0 {
882 Err(anyhow::anyhow!(
883 "Invalid load balancer health check window value"
884 ))?
885 }
886 } else {
887 Err(anyhow::anyhow!(
888 "Invalid load balancer health check window value"
889 ))?
890 }
891 }
892
893 if used_properties.contains("disableProxyCertificateVerification")
894 && config["disableProxyCertificateVerification"]
895 .as_bool()
896 .is_none()
897 {
898 Err(anyhow::anyhow!(
899 "Invalid proxy certificate verification disabling option value"
900 ))?
901 }
902
903 if used_properties.contains("proxyInterceptErrors")
904 && config["proxyInterceptErrors"].as_bool().is_none()
905 {
906 Err(anyhow::anyhow!(
907 "Invalid reverse proxy error interception option value"
908 ))?
909 }
910 }
911 #[cfg(feature = "cache")]
912 "cache" => {
913 if used_properties.contains("cacheVaryHeaders") {
914 if let Some(modules) = config["cacheVaryHeaders"].as_vec() {
915 let modules_iter = modules.iter();
916 for module_name_yaml in modules_iter {
917 if module_name_yaml.as_str().is_none() {
918 Err(anyhow::anyhow!("Invalid varying cache header"))?
919 }
920 }
921 } else {
922 Err(anyhow::anyhow!(
923 "Invalid varying cache headers configuration"
924 ))?
925 }
926 }
927
928 if used_properties.contains("cacheIgnoreHeaders") {
929 if let Some(modules) = config["cacheIgnoreHeaders"].as_vec() {
930 let modules_iter = modules.iter();
931 for module_name_yaml in modules_iter {
932 if module_name_yaml.as_str().is_none() {
933 Err(anyhow::anyhow!("Invalid ignored cache header"))?
934 }
935 }
936 } else {
937 Err(anyhow::anyhow!(
938 "Invalid ignored cache headers configuration"
939 ))?
940 }
941 }
942
943 if used_properties.contains("maximumCacheResponseSize")
944 && !config["maximumCacheResponseSize"].is_null()
945 {
946 if let Some(maximum_cache_response_size) = config["maximumCacheResponseSize"].as_i64() {
947 if maximum_cache_response_size < 0 {
948 Err(anyhow::anyhow!("Invalid maximum cache response size"))?
949 }
950 } else {
951 Err(anyhow::anyhow!("Invalid maximum cache response size"))?
952 }
953 }
954
955 if used_properties.contains("maximumCacheEntries") {
956 if !is_global {
957 Err(anyhow::anyhow!(
958 "Maximum cache entries configuration is not allowed in host configuration"
959 ))?
960 }
961 if !config["maximumCacheEntries"].is_null() {
962 if let Some(maximum_cache_response_size) = config["maximumCacheEntries"].as_i64() {
963 if maximum_cache_response_size < 0 {
964 Err(anyhow::anyhow!("Invalid maximum cache entries"))?
965 }
966 } else {
967 Err(anyhow::anyhow!("Invalid maximum cache entries"))?
968 }
969 }
970 }
971 }
972 #[cfg(feature = "cgi")]
973 "cgi" => {
974 if used_properties.contains("cgiScriptExtensions") {
975 if let Some(cgi_script_extensions) = config["cgiScriptExtensions"].as_vec() {
976 let cgi_script_extensions_iter = cgi_script_extensions.iter();
977 for cgi_script_extension_yaml in cgi_script_extensions_iter {
978 if cgi_script_extension_yaml.as_str().is_none() {
979 Err(anyhow::anyhow!("Invalid CGI script extension"))?
980 }
981 }
982 } else {
983 Err(anyhow::anyhow!(
984 "Invalid CGI script extension configuration"
985 ))?
986 }
987 }
988
989 if used_properties.contains("cgiScriptInterpreters") {
990 if let Some(cgi_script_interpreters) = config["cgiScriptInterpreters"].as_hash() {
991 for (cgi_script_interpreter_extension_unknown, cgi_script_interpreter_params_unknown) in
992 cgi_script_interpreters.iter()
993 {
994 if cgi_script_interpreter_extension_unknown.as_str().is_some() {
995 if !cgi_script_interpreter_params_unknown.is_null() {
996 if let Some(cgi_script_interpreter_params) =
997 cgi_script_interpreter_params_unknown.as_vec()
998 {
999 let cgi_script_interpreter_params_iter = cgi_script_interpreter_params.iter();
1000 for cgi_script_interpreter_param_yaml in cgi_script_interpreter_params_iter {
1001 if cgi_script_interpreter_param_yaml.as_str().is_none() {
1002 Err(anyhow::anyhow!("Invalid CGI script interpreter parameter"))?
1003 }
1004 }
1005 } else {
1006 Err(anyhow::anyhow!("Invalid CGI script interpreter parameters"))?
1007 }
1008 }
1009 } else {
1010 Err(anyhow::anyhow!("Invalid CGI script interpreter extension"))?
1011 }
1012 }
1013 } else {
1014 Err(anyhow::anyhow!(
1015 "Invalid CGI script interpreter configuration"
1016 ))?
1017 }
1018 }
1019 }
1020 #[cfg(feature = "scgi")]
1021 "scgi" => {
1022 if used_properties.contains("scgiTo") && config["scgiTo"].as_str().is_none() {
1023 Err(anyhow::anyhow!("Invalid SCGI target URL value"))?
1024 }
1025
1026 if used_properties.contains("scgiPath") && config["scgiPath"].as_str().is_none() {
1027 Err(anyhow::anyhow!("Invalid SCGI path"))?
1028 }
1029 }
1030 #[cfg(feature = "fcgi")]
1031 "fcgi" => {
1032 if used_properties.contains("fcgiScriptExtensions") {
1033 if let Some(fastcgi_script_extensions) = config["fcgiScriptExtensions"].as_vec() {
1034 let fastcgi_script_extensions_iter = fastcgi_script_extensions.iter();
1035 for fastcgi_script_extension_yaml in fastcgi_script_extensions_iter {
1036 if fastcgi_script_extension_yaml.as_str().is_none() {
1037 Err(anyhow::anyhow!("Invalid CGI script extension"))?
1038 }
1039 }
1040 } else {
1041 Err(anyhow::anyhow!(
1042 "Invalid CGI script extension configuration"
1043 ))?
1044 }
1045 }
1046
1047 if used_properties.contains("fcgiTo") && config["fcgiTo"].as_str().is_none() {
1048 Err(anyhow::anyhow!("Invalid FastCGI target URL value"))?
1049 }
1050
1051 if used_properties.contains("fcgiPath") && config["fcgiPath"].as_str().is_none() {
1052 Err(anyhow::anyhow!("Invalid FastCGI path"))?
1053 }
1054 }
1055 #[cfg(feature = "fauth")]
1056 "fauth" => {
1057 if used_properties.contains("authTo") && config["authTo"].as_str().is_none() {
1058 Err(anyhow::anyhow!(
1059 "Invalid forwarded authentication target URL value"
1060 ))?
1061 }
1062
1063 if used_properties.contains("forwardedAuthCopyHeaders") {
1064 if let Some(modules) = config["forwardedAuthCopyHeaders"].as_vec() {
1065 let modules_iter = modules.iter();
1066 for module_name_yaml in modules_iter {
1067 if module_name_yaml.as_str().is_none() {
1068 Err(anyhow::anyhow!(
1069 "Invalid forwarded authentication response header to copy"
1070 ))?
1071 }
1072 }
1073 } else {
1074 Err(anyhow::anyhow!(
1075 "Invalid forwarded authentication response headers to copy configuration"
1076 ))?
1077 }
1078 }
1079 }
1080 #[cfg(feature = "wsgi")]
1081 "wsgi" => {
1082 if used_properties.contains("wsgiApplicationPath")
1083 && config["wsgiApplicationPath"].as_str().is_none()
1084 {
1085 Err(anyhow::anyhow!("Invalid path to the WSGI application"))?
1086 }
1087
1088 if used_properties.contains("wsgiPath") && config["wsgiPath"].as_str().is_none() {
1089 Err(anyhow::anyhow!("Invalid WSGI request base path"))?
1090 }
1091
1092 if used_properties.contains("wsgiClearModuleImportPath") {
1093 if !is_global {
1094 Err(anyhow::anyhow!(
1095 "WSGI Python module import path clearing option is not allowed in host configuration"
1096 ))?
1097 }
1098 if config["wsgiClearModuleImportPath"].as_bool().is_none() {
1099 Err(anyhow::anyhow!(
1100 "Invalid WSGI Python module import path clearing option value"
1101 ))?
1102 }
1103 }
1104 }
1105 #[cfg(feature = "wsgid")]
1106 "wsgid" => {
1107 if used_properties.contains("wsgidApplicationPath")
1108 && config["wsgidApplicationPath"].as_str().is_none()
1109 {
1110 Err(anyhow::anyhow!(
1111 "Invalid path to the WSGI (with pre-forked process pool) application"
1112 ))?
1113 }
1114
1115 if used_properties.contains("wsgidPath") && config["wsgidPath"].as_str().is_none() {
1116 Err(anyhow::anyhow!(
1117 "Invalid WSGI (with pre-forked process pool) request base path"
1118 ))?
1119 }
1120 }
1121 #[cfg(feature = "asgi")]
1122 "asgi" => {
1123 if used_properties.contains("asgiApplicationPath")
1124 && config["asgiApplicationPath"].as_str().is_none()
1125 {
1126 Err(anyhow::anyhow!("Invalid path to the ASGI application"))?
1127 }
1128
1129 if used_properties.contains("asgiPath") && config["asgiPath"].as_str().is_none() {
1130 Err(anyhow::anyhow!("Invalid ASGI request base path"))?
1131 }
1132
1133 if used_properties.contains("asgiClearModuleImportPath") {
1134 if !is_global {
1135 Err(anyhow::anyhow!(
1136 "ASGI Python module import path clearing option is not allowed in host configuration"
1137 ))?
1138 }
1139 if config["asgiClearModuleImportPath"].as_bool().is_none() {
1140 Err(anyhow::anyhow!(
1141 "Invalid ASGI Python module import path clearing option value"
1142 ))?
1143 }
1144 }
1145 }
1146 _ => (),
1147 }
1148 }
1149
1150 Ok(used_properties.unused())
1151}
1152
1153pub fn prepare_config_for_validation(
1154 config: &Yaml,
1155) -> Result<impl Iterator<Item = (Yaml, bool, bool, bool)>, Box<dyn Error + Send + Sync>> {
1156 let mut global_vector = Vec::new();
1157 if let Some(global_config) = config["global"].as_hash() {
1158 let global_config_yaml = Yaml::Hash(global_config.clone());
1159 global_vector.push(global_config_yaml);
1160 }
1161
1162 let mut host_vector = Vec::new();
1163 let mut error_config_vector = Vec::new();
1164 let mut location_vector = Vec::new();
1165 if !config["hosts"].is_badvalue() {
1166 if let Some(hosts) = config["hosts"].as_vec() {
1167 for host in hosts.iter() {
1168 if !host["errorConfig"].is_badvalue() {
1169 if let Some(error_configs) = host["errorConfig"].as_vec() {
1170 error_config_vector.append(&mut error_configs.clone());
1171 } else {
1172 return Err(anyhow::anyhow!("Invalid location configuration").into());
1173 }
1174 }
1175 if !host["locations"].is_badvalue() {
1176 if let Some(locations) = host["locations"].as_vec() {
1177 location_vector.append(&mut locations.clone());
1178 } else {
1179 return Err(anyhow::anyhow!("Invalid location configuration").into());
1180 }
1181 }
1182 }
1183 host_vector = hosts.clone();
1184 } else {
1185 return Err(anyhow::anyhow!("Invalid virtual host configuration").into());
1186 }
1187 }
1188
1189 let iter = global_vector
1190 .into_iter()
1191 .map(|item| (item, true, false, false))
1192 .chain(
1193 host_vector
1194 .into_iter()
1195 .map(|item| (item, false, false, false)),
1196 )
1197 .chain(
1198 location_vector
1199 .into_iter()
1200 .map(|item| (item, false, true, false)),
1201 )
1202 .chain(
1203 error_config_vector
1204 .into_iter()
1205 .map(|item| (item, false, false, true)),
1206 );
1207
1208 Ok(iter)
1209}