1use crate::config::{ServerConfig, SiteConfig};
2use crate::handlers::*;
3use crate::monitoring::HealthHandler;
4use crate::ssl::SslManager;
5use async_trait::async_trait;
6use pingora::http::ResponseHeader;
7use pingora::prelude::*;
8use std::collections::HashMap;
9use std::sync::Arc;
10use tokio::sync::RwLock;
11
12#[derive(Clone)]
13pub struct WebServerService {
14 config: Arc<RwLock<ServerConfig>>,
15 ssl_managers: Arc<RwLock<HashMap<String, Arc<SslManager>>>>, static_handler: Arc<StaticFileHandler>,
17 api_handler: Arc<ApiHandler>,
18 health_handler: Arc<HealthHandler>,
19 #[allow(dead_code)]
20 proxy_handler: Arc<ProxyHandler>,
21}
22
23impl WebServerService {
24 pub fn new(config: ServerConfig) -> Self {
25 let static_handler = Arc::new(StaticFileHandler::new());
27 let api_handler = Arc::new(ApiHandler::new());
28 let health_handler = Arc::new(HealthHandler::new());
29
30 let ssl_managers = Arc::new(RwLock::new(HashMap::new()));
32
33 WebServerService {
34 config: Arc::new(RwLock::new(config)),
35 ssl_managers,
36 static_handler,
37 api_handler,
38 health_handler,
39 proxy_handler: Arc::new(ProxyHandler::new(crate::config::ProxyConfig::default())),
40 }
41 }
42
43 pub async fn initialize_ssl_managers(&self) -> Result<(), Box<dyn std::error::Error>> {
45 let config = self.config.read().await;
46 let mut ssl_managers = self.ssl_managers.write().await;
47
48 for site in &config.sites {
49 if site.ssl.enabled {
50 log::info!(
51 "Initializing SSL manager for site: {} ({})",
52 site.name,
53 site.hostname
54 );
55
56 match SslManager::from_site_config(site).await {
57 Ok(Some(ssl_manager)) => {
58 match ssl_manager.ensure_certificate(&site.hostname).await {
60 Ok(success) => {
61 if success {
62 log::info!("SSL certificate ready for {}", site.hostname);
63 } else {
64 log::warn!(
65 "Failed to obtain SSL certificate for {}",
66 site.hostname
67 );
68 }
69 }
70 Err(e) => {
71 log::error!("SSL certificate error for {}: {}", site.hostname, e);
72 }
73 }
74
75 ssl_managers.insert(site.hostname.clone(), Arc::new(ssl_manager));
76 }
77 Ok(None) => {
78 log::debug!("SSL not enabled for site: {}", site.name);
79 }
80 Err(e) => {
81 log::error!(
82 "Failed to initialize SSL manager for {}: {}",
83 site.hostname,
84 e
85 );
86 }
87 }
88 }
89 }
90
91 log::info!("SSL managers initialized for {} sites", ssl_managers.len());
92
93 let acme_managers_count = ssl_managers
95 .values()
96 .filter(|manager| manager.is_auto_cert_enabled())
97 .count();
98
99 if acme_managers_count > 0 {
100 log::info!(
101 "Starting certificate renewal monitoring for {} ACME-enabled domains",
102 acme_managers_count
103 );
104
105 for (domain, manager) in ssl_managers.iter() {
107 if manager.is_auto_cert_enabled() {
108 let manager_clone = manager.clone();
109 let domain_clone = domain.clone();
110 tokio::spawn(async move {
111 let mut interval =
113 tokio::time::interval(tokio::time::Duration::from_secs(3600));
114 loop {
115 interval.tick().await;
116 if let Err(e) = manager_clone
117 .check_and_renew_certificate(&domain_clone)
118 .await
119 {
120 log::error!(
121 "Certificate renewal check failed for {domain_clone}: {e}"
122 );
123 }
124 }
125 });
126 }
127 }
128 }
129
130 Ok(())
131 }
132
133 pub async fn get_config(&self) -> ServerConfig {
134 self.config.read().await.clone()
135 }
136
137 pub async fn reload_config(
138 &self,
139 new_config: ServerConfig,
140 ) -> Result<(), Box<dyn std::error::Error>> {
141 new_config.validate()?;
143
144 {
146 let mut config = self.config.write().await;
147 *config = new_config;
148 }
149
150 log::info!("Configuration reloaded successfully");
151 Ok(())
152 }
153
154 pub async fn ensure_ssl_certificate(
155 &self,
156 domain: &str,
157 ) -> Result<bool, Box<dyn std::error::Error>> {
158 let ssl_managers = self.ssl_managers.read().await;
159 if let Some(ssl_manager) = ssl_managers.get(domain) {
160 ssl_manager.ensure_certificate(domain).await
161 } else {
162 Ok(false)
163 }
164 }
165
166 pub async fn check_and_renew_certificates(&self) -> Result<(), Box<dyn std::error::Error>> {
168 let ssl_managers = self.ssl_managers.read().await;
169 let mut renewed_any = false;
170
171 for (domain, ssl_manager) in ssl_managers.iter() {
172 if ssl_manager.is_auto_cert_enabled() {
173 log::debug!("Checking certificate renewal for domain: {}", domain);
174
175 match ssl_manager.check_and_renew_certificate(domain).await {
176 Ok(renewed) => {
177 if renewed {
178 log::info!("Certificate renewed for domain: {}", domain);
179 renewed_any = true;
180 } else {
181 log::debug!(
182 "Certificate for {} is still valid, no renewal needed",
183 domain
184 );
185 }
186 }
187 Err(e) => {
188 log::error!("Failed to check/renew certificate for {}: {}", domain, e);
189 }
190 }
191 }
192 }
193
194 if renewed_any {
195 log::info!("🔄 Some certificates were renewed. Server restart may be required for HTTPS to use new certificates.");
196 }
197
198 Ok(())
199 }
200
201 async fn get_ssl_manager_for_domain(&self, domain: &str) -> Option<Arc<SslManager>> {
202 let ssl_managers = self.ssl_managers.read().await;
203 ssl_managers.get(domain).cloned()
204 }
205
206 async fn find_site_by_request(&self, session: &Session) -> Option<SiteConfig> {
207 let config = self.config.read().await;
208
209 let host_header = session
211 .req_header()
212 .headers
213 .get("Host")
214 .and_then(|h| h.to_str().ok())
215 .unwrap_or("localhost");
216
217 let (hostname, port) = if let Some(pos) = host_header.find(':') {
219 let hostname = &host_header[..pos];
220 let port_str = &host_header[pos + 1..];
221 let port = port_str.parse::<u16>().unwrap_or(8080);
222 (hostname, port)
223 } else {
224 let default_port = if session.req_header().uri.scheme_str() == Some("https") {
226 443
227 } else {
228 80
229 };
230 (host_header, default_port)
231 };
232
233 if let Some(site) = config.find_site_by_host_port(hostname, port).cloned() {
235 return Some(site);
236 }
237
238 let path = session.req_header().uri.path();
240 if port == 80 && path.starts_with("/.well-known/acme-challenge/") {
241 for site in &config.sites {
243 if site.handles_hostname(hostname) && site.ssl.enabled && site.ssl.auto_cert {
244 if let Some(acme_config) = &site.ssl.acme {
245 if acme_config.enabled {
246 log::debug!(
247 "Using site '{}' for ACME challenge on port 80 for hostname '{}'",
248 site.name,
249 hostname
250 );
251 return Some(site.clone());
252 }
253 }
254 }
255 }
256
257 log::debug!(
259 "Looking for any ACME-enabled site for challenge request to '{}'",
260 hostname
261 );
262 for site in &config.sites {
263 if site.ssl.enabled && site.ssl.auto_cert {
264 if let Some(acme_config) = &site.ssl.acme {
265 if acme_config.enabled {
266 log::debug!(
267 "Using ACME-enabled site '{}' for challenge request",
268 site.name
269 );
270 return Some(site.clone());
271 }
272 }
273 }
274 }
275 }
276
277 None
278 }
279
280 async fn handle_ssl_redirect(&self, session: &mut Session, site: &SiteConfig) -> Result<bool> {
281 if site.redirect_to_https && !self.is_https_request(session) {
282 let https_url = format!(
283 "https://{}{}",
284 session
285 .req_header()
286 .headers
287 .get("Host")
288 .and_then(|h| h.to_str().ok())
289 .unwrap_or(&site.hostname),
290 session
291 .req_header()
292 .uri
293 .path_and_query()
294 .map(|pq| pq.as_str())
295 .unwrap_or("/")
296 );
297
298 let mut header = ResponseHeader::build(301, Some(2))?;
299 header.insert_header("Location", https_url)?;
300 header.insert_header("Content-Length", "0")?;
301
302 session
303 .write_response_header(Box::new(header), true)
304 .await?;
305 return Ok(true);
306 }
307 Ok(false)
308 }
309
310 fn is_https_request(&self, session: &Session) -> bool {
311 session
315 .req_header()
316 .uri
317 .scheme()
318 .map(|s| s.as_str() == "https")
319 .unwrap_or(false)
320 }
321
322 async fn handle_acme_challenge_for_site(
323 &self,
324 session: &mut Session,
325 path: &str,
326 site: &SiteConfig,
327 ) -> Result<bool> {
328 if !path.starts_with("/.well-known/acme-challenge/") {
330 return Ok(false);
331 }
332
333 log::debug!(
334 "ACME challenge handler started for site '{}', ssl.enabled={}, ssl.auto_cert={}",
335 site.name,
336 site.ssl.enabled,
337 site.ssl.auto_cert
338 );
339
340 if !site.ssl.enabled || !site.ssl.auto_cert {
342 log::warn!("ACME challenge request for site '{}' but SSL auto_cert not enabled (ssl.enabled={}, ssl.auto_cert={})",
343 site.name, site.ssl.enabled, site.ssl.auto_cert);
344
345 let mut header = ResponseHeader::build(404, Some(3))?;
346 header.insert_header("Content-Type", "application/json")?;
347 header.insert_header("X-Site-Name", &site.hostname)?;
348 header.insert_header("X-ACME-Enabled", "false")?;
349 header.insert_header("X-ACME-Challenge-Status", "ssl-not-enabled")?;
350 header.insert_header("X-ACME-Site", &site.name)?;
351
352 let error_body = format!(
353 r#"{{"error":"ACME Not Enabled","message":"Site {} does not have SSL auto_cert enabled","status":404,"site":"{}","hostname":"{}"}}"#,
354 site.name, site.name, site.hostname
355 );
356 header.insert_header("Content-Length", error_body.len().to_string())?;
357
358 session
359 .write_response_header(Box::new(header), false)
360 .await?;
361 session
362 .write_response_body(Some(error_body.into_bytes().into()), true)
363 .await?;
364 return Ok(true);
365 }
366
367 let acme_enabled = site
368 .ssl
369 .acme
370 .as_ref()
371 .map(|acme| acme.enabled)
372 .unwrap_or(false);
373
374 log::debug!(
375 "ACME config check for site '{}': acme_config_exists={}, acme_enabled={}",
376 site.name,
377 site.ssl.acme.is_some(),
378 acme_enabled
379 );
380
381 if !acme_enabled {
382 log::warn!("ACME challenge request for site '{}' but ACME not enabled in config (acme_config_exists={}, acme_enabled={})",
383 site.name, site.ssl.acme.is_some(), acme_enabled);
384
385 let mut header = ResponseHeader::build(404, Some(3))?;
386 header.insert_header("Content-Type", "application/json")?;
387 header.insert_header("X-Site-Name", &site.hostname)?;
388 header.insert_header("X-ACME-Enabled", "false")?;
389 header.insert_header("X-ACME-Challenge-Status", "acme-disabled")?;
390 header.insert_header("X-ACME-Site", &site.name)?;
391
392 let error_body = format!(
393 r#"{{"error":"ACME Disabled","message":"Site {} has ACME disabled in configuration","status":404,"site":"{}","hostname":"{}"}}"#,
394 site.name, site.name, site.hostname
395 );
396 header.insert_header("Content-Length", error_body.len().to_string())?;
397
398 session
399 .write_response_header(Box::new(header), false)
400 .await?;
401 session
402 .write_response_body(Some(error_body.into_bytes().into()), true)
403 .await?;
404 return Ok(true);
405 }
406
407 log::info!(
408 "Handling ACME challenge request for site '{}': {}",
409 site.name,
410 path
411 );
412 let start_time = std::time::Instant::now();
413
414 if let Some(token) = path.strip_prefix("/.well-known/acme-challenge/") {
416 log::debug!(
417 "Looking for ACME challenge token: {} (took {:?})",
418 token,
419 start_time.elapsed()
420 );
421
422 let challenge_path = std::path::PathBuf::from("./certs")
424 .join("challenges")
425 .join(".well-known")
426 .join("acme-challenge")
427 .join(token);
428
429 log::debug!("Trying to read challenge file from: {:?}", challenge_path);
430
431 if let Ok(content) = tokio::fs::read_to_string(&challenge_path).await {
432 log::info!(
433 "Serving ACME challenge response from filesystem for token: {} (took {:?})",
434 token,
435 start_time.elapsed()
436 );
437 let mut header = ResponseHeader::build(200, Some(3))?;
438 header.insert_header("Content-Type", "text/plain")?;
439 header.insert_header("Content-Length", content.len().to_string())?;
440
441 header.insert_header("X-Site-Name", &site.hostname)?;
443 header.insert_header("X-ACME-Enabled", "true")?;
444 header.insert_header("X-ACME-Challenge-Source", "filesystem")?;
445 header.insert_header("X-ACME-Site", &site.name)?;
446
447 session
448 .write_response_header(Box::new(header), false)
449 .await?;
450 session
451 .write_response_body(Some(content.into_bytes().into()), true)
452 .await?;
453 log::info!(
454 "Successfully sent ACME challenge response for token: {} (took {:?})",
455 token,
456 start_time.elapsed()
457 );
458 return Ok(true);
459 } else {
460 log::debug!(
461 "Challenge file not found at {:?}, checking SSL manager (took {:?})",
462 challenge_path,
463 start_time.elapsed()
464 );
465 }
466 }
467
468 if let Some(ssl_manager) = self.get_ssl_manager_for_domain(&site.hostname).await {
470 log::debug!(
471 "Found SSL manager for domain '{}' (took {:?})",
472 site.hostname,
473 start_time.elapsed()
474 );
475
476 if ssl_manager.handles_acme_challenge(path) {
477 if let Some(token) = path.strip_prefix("/.well-known/acme-challenge/") {
478 log::debug!(
479 "Looking for ACME challenge token in SSL manager: {} (took {:?})",
480 token,
481 start_time.elapsed()
482 );
483
484 if let Some(response) = ssl_manager.get_acme_challenge_response(token).await {
486 log::info!("Serving ACME challenge response from SSL manager for token: {} (took {:?})", token, start_time.elapsed());
487 let mut header = ResponseHeader::build(200, Some(3))?;
488 header.insert_header("Content-Type", "text/plain")?;
489 header.insert_header("Content-Length", response.len().to_string())?;
490
491 header.insert_header("X-Site-Name", &site.hostname)?;
493 header.insert_header("X-ACME-Enabled", "true")?;
494 header.insert_header("X-ACME-Challenge-Source", "ssl-manager")?;
495 header.insert_header("X-ACME-Site", &site.name)?;
496
497 session
498 .write_response_header(Box::new(header), false)
499 .await?;
500 session
501 .write_response_body(Some(response.into_bytes().into()), true)
502 .await?;
503 log::info!(
504 "Successfully sent ACME challenge response for token: {} (took {:?})",
505 token,
506 start_time.elapsed()
507 );
508 return Ok(true);
509 } else {
510 log::debug!(
511 "SSL manager has no challenge response for token: {} (took {:?})",
512 token,
513 start_time.elapsed()
514 );
515 }
516 }
517 } else {
518 log::warn!(
519 "SSL manager does not handle ACME challenges for path: {} (took {:?})",
520 path,
521 start_time.elapsed()
522 );
523 }
524 } else {
525 log::debug!(
526 "No SSL manager found for hostname: {} (took {:?})",
527 site.hostname,
528 start_time.elapsed()
529 );
530 }
531
532 log::warn!(
534 "ACME challenge not found for path: {} (took {:?})",
535 path,
536 start_time.elapsed()
537 );
538
539 let mut header = ResponseHeader::build(404, Some(3))?;
540 header.insert_header("Content-Type", "application/json")?;
541
542 header.insert_header("X-Site-Name", &site.hostname)?;
544 header.insert_header("X-ACME-Enabled", "true")?;
545 header.insert_header("X-ACME-Challenge-Status", "not-found")?;
546 header.insert_header("X-ACME-Site", &site.name)?;
547
548 let error_body = format!(
549 r#"{{"error":"ACME Challenge Not Found","message":"Challenge token not found for site {}","status":404,"site":"{}","hostname":"{}"}}"#,
550 site.name, site.name, site.hostname
551 );
552 header.insert_header("Content-Length", error_body.len().to_string())?;
553
554 session
555 .write_response_header(Box::new(header), false)
556 .await?;
557 session
558 .write_response_body(Some(error_body.into_bytes().into()), true)
559 .await?;
560 Ok(true)
561 }
562
563 async fn apply_site_headers(
564 &self,
565 header: &mut ResponseHeader,
566 site: &SiteConfig,
567 ) -> Result<()> {
568 for (key, value) in &site.headers {
570 header.insert_header(key.clone(), value.clone())?;
571 }
572
573 let config = self.config.read().await;
575 for (key, value) in &config.security.security_headers {
576 header.insert_header(key.clone(), value.clone())?;
577 }
578
579 if config.security.hide_server_header {
581 header.remove_header("Server");
582 } else {
583 header.insert_header(
584 "Server",
585 format!("{}/{}", config.server.name, config.server.version),
586 )?;
587 }
588
589 Ok(())
590 }
591
592 async fn handle_404(&self, session: &mut Session, site: Option<&SiteConfig>) -> Result<()> {
593 if let Some(site) = site {
595 if let Some(error_page) = site.get_error_page(404) {
596 let error_page_path = format!("{}/{}", site.static_dir, error_page);
597 if let Ok(content) = tokio::fs::read(&error_page_path).await {
598 let mut header = ResponseHeader::build(404, Some(3))?;
599 header.insert_header("Content-Type", "text/html")?;
600 header.insert_header("Content-Length", content.len().to_string())?;
601 self.apply_site_headers(&mut header, site).await?;
602
603 session
604 .write_response_header(Box::new(header), false)
605 .await?;
606 session
607 .write_response_body(Some(content.into()), true)
608 .await?;
609 return Ok(());
610 }
611 }
612 }
613
614 let error_response = serde_json::json!({
616 "error": "Not Found",
617 "message": "The requested resource was not found",
618 "status": 404
619 });
620
621 let response_body = error_response.to_string();
622 let response_bytes = response_body.into_bytes();
623 let mut header = ResponseHeader::build(404, Some(3))?;
624 header.insert_header("Content-Type", "application/json")?;
625 header.insert_header("Content-Length", response_bytes.len().to_string())?;
626
627 if let Some(site) = site {
628 self.apply_site_headers(&mut header, site).await?;
629 }
630
631 session
632 .write_response_header(Box::new(header), false)
633 .await?;
634 session
635 .write_response_body(Some(response_bytes.into()), true)
636 .await?;
637
638 Ok(())
639 }
640}
641
642#[async_trait]
643impl ProxyHttp for WebServerService {
644 type CTX = Option<SiteConfig>;
645
646 fn new_ctx(&self) -> Self::CTX {
647 None
648 }
649
650 async fn upstream_peer(
651 &self,
652 _session: &mut Session,
653 _ctx: &mut Self::CTX,
654 ) -> Result<Box<HttpPeer>> {
655 Err(Error::new(ErrorType::InternalError).into_down())
657 }
658
659 async fn request_filter(&self, session: &mut Session, ctx: &mut Self::CTX) -> Result<bool> {
660 let site_config = self.find_site_by_request(session).await;
662 *ctx = site_config.clone();
663
664 let path = session.req_header().uri.path().to_string();
665 let host_header = session
666 .req_header()
667 .headers
668 .get("Host")
669 .and_then(|h| h.to_str().ok())
670 .unwrap_or("localhost");
671
672 if let Some(site) = ctx.as_ref() {
674 log::info!(
675 "Incoming request: {} {} (site: {}, static_dir: {}, host: {})",
676 session.req_header().method,
677 session.req_header().uri,
678 site.name,
679 site.static_dir,
680 host_header
681 );
682 } else {
683 log::warn!(
684 "No site configuration found for request: {} {} (host: {})",
685 session.req_header().method,
686 session.req_header().uri,
687 host_header
688 );
689 }
690
691 if let Some(site) = ctx.as_ref() {
693 if self.handle_ssl_redirect(session, site).await? {
694 return Ok(true);
695 }
696 }
697
698 if path.starts_with("/.well-known/acme-challenge/") {
700 log::debug!(
701 "ACME challenge request detected: {} (site found: {})",
702 path,
703 ctx.is_some()
704 );
705 if let Some(site) = ctx.as_ref() {
706 log::debug!(
707 "Calling handle_acme_challenge_for_site for site '{}'",
708 site.name
709 );
710 if self
711 .handle_acme_challenge_for_site(session, &path, site)
712 .await?
713 {
714 return Ok(true);
715 }
716 log::debug!(
717 "handle_acme_challenge_for_site returned false for site '{}'",
718 site.name
719 );
720 } else {
721 log::warn!(
723 "ACME challenge request but no site configuration found: {}",
724 path
725 );
726 let mut header = ResponseHeader::build(404, Some(3))?;
727 header.insert_header("Content-Type", "application/json")?;
728 header.insert_header("X-ACME-Challenge-Status", "no-site-found")?;
729 header.insert_header("X-ACME-Enabled", "false")?;
730
731 let error_body = r#"{"error":"No Site Found","message":"No site configuration found for ACME challenge","status":404}"#;
732 header.insert_header("Content-Length", error_body.len().to_string())?;
733
734 session
735 .write_response_header(Box::new(header), false)
736 .await?;
737 session
738 .write_response_body(Some(error_body.as_bytes().into()), true)
739 .await?;
740 return Ok(true);
741 }
742 }
743
744 match path.as_str() {
746 path if path.starts_with("/api/health") => {
747 self.health_handler.handle(session, ctx.as_ref()).await?;
748 Ok(true)
749 }
750 path if path.starts_with("/api/") => {
751 self.api_handler.handle(session, ctx.as_ref()).await?;
752 Ok(true)
753 }
754 _ => {
755 if let Some(site) = ctx.as_ref() {
757 if site.proxy.enabled {
758 for route in &site.proxy.routes {
760 if path.starts_with(&route.path) {
761 let proxy_handler = ProxyHandler::new(site.proxy.clone());
763 return proxy_handler
764 .handle_proxy_request(session, site, &path)
765 .await;
766 }
767 }
768 }
769
770 self.static_handler.handle(session, site, &path).await?;
772 } else {
773 self.handle_404(session, ctx.as_ref()).await?;
774 }
775 Ok(true)
776 }
777 }
778 }
779
780 async fn connected_to_upstream(
781 &self,
782 _session: &mut Session,
783 _reused: bool,
784 _peer: &HttpPeer,
785 #[cfg(unix)] _fd: std::os::unix::io::RawFd,
786 #[cfg(windows)] _fd: std::os::windows::io::RawSocket,
787 _digest: Option<&pingora::protocols::Digest>,
788 _ctx: &mut Self::CTX,
789 ) -> Result<()> {
790 Ok(())
792 }
793
794 async fn upstream_request_filter(
795 &self,
796 _session: &mut Session,
797 _upstream_request: &mut pingora::http::RequestHeader,
798 _ctx: &mut Self::CTX,
799 ) -> Result<()> {
800 Ok(())
802 }
803
804 async fn response_filter(
805 &self,
806 _session: &mut Session,
807 _upstream_response: &mut pingora::http::ResponseHeader,
808 _ctx: &mut Self::CTX,
809 ) -> Result<()> {
810 Ok(())
812 }
813
814 async fn logging(
815 &self,
816 session: &mut Session,
817 _e: Option<&pingora::Error>,
818 ctx: &mut Self::CTX,
819 ) {
820 let config = self.config.read().await;
821 if config.logging.log_requests {
822 let site_name = ctx.as_ref().map(|s| s.name.as_str()).unwrap_or("unknown");
823 let method = session.req_header().method.as_str();
824 let uri = session.req_header().uri.to_string();
825 let status = session
826 .response_written()
827 .map(|r| r.status.as_u16())
828 .unwrap_or(0);
829
830 log::info!(
831 "Request completed: {} {} {} {} (site: {})",
832 session
833 .client_addr()
834 .map(|addr| addr.to_string())
835 .unwrap_or_else(|| "unknown".to_string()),
836 method,
837 uri,
838 status,
839 site_name
840 );
841 }
842 }
843}
844
845#[cfg(test)]
846mod tests {
847 use super::*;
848 use crate::config::{LoggingConfig, PerformanceConfig, SecurityConfig, ServerInfo};
849 use std::collections::HashMap;
850
851 fn create_test_config() -> ServerConfig {
852 ServerConfig {
853 server: ServerInfo {
854 name: "test-server".to_string(),
855 version: "1.0.0".to_string(),
856 description: "Test server".to_string(),
857 },
858 sites: vec![SiteConfig {
859 name: "test-site".to_string(),
860 hostname: "localhost".to_string(),
861 hostnames: vec![],
862 port: 8080,
863 static_dir: "/tmp/static".to_string(),
864 default: true,
865 api_only: false,
866 headers: HashMap::new(),
867 ssl: crate::config::SiteSslConfig::default(),
868 redirect_to_https: false,
869 index_files: vec!["index.html".to_string()],
870 error_pages: HashMap::new(),
871 compression: Default::default(),
872 cache: Default::default(),
873 access_control: Default::default(),
874 proxy: crate::config::ProxyConfig::default(),
875 }],
876 logging: LoggingConfig::default(),
877 performance: PerformanceConfig::default(),
878 security: SecurityConfig::default(),
879 }
880 }
881
882 #[tokio::test]
883 async fn test_web_server_service_creation() {
884 let config = create_test_config();
885 let _service = WebServerService::new(config);
886 }
888
889 #[tokio::test]
890 async fn test_config_reload() {
891 let config = create_test_config();
892 let service = WebServerService::new(config.clone());
893
894 let mut new_config = config.clone();
895 new_config.server.name = "updated-server".to_string();
896
897 let result = service.reload_config(new_config).await;
898 assert!(result.is_ok());
899
900 let updated_config = service.get_config().await;
901 assert_eq!(updated_config.server.name, "updated-server");
902 }
903}