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