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    config_path: Arc<RwLock<Option<String>>>,
16    ssl_managers: Arc<RwLock<HashMap<String, Arc<SslManager>>>>, // hostname -> SslManager
17    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        // Initialize handlers
25        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        // Initialize SSL managers storage
30        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    /// Set the configuration file path for hot reloading
43    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        // Also set it globally for the API handler
48        ApiHandler::set_config_path(path);
49    }
50
51    /// Get the configuration file path
52    pub async fn get_config_path(&self) -> Option<String> {
53        self.config_path.read().await.clone()
54    }
55
56    /// Initialize SSL managers for all sites with SSL enabled
57    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                        // Initialize certificate for this domain
72                        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        // Start automatic certificate renewal scheduler
107        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            // Start background certificate renewal monitoring
118            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                        // Check certificates every hour
124                        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        // Validate new configuration
154        new_config.validate()?;
155
156        // Update configuration
157        {
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    /// Check and renew certificates for all SSL-enabled sites
179    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        // Extract host and port information
221        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        // Parse host and port
229        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            // Default to port 80 for HTTP or 443 for HTTPS based on scheme
236            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        // First try exact hostname:port match
245        if let Some(site) = config.find_site_by_host_port(hostname, port).cloned() {
246            return Some(site);
247        }
248
249        // For ACME challenge requests on port 80, find any site with the same hostname that has ACME enabled
250        let path = session.req_header().uri.path();
251        if port == 80 && path.starts_with("/.well-known/acme-challenge/") {
252            // Look for any site with this hostname that has ACME enabled
253            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            // If no exact hostname match, try to find any ACME-enabled site (for wildcard scenarios)
269            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        // Check if the request is HTTPS
320        // This is a simplified check - in production, you might need to check
321        // X-Forwarded-Proto header if behind a reverse proxy
322        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        // Check if this is an ACME challenge request
337        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        // Check if the site has ACME enabled
349        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        // For ACME challenges, prioritize filesystem access to avoid blocking on SSL manager initialization
423        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            // First try reading challenge file directly from filesystem (fastest path)
431            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                // Add ACME-specific headers for debugging
450                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        // Fallback to SSL manager (only if filesystem lookup failed)
477        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                    // Try to get challenge response from SSL manager
493                    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                        // Add ACME-specific headers for debugging
500                        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        // Challenge not found, return 404 with ACME debugging headers
541        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        // Add ACME debugging headers
551        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        // Apply custom headers from site configuration
577        for (key, value) in &site.headers {
578            header.insert_header(key.clone(), value.clone())?;
579        }
580
581        // Apply security headers
582        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        // Hide server header if configured
588        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        // Check if site has custom 404 page
602        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        // Default 404 response
623        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        // Since we're handling requests locally, we don't need an upstream peer
664        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        // Find the matching site configuration
669        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        // Log the incoming request
681        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        // Handle HTTPS redirect if configured
700        if let Some(site) = ctx.as_ref() {
701            if self.handle_ssl_redirect(session, site).await? {
702                return Ok(true);
703            }
704        }
705
706        // Handle ACME challenge requests
707        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                // ACME challenge request but no site found
730                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        // Route request to appropriate handler
750        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                // Check if site has proxy enabled and route matches
761                if let Some(site) = ctx.as_ref() {
762                    if site.proxy.enabled {
763                        // Check if request matches any proxy routes
764                        for route in &site.proxy.routes {
765                            if path.starts_with(&route.path) {
766                                // Create a temporary proxy handler for this request
767                                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                    // No proxy route matched, handle as static files
776                    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        // Not used for local serving
796        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        // Not used for local serving
806        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        // Not used for local serving
816        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        // Service creation should succeed
895    }
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}