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        if used_properties.contains("disableProxyXForwarded")
912          && config["disableProxyXForwarded"].as_bool().is_none()
913        {
914          Err(anyhow::anyhow!(
915            "Invalid X-Forwarded-* header disabling on reverse proxy option value"
916          ))?
917        }
918      }
919      #[cfg(feature = "cache")]
920      "cache" => {
921        if used_properties.contains("cacheVaryHeaders") {
922          if let Some(modules) = config["cacheVaryHeaders"].as_vec() {
923            let modules_iter = modules.iter();
924            for module_name_yaml in modules_iter {
925              if module_name_yaml.as_str().is_none() {
926                Err(anyhow::anyhow!("Invalid varying cache header"))?
927              }
928            }
929          } else {
930            Err(anyhow::anyhow!(
931              "Invalid varying cache headers configuration"
932            ))?
933          }
934        }
935
936        if used_properties.contains("cacheIgnoreHeaders") {
937          if let Some(modules) = config["cacheIgnoreHeaders"].as_vec() {
938            let modules_iter = modules.iter();
939            for module_name_yaml in modules_iter {
940              if module_name_yaml.as_str().is_none() {
941                Err(anyhow::anyhow!("Invalid ignored cache header"))?
942              }
943            }
944          } else {
945            Err(anyhow::anyhow!(
946              "Invalid ignored cache headers configuration"
947            ))?
948          }
949        }
950
951        if used_properties.contains("maximumCacheResponseSize")
952          && !config["maximumCacheResponseSize"].is_null()
953        {
954          if let Some(maximum_cache_response_size) = config["maximumCacheResponseSize"].as_i64() {
955            if maximum_cache_response_size < 0 {
956              Err(anyhow::anyhow!("Invalid maximum cache response size"))?
957            }
958          } else {
959            Err(anyhow::anyhow!("Invalid maximum cache response size"))?
960          }
961        }
962
963        if used_properties.contains("maximumCacheEntries") {
964          if !is_global {
965            Err(anyhow::anyhow!(
966              "Maximum cache entries configuration is not allowed in host configuration"
967            ))?
968          }
969          if !config["maximumCacheEntries"].is_null() {
970            if let Some(maximum_cache_response_size) = config["maximumCacheEntries"].as_i64() {
971              if maximum_cache_response_size < 0 {
972                Err(anyhow::anyhow!("Invalid maximum cache entries"))?
973              }
974            } else {
975              Err(anyhow::anyhow!("Invalid maximum cache entries"))?
976            }
977          }
978        }
979      }
980      #[cfg(feature = "cgi")]
981      "cgi" => {
982        if used_properties.contains("cgiScriptExtensions") {
983          if let Some(cgi_script_extensions) = config["cgiScriptExtensions"].as_vec() {
984            let cgi_script_extensions_iter = cgi_script_extensions.iter();
985            for cgi_script_extension_yaml in cgi_script_extensions_iter {
986              if cgi_script_extension_yaml.as_str().is_none() {
987                Err(anyhow::anyhow!("Invalid CGI script extension"))?
988              }
989            }
990          } else {
991            Err(anyhow::anyhow!(
992              "Invalid CGI script extension configuration"
993            ))?
994          }
995        }
996
997        if used_properties.contains("cgiScriptInterpreters") {
998          if let Some(cgi_script_interpreters) = config["cgiScriptInterpreters"].as_hash() {
999            for (cgi_script_interpreter_extension_unknown, cgi_script_interpreter_params_unknown) in
1000              cgi_script_interpreters.iter()
1001            {
1002              if cgi_script_interpreter_extension_unknown.as_str().is_some() {
1003                if !cgi_script_interpreter_params_unknown.is_null() {
1004                  if let Some(cgi_script_interpreter_params) =
1005                    cgi_script_interpreter_params_unknown.as_vec()
1006                  {
1007                    let cgi_script_interpreter_params_iter = cgi_script_interpreter_params.iter();
1008                    for cgi_script_interpreter_param_yaml in cgi_script_interpreter_params_iter {
1009                      if cgi_script_interpreter_param_yaml.as_str().is_none() {
1010                        Err(anyhow::anyhow!("Invalid CGI script interpreter parameter"))?
1011                      }
1012                    }
1013                  } else {
1014                    Err(anyhow::anyhow!("Invalid CGI script interpreter parameters"))?
1015                  }
1016                }
1017              } else {
1018                Err(anyhow::anyhow!("Invalid CGI script interpreter extension"))?
1019              }
1020            }
1021          } else {
1022            Err(anyhow::anyhow!(
1023              "Invalid CGI script interpreter configuration"
1024            ))?
1025          }
1026        }
1027      }
1028      #[cfg(feature = "scgi")]
1029      "scgi" => {
1030        if used_properties.contains("scgiTo") && config["scgiTo"].as_str().is_none() {
1031          Err(anyhow::anyhow!("Invalid SCGI target URL value"))?
1032        }
1033
1034        if used_properties.contains("scgiPath") && config["scgiPath"].as_str().is_none() {
1035          Err(anyhow::anyhow!("Invalid SCGI path"))?
1036        }
1037      }
1038      #[cfg(feature = "fcgi")]
1039      "fcgi" => {
1040        if used_properties.contains("fcgiScriptExtensions") {
1041          if let Some(fastcgi_script_extensions) = config["fcgiScriptExtensions"].as_vec() {
1042            let fastcgi_script_extensions_iter = fastcgi_script_extensions.iter();
1043            for fastcgi_script_extension_yaml in fastcgi_script_extensions_iter {
1044              if fastcgi_script_extension_yaml.as_str().is_none() {
1045                Err(anyhow::anyhow!("Invalid CGI script extension"))?
1046              }
1047            }
1048          } else {
1049            Err(anyhow::anyhow!(
1050              "Invalid CGI script extension configuration"
1051            ))?
1052          }
1053        }
1054
1055        if used_properties.contains("fcgiTo") && config["fcgiTo"].as_str().is_none() {
1056          Err(anyhow::anyhow!("Invalid FastCGI target URL value"))?
1057        }
1058
1059        if used_properties.contains("fcgiPath") && config["fcgiPath"].as_str().is_none() {
1060          Err(anyhow::anyhow!("Invalid FastCGI path"))?
1061        }
1062      }
1063      #[cfg(feature = "fauth")]
1064      "fauth" => {
1065        if used_properties.contains("authTo") && config["authTo"].as_str().is_none() {
1066          Err(anyhow::anyhow!(
1067            "Invalid forwarded authentication target URL value"
1068          ))?
1069        }
1070
1071        if used_properties.contains("forwardedAuthCopyHeaders") {
1072          if let Some(modules) = config["forwardedAuthCopyHeaders"].as_vec() {
1073            let modules_iter = modules.iter();
1074            for module_name_yaml in modules_iter {
1075              if module_name_yaml.as_str().is_none() {
1076                Err(anyhow::anyhow!(
1077                  "Invalid forwarded authentication response header to copy"
1078                ))?
1079              }
1080            }
1081          } else {
1082            Err(anyhow::anyhow!(
1083              "Invalid forwarded authentication response headers to copy configuration"
1084            ))?
1085          }
1086        }
1087      }
1088      #[cfg(feature = "wsgi")]
1089      "wsgi" => {
1090        if used_properties.contains("wsgiApplicationPath")
1091          && config["wsgiApplicationPath"].as_str().is_none()
1092        {
1093          Err(anyhow::anyhow!("Invalid path to the WSGI application"))?
1094        }
1095
1096        if used_properties.contains("wsgiPath") && config["wsgiPath"].as_str().is_none() {
1097          Err(anyhow::anyhow!("Invalid WSGI request base path"))?
1098        }
1099
1100        if used_properties.contains("wsgiClearModuleImportPath") {
1101          if !is_global {
1102            Err(anyhow::anyhow!(
1103              "WSGI Python module import path clearing option is not allowed in host configuration"
1104            ))?
1105          }
1106          if config["wsgiClearModuleImportPath"].as_bool().is_none() {
1107            Err(anyhow::anyhow!(
1108              "Invalid WSGI Python module import path clearing option value"
1109            ))?
1110          }
1111        }
1112      }
1113      #[cfg(feature = "wsgid")]
1114      "wsgid" => {
1115        if used_properties.contains("wsgidApplicationPath")
1116          && config["wsgidApplicationPath"].as_str().is_none()
1117        {
1118          Err(anyhow::anyhow!(
1119            "Invalid path to the WSGI (with pre-forked process pool) application"
1120          ))?
1121        }
1122
1123        if used_properties.contains("wsgidPath") && config["wsgidPath"].as_str().is_none() {
1124          Err(anyhow::anyhow!(
1125            "Invalid WSGI (with pre-forked process pool) request base path"
1126          ))?
1127        }
1128      }
1129      #[cfg(feature = "asgi")]
1130      "asgi" => {
1131        if used_properties.contains("asgiApplicationPath")
1132          && config["asgiApplicationPath"].as_str().is_none()
1133        {
1134          Err(anyhow::anyhow!("Invalid path to the ASGI application"))?
1135        }
1136
1137        if used_properties.contains("asgiPath") && config["asgiPath"].as_str().is_none() {
1138          Err(anyhow::anyhow!("Invalid ASGI request base path"))?
1139        }
1140
1141        if used_properties.contains("asgiClearModuleImportPath") {
1142          if !is_global {
1143            Err(anyhow::anyhow!(
1144              "ASGI Python module import path clearing option is not allowed in host configuration"
1145            ))?
1146          }
1147          if config["asgiClearModuleImportPath"].as_bool().is_none() {
1148            Err(anyhow::anyhow!(
1149              "Invalid ASGI Python module import path clearing option value"
1150            ))?
1151          }
1152        }
1153      }
1154      _ => (),
1155    }
1156  }
1157
1158  Ok(used_properties.unused())
1159}
1160
1161pub fn prepare_config_for_validation(
1162  config: &Yaml,
1163) -> Result<impl Iterator<Item = (Yaml, bool, bool, bool)>, Box<dyn Error + Send + Sync>> {
1164  let mut global_vector = Vec::new();
1165  if let Some(global_config) = config["global"].as_hash() {
1166    let global_config_yaml = Yaml::Hash(global_config.clone());
1167    global_vector.push(global_config_yaml);
1168  }
1169
1170  let mut host_vector = Vec::new();
1171  let mut error_config_vector = Vec::new();
1172  let mut location_vector = Vec::new();
1173  if !config["hosts"].is_badvalue() {
1174    if let Some(hosts) = config["hosts"].as_vec() {
1175      for host in hosts.iter() {
1176        if !host["errorConfig"].is_badvalue() {
1177          if let Some(error_configs) = host["errorConfig"].as_vec() {
1178            error_config_vector.append(&mut error_configs.clone());
1179          } else {
1180            return Err(anyhow::anyhow!("Invalid location configuration").into());
1181          }
1182        }
1183        if !host["locations"].is_badvalue() {
1184          if let Some(locations) = host["locations"].as_vec() {
1185            location_vector.append(&mut locations.clone());
1186          } else {
1187            return Err(anyhow::anyhow!("Invalid location configuration").into());
1188          }
1189        }
1190      }
1191      host_vector = hosts.clone();
1192    } else {
1193      return Err(anyhow::anyhow!("Invalid virtual host configuration").into());
1194    }
1195  }
1196
1197  let iter = global_vector
1198    .into_iter()
1199    .map(|item| (item, true, false, false))
1200    .chain(
1201      host_vector
1202        .into_iter()
1203        .map(|item| (item, false, false, false)),
1204    )
1205    .chain(
1206      location_vector
1207        .into_iter()
1208        .map(|item| (item, false, true, false)),
1209    )
1210    .chain(
1211      error_config_vector
1212        .into_iter()
1213        .map(|item| (item, false, false, true)),
1214    );
1215
1216  Ok(iter)
1217}