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 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, 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 new_config.validate()?;
50
51 {
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 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 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 (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 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 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 for (key, value) in &site.headers {
173 header.insert_header(key.clone(), value.clone())?;
174 }
175
176 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 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 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 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 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 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 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 if let Some(site) = ctx.as_ref() {
288 if self.handle_ssl_redirect(session, site).await? {
289 return Ok(true);
290 }
291 }
292
293 if self.handle_acme_challenge(session, &path).await? {
295 return Ok(true);
296 }
297
298 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 if let Some(site) = ctx.as_ref() {
311 if site.proxy.enabled {
312 for route in &site.proxy.routes {
314 if path.starts_with(&route.path) {
315 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 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 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 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 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 }
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}