Skip to main content

gurty_cli/
request_handler.rs

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                    // fall
303                }
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}