bws_web_server/server/
service.rs

1use crate::config::{ServerConfig, SiteConfig};
2use crate::handlers::*;
3use crate::ssl::SslManager;
4use async_trait::async_trait;
5use pingora::http::ResponseHeader;
6use pingora::prelude::*;
7use std::sync::Arc;
8use tokio::sync::RwLock;
9
10pub struct WebServerService {
11    config: Arc<RwLock<ServerConfig>>,
12    ssl_manager: Option<Arc<SslManager>>,
13    static_handler: Arc<StaticFileHandler>,
14    api_handler: Arc<ApiHandler>,
15    health_handler: Arc<HealthHandler>,
16    #[allow(dead_code)]
17    proxy_handler: Arc<ProxyHandler>,
18}
19
20impl WebServerService {
21    pub fn new(config: ServerConfig) -> Self {
22        // For now, don't initialize SSL manager synchronously
23        // TODO: Initialize SSL manager in background after service starts
24
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        WebServerService {
31            config: Arc::new(RwLock::new(config)),
32            ssl_manager: None, // Will be initialized later if needed
33            static_handler,
34            api_handler,
35            health_handler,
36            proxy_handler: Arc::new(ProxyHandler::new(crate::config::ProxyConfig::default())),
37        }
38    }
39
40    pub async fn get_config(&self) -> ServerConfig {
41        self.config.read().await.clone()
42    }
43
44    pub async fn reload_config(
45        &self,
46        new_config: ServerConfig,
47    ) -> Result<(), Box<dyn std::error::Error>> {
48        // Validate new configuration
49        new_config.validate()?;
50
51        // Update configuration
52        {
53            let mut config = self.config.write().await;
54            *config = new_config;
55        }
56
57        log::info!("Configuration reloaded successfully");
58        Ok(())
59    }
60
61    pub async fn ensure_ssl_certificate(
62        &self,
63        domain: &str,
64    ) -> Result<bool, Box<dyn std::error::Error>> {
65        if let Some(ssl_manager) = &self.ssl_manager {
66            ssl_manager.ensure_certificate(domain).await
67        } else {
68            Ok(false)
69        }
70    }
71
72    async fn find_site_by_request(&self, session: &Session) -> Option<SiteConfig> {
73        let config = self.config.read().await;
74
75        // Extract host and port information
76        let host_header = session
77            .req_header()
78            .headers
79            .get("Host")
80            .and_then(|h| h.to_str().ok())
81            .unwrap_or("localhost");
82
83        // Parse host and port
84        let (hostname, port) = if let Some(pos) = host_header.find(':') {
85            let hostname = &host_header[..pos];
86            let port_str = &host_header[pos + 1..];
87            let port = port_str.parse::<u16>().unwrap_or(8080);
88            (hostname, port)
89        } else {
90            // Default to 8080 if no port specified
91            (host_header, 8080)
92        };
93
94        config.find_site_by_host_port(hostname, port).cloned()
95    }
96
97    async fn handle_ssl_redirect(&self, session: &mut Session, site: &SiteConfig) -> Result<bool> {
98        if site.redirect_to_https && !self.is_https_request(session) {
99            let https_url = format!(
100                "https://{}{}",
101                session
102                    .req_header()
103                    .headers
104                    .get("Host")
105                    .and_then(|h| h.to_str().ok())
106                    .unwrap_or(&site.hostname),
107                session
108                    .req_header()
109                    .uri
110                    .path_and_query()
111                    .map(|pq| pq.as_str())
112                    .unwrap_or("/")
113            );
114
115            let mut header = ResponseHeader::build(301, Some(2))?;
116            header.insert_header("Location", https_url)?;
117            header.insert_header("Content-Length", "0")?;
118
119            session
120                .write_response_header(Box::new(header), true)
121                .await?;
122            return Ok(true);
123        }
124        Ok(false)
125    }
126
127    fn is_https_request(&self, session: &Session) -> bool {
128        // Check if the request is HTTPS
129        // This is a simplified check - in production, you might need to check
130        // X-Forwarded-Proto header if behind a reverse proxy
131        session
132            .req_header()
133            .uri
134            .scheme()
135            .map(|s| s.as_str() == "https")
136            .unwrap_or(false)
137    }
138
139    async fn handle_acme_challenge(&self, session: &mut Session, path: &str) -> Result<bool> {
140        if let Some(ssl_manager) = &self.ssl_manager {
141            if ssl_manager.handles_acme_challenge(path) {
142                if let Some(token) = path.strip_prefix("/.well-known/acme-challenge/") {
143                    if let Some(response) = ssl_manager.get_acme_challenge_response(token).await {
144                        let mut header = ResponseHeader::build(200, Some(3))?;
145                        header.insert_header("Content-Type", "text/plain")?;
146                        header.insert_header("Content-Length", response.len().to_string())?;
147
148                        session
149                            .write_response_header(Box::new(header), false)
150                            .await?;
151                        session
152                            .write_response_body(Some(response.into_bytes().into()), true)
153                            .await?;
154                        return Ok(true);
155                    }
156                }
157
158                // Challenge not found, return 404
159                self.handle_404(session, None).await?;
160                return Ok(true);
161            }
162        }
163        Ok(false)
164    }
165
166    async fn apply_site_headers(
167        &self,
168        header: &mut ResponseHeader,
169        site: &SiteConfig,
170    ) -> Result<()> {
171        // Apply custom headers from site configuration
172        for (key, value) in &site.headers {
173            header.insert_header(key.clone(), value.clone())?;
174        }
175
176        // Apply security headers
177        let config = self.config.read().await;
178        for (key, value) in &config.security.security_headers {
179            header.insert_header(key.clone(), value.clone())?;
180        }
181
182        // Hide server header if configured
183        if config.security.hide_server_header {
184            header.remove_header("Server");
185        } else {
186            header.insert_header(
187                "Server",
188                format!("{}/{}", config.server.name, config.server.version),
189            )?;
190        }
191
192        Ok(())
193    }
194
195    async fn handle_404(&self, session: &mut Session, site: Option<&SiteConfig>) -> Result<()> {
196        // Check if site has custom 404 page
197        if let Some(site) = site {
198            if let Some(error_page) = site.get_error_page(404) {
199                let error_page_path = format!("{}/{}", site.static_dir, error_page);
200                if let Ok(content) = tokio::fs::read(&error_page_path).await {
201                    let mut header = ResponseHeader::build(404, Some(3))?;
202                    header.insert_header("Content-Type", "text/html")?;
203                    header.insert_header("Content-Length", content.len().to_string())?;
204                    self.apply_site_headers(&mut header, site).await?;
205
206                    session
207                        .write_response_header(Box::new(header), false)
208                        .await?;
209                    session
210                        .write_response_body(Some(content.into()), true)
211                        .await?;
212                    return Ok(());
213                }
214            }
215        }
216
217        // Default 404 response
218        let error_response = serde_json::json!({
219            "error": "Not Found",
220            "message": "The requested resource was not found",
221            "status": 404
222        });
223
224        let response_body = error_response.to_string();
225        let response_bytes = response_body.into_bytes();
226        let mut header = ResponseHeader::build(404, Some(3))?;
227        header.insert_header("Content-Type", "application/json")?;
228        header.insert_header("Content-Length", response_bytes.len().to_string())?;
229
230        if let Some(site) = site {
231            self.apply_site_headers(&mut header, site).await?;
232        }
233
234        session
235            .write_response_header(Box::new(header), false)
236            .await?;
237        session
238            .write_response_body(Some(response_bytes.into()), true)
239            .await?;
240
241        Ok(())
242    }
243}
244
245#[async_trait]
246impl ProxyHttp for WebServerService {
247    type CTX = Option<SiteConfig>;
248
249    fn new_ctx(&self) -> Self::CTX {
250        None
251    }
252
253    async fn upstream_peer(
254        &self,
255        _session: &mut Session,
256        _ctx: &mut Self::CTX,
257    ) -> Result<Box<HttpPeer>> {
258        // Since we're handling requests locally, we don't need an upstream peer
259        Err(Error::new(ErrorType::InternalError).into_down())
260    }
261
262    async fn request_filter(&self, session: &mut Session, ctx: &mut Self::CTX) -> Result<bool> {
263        // Find the matching site configuration
264        let site_config = self.find_site_by_request(session).await;
265        *ctx = site_config.clone();
266
267        let path = session.req_header().uri.path().to_string();
268
269        // Log the incoming request
270        if let Some(site) = ctx.as_ref() {
271            log::info!(
272                "Incoming request: {} {} (site: {}, static_dir: {})",
273                session.req_header().method,
274                session.req_header().uri,
275                site.name,
276                site.static_dir
277            );
278        } else {
279            log::warn!(
280                "No site configuration found for request: {} {}",
281                session.req_header().method,
282                session.req_header().uri
283            );
284        }
285
286        // Handle HTTPS redirect if configured
287        if let Some(site) = ctx.as_ref() {
288            if self.handle_ssl_redirect(session, site).await? {
289                return Ok(true);
290            }
291        }
292
293        // Handle ACME challenge requests
294        if self.handle_acme_challenge(session, &path).await? {
295            return Ok(true);
296        }
297
298        // Route request to appropriate handler
299        match path.as_str() {
300            path if path.starts_with("/api/health") => {
301                self.health_handler.handle(session, ctx.as_ref()).await?;
302                Ok(true)
303            }
304            path if path.starts_with("/api/") => {
305                self.api_handler.handle(session, ctx.as_ref()).await?;
306                Ok(true)
307            }
308            _ => {
309                // Check if site has proxy enabled and route matches
310                if let Some(site) = ctx.as_ref() {
311                    if site.proxy.enabled {
312                        // Check if request matches any proxy routes
313                        for route in &site.proxy.routes {
314                            if path.starts_with(&route.path) {
315                                // Create a temporary proxy handler for this request
316                                let proxy_handler = ProxyHandler::new(site.proxy.clone());
317                                return proxy_handler
318                                    .handle_proxy_request(session, site, &path)
319                                    .await;
320                            }
321                        }
322                    }
323
324                    // No proxy route matched, handle as static files
325                    self.static_handler.handle(session, site, &path).await?;
326                } else {
327                    self.handle_404(session, ctx.as_ref()).await?;
328                }
329                Ok(true)
330            }
331        }
332    }
333
334    async fn connected_to_upstream(
335        &self,
336        _session: &mut Session,
337        _reused: bool,
338        _peer: &HttpPeer,
339        #[cfg(unix)] _fd: std::os::unix::io::RawFd,
340        #[cfg(windows)] _fd: std::os::windows::io::RawSocket,
341        _digest: Option<&pingora::protocols::Digest>,
342        _ctx: &mut Self::CTX,
343    ) -> Result<()> {
344        // Not used for local serving
345        Ok(())
346    }
347
348    async fn upstream_request_filter(
349        &self,
350        _session: &mut Session,
351        _upstream_request: &mut pingora::http::RequestHeader,
352        _ctx: &mut Self::CTX,
353    ) -> Result<()> {
354        // Not used for local serving
355        Ok(())
356    }
357
358    async fn response_filter(
359        &self,
360        _session: &mut Session,
361        _upstream_response: &mut pingora::http::ResponseHeader,
362        _ctx: &mut Self::CTX,
363    ) -> Result<()> {
364        // Not used for local serving
365        Ok(())
366    }
367
368    async fn logging(
369        &self,
370        session: &mut Session,
371        _e: Option<&pingora::Error>,
372        ctx: &mut Self::CTX,
373    ) {
374        let config = self.config.read().await;
375        if config.logging.log_requests {
376            let site_name = ctx.as_ref().map(|s| s.name.as_str()).unwrap_or("unknown");
377            let method = session.req_header().method.as_str();
378            let uri = session.req_header().uri.to_string();
379            let status = session
380                .response_written()
381                .map(|r| r.status.as_u16())
382                .unwrap_or(0);
383
384            log::info!(
385                "Request completed: {} {} {} {} (site: {})",
386                session
387                    .client_addr()
388                    .map(|addr| addr.to_string())
389                    .unwrap_or_else(|| "unknown".to_string()),
390                method,
391                uri,
392                status,
393                site_name
394            );
395        }
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402    use crate::config::{LoggingConfig, PerformanceConfig, SecurityConfig, ServerInfo};
403    use std::collections::HashMap;
404
405    fn create_test_config() -> ServerConfig {
406        ServerConfig {
407            server: ServerInfo {
408                name: "test-server".to_string(),
409                version: "1.0.0".to_string(),
410                description: "Test server".to_string(),
411            },
412            sites: vec![SiteConfig {
413                name: "test-site".to_string(),
414                hostname: "localhost".to_string(),
415                port: 8080,
416                static_dir: "/tmp/static".to_string(),
417                default: true,
418                api_only: false,
419                headers: HashMap::new(),
420                ssl: crate::config::SiteSslConfig::default(),
421                redirect_to_https: false,
422                index_files: vec!["index.html".to_string()],
423                error_pages: HashMap::new(),
424                compression: Default::default(),
425                cache: Default::default(),
426                access_control: Default::default(),
427                proxy: crate::config::ProxyConfig::default(),
428            }],
429            logging: LoggingConfig::default(),
430            performance: PerformanceConfig::default(),
431            security: SecurityConfig::default(),
432        }
433    }
434
435    #[tokio::test]
436    async fn test_web_server_service_creation() {
437        let config = create_test_config();
438        let _service = WebServerService::new(config);
439        // Service creation should succeed
440    }
441
442    #[tokio::test]
443    async fn test_config_reload() {
444        let config = create_test_config();
445        let service = WebServerService::new(config.clone());
446
447        let mut new_config = config.clone();
448        new_config.server.name = "updated-server".to_string();
449
450        let result = service.reload_config(new_config).await;
451        assert!(result.is_ok());
452
453        let updated_config = service.get_config().await;
454        assert_eq!(updated_config.server.name, "updated-server");
455    }
456}