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