diff --git a/src/handler/file.rs b/src/handler/file.rs index 81d6af8..8bcfbaf 100644 --- a/src/handler/file.rs +++ b/src/handler/file.rs @@ -3,22 +3,27 @@ use std::{ path::{Path, PathBuf}, }; -/// Attempts to safely join a root directory and a requested relative path. +use crate::{ + handler::handler::{Handler, HandlerFactory}, + hteapot::{HttpHeaders, HttpResponse, HttpStatus}, + utils::{Context, get_mime_tipe}, +}; + +/// Safely joins a root directory with a requested relative path. /// -/// Ensures that the resulting path: -/// - Resolves symbolic links and `..` segments via `canonicalize` -/// - Remains within the bounds of the specified root directory -/// - Actually exists on disk +/// Ensures that: +/// - Symbolic links and `..` segments are resolved (`canonicalize`) +/// - The resulting path stays within `root` +/// - The path exists on disk /// -/// This protects against directory traversal vulnerabilities, such as accessing -/// files outside of the intended root (e.g., `/etc/passwd`). +/// This prevents directory traversal attacks (e.g., accessing `/etc/passwd`). /// /// # Arguments -/// * `root` - The root directory from which serving is allowed. -/// * `requested_path` - The path requested by the client (usually from the URL). +/// * `root` - Allowed root directory. +/// * `requested_path` - Path requested by the client. /// /// # Returns -/// `Some(PathBuf)` if the resolved path exists and is within the root. `None` otherwise. +/// `Some(PathBuf)` if the path is valid and exists, `None` otherwise. /// /// # Example /// ``` @@ -34,7 +39,6 @@ pub fn safe_join_paths(root: &str, requested_path: &str) -> Option { } let canonical_path = requested_full_path.canonicalize().ok()?; - if canonical_path.starts_with(&root_path) { Some(canonical_path) } else { @@ -42,19 +46,86 @@ pub fn safe_join_paths(root: &str, requested_path: &str) -> Option { } } -/// Reads the content of a file from the filesystem. -/// -/// # Arguments -/// * `path` - A reference to a `PathBuf` representing the target file. -/// -/// # Returns -/// `Some(Vec)` if the file is read successfully, or `None` if an error occurs. -/// -/// # Notes -/// Uses `PathBuf` instead of `&str` to clearly express intent and reduce path handling bugs. -/// -/// # See Also -/// [`std::fs::read`](https://doc.rust-lang.org/std/fs/fn.read.html) -pub fn serve_file(path: &PathBuf) -> Option> { - fs::read(path).ok() +/// Handles serving static files from a root directory, including index files. +pub struct FileHandler { + root: String, + index: String, +} + +impl FileHandler {} + +impl Handler for FileHandler { + fn run(&self, ctx: &mut Context) -> Box { + let logger = ctx.log.with_component("HTTP"); + + // Resolve the requested path safely + let safe_path_result = if ctx.request.path == "/" { + // Special handling for the root path: serve the index file + Path::new(&self.root) + .canonicalize() + .ok() + .map(|root_path| root_path.join(&self.index)) + .filter(|index_path| index_path.exists()) + } else { + // Other paths: use safe join + safe_join_paths(&self.root, &ctx.request.path) + }; + + // Handle directories or invalid paths + let safe_path = match safe_path_result { + Some(path) => { + if path.is_dir() { + let index_path = path.join(&self.index); + if index_path.exists() { + index_path + } else { + logger.warn(format!( + "Index file not found in directory: {}", + ctx.request.path + )); + return HttpResponse::new(HttpStatus::NotFound, "Index not found", None); + } + } else { + path + } + } + None => { + logger.warn(format!( + "Path not found or access denied: {}", + ctx.request.path + )); + return HttpResponse::new(HttpStatus::NotFound, "Not found", None); + } + }; + + // Determine MIME type + let mimetype = get_mime_tipe(&safe_path.to_string_lossy().to_string()); + + // Read file content + match fs::read(&safe_path).ok() { + Some(content) => { + let mut headers = HttpHeaders::new(); + headers.insert("Content-Type", &mimetype); + headers.insert("X-Content-Type-Options", "nosniff"); + let response = HttpResponse::new(HttpStatus::OK, content, Some(headers)); + + // Cache the response if caching is enabled + if let Some(cache) = ctx.cache.as_deref_mut() { + cache.set(ctx.request.clone(), (*response).clone()); + } + + response + } + None => HttpResponse::new(HttpStatus::NotFound, "Not found", None), + } + } +} + +impl HandlerFactory for FileHandler { + fn is(ctx: &Context) -> Option> { + Some(Box::new(FileHandler { + root: ctx.config.root.to_string(), + index: ctx.config.index.to_string(), + })) + } } diff --git a/src/handler/handler.rs b/src/handler/handler.rs index 1ac2802..8458f6a 100644 --- a/src/handler/handler.rs +++ b/src/handler/handler.rs @@ -1,6 +1,9 @@ -use crate::{config::Config, hteapot::HttpRequest}; +use crate::{hteapot::HttpResponseCommon, utils::Context}; pub trait Handler { - fn is(config: Config, request: HttpRequest) -> bool; - fn run(request: HttpRequest) -> Box; + fn run(&self, context: &mut Context) -> Box; +} + +pub trait HandlerFactory { + fn is(context: &Context) -> Option>; } diff --git a/src/handler/mod.rs b/src/handler/mod.rs index 56e4b7a..a0576a5 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -1,3 +1,63 @@ +use crate::{ + handler::handler::{Handler, HandlerFactory}, + utils::Context, +}; + pub mod file; -pub mod handler; +mod handler; pub mod proxy; + +/// Type alias for a handler factory function. +/// +/// A factory takes a reference to the current `Config` and `HttpRequest` +/// and returns an `Option>`. It returns `Some(handler)` +/// if it can handle the request, or `None` if it cannot. +type Factory = fn(&Context) -> Option>; + +/// List of all available handler factories. +/// +/// New handlers can be added to this array to make them available +/// for request processing. +static HANDLERS: &[Factory] = &[proxy::ProxyHandler::is, file::FileHandler::is]; + +/// Returns the first handler that can process the given request. +/// +/// Iterates over all registered handler factories in `HANDLERS`. +/// Calls each factory with the provided `config` and `request`. +/// Returns `Some(Box)` if a suitable handler is found, +/// or `None` if no handler can handle the request. +/// +/// # Examples +/// +/// ```rust +/// let handler = get_handler(&config, &request); +/// if let Some(h) = handler { +/// let response = h.run(&request); +/// // process the response +/// } +/// ``` + +pub struct HandlerEngine { + handlers: Vec, +} + +impl HandlerEngine { + pub fn new() -> HandlerEngine { + let mut handlers = Vec::new(); + handlers.extend_from_slice(HANDLERS); + HandlerEngine { handlers } + } + + pub fn add_handler(&mut self, handler: Factory) { + self.handlers.insert(0, handler); + } + + pub fn get_handler(&self, ctx: &Context) -> Option> { + for h in self.handlers.iter() { + if let Some(handler) = h(ctx) { + return Some(handler); + } + } + None + } +} diff --git a/src/handler/proxy.rs b/src/handler/proxy.rs index 4902a86..d6a7457 100644 --- a/src/handler/proxy.rs +++ b/src/handler/proxy.rs @@ -1,41 +1,87 @@ -use crate::config::Config; -use crate::hteapot::HttpRequest; +use crate::handler::handler::{Handler, HandlerFactory}; +use crate::hteapot::{HttpMethod, HttpResponse, HttpResponseCommon, HttpStatus, TunnelResponse}; +use crate::utils::Context; -/// Determines whether a given HTTP request should be proxied based on the configuration. +/// Handles HTTP proxying based on server configuration. /// -/// If a matching proxy rule is found in `config.proxy_rules`, the function rewrites the -/// request path and updates the `Host` header accordingly. +/// Determines whether a request matches any proxy rules and forwards it +/// to the corresponding upstream server, rewriting the path and `Host` header +/// as needed. /// -/// # Arguments -/// * `config` - Server configuration containing proxy rules. -/// * `req` - The original HTTP request. -/// -/// # Returns -/// `Some((proxy_url, modified_request))` if the request should be proxied, otherwise `None`. -pub fn is_proxy(config: &Config, req: HttpRequest) -> Option<(String, HttpRequest)> { - for proxy_path in config.proxy_rules.keys() { - let path_match = req.path.strip_prefix(proxy_path); - if path_match.is_some() { - let new_path = path_match.unwrap(); - let url = config.proxy_rules.get(proxy_path).unwrap().clone(); - let url = if url.is_empty() { - let proxy_url = req.headers.get("host")?; - proxy_url.to_owned() - } else { - url - }; - let mut proxy_req = req.clone(); - proxy_req.path = new_path.to_string(); - proxy_req.headers.remove("Host"); - let host_parts: Vec<_> = url.split("://").collect(); - let host = if host_parts.len() == 1 { - host_parts.first().unwrap() - } else { - host_parts.last().clone().unwrap() - }; - proxy_req.headers.insert("host", host); - return Some((url, proxy_req)); +/// # Fields +/// * `new_path` - Path to use for the proxied request. +/// * `url` - Target upstream URL. +pub struct ProxyHandler { + new_path: String, + url: String, +} + +impl Handler for ProxyHandler { + fn run(&self, ctx: &mut Context) -> Box { + let _proxy_logger = &ctx.log.with_component("proxy"); + + // Return a tunnel response immediately for OPTIONS requests + if ctx.request.method == HttpMethod::OPTIONS { + return TunnelResponse::new(&ctx.request.path); + } + + // Prepare a modified request for proxying + let mut proxy_req = ctx.request.clone(); + proxy_req.path = self.new_path.clone(); + proxy_req.headers.remove("Host"); + + // Determine the upstream host from the URL + let host_parts: Vec<&str> = self.url.split("://").collect(); + let host = if host_parts.len() == 1 { + host_parts.first().unwrap() + } else { + host_parts.last().unwrap() + }; + proxy_req.headers.insert("host", host); + + // Forward the request and handle errors + let response = proxy_req.brew(&self.url).unwrap_or(HttpResponse::new( + HttpStatus::NotAcceptable, + "", + None, + )); + + // Cache the response if caching is enabled + if let Some(cache) = ctx.cache.as_deref_mut() { + cache.set(ctx.request.clone(), (*response).clone()); } + + response + } +} + +impl HandlerFactory for ProxyHandler { + fn is(ctx: &Context) -> Option> { + // OPTIONS requests are always handled + if ctx.request.method == HttpMethod::OPTIONS { + return Some(Box::new(ProxyHandler { + url: String::new(), + new_path: String::new(), + })); + } + + // Check if the request matches any configured proxy rules + for proxy_path in ctx.config.proxy_rules.keys() { + if let Some(path_match) = ctx.request.path.strip_prefix(proxy_path) { + let new_path = path_match.to_string(); + let url = ctx.config.proxy_rules.get(proxy_path).unwrap(); + let url = if url.is_empty() { + // If the rule URL is empty, fallback to Host header + let proxy_url = ctx.request.headers.get("host")?; + proxy_url.to_owned() + } else { + url.to_string() + }; + + return Some(Box::new(ProxyHandler { url, new_path })); + } + } + + None } - None } diff --git a/src/hteapot/brew.rs b/src/hteapot/brew.rs index 286f7b2..04202ba 100644 --- a/src/hteapot/brew.rs +++ b/src/hteapot/brew.rs @@ -168,8 +168,8 @@ mod tests { let mut request = HttpRequest::new(HttpMethod::GET, "/data"); request.headers.insert("Content-Type", "application/json"); assert_eq!( - request.headers.get("Content-Type"), - Some(&"application/json".to_string()) + request.headers.get_owned("Content-Type"), + Some("application/json".to_string()) ); } diff --git a/src/hteapot/http/headers.rs b/src/hteapot/http/headers.rs index 3b6c616..b55d50f 100644 --- a/src/hteapot/http/headers.rs +++ b/src/hteapot/http/headers.rs @@ -36,11 +36,11 @@ impl Deref for CaseInsensitiveString { } #[derive(Debug, Default, Clone)] -pub struct Headers(HashMap); +pub struct HttpHeaders(HashMap); -impl Headers { +impl HttpHeaders { pub fn new() -> Self { - Headers(HashMap::new()) + HttpHeaders(HashMap::new()) } pub fn insert(&mut self, key: &str, value: &str) { @@ -53,6 +53,10 @@ impl Headers { self.0.get(&CaseInsensitiveString(key.to_string())) } + pub fn get_owned(&self, key: &str) -> Option { + self.0.get(&CaseInsensitiveString(key.to_string())).cloned() + } + pub fn len(&self) -> usize { self.0.len() } @@ -68,9 +72,13 @@ impl Headers { pub fn remove(&mut self, key: &str) -> Option { self.0.remove(&CaseInsensitiveString(key.to_string())) } + + pub fn iter(&self) -> std::collections::hash_map::Iter<'_, CaseInsensitiveString, String> { + self.0.iter() + } } -impl IntoIterator for Headers { +impl IntoIterator for HttpHeaders { type Item = (CaseInsensitiveString, String); type IntoIter = hash_map::IntoIter; @@ -79,7 +87,7 @@ impl IntoIterator for Headers { } } -impl<'a> IntoIterator for &'a Headers { +impl<'a> IntoIterator for &'a HttpHeaders { type Item = (&'a CaseInsensitiveString, &'a String); type IntoIter = hash_map::Iter<'a, CaseInsensitiveString, String>; @@ -88,7 +96,7 @@ impl<'a> IntoIterator for &'a Headers { } } -impl<'a> IntoIterator for &'a mut Headers { +impl<'a> IntoIterator for &'a mut HttpHeaders { type Item = (&'a CaseInsensitiveString, &'a mut String); type IntoIter = hash_map::IterMut<'a, CaseInsensitiveString, String>; @@ -97,7 +105,7 @@ impl<'a> IntoIterator for &'a mut Headers { } } -impl PartialEq for Headers { +impl PartialEq for HttpHeaders { fn eq(&self, other: &Self) -> bool { other.0 == self.0 } @@ -106,7 +114,7 @@ impl PartialEq for Headers { #[macro_export] macro_rules! headers { ( $($k:expr => $v:expr),* $(,)? ) => {{ - let mut headers = crate::hteapot::HttpHeaders::new(); + let mut headers = hteapot::HttpHeaders::new(); $( headers.insert($k, $v); )* Some(headers) }}; @@ -115,7 +123,7 @@ macro_rules! headers { #[cfg(test)] #[test] fn test_caseinsensitive() { - let mut headers = Headers::new(); + let mut headers = HttpHeaders::new(); headers.insert("X-Test-Header", "Value"); assert!(headers.get("x-test-header").is_some()); assert!(headers.get("x-test-header").unwrap() == "Value"); @@ -125,7 +133,7 @@ fn test_caseinsensitive() { #[cfg(test)] #[test] fn test_remove() { - let mut headers = Headers::new(); + let mut headers = HttpHeaders::new(); headers.insert("X-Test-Header", "Value"); assert!(headers.get("x-test-header").is_some()); assert!(headers.get("x-test-header").unwrap() == "Value"); diff --git a/src/hteapot/http/methods.rs b/src/hteapot/http/methods.rs index 78592f6..ab374fb 100644 --- a/src/hteapot/http/methods.rs +++ b/src/hteapot/http/methods.rs @@ -32,7 +32,8 @@ impl HttpMethod { /// assert_eq!(custom, HttpMethod::Other("CUSTOM".into())); /// ``` pub fn from_str(method: &str) -> HttpMethod { - match method { + let method = method.to_uppercase(); + match method.as_str() { "GET" => HttpMethod::GET, "POST" => HttpMethod::POST, "PUT" => HttpMethod::PUT, diff --git a/src/hteapot/http/mod.rs b/src/hteapot/http/mod.rs index eebf030..32345b2 100644 --- a/src/hteapot/http/mod.rs +++ b/src/hteapot/http/mod.rs @@ -2,6 +2,6 @@ mod headers; mod methods; mod status; -pub use headers::Headers; +pub use headers::HttpHeaders; pub use methods::HttpMethod; pub use status::HttpStatus; diff --git a/src/hteapot/mod.rs b/src/hteapot/mod.rs index 055eb5f..5c841a1 100644 --- a/src/hteapot/mod.rs +++ b/src/hteapot/mod.rs @@ -31,11 +31,11 @@ use std::time::Duration; // Public API exposed by this module pub use self::request::HttpRequest; pub use engine::Hteapot; -pub use http::Headers as HttpHeaders; +pub use http::HttpHeaders; pub use http::HttpMethod; - pub use http::HttpStatus; -pub use response::{HttpResponse, StreamedResponse, TunnelResponse}; + +pub use response::{HttpResponse, HttpResponseCommon, StreamedResponse, TunnelResponse}; /// Crate version as set by `Cargo.toml`. pub const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -48,7 +48,7 @@ const KEEP_ALIVE_TTL: Duration = Duration::from_secs(10); #[cfg(test)] mod tests { use crate::{HttpResponse, HttpStatus}; - use http::Headers as HttpHeaders; + use http::HttpHeaders; const VERSION: &str = env!("CARGO_PKG_VERSION"); use super::*; diff --git a/src/hteapot/request.rs b/src/hteapot/request.rs index 176122e..3a3a470 100644 --- a/src/hteapot/request.rs +++ b/src/hteapot/request.rs @@ -7,8 +7,8 @@ // - No URI normalization or encoding // +use super::HttpHeaders; use super::HttpMethod; -use super::http::Headers; use std::hash::Hash; use std::{cmp::min, collections::HashMap, net::TcpStream, str}; @@ -23,7 +23,7 @@ pub struct HttpRequest { pub method: HttpMethod, pub path: String, pub args: HashMap, - pub headers: Headers, + pub headers: HttpHeaders, pub body: Vec, stream: Option, } @@ -58,7 +58,7 @@ impl HttpRequest { method, path: path.to_string(), args: HashMap::new(), - headers: Headers::new(), + headers: HttpHeaders::new(), body: Vec::new(), stream: None, }; @@ -70,7 +70,7 @@ impl HttpRequest { method: HttpMethod::Other(String::new()), path: String::new(), args: HashMap::new(), - headers: Headers::new(), + headers: HttpHeaders::new(), body: Vec::new(), stream: None, } @@ -126,7 +126,7 @@ impl HttpRequestBuilder { method: HttpMethod::GET, path: String::new(), args: HashMap::new(), - headers: Headers::new(), + headers: HttpHeaders::new(), body: Vec::new(), stream: None, }, diff --git a/src/hteapot/response.rs b/src/hteapot/response.rs index bac0b2d..1e695a6 100644 --- a/src/hteapot/response.rs +++ b/src/hteapot/response.rs @@ -7,7 +7,7 @@ //! //! All response types implement the [`HttpResponseCommon`] trait. -use super::http::Headers; +use super::HttpHeaders; use super::HttpStatus; use super::{BUFFER_SIZE, VERSION}; @@ -25,7 +25,7 @@ use std::{io, thread}; #[derive(Clone)] pub struct BaseResponse { pub status: HttpStatus, - pub headers: Headers, + pub headers: HttpHeaders, } impl BaseResponse { @@ -89,9 +89,9 @@ impl HttpResponse { pub fn new>( status: HttpStatus, content: B, - headers: Option, + headers: Option, ) -> Box { - let mut headers = headers.unwrap_or(Headers::new()); + let mut headers = headers.unwrap_or(HttpHeaders::new()); let content = content.as_ref(); headers.insert("Content-Length", &content.len().to_string()); @@ -111,7 +111,7 @@ impl HttpResponse { HttpResponse { base: BaseResponse { status: HttpStatus::IAmATeapot, - headers: Headers::new(), + headers: HttpHeaders::new(), }, content: vec![], raw: Some(raw), @@ -237,7 +237,7 @@ impl StreamedResponse { let mut base = BaseResponse { status: HttpStatus::OK, - headers: Headers::new(), + headers: HttpHeaders::new(), }; base.headers.insert("Transfer-Encoding", "chunked"); @@ -312,7 +312,7 @@ impl TunnelResponse { return Box::new(TunnelResponse { base: BaseResponse { status: HttpStatus::OK, - headers: Headers::new(), + headers: HttpHeaders::new(), // headers: headers! {"connection" => "keep-alive"}.unwrap(), }, addr: addr.to_string(), diff --git a/src/main.rs b/src/main.rs index f5978a0..c101fc3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,20 +44,17 @@ mod utils; use std::fs; use std::io; -use std::path::Path; use std::sync::Mutex; use cache::Cache; -use hteapot::HttpMethod; -use hteapot::TunnelResponse; + use hteapot::{Hteapot, HttpRequest, HttpResponse, HttpStatus}; -use utils::get_mime_tipe; use logger::{LogLevel, Logger}; use std::time::Instant; -use handler::file::{safe_join_paths, serve_file}; -use handler::proxy::is_proxy; +use crate::handler::HandlerEngine; +use crate::utils::Context; /// Main entry point of the Hteapot server. /// @@ -146,7 +143,7 @@ fn main() { // Set up the cache with thread-safe locking // The Mutex ensures that only one thread can access the cache at a time, // preventing race conditions when reading and writing to the cache. - let cache: Mutex>> = + let cache: Mutex> = Mutex::new(Cache::new(config.cache_ttl as u64)); // Initialize the cache with TTL // Create a new threaded HTTP server with the provided host, port, and number of threads @@ -173,62 +170,19 @@ fn main() { // Create separate loggers for each component (proxy, cache, and HTTP) // This allows for more granular control over logging and better separation of concerns - let proxy_logger = logger.with_component("proxy"); + let cache_logger = logger.with_component("cache"); let http_logger = logger.with_component("http"); + let handlers = HandlerEngine::new(); // Start listening for HTTP requests server.listen(move |req: HttpRequest| { // SERVER CORE: For each incoming request, we handle it in this closure let start_time = Instant::now(); // Track request processing time let req_method = req.method.to_str(); // Get the HTTP method (e.g., GET, POST) - let req_path = req.path.clone(); // Get the requested path // Log the incoming request method and path http_logger.info(format!("Request {} {}", req_method, req.path)); - if proxy_only && req.method == HttpMethod::CONNECT { - return TunnelResponse::new(&req.path); - } - // Check if the request should be proxied (either because proxy-only mode is on, or it matches a rule) - let is_proxy = is_proxy(&config, req.clone() as HttpRequest); - if proxy_only || is_proxy.is_some() { - // ⚠️ TODO: refactor proxy handling - // If proxying is enabled or this request matches a proxy rule, handle it - if req.method == hteapot::HttpMethod::CONNECT { - return TunnelResponse::new(&req.path); - } - if is_proxy.is_none() { - proxy_logger.error("Error in proxy".to_string()); - return HttpResponse::new(HttpStatus::NotAcceptable, "", None); - } - let (host, proxy_req) = is_proxy.unwrap(); - // Get the target host and modified request - proxy_logger.info(format!( - "Proxying request {} {} to {}", - req_method, req_path, host - )); - - // Perform the proxy request (forward the request to the target server) - let res = proxy_req.brew(host.as_str()); - let elapsed = start_time.elapsed(); // Measure the time taken to process the proxy request - if res.is_ok() { - // If the proxy request is successful, log the time taken and return the response - let response = res.unwrap(); - proxy_logger.debug(format!( - "Proxy request processed in {:.6}ms", - elapsed.as_secs_f64() * 1000.0 // Log the time taken in milliseconds - )); - return response; - } else { - // If the proxy request fails, log the error and return a 500 Internal Server Error - proxy_logger.error(format!("Proxy request failed: {:?}", res.err())); - return HttpResponse::new( - HttpStatus::InternalServerError, - "Internal Server Error", - None, - ); - } - } if config.cache { let cache_start = Instant::now(); // Track cache operation time @@ -240,7 +194,7 @@ fn main() { "Request processed in {:.6}ms", elapsed.as_secs_f64() * 1000.0 // Log the time taken in milliseconds )); - return response; + return Box::new(response); } else { cache_logger.debug(format!("cache miss for {}", &req.path)); } @@ -251,56 +205,22 @@ fn main() { )); } - // If the request is not a proxy request, resolve the requested path safely - let safe_path_result = if req.path == "/" { - // Special handling for the root "/" path - let root_path = Path::new(&config.root).canonicalize(); - if root_path.is_ok() { - // If the root path exists and is valid, try to join the index file - let index_path = root_path.unwrap().join(&config.index); - if index_path.exists() { - Some(index_path) // If index exists, return its path - } else { - None // If no index exists, return None - } + let mut ctx = Context { + request: &req, + log: &logger, + config: &config, + cache: if config.cache { + Some(&mut cache.lock().unwrap()) } else { - None // If the root path is invalid, return None - } - } else { - // For any other path, resolve it safely using the `safe_join_paths` function - safe_join_paths(&config.root, &req.path) + None + }, }; - // Handle the case where the resolved path is a directory - let safe_path = match safe_path_result { - Some(path) => { - if path.is_dir() { - // If it's a directory, check for the index file in that directory - let index_path = path.join(&config.index); - if index_path.exists() { - index_path // If index exists, return its path - } else { - // If no index file exists, log a warning and return a 404 response - http_logger - .warn(format!("Index file not found in directory: {}", req.path)); - return HttpResponse::new(HttpStatus::NotFound, "Index not found", None); - } - } else { - path // If it's not a directory, just return the path - } - } - None => { - // If the path is invalid or access is denied, return a 404 response - http_logger.warn(format!("Path not found or access denied: {}", req.path)); - return HttpResponse::new(HttpStatus::NotFound, "Not found", None); - } - }; - - // Determine the MIME type for the file based on its extension - let mimetype = get_mime_tipe(&safe_path.to_string_lossy().to_string()); - - // Try to serve the file from the cache, or read it from disk if not cached - let content: Option> = serve_file(&safe_path); + let response = handlers.get_handler(&ctx); + if response.is_none() { + return HttpResponse::new(HttpStatus::InternalServerError, "content", None); + } + let response = response.unwrap().run(&mut ctx); // Log how long the request took to process let elapsed = start_time.elapsed(); @@ -308,26 +228,7 @@ fn main() { "Request processed in {:.6}ms", elapsed.as_secs_f64() * 1000.0 // Log the time taken in milliseconds )); - + response // If content was found, return it with the appropriate headers, otherwise return a 404 - match content { - Some(c) => { - // If content is found, create response with proper headers and a 200 OK status - let headers = headers!( - "Content-Type" => &mimetype, - "X-Content-Type-Options" => "nosniff" - ); - let response = HttpResponse::new(HttpStatus::OK, c, headers); - if config.cache { - let mut cache_lock = cache.lock().expect("Error locking cache"); - cache_lock.set(req.clone(), response.clone()) - } - response - } - None => { - // If no content is found, return a 404 Not Found response - HttpResponse::new(HttpStatus::NotFound, "Not found", None) - } - } }); } diff --git a/src/utils.rs b/src/utils.rs index 8e56ca4..98b8a8f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,12 @@ use std::path::Path; +use crate::{ + cache::Cache, + config::Config, + hteapot::{HttpRequest, HttpResponse}, + logger::Logger, +}; + /// Returns the MIME type based on the file extension of a given path. /// /// This function maps common file extensions to their appropriate @@ -27,7 +34,7 @@ pub fn get_mime_tipe(path: &String) -> String { // Suggest using `to_str()` directly on the extension // Alternative way to get the extension // .and_then(|ext| ext.to_str()) - + let mimetipe = match extension { // Text "html" | "htm" => "text/html; charset=utf-8", @@ -39,7 +46,7 @@ pub fn get_mime_tipe(path: &String) -> String { "txt" => "text/plain", "md" => "text/markdown", "csv" => "text/csv", - + // Images "ico" => "image/x-icon", "png" => "image/png", @@ -49,19 +56,19 @@ pub fn get_mime_tipe(path: &String) -> String { "webp" => "image/webp", "bmp" => "image/bmp", "tiff" | "tif" => "image/tiff", - + // Audio "mp3" => "audio/mpeg", "wav" => "audio/wav", "ogg" => "audio/ogg", "flac" => "audio/flac", - + // Video "mp4" => "video/mp4", "webm" => "video/webm", "avi" => "video/x-msvideo", "mkv" => "video/x-matroska", - + // Documents "pdf" => "application/pdf", "doc" => "application/msword", @@ -70,20 +77,20 @@ pub fn get_mime_tipe(path: &String) -> String { "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "ppt" => "application/vnd.ms-powerpoint", "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation", - + // Archives "zip" => "application/zip", "tar" => "application/x-tar", "gz" => "application/gzip", "7z" => "application/x-7z-compressed", "rar" => "application/vnd.rar", - + // Fonts "ttf" => "font/ttf", "otf" => "font/otf", "woff" => "font/woff", "woff2" => "font/woff2", - + // For unknown types, use a safe default _ => "application/octet-stream", }; @@ -92,4 +99,11 @@ pub fn get_mime_tipe(path: &String) -> String { } //TODO: make a parser args to config -//pub fn args_to_dict(list: Vec) -> HashMap {} \ No newline at end of file +//pub fn args_to_dict(list: Vec) -> HashMap {} + +pub struct Context<'a> { + pub request: &'a HttpRequest, + pub log: &'a Logger, + pub config: &'a Config, + pub cache: Option<&'a mut Cache>, +}