ferron/util/
validate_config.rs

1use 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
9// Struct to store used configuration properties
10struct 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
50// Internal configuration file validators
51pub 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}