1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::path::Path;
4
5#[derive(Debug, Deserialize, Serialize, Clone)]
6pub struct SiteConfig {
7 pub name: String,
8 pub hostname: String,
9 #[serde(default)]
10 pub hostnames: Vec<String>, pub port: u16,
12 pub static_dir: String,
13 #[serde(default)]
14 pub default: bool,
15 #[serde(default)]
16 pub api_only: bool,
17 #[serde(default)]
18 pub headers: HashMap<String, String>,
19 #[serde(default)]
20 pub redirect_to_https: bool,
21 #[serde(default)]
22 pub index_files: Vec<String>,
23 #[serde(default)]
24 pub error_pages: HashMap<u16, String>,
25 #[serde(default)]
26 pub compression: CompressionConfig,
27 #[serde(default)]
28 pub cache: CacheConfig,
29 #[serde(default)]
30 pub access_control: AccessControlConfig,
31 #[serde(default)]
32 pub ssl: SiteSslConfig,
33 #[serde(default)]
34 pub proxy: ProxyConfig,
35}
36
37#[derive(Debug, Deserialize, Serialize, Clone, Default)]
38pub struct SiteSslConfig {
39 #[serde(default)]
40 pub enabled: bool,
41 #[serde(default)]
42 pub auto_cert: bool,
43 #[serde(default)]
44 pub domains: Vec<String>, #[serde(default)]
46 pub cert_file: Option<String>, #[serde(default)]
48 pub key_file: Option<String>, #[serde(default)]
50 pub acme: Option<SiteAcmeConfig>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, Default)]
54pub struct SiteAcmeConfig {
55 #[serde(default)]
56 pub enabled: bool,
57 #[serde(default)]
58 pub email: String,
59 #[serde(default)]
60 pub staging: bool,
61 #[serde(default)]
62 pub challenge_dir: Option<String>, }
64
65#[derive(Debug, Deserialize, Serialize, Clone, Default)]
66pub struct ProxyConfig {
67 #[serde(default)]
68 pub enabled: bool,
69 #[serde(default)]
70 pub upstreams: Vec<UpstreamConfig>,
71 #[serde(default)]
72 pub routes: Vec<ProxyRoute>,
73 #[serde(default)]
74 pub health_check: HealthCheckConfig,
75 #[serde(default)]
76 pub load_balancing: LoadBalancingConfig,
77 #[serde(default)]
78 pub timeout: TimeoutConfig,
79 #[serde(default)]
80 pub headers: ProxyHeadersConfig,
81}
82
83#[derive(Debug, Deserialize, Serialize, Clone)]
84pub struct UpstreamConfig {
85 pub name: String,
86 pub url: String,
87 #[serde(default = "default_weight")]
88 pub weight: u32,
89 #[serde(default)]
90 pub max_conns: Option<u32>,
91}
92
93#[derive(Debug, Deserialize, Serialize, Clone)]
94pub struct ProxyRoute {
95 pub path: String,
96 pub upstream: String, #[serde(default)]
98 pub strip_prefix: bool,
99 #[serde(default)]
100 pub rewrite_target: Option<String>,
101 #[serde(default)]
102 pub websocket: bool, }
104
105#[derive(Debug, Deserialize, Serialize, Clone)]
106pub struct HealthCheckConfig {
107 #[serde(default)]
108 pub enabled: bool,
109 #[serde(default = "default_health_path")]
110 pub path: String,
111 #[serde(default = "default_health_interval")]
112 pub interval: u64, #[serde(default = "default_health_timeout")]
114 pub timeout: u64, #[serde(default = "default_health_retries")]
116 pub retries: u32,
117}
118
119#[derive(Debug, Deserialize, Serialize, Clone)]
120pub struct LoadBalancingConfig {
121 #[serde(default = "default_lb_method")]
122 pub method: String, #[serde(default)]
124 pub sticky_sessions: bool,
125}
126
127#[derive(Debug, Deserialize, Serialize, Clone)]
128pub struct TimeoutConfig {
129 #[serde(default = "default_connect_timeout")]
130 pub connect: u64, #[serde(default = "default_read_timeout")]
132 pub read: u64, #[serde(default = "default_write_timeout")]
134 pub write: u64, }
136
137#[derive(Debug, Deserialize, Serialize, Clone)]
138pub struct ProxyHeadersConfig {
139 #[serde(default)]
140 pub preserve_host: bool,
141 #[serde(default)]
142 pub add_forwarded: bool,
143 #[serde(default)]
144 pub add_x_forwarded: bool,
145 #[serde(default)]
146 pub remove: Vec<String>,
147 #[serde(default)]
148 pub add: HashMap<String, String>,
149}
150
151fn default_weight() -> u32 {
152 1
153}
154fn default_health_path() -> String {
155 "/health".to_string()
156}
157fn default_health_interval() -> u64 {
158 30
159}
160fn default_health_timeout() -> u64 {
161 5
162}
163fn default_health_retries() -> u32 {
164 3
165}
166fn default_lb_method() -> String {
167 "round_robin".to_string()
168}
169fn default_connect_timeout() -> u64 {
170 10
171}
172fn default_read_timeout() -> u64 {
173 30
174}
175fn default_write_timeout() -> u64 {
176 30
177}
178
179impl Default for HealthCheckConfig {
180 fn default() -> Self {
181 Self {
182 enabled: false,
183 path: default_health_path(),
184 interval: default_health_interval(),
185 timeout: default_health_timeout(),
186 retries: default_health_retries(),
187 }
188 }
189}
190
191impl Default for LoadBalancingConfig {
192 fn default() -> Self {
193 Self {
194 method: default_lb_method(),
195 sticky_sessions: false,
196 }
197 }
198}
199
200impl Default for TimeoutConfig {
201 fn default() -> Self {
202 Self {
203 connect: default_connect_timeout(),
204 read: default_read_timeout(),
205 write: default_write_timeout(),
206 }
207 }
208}
209
210impl Default for ProxyHeadersConfig {
211 fn default() -> Self {
212 Self {
213 preserve_host: true,
214 add_forwarded: true,
215 add_x_forwarded: true,
216 remove: Vec::new(),
217 add: HashMap::new(),
218 }
219 }
220}
221
222#[derive(Debug, Deserialize, Serialize, Clone)]
223pub struct CompressionConfig {
224 #[serde(default)]
225 pub enabled: bool,
226 #[serde(default = "default_compression_types")]
227 pub types: Vec<String>,
228 #[serde(default = "default_compression_level")]
229 pub level: u32,
230 #[serde(default = "default_min_size")]
231 pub min_size: usize,
232}
233
234#[derive(Debug, Deserialize, Serialize, Clone)]
235pub struct CacheConfig {
236 #[serde(default)]
237 pub enabled: bool,
238 #[serde(default = "default_cache_control")]
239 pub cache_control: String,
240 #[serde(default)]
241 pub etag_enabled: bool,
242 #[serde(default)]
243 pub last_modified_enabled: bool,
244 #[serde(default)]
245 pub max_age_static: u32,
246 #[serde(default)]
247 pub max_age_dynamic: u32,
248}
249
250#[derive(Debug, Deserialize, Serialize, Clone)]
251pub struct AccessControlConfig {
252 #[serde(default)]
253 pub allow_methods: Vec<String>,
254 #[serde(default)]
255 pub allow_headers: Vec<String>,
256 #[serde(default)]
257 pub allow_origins: Vec<String>,
258 #[serde(default)]
259 pub allow_credentials: bool,
260 #[serde(default)]
261 pub max_age: u32,
262}
263
264fn default_compression_types() -> Vec<String> {
266 vec![
267 "text/html".to_string(),
268 "text/css".to_string(),
269 "text/javascript".to_string(),
270 "application/javascript".to_string(),
271 "application/json".to_string(),
272 "text/xml".to_string(),
273 "application/xml".to_string(),
274 "text/plain".to_string(),
275 ]
276}
277
278fn default_compression_level() -> u32 {
279 6
280}
281
282fn default_min_size() -> usize {
283 1024 }
285
286fn default_cache_control() -> String {
287 "public, max-age=3600".to_string()
288}
289
290impl Default for CompressionConfig {
291 fn default() -> Self {
292 Self {
293 enabled: true,
294 types: default_compression_types(),
295 level: default_compression_level(),
296 min_size: default_min_size(),
297 }
298 }
299}
300
301impl Default for CacheConfig {
302 fn default() -> Self {
303 Self {
304 enabled: true,
305 cache_control: default_cache_control(),
306 etag_enabled: true,
307 last_modified_enabled: true,
308 max_age_static: 3600, max_age_dynamic: 300, }
311 }
312}
313
314impl Default for AccessControlConfig {
315 fn default() -> Self {
316 Self {
317 allow_methods: vec!["GET".to_string(), "HEAD".to_string(), "OPTIONS".to_string()],
318 allow_headers: vec!["Content-Type".to_string(), "Authorization".to_string()],
319 allow_origins: vec!["*".to_string()],
320 allow_credentials: false,
321 max_age: 86400, }
323 }
324}
325
326impl SiteConfig {
327 pub fn validate(&self) -> Result<(), Box<dyn std::error::Error>> {
328 if self.name.is_empty() {
330 return Err("Site name cannot be empty".into());
331 }
332
333 if self.hostname.is_empty() {
334 return Err("Site hostname cannot be empty".into());
335 }
336
337 if self.port == 0 {
338 return Err("Site port must be greater than 0".into());
339 }
340
341 if self.static_dir.is_empty() {
342 return Err("Site static_dir cannot be empty".into());
343 }
344
345 let static_path = Path::new(&self.static_dir);
347 if !static_path.exists() {
348 log::warn!(
349 "Static directory does not exist for site '{}': {}",
350 self.name,
351 self.static_dir
352 );
353 }
354
355 if !self.is_valid_hostname() {
357 return Err(format!("Invalid hostname format: {}", self.hostname).into());
358 }
359
360 for (i, hostname) in self.hostnames.iter().enumerate() {
362 if hostname.is_empty() {
363 return Err(format!("Additional hostname {} cannot be empty", i + 1).into());
364 }
365 if !self.is_hostname_valid(hostname) {
366 return Err(format!("Invalid additional hostname format: {}", hostname).into());
367 }
368 }
369
370 if self.port < 1 {
372 return Err(format!("Invalid port number: {}", self.port).into());
373 }
374
375 if self.ssl.enabled {
377 if self.ssl.auto_cert {
378 if let Some(acme) = &self.ssl.acme {
379 if acme.email.is_empty() {
380 return Err("ACME email is required when auto_cert is enabled".into());
381 }
382 } else {
383 return Err("ACME configuration is required when auto_cert is enabled".into());
384 }
385 } else if self.ssl.cert_file.is_none() || self.ssl.key_file.is_none() {
386 return Err("Manual SSL requires both cert_file and key_file".into());
387 }
388 }
389
390 for index_file in &self.index_files {
392 if index_file.is_empty() {
393 return Err("Index file name cannot be empty".into());
394 }
395 }
396
397 for (status_code, error_page) in &self.error_pages {
399 if *status_code < 100 || *status_code > 999 {
400 return Err(format!("Invalid HTTP status code: {}", status_code).into());
401 }
402 if error_page.is_empty() {
403 return Err(
404 format!("Error page path cannot be empty for status {}", status_code).into(),
405 );
406 }
407 }
408
409 self.compression.validate()?;
411
412 self.cache.validate()?;
414
415 self.access_control.validate()?;
417
418 Ok(())
419 }
420
421 fn is_valid_hostname(&self) -> bool {
422 self.is_hostname_valid(&self.hostname)
423 }
424
425 fn is_hostname_valid(&self, hostname: &str) -> bool {
426 if hostname.is_empty() || hostname.len() > 253 {
428 return false;
429 }
430
431 if hostname == "localhost"
433 || hostname.starts_with("127.")
434 || hostname.starts_with("0.0.0.0")
435 {
436 return true;
437 }
438
439 hostname.split('.').all(|label| {
441 !label.is_empty()
442 && label.len() <= 63
443 && label.chars().all(|c| c.is_alphanumeric() || c == '-')
444 && !label.starts_with('-')
445 && !label.ends_with('-')
446 })
447 }
448
449 pub fn get_ssl_domain(&self) -> Option<&str> {
450 if self.ssl.enabled {
451 Some(&self.hostname)
453 } else {
454 None
455 }
456 }
457
458 pub fn get_all_ssl_domains(&self) -> Vec<&str> {
459 if self.ssl.enabled {
460 let mut domains = vec![self.hostname.as_str()];
461 domains.extend(self.hostnames.iter().map(|h| h.as_str()));
463 domains.extend(self.ssl.domains.iter().map(|h| h.as_str()));
465 domains
466 } else {
467 Vec::new()
468 }
469 }
470
471 pub fn is_ssl_enabled(&self) -> bool {
472 self.ssl.enabled
473 }
474
475 pub fn get_index_files(&self) -> Vec<&str> {
476 if self.index_files.is_empty() {
477 vec!["index.html", "index.htm"]
478 } else {
479 self.index_files.iter().map(|s| s.as_str()).collect()
480 }
481 }
482
483 pub fn get_error_page(&self, status_code: u16) -> Option<&str> {
484 self.error_pages.get(&status_code).map(|s| s.as_str())
485 }
486
487 pub fn should_compress(&self, content_type: &str, content_length: usize) -> bool {
488 self.compression.enabled
489 && content_length >= self.compression.min_size
490 && self
491 .compression
492 .types
493 .iter()
494 .any(|t| content_type.starts_with(t))
495 }
496
497 pub fn get_cache_headers(&self, is_static: bool) -> Vec<(String, String)> {
498 let mut headers = Vec::new();
499
500 if self.cache.enabled {
501 let max_age = if is_static {
502 self.cache.max_age_static
503 } else {
504 self.cache.max_age_dynamic
505 };
506
507 headers.push((
508 "Cache-Control".to_string(),
509 format!("public, max-age={}", max_age),
510 ));
511
512 if self.cache.etag_enabled {
513 headers.push(("ETag".to_string(), "\"placeholder\"".to_string()));
516 }
517
518 if self.cache.last_modified_enabled {
519 headers.push(("Last-Modified".to_string(), "placeholder".to_string()));
522 }
523 }
524
525 headers
526 }
527
528 pub fn get_cors_headers(&self) -> Vec<(String, String)> {
529 let mut headers = Vec::new();
530
531 if !self.access_control.allow_origins.is_empty() {
532 headers.push((
533 "Access-Control-Allow-Origin".to_string(),
534 self.access_control.allow_origins.join(", "),
535 ));
536 }
537
538 if !self.access_control.allow_methods.is_empty() {
539 headers.push((
540 "Access-Control-Allow-Methods".to_string(),
541 self.access_control.allow_methods.join(", "),
542 ));
543 }
544
545 if !self.access_control.allow_headers.is_empty() {
546 headers.push((
547 "Access-Control-Allow-Headers".to_string(),
548 self.access_control.allow_headers.join(", "),
549 ));
550 }
551
552 if self.access_control.allow_credentials {
553 headers.push((
554 "Access-Control-Allow-Credentials".to_string(),
555 "true".to_string(),
556 ));
557 }
558
559 if self.access_control.max_age > 0 {
560 headers.push((
561 "Access-Control-Max-Age".to_string(),
562 self.access_control.max_age.to_string(),
563 ));
564 }
565
566 headers
567 }
568
569 pub fn url(&self) -> String {
570 let protocol = if self.is_ssl_enabled() {
571 "https"
572 } else {
573 "http"
574 };
575 let port_suffix = match (self.is_ssl_enabled(), self.port) {
576 (true, 443) | (false, 80) => String::new(),
577 _ => format!(":{}", self.port),
578 };
579 format!("{}://{}{}", protocol, self.hostname, port_suffix)
580 }
581
582 pub fn handles_hostname(&self, hostname: &str) -> bool {
584 if self.hostname == hostname {
586 return true;
587 }
588
589 self.hostnames.iter().any(|h| h == hostname)
591 }
592
593 pub fn get_all_hostnames(&self) -> Vec<&str> {
595 let mut hostnames = vec![self.hostname.as_str()];
596 hostnames.extend(self.hostnames.iter().map(|h| h.as_str()));
597 hostnames
598 }
599
600 pub fn handles_hostname_port(&self, hostname: &str, port: u16) -> bool {
602 self.port == port && self.handles_hostname(hostname)
603 }
604}
605
606impl CompressionConfig {
607 fn validate(&self) -> Result<(), Box<dyn std::error::Error>> {
608 if self.level > 9 {
609 return Err("Compression level must be between 0 and 9".into());
610 }
611
612 if self.types.is_empty() && self.enabled {
613 return Err("Compression types cannot be empty when compression is enabled".into());
614 }
615
616 Ok(())
617 }
618}
619
620impl CacheConfig {
621 fn validate(&self) -> Result<(), Box<dyn std::error::Error>> {
622 if self.max_age_static > 365 * 24 * 3600 {
625 log::warn!("Static cache max_age is very large (> 1 year)");
626 }
627
628 if self.max_age_dynamic > 24 * 3600 {
629 log::warn!("Dynamic cache max_age is very large (> 1 day)");
630 }
631
632 Ok(())
633 }
634}
635
636impl AccessControlConfig {
637 fn validate(&self) -> Result<(), Box<dyn std::error::Error>> {
638 let valid_methods = [
640 "GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH", "TRACE", "CONNECT",
641 ];
642
643 for method in &self.allow_methods {
644 if !valid_methods.contains(&method.as_str()) {
645 return Err(format!("Invalid HTTP method: {}", method).into());
646 }
647 }
648
649 if self.max_age > 7 * 24 * 3600 {
651 log::warn!("CORS max_age is very large (> 1 week)");
652 }
653
654 Ok(())
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661
662 #[test]
663 fn test_site_config_validation() {
664 let mut site = SiteConfig {
665 name: "test".to_string(),
666 hostname: "example.com".to_string(),
667 hostnames: vec![],
668 port: 8080,
669 static_dir: "/tmp".to_string(),
670 default: false,
671 api_only: false,
672 headers: HashMap::new(),
673 redirect_to_https: false,
674 index_files: vec![],
675 error_pages: HashMap::new(),
676 compression: CompressionConfig::default(),
677 cache: CacheConfig::default(),
678 access_control: AccessControlConfig::default(),
679 ssl: SiteSslConfig::default(),
680 proxy: ProxyConfig::default(),
681 };
682
683 assert!(site.validate().is_ok());
684
685 site.hostname = "".to_string();
687 assert!(site.validate().is_err());
688
689 site.hostname = "example.com".to_string();
691 site.port = 0;
692 assert!(site.validate().is_err());
693
694 site.port = 0;
696 assert!(site.validate().is_err());
697 }
698
699 #[test]
700 fn test_hostname_validation() {
701 let site = SiteConfig {
702 name: "test".to_string(),
703 hostname: "localhost".to_string(),
704 hostnames: vec![],
705 port: 8080,
706 static_dir: "/tmp".to_string(),
707 default: false,
708 api_only: false,
709 headers: HashMap::new(),
710 redirect_to_https: false,
711 index_files: vec![],
712 error_pages: HashMap::new(),
713 compression: CompressionConfig::default(),
714 cache: CacheConfig::default(),
715 access_control: AccessControlConfig::default(),
716 ssl: SiteSslConfig::default(),
717 proxy: ProxyConfig::default(),
718 };
719
720 assert!(site.is_valid_hostname());
721
722 let mut invalid_site = site.clone();
723 invalid_site.hostname = "invalid..hostname".to_string();
724 assert!(!invalid_site.is_valid_hostname());
725
726 invalid_site.hostname = "-invalid.hostname".to_string();
727 assert!(!invalid_site.is_valid_hostname());
728 }
729
730 #[test]
731 fn test_compression_config() {
732 let site = SiteConfig {
733 name: "test".to_string(),
734 hostname: "example.com".to_string(),
735 hostnames: vec![],
736 port: 8080,
737 static_dir: "/tmp".to_string(),
738 default: false,
739 api_only: false,
740 headers: HashMap::new(),
741 redirect_to_https: false,
742 index_files: vec![],
743 error_pages: HashMap::new(),
744 compression: CompressionConfig::default(),
745 cache: CacheConfig::default(),
746 access_control: AccessControlConfig::default(),
747 ssl: SiteSslConfig::default(),
748 proxy: ProxyConfig::default(),
749 };
750
751 assert!(site.should_compress("text/html", 2048));
752 assert!(!site.should_compress("text/html", 512)); assert!(!site.should_compress("image/png", 2048)); }
755
756 #[test]
757 fn test_site_url_generation() {
758 let mut site = SiteConfig {
759 name: "test".to_string(),
760 hostname: "example.com".to_string(),
761 hostnames: vec![],
762 port: 8080,
763 static_dir: "/tmp".to_string(),
764 default: false,
765 api_only: false,
766 headers: HashMap::new(),
767 redirect_to_https: false,
768 index_files: vec![],
769 error_pages: HashMap::new(),
770 compression: CompressionConfig::default(),
771 cache: CacheConfig::default(),
772 access_control: AccessControlConfig::default(),
773 ssl: SiteSslConfig::default(),
774 proxy: ProxyConfig::default(),
775 };
776
777 assert_eq!(site.url(), "http://example.com:8080");
778
779 site.ssl.enabled = true;
780 site.port = 443;
781 assert_eq!(site.url(), "https://example.com");
782
783 site.port = 8443;
784 assert_eq!(site.url(), "https://example.com:8443");
785 }
786
787 #[test]
788 fn test_multi_hostname_functionality() {
789 let site = SiteConfig {
790 name: "test".to_string(),
791 hostname: "example.com".to_string(),
792 hostnames: vec!["www.example.com".to_string(), "example.org".to_string()],
793 port: 8080,
794 static_dir: "/tmp".to_string(),
795 default: false,
796 api_only: false,
797 headers: HashMap::new(),
798 redirect_to_https: false,
799 index_files: vec![],
800 error_pages: HashMap::new(),
801 compression: CompressionConfig::default(),
802 cache: CacheConfig::default(),
803 access_control: AccessControlConfig::default(),
804 ssl: SiteSslConfig::default(),
805 proxy: ProxyConfig::default(),
806 };
807
808 assert!(site.handles_hostname("example.com"));
810 assert!(site.handles_hostname("www.example.com"));
811 assert!(site.handles_hostname("example.org"));
812 assert!(!site.handles_hostname("different.com"));
813
814 assert!(site.handles_hostname_port("example.com", 8080));
816 assert!(site.handles_hostname_port("www.example.com", 8080));
817 assert!(site.handles_hostname_port("example.org", 8080));
818 assert!(!site.handles_hostname_port("example.com", 8081));
819 assert!(!site.handles_hostname_port("different.com", 8080));
820
821 let all_hostnames = site.get_all_hostnames();
823 assert_eq!(all_hostnames.len(), 3);
824 assert!(all_hostnames.contains(&"example.com"));
825 assert!(all_hostnames.contains(&"www.example.com"));
826 assert!(all_hostnames.contains(&"example.org"));
827 }
828
829 #[test]
830 fn test_multi_hostname_ssl_domains() {
831 let mut site = SiteConfig {
832 name: "test".to_string(),
833 hostname: "example.com".to_string(),
834 hostnames: vec!["www.example.com".to_string(), "api.example.com".to_string()],
835 port: 443,
836 static_dir: "/tmp".to_string(),
837 default: false,
838 api_only: false,
839 headers: HashMap::new(),
840 redirect_to_https: false,
841 index_files: vec![],
842 error_pages: HashMap::new(),
843 compression: CompressionConfig::default(),
844 cache: CacheConfig::default(),
845 access_control: AccessControlConfig::default(),
846 ssl: SiteSslConfig::default(),
847 proxy: ProxyConfig::default(),
848 };
849
850 site.ssl.enabled = true;
852 site.ssl.domains = vec!["cdn.example.com".to_string()];
853
854 let ssl_domains = site.get_all_ssl_domains();
856 assert_eq!(ssl_domains.len(), 4);
857 assert!(ssl_domains.contains(&"example.com"));
858 assert!(ssl_domains.contains(&"www.example.com"));
859 assert!(ssl_domains.contains(&"api.example.com"));
860 assert!(ssl_domains.contains(&"cdn.example.com"));
861 }
862
863 #[test]
864 fn test_hostname_validation_with_additional_hostnames() {
865 let mut site = SiteConfig {
866 name: "test".to_string(),
867 hostname: "example.com".to_string(),
868 hostnames: vec![
869 "www.example.com".to_string(),
870 "valid.example.org".to_string(),
871 ],
872 port: 8080,
873 static_dir: "/tmp".to_string(),
874 default: false,
875 api_only: false,
876 headers: HashMap::new(),
877 redirect_to_https: false,
878 index_files: vec![],
879 error_pages: HashMap::new(),
880 compression: CompressionConfig::default(),
881 cache: CacheConfig::default(),
882 access_control: AccessControlConfig::default(),
883 ssl: SiteSslConfig::default(),
884 proxy: ProxyConfig::default(),
885 };
886
887 assert!(site.validate().is_ok());
889
890 site.hostnames = vec!["invalid..hostname".to_string()];
892 assert!(site.validate().is_err());
893
894 site.hostnames = vec!["".to_string()];
896 assert!(site.validate().is_err());
897 }
898}