1use crate::{
2 handlers::{FileHandler, DirectoryHandler, DefaultFileHandler, DefaultDirectoryHandler},
3 config::GurtConfig,
4 security::SecurityMiddleware,
5};
6use gurtlib::prelude::*;
7use std::path::Path;
8use std::sync::Arc;
9use tracing;
10
11pub struct RequestHandlerBuilder {
12 file_handler: Arc<dyn FileHandler>,
13 directory_handler: Arc<dyn DirectoryHandler>,
14 base_directory: std::path::PathBuf,
15 config: Option<Arc<GurtConfig>>,
16}
17
18impl RequestHandlerBuilder {
19 pub fn new<P: AsRef<Path>>(base_directory: P) -> Self {
20 Self {
21 file_handler: Arc::new(DefaultFileHandler),
22 directory_handler: Arc::new(DefaultDirectoryHandler),
23 base_directory: base_directory.as_ref().to_path_buf(),
24 config: None,
25 }
26 }
27
28 pub fn with_file_handler<H: FileHandler + 'static>(mut self, handler: H) -> Self {
29 self.file_handler = Arc::new(handler);
30 self
31 }
32
33 pub fn with_directory_handler<H: DirectoryHandler + 'static>(mut self, handler: H) -> Self {
34 self.directory_handler = Arc::new(handler);
35 self
36 }
37
38 pub fn with_config(mut self, config: Arc<GurtConfig>) -> Self {
39 self.config = Some(config);
40 self
41 }
42
43 pub fn build(self) -> RequestHandler {
44 let security = self.config.as_ref().map(|config| SecurityMiddleware::new(config.clone()));
45
46 RequestHandler {
47 file_handler: self.file_handler,
48 directory_handler: self.directory_handler,
49 base_directory: self.base_directory,
50 config: self.config,
51 security,
52 }
53 }
54}
55
56pub struct RequestHandler {
57 file_handler: Arc<dyn FileHandler>,
58 directory_handler: Arc<dyn DirectoryHandler>,
59 base_directory: std::path::PathBuf,
60 config: Option<Arc<GurtConfig>>,
61 security: Option<SecurityMiddleware>,
62}
63
64impl RequestHandler {
65 pub fn builder<P: AsRef<Path>>(base_directory: P) -> RequestHandlerBuilder {
66 RequestHandlerBuilder::new(base_directory)
67 }
68
69 fn apply_custom_error_page(&self, mut response: GurtResponse) -> GurtResponse {
70 if response.status_code >= 400 {
71 let custom_content = self.get_custom_error_page(response.status_code)
72 .unwrap_or_else(|| self.get_fallback_error_page(response.status_code));
73
74 response.body = custom_content.into_bytes();
75 response = response.with_header("Content-Type", "text/html");
76 tracing::debug!("Applied error page for status {}", response.status_code);
77 }
78 response
79 }
80
81 fn get_custom_error_page(&self, status_code: u16) -> Option<String> {
82 if let Some(config) = &self.config {
83 if let Some(error_pages) = &config.error_pages {
84 error_pages.get_page_content(status_code, &self.base_directory)
85 } else {
86 None
87 }
88 } else {
89 None
90 }
91 }
92
93 fn get_fallback_error_page(&self, status_code: u16) -> String {
94 let (title, message) = match status_code {
95 400 => ("Bad Request", "The request could not be understood by the server."),
96 401 => ("Unauthorized", "Authentication is required to access this resource."),
97 403 => ("Forbidden", "Access to this resource is denied by server policy."),
98 404 => ("Not Found", "The requested resource was not found on this server."),
99 405 => ("Method Not Allowed", "The request method is not allowed for this resource."),
100 429 => ("Too Many Requests", "You have exceeded the rate limit. Please try again later."),
101 500 => ("Internal Server Error", "The server encountered an error processing your request."),
102 502 => ("Bad Gateway", "The server received an invalid response from an upstream server."),
103 503 => ("Service Unavailable", "The server is temporarily unavailable. Please try again later."),
104 504 => ("Gateway Timeout", "The server did not receive a timely response from an upstream server."),
105 _ => ("Error", "An error occurred while processing your request."),
106 };
107
108 format!(include_str!("../templates/error.html"), status_code, title, status_code, title, message)
109 }
110
111 pub fn check_security(&self, ctx: &ServerContext) -> Option<std::result::Result<GurtResponse, GurtError>> {
112 if let Some(security) = &self.security {
113 let client_ip = ctx.client_ip();
114 let method = ctx.method();
115
116 if !security.is_method_allowed(method) {
117 tracing::warn!("Method {} not allowed from {}", method, client_ip);
118 let response = security.create_method_not_allowed_response()
119 .map(|r| self.apply_global_headers(r));
120 return Some(response);
121 }
122
123 if !security.check_rate_limit(client_ip) {
124 let response = security.create_rate_limit_response()
125 .map(|r| self.apply_global_headers(r));
126 return Some(response);
127 }
128
129 if !security.check_connection_limit(client_ip) {
130 let response = security.create_rate_limit_response()
131 .map(|r| self.apply_global_headers(r));
132 return Some(response);
133 }
134 }
135
136 None
137 }
138
139 pub fn register_connection(&self, client_ip: std::net::IpAddr) {
140 if let Some(security) = &self.security {
141 security.register_connection(client_ip);
142 }
143 }
144
145 pub fn unregister_connection(&self, client_ip: std::net::IpAddr) {
146 if let Some(security) = &self.security {
147 security.unregister_connection(client_ip);
148 }
149 }
150
151 fn is_file_denied(&self, file_path: &Path) -> bool {
152 if let Some(config) = &self.config {
153 let path_str = file_path.to_string_lossy();
154
155 let relative_path = if let Ok(canonical_file) = file_path.canonicalize() {
156 if let Ok(canonical_base) = self.base_directory.canonicalize() {
157 canonical_file.strip_prefix(&canonical_base)
158 .map(|p| p.to_string_lossy().to_string())
159 .unwrap_or_else(|_| path_str.to_string())
160 } else {
161 path_str.to_string()
162 }
163 } else {
164 path_str.to_string()
165 };
166
167 let is_denied = config.should_deny_file(&path_str) || config.should_deny_file(&relative_path);
168
169 if is_denied {
170 tracing::warn!("File access denied by security policy: {}", relative_path);
171 }
172
173 is_denied
174 } else {
175 false
176 }
177 }
178
179 fn apply_global_headers(&self, mut response: GurtResponse) -> GurtResponse {
180 response = self.apply_custom_error_page(response);
181
182 if let Some(config) = &self.config {
183 if let Some(headers) = &config.headers {
184 for (key, value) in headers {
185 response = response.with_header(key, value);
186 }
187 }
188 }
189 response
190 }
191
192 fn create_forbidden_response(&self) -> std::result::Result<GurtResponse, GurtError> {
193 let response = GurtResponse::forbidden()
194 .with_header("Content-Type", "text/html");
195
196 Ok(self.apply_global_headers(response))
197 }
198
199 pub async fn handle_root_request_with_context(&self, ctx: ServerContext) -> std::result::Result<GurtResponse, GurtError> {
200 let client_ip = ctx.client_ip();
201
202 self.register_connection(client_ip);
203
204 if let Some(security_response) = self.check_security(&ctx) {
205 self.unregister_connection(client_ip);
206 return security_response;
207 }
208
209 let result = self.handle_root_request().await;
210 self.unregister_connection(client_ip);
211 result
212 }
213
214 pub async fn handle_file_request_with_context(&self, request_path: &str, ctx: ServerContext) -> std::result::Result<GurtResponse, GurtError> {
215 let client_ip = ctx.client_ip();
216
217 self.register_connection(client_ip);
218
219 if let Some(security_response) = self.check_security(&ctx) {
220 self.unregister_connection(client_ip);
221 return security_response;
222 }
223
224 let result = self.handle_file_request(request_path).await;
225 self.unregister_connection(client_ip);
226 result
227 }
228
229 pub async fn handle_method_request_with_context(&self, ctx: ServerContext) -> std::result::Result<GurtResponse, GurtError> {
230 let client_ip = ctx.client_ip();
231 let method = ctx.method();
232
233 self.register_connection(client_ip);
234
235 if let Some(security_response) = self.check_security(&ctx) {
236 self.unregister_connection(client_ip);
237 return security_response;
238 }
239
240 let result = match method {
241 gurtlib::message::GurtMethod::GET => {
242 if ctx.path() == "/" {
243 self.handle_root_request().await
244 } else {
245 self.handle_file_request(ctx.path()).await
246 }
247 }
248 gurtlib::message::GurtMethod::HEAD => {
249 let mut response = if ctx.path() == "/" {
250 self.handle_root_request().await?
251 } else {
252 self.handle_file_request(ctx.path()).await?
253 };
254 response.body = Vec::new();
255 Ok(response)
256 }
257 gurtlib::message::GurtMethod::OPTIONS => {
258 let allowed_methods = if let Some(config) = &self.config {
259 if let Some(security) = &config.security {
260 security.allowed_methods.join(", ")
261 } else {
262 "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH".to_string()
263 }
264 } else {
265 "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH".to_string()
266 };
267
268 let response = GurtResponse::ok()
269 .with_header("Allow", &allowed_methods)
270 .with_header("Content-Type", "text/plain")
271 .with_string_body("Allowed methods");
272 Ok(self.apply_global_headers(response))
273 }
274 _ => {
275 let response = GurtResponse::new(gurtlib::protocol::GurtStatusCode::MethodNotAllowed)
276 .with_header("Content-Type", "text/html");
277 Ok(self.apply_global_headers(response))
278 }
279 };
280
281 self.unregister_connection(client_ip);
282 result
283 }
284
285 pub async fn handle_root_request(&self) -> std::result::Result<GurtResponse, GurtError> {
286 let index_path = self.base_directory.join("index.html");
287
288 if index_path.exists() && index_path.is_file() {
289 if self.is_file_denied(&index_path) {
290 return self.create_forbidden_response();
291 }
292
293 match self.file_handler.handle_file(&index_path) {
294 Ok(content) => {
295 let content_type = self.file_handler.get_content_type(&index_path);
296 let response = GurtResponse::ok()
297 .with_header("Content-Type", &content_type)
298 .with_body(content);
299 return Ok(self.apply_global_headers(response));
300 }
301 Err(_) => {
302 }
304 }
305 }
306
307 match self.directory_handler.handle_directory(&self.base_directory, "/") {
308 Ok(listing) => {
309 let response = GurtResponse::ok()
310 .with_header("Content-Type", "text/html")
311 .with_string_body(listing);
312 Ok(self.apply_global_headers(response))
313 }
314 Err(_) => {
315 let response = GurtResponse::internal_server_error()
316 .with_header("Content-Type", "text/html");
317 Ok(self.apply_global_headers(response))
318 }
319 }
320 }
321
322 pub async fn handle_file_request(&self, request_path: &str) -> std::result::Result<GurtResponse, GurtError> {
323 let path_without_query = if let Some(query_start) = request_path.find('?') {
324 &request_path[..query_start]
325 } else {
326 request_path
327 };
328
329 let mut relative_path = path_without_query.strip_prefix('/').unwrap_or(path_without_query).to_string();
330
331 while relative_path.starts_with('/') || relative_path.starts_with('\\') {
332 relative_path = relative_path[1..].to_string();
333 }
334
335 let relative_path = if relative_path.is_empty() {
336 ".".to_string()
337 } else {
338 relative_path
339 };
340
341 let file_path = self.base_directory.join(&relative_path);
342
343 if self.is_file_denied(&file_path) {
344 return self.create_forbidden_response();
345 }
346
347 match file_path.canonicalize() {
348 Ok(canonical_path) => {
349 let canonical_base = match self.base_directory.canonicalize() {
350 Ok(base) => base,
351 Err(_) => {
352 return Ok(GurtResponse::internal_server_error()
353 .with_header("Content-Type", "text/html"));
354 }
355 };
356
357 if !canonical_path.starts_with(&canonical_base) {
358 let response = GurtResponse::bad_request()
359 .with_header("Content-Type", "text/html");
360 return Ok(self.apply_global_headers(response));
361 }
362
363 if self.is_file_denied(&canonical_path) {
364 return self.create_forbidden_response();
365 }
366
367 if canonical_path.is_file() {
368 self.handle_file_response(&canonical_path).await
369 } else if canonical_path.is_dir() {
370 self.handle_directory_response(&canonical_path, request_path).await
371 } else {
372 self.handle_not_found_response().await
373 }
374 }
375 Err(_) => {
376 self.handle_not_found_response().await
377 }
378 }
379 }
380
381 async fn handle_file_response(&self, path: &Path) -> std::result::Result<GurtResponse, GurtError> {
382 match self.file_handler.handle_file(path) {
383 Ok(content) => {
384 let content_type = self.file_handler.get_content_type(path);
385 let response = GurtResponse::ok()
386 .with_header("Content-Type", &content_type)
387 .with_body(content);
388 Ok(self.apply_global_headers(response))
389 }
390 Err(_) => {
391 let response = GurtResponse::internal_server_error()
392 .with_header("Content-Type", "text/html");
393 Ok(self.apply_global_headers(response))
394 }
395 }
396 }
397
398 async fn handle_directory_response(&self, canonical_path: &Path, request_path: &str) -> std::result::Result<GurtResponse, GurtError> {
399 let index_path = canonical_path.join("index.html");
400 if index_path.is_file() {
401 self.handle_file_response(&index_path).await
402 } else {
403 match self.directory_handler.handle_directory(canonical_path, request_path) {
404 Ok(listing) => {
405 let response = GurtResponse::ok()
406 .with_header("Content-Type", "text/html")
407 .with_string_body(listing);
408 Ok(self.apply_global_headers(response))
409 }
410 Err(_) => {
411 let response = GurtResponse::internal_server_error()
412 .with_header("Content-Type", "text/html");
413 Ok(self.apply_global_headers(response))
414 }
415 }
416 }
417 }
418
419 async fn handle_not_found_response(&self) -> std::result::Result<GurtResponse, GurtError> {
420 let content = self.get_custom_error_page(404)
421 .unwrap_or_else(|| crate::handlers::get_404_html().to_string());
422
423 let response = GurtResponse::not_found()
424 .with_header("Content-Type", "text/html")
425 .with_string_body(content);
426 Ok(self.apply_global_headers(response))
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use gurtlib::GurtStatusCode;
434 use std::fs;
435 use std::env;
436
437 fn create_test_handler() -> RequestHandler {
438 let temp_dir = env::temp_dir().join("gurty_request_handler_test");
439 let _ = fs::create_dir_all(&temp_dir);
440
441 RequestHandler::builder(&temp_dir).build()
442 }
443
444 fn create_test_handler_with_config() -> RequestHandler {
445 let temp_dir = env::temp_dir().join("gurty_request_handler_test_config");
446 let _ = fs::create_dir_all(&temp_dir);
447
448 let config = Arc::new(GurtConfig::default());
449 RequestHandler::builder(&temp_dir)
450 .with_config(config)
451 .build()
452 }
453
454 #[test]
455 fn test_request_handler_builder() {
456 let temp_dir = env::temp_dir().join("gurty_builder_test");
457 let _ = fs::create_dir_all(&temp_dir);
458
459 let handler = RequestHandler::builder(&temp_dir).build();
460
461 assert_eq!(handler.base_directory, temp_dir);
462 assert!(handler.config.is_none());
463 assert!(handler.security.is_none());
464
465 let _ = fs::remove_dir_all(&temp_dir);
466 }
467
468 #[test]
469 fn test_request_handler_builder_with_config() {
470 let temp_dir = env::temp_dir().join("gurty_builder_config_test");
471 let _ = fs::create_dir_all(&temp_dir);
472
473 let config = Arc::new(GurtConfig::default());
474 let handler = RequestHandler::builder(&temp_dir)
475 .with_config(config.clone())
476 .build();
477
478 assert!(handler.config.is_some());
479 assert!(handler.security.is_some());
480
481 let _ = fs::remove_dir_all(&temp_dir);
482 }
483
484 #[test]
485 fn test_fallback_error_page_generation() {
486 let handler = create_test_handler();
487
488 let error_404 = handler.get_fallback_error_page(404);
489 assert!(error_404.contains("404 Not Found"));
490 assert!(error_404.contains("not found"));
491
492 let error_500 = handler.get_fallback_error_page(500);
493 assert!(error_500.contains("500 Internal Server Error"));
494 assert!(error_500.contains("processing your request"));
495
496 let error_429 = handler.get_fallback_error_page(429);
497 assert!(error_429.contains("429 Too Many Requests"));
498 assert!(error_429.contains("rate limit"));
499 }
500
501 #[test]
502 fn test_custom_error_page_with_config() {
503 let handler = create_test_handler_with_config();
504
505 let result = handler.get_custom_error_page(404);
506 assert!(result.is_none());
507 }
508
509 #[test]
510 fn test_apply_global_headers_without_config() {
511 let handler = create_test_handler();
512 let response = GurtResponse::ok();
513
514 let modified_response = handler.apply_global_headers(response);
515
516 assert_eq!(modified_response.status_code, 200);
517 }
518
519 #[test]
520 fn test_apply_global_headers_with_config() {
521 let temp_dir = env::temp_dir().join("gurty_headers_test");
522 let _ = fs::create_dir_all(&temp_dir);
523
524 let mut config = GurtConfig::default();
525 let mut headers = std::collections::HashMap::new();
526 headers.insert("X-Test-Header".to_string(), "test-value".to_string());
527 config.headers = Some(headers);
528
529 let handler = RequestHandler::builder(&temp_dir)
530 .with_config(Arc::new(config))
531 .build();
532
533 let response = GurtResponse::ok();
534 let modified_response = handler.apply_global_headers(response);
535
536 assert!(modified_response.headers.contains_key("x-test-header"));
537 assert_eq!(modified_response.headers.get("x-test-header").unwrap(), "test-value");
538
539 let _ = fs::remove_dir_all(&temp_dir);
540 }
541
542 #[test]
543 fn test_apply_custom_error_page() {
544 let handler = create_test_handler();
545 let mut response = GurtResponse::new(GurtStatusCode::NotFound);
546 response.body = b"Not Found".to_vec();
547
548 let modified_response = handler.apply_custom_error_page(response);
549
550 assert!(modified_response.status_code >= 400);
551 let body_str = String::from_utf8_lossy(&modified_response.body);
552 assert!(body_str.contains("html"));
553 }
554
555 #[test]
556 fn test_apply_custom_error_page_for_success() {
557 let handler = create_test_handler();
558 let mut response = GurtResponse::ok();
559 response.body = b"Success".to_vec();
560
561 let modified_response = handler.apply_custom_error_page(response);
562
563 assert_eq!(modified_response.status_code, 200);
564 assert_eq!(modified_response.body, b"Success".to_vec());
565 }
566}