bws_web_server/
lib.rs

1// a function read file content
2use async_trait::async_trait;
3use pingora::http::ResponseHeader;
4use pingora::prelude::*;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::fs;
8
9#[derive(Debug, Deserialize, Serialize, Clone)]
10pub struct ServerConfig {
11    pub server: ServerInfo,
12    pub sites: Vec<SiteConfig>,
13}
14
15#[derive(Debug, Deserialize, Serialize, Clone)]
16pub struct ServerInfo {
17    pub name: String,
18}
19
20#[derive(Debug, Deserialize, Serialize, Clone)]
21pub struct SiteConfig {
22    pub name: String,
23    pub hostname: String,
24    pub port: u16,
25    pub static_dir: String,
26    #[serde(default)]
27    pub default: bool,
28    #[serde(default)]
29    pub api_only: bool,
30    #[serde(default)]
31    pub headers: HashMap<String, String>,
32}
33
34impl ServerConfig {
35    pub fn load_from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
36        let content = fs::read_to_string(path)?;
37        let config: ServerConfig = toml::from_str(&content)?;
38        Ok(config)
39    }
40
41    pub fn find_site_by_host_port(&self, host: &str, port: u16) -> Option<&SiteConfig> {
42        // First try to match both hostname and port
43        for site in &self.sites {
44            if site.hostname == host && site.port == port {
45                return Some(site);
46            }
47        }
48
49        // Then try to match just the port (for cases where hostname might not match exactly)
50        for site in &self.sites {
51            if site.port == port {
52                return Some(site);
53            }
54        }
55
56        // Finally, return the default site if no match
57        self.sites.iter().find(|site| site.default)
58    }
59}
60
61pub fn read_file_content(file_path: &str) -> std::io::Result<String> {
62    fs::read_to_string(file_path)
63}
64
65pub fn read_file_bytes(file_path: &str) -> std::io::Result<Vec<u8>> {
66    fs::read(file_path)
67}
68
69pub fn get_mime_type(file_path: &str) -> &'static str {
70    let path = std::path::Path::new(file_path);
71    match path.extension().and_then(|ext| ext.to_str()) {
72        Some("html") | Some("htm") => "text/html",
73        Some("css") => "text/css",
74        Some("js") => "application/javascript",
75        Some("json") => "application/json",
76        Some("png") => "image/png",
77        Some("jpg") | Some("jpeg") => "image/jpeg",
78        Some("gif") => "image/gif",
79        Some("svg") => "image/svg+xml",
80        Some("ico") => "image/x-icon",
81        Some("txt") => "text/plain",
82        Some("pdf") => "application/pdf",
83        Some("woff") | Some("woff2") => "font/woff",
84        Some("ttf") => "font/ttf",
85        Some("xml") => "application/xml",
86        _ => "application/octet-stream",
87    }
88}
89
90pub struct WebServerService {
91    config: ServerConfig,
92}
93
94impl WebServerService {
95    pub fn new(config: ServerConfig) -> Self {
96        WebServerService { config }
97    }
98
99    pub fn get_config(&self) -> &ServerConfig {
100        &self.config
101    }
102}
103
104#[async_trait]
105impl ProxyHttp for WebServerService {
106    type CTX = Option<SiteConfig>;
107
108    fn new_ctx(&self) -> Self::CTX {
109        None
110    }
111
112    async fn upstream_peer(
113        &self,
114        _session: &mut Session,
115        _ctx: &mut Self::CTX,
116    ) -> Result<Box<HttpPeer>> {
117        // Since we're handling requests locally, we don't need an upstream peer
118        // This should not be called since we handle everything in request_filter
119        Err(Error::new(ErrorType::InternalError).into_down())
120    }
121
122    async fn request_filter(&self, session: &mut Session, ctx: &mut Self::CTX) -> Result<bool> {
123        // Extract host and port information
124        let host_header = session
125            .req_header()
126            .headers
127            .get("Host")
128            .and_then(|h| h.to_str().ok())
129            .unwrap_or("localhost");
130
131        // Parse host and port
132        let (hostname, port) = if let Some(pos) = host_header.find(':') {
133            let hostname = &host_header[..pos];
134            let port_str = &host_header[pos + 1..];
135            let port = port_str.parse::<u16>().unwrap_or(8080);
136            (hostname, port)
137        } else {
138            // Default to 8080 if no port specified
139            (host_header, 8080)
140        };
141
142        // Find the matching site configuration and store it in context
143        let site_config = self.config.find_site_by_host_port(hostname, port);
144        *ctx = site_config.cloned();
145
146        let path = session.req_header().uri.path().to_string();
147
148        // Log the incoming request with site info
149        if let Some(site) = ctx.as_ref() {
150            log::info!(
151                "Incoming request: {} {} (site: {}, static_dir: {})",
152                session.req_header().method,
153                session.req_header().uri,
154                site.name,
155                site.static_dir
156            );
157        } else {
158            log::warn!(
159                "No site configuration found for {}:{}, using default",
160                hostname,
161                port
162            );
163        }
164
165        match path.as_str() {
166            "/api/health" => {
167                self.handle_health(session, ctx.as_ref()).await?;
168                Ok(true)
169            }
170            "/api/file" => {
171                self.handle_file_content(session, ctx.as_ref()).await?;
172                Ok(true)
173            }
174            "/api/sites" => {
175                self.handle_sites_info(session, ctx.as_ref()).await?;
176                Ok(true)
177            }
178            "/" => {
179                // Serve static index.html from the site's static directory
180                if let Some(site) = ctx.as_ref() {
181                    let file_path = format!("{}/index.html", site.static_dir);
182                    self.handle_static_file(session, &file_path, ctx.as_ref())
183                        .await?;
184                } else {
185                    self.handle_404(session, ctx.as_ref()).await?;
186                }
187                Ok(true)
188            }
189            path if path.starts_with("/static/") => {
190                // Serve static files from the site's static directory
191                if let Some(site) = ctx.as_ref() {
192                    let file_path = format!("{}{}", site.static_dir, &path[7..]); // Remove "/static" prefix
193                    self.handle_static_file(session, &file_path, ctx.as_ref())
194                        .await?;
195                } else {
196                    self.handle_404(session, ctx.as_ref()).await?;
197                }
198                Ok(true)
199            }
200            path if path.ends_with(".html") => {
201                // Serve HTML files from the site's static directory
202                if let Some(site) = ctx.as_ref() {
203                    let file_path = format!("{}{}", site.static_dir, path);
204                    self.handle_static_file(session, &file_path, ctx.as_ref())
205                        .await?;
206                } else {
207                    self.handle_404(session, ctx.as_ref()).await?;
208                }
209                Ok(true)
210            }
211            _ => {
212                self.handle_404(session, ctx.as_ref()).await?;
213                Ok(true)
214            }
215        }
216    }
217}
218
219impl WebServerService {
220    /// Apply site-specific headers to the response header
221    fn apply_site_headers(
222        &self,
223        header: &mut ResponseHeader,
224        site_config: Option<&SiteConfig>,
225    ) -> Result<()> {
226        if let Some(site) = site_config {
227            for (key, value) in &site.headers {
228                header.insert_header(key.clone(), value.clone())?;
229            }
230        }
231        Ok(())
232    }
233
234    async fn handle_static_file(
235        &self,
236        session: &mut Session,
237        file_path: &str,
238        site_config: Option<&SiteConfig>,
239    ) -> Result<()> {
240        match read_file_bytes(file_path) {
241            Ok(content) => {
242                let mime_type = get_mime_type(file_path);
243                let mut header = ResponseHeader::build(200, Some(4))?;
244                header.insert_header("Content-Type", mime_type)?;
245                header.insert_header("Content-Length", content.len().to_string())?;
246
247                // Add cache headers for static files
248                if file_path.starts_with("static/") {
249                    header.insert_header("Cache-Control", "public, max-age=3600")?;
250                }
251
252                // Apply site-specific headers
253                self.apply_site_headers(&mut header, site_config)?;
254
255                session
256                    .write_response_header(Box::new(header), false)
257                    .await?;
258                session
259                    .write_response_body(Some(content.into()), true)
260                    .await?;
261            }
262            Err(e) => {
263                log::warn!("Failed to read static file {}: {}", file_path, e);
264
265                // Return 404 for missing static files
266                let error_response = serde_json::json!({
267                    "error": "File Not Found",
268                    "message": format!("The requested file '{}' was not found", file_path),
269                    "status": 404
270                });
271
272                let response_body = error_response.to_string();
273                let response_bytes = response_body.into_bytes();
274                let mut header = ResponseHeader::build(404, Some(4))?;
275                header.insert_header("Content-Type", "application/json")?;
276                header.insert_header("Content-Length", response_bytes.len().to_string())?;
277
278                // Apply site-specific headers even for error responses
279                self.apply_site_headers(&mut header, site_config)?;
280
281                session
282                    .write_response_header(Box::new(header), false)
283                    .await?;
284                session
285                    .write_response_body(Some(response_bytes.into()), true)
286                    .await?;
287            }
288        }
289
290        Ok(())
291    }
292
293    async fn handle_sites_info(
294        &self,
295        session: &mut Session,
296        site_config: Option<&SiteConfig>,
297    ) -> Result<()> {
298        let response = serde_json::json!({
299            "server": self.config.server.name,
300            "sites": self.config.sites.iter().map(|site| serde_json::json!({
301                "name": site.name,
302                "hostname": site.hostname,
303                "port": site.port,
304                "static_dir": site.static_dir,
305                "default": site.default,
306                "api_only": site.api_only,
307                "headers": site.headers,
308                "url": format!("http://{}:{}", site.hostname, site.port)
309            })).collect::<Vec<_>>(),
310            "total_sites": self.config.sites.len()
311        });
312
313        let response_body = response.to_string();
314        let response_bytes = response_body.into_bytes();
315        let mut header = ResponseHeader::build(200, Some(4))?;
316        header.insert_header("Content-Type", "application/json")?;
317        header.insert_header("Content-Length", response_bytes.len().to_string())?;
318
319        // Apply site-specific headers
320        self.apply_site_headers(&mut header, site_config)?;
321
322        session
323            .write_response_header(Box::new(header), false)
324            .await?;
325        session
326            .write_response_body(Some(response_bytes.into()), true)
327            .await?;
328
329        Ok(())
330    }
331
332    async fn handle_health(
333        &self,
334        session: &mut Session,
335        site_config: Option<&SiteConfig>,
336    ) -> Result<()> {
337        let response = serde_json::json!({
338            "status": "ok",
339            "timestamp": chrono::Utc::now().to_rfc3339(),
340            "service": "bws-web-server"
341        });
342
343        let response_body = response.to_string();
344        let response_bytes = response_body.into_bytes();
345        let mut header = ResponseHeader::build(200, Some(4))?;
346        header.insert_header("Content-Type", "application/json")?;
347        header.insert_header("Content-Length", response_bytes.len().to_string())?;
348
349        // Apply site-specific headers
350        self.apply_site_headers(&mut header, site_config)?;
351
352        session
353            .write_response_header(Box::new(header), false)
354            .await?;
355        session
356            .write_response_body(Some(response_bytes.into()), true)
357            .await?;
358
359        Ok(())
360    }
361
362    async fn handle_file_content(
363        &self,
364        session: &mut Session,
365        site_config: Option<&SiteConfig>,
366    ) -> Result<()> {
367        // Extract file path from query parameters
368        let query = session.req_header().uri.query().unwrap_or("");
369        let file_path = query
370            .split('&')
371            .find(|param| param.starts_with("path="))
372            .and_then(|param| param.split('=').nth(1))
373            .unwrap_or("");
374
375        if file_path.is_empty() {
376            let error_response = serde_json::json!({
377                "error": "Missing 'path' query parameter",
378                "example": "/api/file?path=Cargo.toml"
379            });
380
381            let response_body = error_response.to_string();
382            let response_bytes = response_body.into_bytes();
383            let mut header = ResponseHeader::build(400, Some(4))?;
384            header.insert_header("Content-Type", "application/json")?;
385            header.insert_header("Content-Length", response_bytes.len().to_string())?;
386
387            // Apply site-specific headers
388            self.apply_site_headers(&mut header, site_config)?;
389
390            session
391                .write_response_header(Box::new(header), false)
392                .await?;
393            session
394                .write_response_body(Some(response_bytes.into()), true)
395                .await?;
396
397            return Ok(());
398        }
399
400        match read_file_content(file_path) {
401            Ok(content) => {
402                let response = serde_json::json!({
403                    "file_path": file_path,
404                    "content": content,
405                    "size": content.len()
406                });
407
408                let response_body = response.to_string();
409                let response_bytes = response_body.into_bytes();
410                let mut header = ResponseHeader::build(200, Some(4))?;
411                header.insert_header("Content-Type", "application/json")?;
412                header.insert_header("Content-Length", response_bytes.len().to_string())?;
413
414                // Apply site-specific headers
415                self.apply_site_headers(&mut header, site_config)?;
416
417                session
418                    .write_response_header(Box::new(header), false)
419                    .await?;
420                session
421                    .write_response_body(Some(response_bytes.into()), true)
422                    .await?;
423            }
424            Err(e) => {
425                let error_response = serde_json::json!({
426                    "error": format!("Failed to read file: {}", e),
427                    "file_path": file_path
428                });
429
430                let response_body = error_response.to_string();
431                let response_bytes = response_body.into_bytes();
432                let mut header = ResponseHeader::build(404, Some(4))?;
433                header.insert_header("Content-Type", "application/json")?;
434                header.insert_header("Content-Length", response_bytes.len().to_string())?;
435
436                // Apply site-specific headers
437                self.apply_site_headers(&mut header, site_config)?;
438
439                session
440                    .write_response_header(Box::new(header), false)
441                    .await?;
442                session
443                    .write_response_body(Some(response_bytes.into()), true)
444                    .await?;
445            }
446        }
447
448        Ok(())
449    }
450
451    async fn handle_404(
452        &self,
453        session: &mut Session,
454        site_config: Option<&SiteConfig>,
455    ) -> Result<()> {
456        let error_response = serde_json::json!({
457            "error": "Not Found",
458            "message": "The requested endpoint does not exist",
459            "available_endpoints": ["/", "/api/health", "/api/file"]
460        });
461
462        let response_body = error_response.to_string();
463        let response_bytes = response_body.into_bytes();
464        let mut header = ResponseHeader::build(404, Some(4))?;
465        header.insert_header("Content-Type", "application/json")?;
466        header.insert_header("Content-Length", response_bytes.len().to_string())?;
467
468        // Apply site-specific headers
469        self.apply_site_headers(&mut header, site_config)?;
470
471        session
472            .write_response_header(Box::new(header), false)
473            .await?;
474        session
475            .write_response_body(Some(response_bytes.into()), true)
476            .await?;
477
478        Ok(())
479    }
480}