bws_web_server/server/
service.rs

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>>>>, // hostname -> SslManager
16    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        // Initialize handlers
26        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        // Initialize SSL managers storage
31        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    /// Initialize SSL managers for all sites with SSL enabled
44    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                        // Initialize certificate for this domain
59                        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        // Start automatic certificate renewal scheduler
94        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            // Start background certificate renewal monitoring
106            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                        // Check certificates every hour
112                        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        // Validate new configuration
142        new_config.validate()?;
143
144        // Update configuration
145        {
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    /// Check and renew certificates for all SSL-enabled sites
167    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        // Extract host and port information
210        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        // Parse host and port
218        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            // Default to port 80 for HTTP or 443 for HTTPS based on scheme
225            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        // First try exact hostname:port match
234        if let Some(site) = config.find_site_by_host_port(hostname, port).cloned() {
235            return Some(site);
236        }
237
238        // For ACME challenge requests on port 80, find any site with the same hostname that has ACME enabled
239        let path = session.req_header().uri.path();
240        if port == 80 && path.starts_with("/.well-known/acme-challenge/") {
241            // Look for any site with this hostname that has ACME enabled
242            for site in &config.sites {
243                if site.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            // If no exact hostname match, try to find any ACME-enabled site (for wildcard scenarios)
258            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        // Check if the request is HTTPS
312        // This is a simplified check - in production, you might need to check
313        // X-Forwarded-Proto header if behind a reverse proxy
314        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        // Check if this is an ACME challenge request
329        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        // Check if the site has ACME enabled
341        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        // For ACME challenges, prioritize filesystem access to avoid blocking on SSL manager initialization
415        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            // First try reading challenge file directly from filesystem (fastest path)
423            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                // Add ACME-specific headers for debugging
442                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        // Fallback to SSL manager (only if filesystem lookup failed)
469        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                    // Try to get challenge response from SSL manager
485                    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                        // Add ACME-specific headers for debugging
492                        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        // Challenge not found, return 404 with ACME debugging headers
533        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        // Add ACME debugging headers
543        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        // Apply custom headers from site configuration
569        for (key, value) in &site.headers {
570            header.insert_header(key.clone(), value.clone())?;
571        }
572
573        // Apply security headers
574        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        // Hide server header if configured
580        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        // Check if site has custom 404 page
594        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        // Default 404 response
615        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        // Since we're handling requests locally, we don't need an upstream peer
656        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        // Find the matching site configuration
661        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        // Log the incoming request
673        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        // Handle HTTPS redirect if configured
692        if let Some(site) = ctx.as_ref() {
693            if self.handle_ssl_redirect(session, site).await? {
694                return Ok(true);
695            }
696        }
697
698        // Handle ACME challenge requests
699        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                // ACME challenge request but no site found
722                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        // Route request to appropriate handler
745        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                // Check if site has proxy enabled and route matches
756                if let Some(site) = ctx.as_ref() {
757                    if site.proxy.enabled {
758                        // Check if request matches any proxy routes
759                        for route in &site.proxy.routes {
760                            if path.starts_with(&route.path) {
761                                // Create a temporary proxy handler for this request
762                                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                    // No proxy route matched, handle as static files
771                    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        // Not used for local serving
791        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        // Not used for local serving
801        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        // Not used for local serving
811        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                port: 8080,
862                static_dir: "/tmp/static".to_string(),
863                default: true,
864                api_only: false,
865                headers: HashMap::new(),
866                ssl: crate::config::SiteSslConfig::default(),
867                redirect_to_https: false,
868                index_files: vec!["index.html".to_string()],
869                error_pages: HashMap::new(),
870                compression: Default::default(),
871                cache: Default::default(),
872                access_control: Default::default(),
873                proxy: crate::config::ProxyConfig::default(),
874            }],
875            logging: LoggingConfig::default(),
876            performance: PerformanceConfig::default(),
877            security: SecurityConfig::default(),
878        }
879    }
880
881    #[tokio::test]
882    async fn test_web_server_service_creation() {
883        let config = create_test_config();
884        let _service = WebServerService::new(config);
885        // Service creation should succeed
886    }
887
888    #[tokio::test]
889    async fn test_config_reload() {
890        let config = create_test_config();
891        let service = WebServerService::new(config.clone());
892
893        let mut new_config = config.clone();
894        new_config.server.name = "updated-server".to_string();
895
896        let result = service.reload_config(new_config).await;
897        assert!(result.is_ok());
898
899        let updated_config = service.get_config().await;
900        assert_eq!(updated_config.server.name, "updated-server");
901    }
902}