1use 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 for site in &self.sites {
44 if site.hostname == host && site.port == port {
45 return Some(site);
46 }
47 }
48
49 for site in &self.sites {
51 if site.port == port {
52 return Some(site);
53 }
54 }
55
56 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 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 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 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 (host_header, 8080)
140 };
141
142 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 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 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 if let Some(site) = ctx.as_ref() {
192 let file_path = format!("{}{}", site.static_dir, &path[7..]); 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 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 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 if file_path.starts_with("static/") {
249 header.insert_header("Cache-Control", "public, max-age=3600")?;
250 }
251
252 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 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 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 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 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 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 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 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 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 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}