From 9942357d263532cc03c7ad8629a44cbb41110f00 Mon Sep 17 00:00:00 2001 From: Alberto Ruiz <17555470+Az107@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:15:35 +0200 Subject: [PATCH 1/6] basic traits and POCs --- src/handler/file.rs | 19 +++++++++++++++++++ src/handler/handler.rs | 4 ++-- src/handler/mod.rs | 2 +- src/handler/proxy.rs | 19 +++++++++++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/handler/file.rs b/src/handler/file.rs index 81d6af8..3fae1d3 100644 --- a/src/handler/file.rs +++ b/src/handler/file.rs @@ -3,6 +3,8 @@ use std::{ path::{Path, PathBuf}, }; +use crate::handler::handler::Handler; + /// Attempts to safely join a root directory and a requested relative path. /// /// Ensures that the resulting path: @@ -58,3 +60,20 @@ pub fn safe_join_paths(root: &str, requested_path: &str) -> Option { pub fn serve_file(path: &PathBuf) -> Option> { fs::read(path).ok() } + +struct FileHandler {} + +impl FileHandler {} + +impl Handler for FileHandler { + fn is( + config: &crate::config::Config, + request: &crate::hteapot::HttpRequest, + ) -> Option> { + Some(Box::new(FileHandler {})) + } + + fn run(&self, request: crate::hteapot::HttpRequest) -> Box { + todo!() + } +} diff --git a/src/handler/handler.rs b/src/handler/handler.rs index 1ac2802..6a85ca0 100644 --- a/src/handler/handler.rs +++ b/src/handler/handler.rs @@ -1,6 +1,6 @@ use crate::{config::Config, hteapot::HttpRequest}; pub trait Handler { - fn is(config: Config, request: HttpRequest) -> bool; - fn run(request: HttpRequest) -> Box; + fn is(config: &Config, request: &HttpRequest) -> Option>; + fn run(&self, request: HttpRequest) -> Box; } diff --git a/src/handler/mod.rs b/src/handler/mod.rs index 56e4b7a..b18fc4f 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -1,3 +1,3 @@ pub mod file; -pub mod handler; +mod handler; pub mod proxy; diff --git a/src/handler/proxy.rs b/src/handler/proxy.rs index 4902a86..39a80e0 100644 --- a/src/handler/proxy.rs +++ b/src/handler/proxy.rs @@ -1,4 +1,5 @@ use crate::config::Config; +use crate::handler::handler::Handler; use crate::hteapot::HttpRequest; /// Determines whether a given HTTP request should be proxied based on the configuration. @@ -39,3 +40,21 @@ pub fn is_proxy(config: &Config, req: HttpRequest) -> Option<(String, HttpReques } None } + +pub struct ProxyHandler {} + +impl Handler for ProxyHandler { + fn is(config: &Config, request: &HttpRequest) -> Option> { + for proxy_path in config.proxy_rules.keys() { + let path_match = request.path.strip_prefix(proxy_path); + if path_match.is_some() { + return Some(Box::new(ProxyHandler {})); + } + } + return None; + } + + fn run(&self, request: HttpRequest) -> Box { + todo!() + } +} From eddd6d45b6858153e59ceee85c54c78472e57d68 Mon Sep 17 00:00:00 2001 From: Alberto Ruiz <17555470+Az107@users.noreply.github.com> Date: Sun, 5 Oct 2025 20:28:26 +0200 Subject: [PATCH 2/6] Do Handling system and add file handler --- src/handler/file.rs | 97 ++++++++++++++++++++++++++++++++++++++---- src/handler/handler.rs | 9 ++-- src/handler/mod.rs | 44 +++++++++++++++++++ src/handler/proxy.rs | 19 ++++++--- src/hteapot/mod.rs | 2 +- src/main.rs | 80 ++++------------------------------ 6 files changed, 160 insertions(+), 91 deletions(-) diff --git a/src/handler/file.rs b/src/handler/file.rs index 3fae1d3..aca3632 100644 --- a/src/handler/file.rs +++ b/src/handler/file.rs @@ -3,7 +3,12 @@ use std::{ path::{Path, PathBuf}, }; -use crate::handler::handler::Handler; +use crate::{ + handler::handler::{Handler, HandlerFactory}, + headers, + hteapot::{HttpResponse, HttpStatus}, + utils::get_mime_tipe, +}; /// Attempts to safely join a root directory and a requested relative path. /// @@ -61,19 +66,95 @@ pub fn serve_file(path: &PathBuf) -> Option> { fs::read(path).ok() } -struct FileHandler {} +pub struct FileHandler { + root: String, + index: String, +} impl FileHandler {} impl Handler for FileHandler { - fn is( - config: &crate::config::Config, + fn run( + &self, request: &crate::hteapot::HttpRequest, - ) -> Option> { - Some(Box::new(FileHandler {})) + ) -> Box { + // If the request is not a proxy request, resolve the requested path safely + let safe_path_result = if request.path == "/" { + // Special handling for the root "/" path + let root_path = Path::new(&self.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(&self.index); + if index_path.exists() { + Some(index_path) // If index exists, return its path + } else { + None // If no index exists, return None + } + } 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(&self.root, &request.path) + }; + + // 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(&self.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: {}", + // request.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: {}", request.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 = fs::read(&safe_path).ok(); + 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" + ); + HttpResponse::new(HttpStatus::OK, c, headers) + } + None => { + // If no content is found, return a 404 Not Found response + HttpResponse::new(HttpStatus::NotFound, "Not found", None) + } + } } +} - fn run(&self, request: crate::hteapot::HttpRequest) -> Box { - todo!() +impl HandlerFactory for FileHandler { + fn is( + config: &crate::config::Config, + _request: &crate::hteapot::HttpRequest, + ) -> Option> { + Some(Box::new(FileHandler { + root: config.root.to_string(), + index: config.index.to_string(), + })) } } diff --git a/src/handler/handler.rs b/src/handler/handler.rs index 6a85ca0..77664c9 100644 --- a/src/handler/handler.rs +++ b/src/handler/handler.rs @@ -1,6 +1,9 @@ -use crate::{config::Config, hteapot::HttpRequest}; +use crate::{config::Config, hteapot::HttpRequest, hteapot::HttpResponseCommon}; pub trait Handler { - fn is(config: &Config, request: &HttpRequest) -> Option>; - fn run(&self, request: HttpRequest) -> Box; + fn run(&self, request: &HttpRequest) -> Box; +} + +pub trait HandlerFactory { + fn is(config: &Config, request: &HttpRequest) -> Option>; } diff --git a/src/handler/mod.rs b/src/handler/mod.rs index b18fc4f..e057146 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -1,3 +1,47 @@ +use crate::{ + config::Config, + handler::handler::{Handler, HandlerFactory}, + hteapot::HttpRequest, +}; + pub mod file; 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(&Config, &HttpRequest) -> 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] = &[file::FileHandler::is, proxy::ProxyHandler::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 fn get_handler(config: &Config, request: &HttpRequest) -> Option> { + for h in HANDLERS { + if let Some(handler) = h(config, request) { + return Some(handler); + } + } + None +} diff --git a/src/handler/proxy.rs b/src/handler/proxy.rs index 39a80e0..97d82fb 100644 --- a/src/handler/proxy.rs +++ b/src/handler/proxy.rs @@ -1,6 +1,6 @@ use crate::config::Config; -use crate::handler::handler::Handler; -use crate::hteapot::HttpRequest; +use crate::handler::handler::{Handler, HandlerFactory}; +use crate::hteapot::{HttpMethod, HttpRequest, HttpResponseCommon, TunnelResponse}; /// Determines whether a given HTTP request should be proxied based on the configuration. /// @@ -44,7 +44,16 @@ pub fn is_proxy(config: &Config, req: HttpRequest) -> Option<(String, HttpReques pub struct ProxyHandler {} impl Handler for ProxyHandler { - fn is(config: &Config, request: &HttpRequest) -> Option> { + fn run(&self, request: &HttpRequest) -> Box { + if request.method == HttpMethod::OPTIONS { + return TunnelResponse::new(&request.path); + } + request.brew("addr").unwrap() + } +} + +impl HandlerFactory for ProxyHandler { + fn is(config: &Config, request: &HttpRequest) -> Option> { for proxy_path in config.proxy_rules.keys() { let path_match = request.path.strip_prefix(proxy_path); if path_match.is_some() { @@ -53,8 +62,4 @@ impl Handler for ProxyHandler { } return None; } - - fn run(&self, request: HttpRequest) -> Box { - todo!() - } } diff --git a/src/hteapot/mod.rs b/src/hteapot/mod.rs index 055eb5f..d005e2b 100644 --- a/src/hteapot/mod.rs +++ b/src/hteapot/mod.rs @@ -35,7 +35,7 @@ pub use http::Headers as 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"); diff --git a/src/main.rs b/src/main.rs index f5978a0..441e923 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,6 +59,8 @@ use std::time::Instant; use handler::file::{safe_join_paths, serve_file}; use handler::proxy::is_proxy; +use crate::handler::get_handler; + /// Main entry point of the Hteapot server. /// /// Handles command-line interface, config file parsing, optional file-serving mode, @@ -250,84 +252,18 @@ fn main() { cache_elapsed.as_micros() )); } - - // 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 - } - } 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) - }; - - // 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 = get_handler(&config, &req); + if response.is_none() { + return HttpResponse::new(HttpStatus::InternalServerError, "content", None); + } + let response = response.unwrap().run(&req); // Log how long the request took to process let elapsed = start_time.elapsed(); http_logger.debug(format!( "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) - } - } }); } From 1ff88e27ccca0014d3da0a0932a865de79dbe196 Mon Sep 17 00:00:00 2001 From: Alberto Ruiz <17555470+Az107@users.noreply.github.com> Date: Mon, 6 Oct 2025 22:18:28 +0200 Subject: [PATCH 3/6] Improve context sharing with habdlers. Finish ProxyHandler --- src/handler/file.rs | 51 ++++++++++---------------------- src/handler/handler.rs | 6 ++-- src/handler/mod.rs | 11 ++++--- src/handler/proxy.rs | 57 ++++++++++++++++++++++++++++------- src/main.rs | 67 ++++++++++-------------------------------- src/utils.rs | 31 +++++++++++++------ 6 files changed, 108 insertions(+), 115 deletions(-) diff --git a/src/handler/file.rs b/src/handler/file.rs index aca3632..2ac7e0b 100644 --- a/src/handler/file.rs +++ b/src/handler/file.rs @@ -7,7 +7,7 @@ use crate::{ handler::handler::{Handler, HandlerFactory}, headers, hteapot::{HttpResponse, HttpStatus}, - utils::get_mime_tipe, + utils::{Context, get_mime_tipe}, }; /// Attempts to safely join a root directory and a requested relative path. @@ -49,23 +49,6 @@ 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() -} - pub struct FileHandler { root: String, index: String, @@ -74,12 +57,10 @@ pub struct FileHandler { impl FileHandler {} impl Handler for FileHandler { - fn run( - &self, - request: &crate::hteapot::HttpRequest, - ) -> Box { + fn run(&self, ctx: &Context) -> Box { + let logger = ctx.log.with_component("HTTP"); // If the request is not a proxy request, resolve the requested path safely - let safe_path_result = if request.path == "/" { + let safe_path_result = if ctx.request.path == "/" { // Special handling for the root "/" path let root_path = Path::new(&self.root).canonicalize(); if root_path.is_ok() { @@ -95,7 +76,7 @@ impl Handler for FileHandler { } } else { // For any other path, resolve it safely using the `safe_join_paths` function - safe_join_paths(&self.root, &request.path) + safe_join_paths(&self.root, &ctx.request.path) }; // Handle the case where the resolved path is a directory @@ -108,10 +89,10 @@ impl Handler for FileHandler { 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: {}", - // request.path - // )); + logger.warn(format!( + "Index file not found in directory: {}", + ctx.request.path + )); return HttpResponse::new(HttpStatus::NotFound, "Index not found", None); } } else { @@ -120,7 +101,10 @@ impl Handler for FileHandler { } None => { // If the path is invalid or access is denied, return a 404 response - // http_logger.warn(format!("Path not found or access denied: {}", request.path)); + logger.warn(format!( + "Path not found or access denied: {}", + ctx.request.path + )); return HttpResponse::new(HttpStatus::NotFound, "Not found", None); } }; @@ -148,13 +132,10 @@ impl Handler for FileHandler { } impl HandlerFactory for FileHandler { - fn is( - config: &crate::config::Config, - _request: &crate::hteapot::HttpRequest, - ) -> Option> { + fn is(ctx: &Context) -> Option> { Some(Box::new(FileHandler { - root: config.root.to_string(), - index: config.index.to_string(), + 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 77664c9..255b6e7 100644 --- a/src/handler/handler.rs +++ b/src/handler/handler.rs @@ -1,9 +1,9 @@ -use crate::{config::Config, hteapot::HttpRequest, hteapot::HttpResponseCommon}; +use crate::{hteapot::HttpResponseCommon, utils::Context}; pub trait Handler { - fn run(&self, request: &HttpRequest) -> Box; + fn run(&self, context: &Context) -> Box; } pub trait HandlerFactory { - fn is(config: &Config, request: &HttpRequest) -> Option>; + fn is(context: &Context) -> Option>; } diff --git a/src/handler/mod.rs b/src/handler/mod.rs index e057146..9272b3f 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -1,7 +1,6 @@ use crate::{ - config::Config, handler::handler::{Handler, HandlerFactory}, - hteapot::HttpRequest, + utils::Context, }; pub mod file; @@ -13,13 +12,13 @@ pub mod proxy; /// 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(&Config, &HttpRequest) -> Option>; +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] = &[file::FileHandler::is, proxy::ProxyHandler::is]; +static HANDLERS: &[Factory] = &[proxy::ProxyHandler::is, file::FileHandler::is]; /// Returns the first handler that can process the given request. /// @@ -37,9 +36,9 @@ static HANDLERS: &[Factory] = &[file::FileHandler::is, proxy::ProxyHandler::is]; /// // process the response /// } /// ``` -pub fn get_handler(config: &Config, request: &HttpRequest) -> Option> { +pub fn get_handler(ctx: &Context) -> Option> { for h in HANDLERS { - if let Some(handler) = h(config, request) { + if let Some(handler) = h(ctx) { return Some(handler); } } diff --git a/src/handler/proxy.rs b/src/handler/proxy.rs index 97d82fb..16a4011 100644 --- a/src/handler/proxy.rs +++ b/src/handler/proxy.rs @@ -1,6 +1,11 @@ +use std::path; + use crate::config::Config; use crate::handler::handler::{Handler, HandlerFactory}; -use crate::hteapot::{HttpMethod, HttpRequest, HttpResponseCommon, TunnelResponse}; +use crate::hteapot::{ + HttpMethod, HttpRequest, HttpResponse, HttpResponseCommon, HttpStatus, TunnelResponse, +}; +use crate::utils::Context; /// Determines whether a given HTTP request should be proxied based on the configuration. /// @@ -28,7 +33,7 @@ pub fn is_proxy(config: &Config, req: HttpRequest) -> Option<(String, HttpReques 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_parts: Vec<&str> = url.split("://").collect(); let host = if host_parts.len() == 1 { host_parts.first().unwrap() } else { @@ -41,23 +46,53 @@ pub fn is_proxy(config: &Config, req: HttpRequest) -> Option<(String, HttpReques None } -pub struct ProxyHandler {} +pub struct ProxyHandler { + new_path: String, + url: String, +} impl Handler for ProxyHandler { - fn run(&self, request: &HttpRequest) -> Box { - if request.method == HttpMethod::OPTIONS { - return TunnelResponse::new(&request.path); + fn run(&self, ctx: &Context) -> Box { + if ctx.request.method == HttpMethod::OPTIONS { + return TunnelResponse::new(&ctx.request.path); } - request.brew("addr").unwrap() + let mut proxy_req = ctx.request.clone(); + proxy_req.path = self.new_path.clone(); + proxy_req.headers.remove("Host"); + let host_parts: Vec<&str> = self.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); + proxy_req + .brew(&self.url) + .unwrap_or(HttpResponse::new(HttpStatus::NotAcceptable, "", None)) } } impl HandlerFactory for ProxyHandler { - fn is(config: &Config, request: &HttpRequest) -> Option> { - for proxy_path in config.proxy_rules.keys() { - let path_match = request.path.strip_prefix(proxy_path); + fn is(ctx: &Context) -> Option> { + if ctx.request.method == HttpMethod::OPTIONS { + return Some(Box::new(ProxyHandler { + url: String::new(), + new_path: String::new(), + })); + } + for proxy_path in ctx.config.proxy_rules.keys() { + let path_match = ctx.request.path.strip_prefix(proxy_path); if path_match.is_some() { - return Some(Box::new(ProxyHandler {})); + let new_path = path_match.unwrap().to_string(); + let url = &ctx.config.proxy_rules.get(proxy_path).unwrap().clone(); + let url = if url.is_empty() { + let proxy_url = &ctx.request.headers.get("host")?; + proxy_url.to_owned() + } else { + url + } + .to_string(); + return Some(Box::new(ProxyHandler { url, new_path })); } } return None; diff --git a/src/main.rs b/src/main.rs index 441e923..a9ba520 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,24 +42,24 @@ mod logger; mod shutdown; mod utils; +use std::any::Any; 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::config::Config; use crate::handler::get_handler; +use crate::utils::Context; /// Main entry point of the Hteapot server. /// @@ -148,7 +148,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 @@ -184,53 +184,10 @@ fn main() { // 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 + //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 @@ -242,7 +199,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)); } @@ -252,11 +209,19 @@ fn main() { cache_elapsed.as_micros() )); } - let response = get_handler(&config, &req); + + let ctx = Context { + request: &req, + log: &logger, + config: &config, + }; + + let response = get_handler(&ctx); if response.is_none() { return HttpResponse::new(HttpStatus::InternalServerError, "content", None); } - let response = response.unwrap().run(&req); + let response = response.unwrap().run(&ctx); + // Log how long the request took to process let elapsed = start_time.elapsed(); http_logger.debug(format!( diff --git a/src/utils.rs b/src/utils.rs index 8e56ca4..f722eda 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,10 @@ 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, +} From ab9b2d92bdf9a66d5e796662679c448c8ee360d4 Mon Sep 17 00:00:00 2001 From: Alberto Ruiz <17555470+Az107@users.noreply.github.com> Date: Tue, 7 Oct 2025 10:57:06 +0200 Subject: [PATCH 4/6] add Cache to Handler Context --- src/handler/file.rs | 9 ++++++-- src/handler/handler.rs | 2 +- src/handler/proxy.rs | 50 +++++++++++------------------------------- src/main.rs | 19 ++++++++-------- src/utils.rs | 1 + 5 files changed, 31 insertions(+), 50 deletions(-) diff --git a/src/handler/file.rs b/src/handler/file.rs index 2ac7e0b..fb4dcae 100644 --- a/src/handler/file.rs +++ b/src/handler/file.rs @@ -57,7 +57,7 @@ pub struct FileHandler { impl FileHandler {} impl Handler for FileHandler { - fn run(&self, ctx: &Context) -> Box { + fn run(&self, ctx: &mut Context) -> Box { let logger = ctx.log.with_component("HTTP"); // If the request is not a proxy request, resolve the requested path safely let safe_path_result = if ctx.request.path == "/" { @@ -121,7 +121,12 @@ impl Handler for FileHandler { "Content-Type" => &mimetype, "X-Content-Type-Options" => "nosniff" ); - HttpResponse::new(HttpStatus::OK, c, headers) + let response = HttpResponse::new(HttpStatus::OK, c, headers); + if ctx.cache.is_some() { + let cache = ctx.cache.as_deref_mut().unwrap(); + cache.set(ctx.request.clone(), (*response).clone()); + } + response } None => { // If no content is found, return a 404 Not Found response diff --git a/src/handler/handler.rs b/src/handler/handler.rs index 255b6e7..8458f6a 100644 --- a/src/handler/handler.rs +++ b/src/handler/handler.rs @@ -1,7 +1,7 @@ use crate::{hteapot::HttpResponseCommon, utils::Context}; pub trait Handler { - fn run(&self, context: &Context) -> Box; + fn run(&self, context: &mut Context) -> Box; } pub trait HandlerFactory { diff --git a/src/handler/proxy.rs b/src/handler/proxy.rs index 16a4011..1e947c1 100644 --- a/src/handler/proxy.rs +++ b/src/handler/proxy.rs @@ -1,10 +1,5 @@ -use std::path; - -use crate::config::Config; use crate::handler::handler::{Handler, HandlerFactory}; -use crate::hteapot::{ - HttpMethod, HttpRequest, HttpResponse, HttpResponseCommon, HttpStatus, TunnelResponse, -}; +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. @@ -18,33 +13,6 @@ use crate::utils::Context; /// /// # 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<&str> = 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)); - } - } - None -} pub struct ProxyHandler { new_path: String, @@ -52,7 +20,8 @@ pub struct ProxyHandler { } impl Handler for ProxyHandler { - fn run(&self, ctx: &Context) -> Box { + fn run(&self, ctx: &mut Context) -> Box { + let proxy_logger = &ctx.log.with_component("proxy"); if ctx.request.method == HttpMethod::OPTIONS { return TunnelResponse::new(&ctx.request.path); } @@ -66,9 +35,16 @@ impl Handler for ProxyHandler { host_parts.last().clone().unwrap() }; proxy_req.headers.insert("host", host); - proxy_req - .brew(&self.url) - .unwrap_or(HttpResponse::new(HttpStatus::NotAcceptable, "", None)) + let response = proxy_req.brew(&self.url).unwrap_or(HttpResponse::new( + HttpStatus::NotAcceptable, + "", + None, + )); + if ctx.cache.is_some() { + let cache = ctx.cache.as_deref_mut().unwrap(); + cache.set(ctx.request.clone(), (*response).clone()); + } + response } } diff --git a/src/main.rs b/src/main.rs index a9ba520..ca12108 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,22 +42,17 @@ mod logger; mod shutdown; mod utils; -use std::any::Any; use std::fs; use std::io; use std::sync::Mutex; use cache::Cache; -use hteapot::HttpMethod; -use hteapot::TunnelResponse; + use hteapot::{Hteapot, HttpRequest, HttpResponse, HttpStatus}; use logger::{LogLevel, Logger}; use std::time::Instant; -use handler::proxy::is_proxy; - -use crate::config::Config; use crate::handler::get_handler; use crate::utils::Context; @@ -175,7 +170,7 @@ 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"); @@ -184,7 +179,6 @@ fn main() { // 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)); @@ -210,17 +204,22 @@ fn main() { )); } - let ctx = Context { + let mut ctx = Context { request: &req, log: &logger, config: &config, + cache: if config.cache { + Some(&mut cache.lock().unwrap()) + } else { + None + }, }; let response = get_handler(&ctx); if response.is_none() { return HttpResponse::new(HttpStatus::InternalServerError, "content", None); } - let response = response.unwrap().run(&ctx); + let response = response.unwrap().run(&mut ctx); // Log how long the request took to process let elapsed = start_time.elapsed(); diff --git a/src/utils.rs b/src/utils.rs index f722eda..98b8a8f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -105,4 +105,5 @@ pub struct Context<'a> { pub request: &'a HttpRequest, pub log: &'a Logger, pub config: &'a Config, + pub cache: Option<&'a mut Cache>, } From 73f5b1a47af56ad6de6b6ef977f6536d34827eab Mon Sep 17 00:00:00 2001 From: Alberto Ruiz <17555470+Az107@users.noreply.github.com> Date: Tue, 7 Oct 2025 16:08:36 +0200 Subject: [PATCH 5/6] add HandlerEngine --- src/handler/file.rs | 77 ++++++++++++++++++-------------------------- src/handler/mod.rs | 27 +++++++++++++--- src/handler/proxy.rs | 55 ++++++++++++++++++------------- src/main.rs | 5 +-- 4 files changed, 90 insertions(+), 74 deletions(-) diff --git a/src/handler/file.rs b/src/handler/file.rs index fb4dcae..e2d921b 100644 --- a/src/handler/file.rs +++ b/src/handler/file.rs @@ -10,22 +10,21 @@ use crate::{ utils::{Context, get_mime_tipe}, }; -/// Attempts to safely join a root directory and a requested relative path. +/// 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 /// ``` @@ -41,7 +40,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 { @@ -49,6 +47,7 @@ pub fn safe_join_paths(root: &str, requested_path: &str) -> Option { } } +/// Handles serving static files from a root directory, including index files. pub struct FileHandler { root: String, index: String, @@ -59,36 +58,28 @@ impl FileHandler {} impl Handler for FileHandler { fn run(&self, ctx: &mut Context) -> Box { let logger = ctx.log.with_component("HTTP"); - // If the request is not a proxy request, resolve the requested path safely + + // Resolve the requested path safely let safe_path_result = if ctx.request.path == "/" { - // Special handling for the root "/" path - let root_path = Path::new(&self.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(&self.index); - if index_path.exists() { - Some(index_path) // If index exists, return its path - } else { - None // If no index exists, return None - } - } else { - None // If the root path is invalid, return None - } + // 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 { - // For any other path, resolve it safely using the `safe_join_paths` function + // Other paths: use safe join safe_join_paths(&self.root, &ctx.request.path) }; - // Handle the case where the resolved path is a directory + // Handle directories or invalid paths 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(&self.index); if index_path.exists() { - index_path // If index exists, return its path + index_path } else { - // If no index file exists, log a warning and return a 404 response logger.warn(format!( "Index file not found in directory: {}", ctx.request.path @@ -96,11 +87,10 @@ impl Handler for FileHandler { return HttpResponse::new(HttpStatus::NotFound, "Index not found", None); } } else { - path // If it's not a directory, just return the path + path } } None => { - // If the path is invalid or access is denied, return a 404 response logger.warn(format!( "Path not found or access denied: {}", ctx.request.path @@ -109,29 +99,26 @@ impl Handler for FileHandler { } }; - // Determine the MIME type for the file based on its extension + // Determine MIME type 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 = fs::read(&safe_path).ok(); - match content { - Some(c) => { - // If content is found, create response with proper headers and a 200 OK status + // Read file content + match fs::read(&safe_path).ok() { + Some(content) => { let headers = headers!( "Content-Type" => &mimetype, "X-Content-Type-Options" => "nosniff" ); - let response = HttpResponse::new(HttpStatus::OK, c, headers); - if ctx.cache.is_some() { - let cache = ctx.cache.as_deref_mut().unwrap(); + let response = HttpResponse::new(HttpStatus::OK, content, 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 => { - // If no content is found, return a 404 Not Found response - HttpResponse::new(HttpStatus::NotFound, "Not found", None) - } + None => HttpResponse::new(HttpStatus::NotFound, "Not found", None), } } } diff --git a/src/handler/mod.rs b/src/handler/mod.rs index 9272b3f..a0576a5 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -36,11 +36,28 @@ static HANDLERS: &[Factory] = &[proxy::ProxyHandler::is, file::FileHandler::is]; /// // process the response /// } /// ``` -pub fn get_handler(ctx: &Context) -> Option> { - for h in HANDLERS { - if let Some(handler) = h(ctx) { - return Some(handler); + +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 } - None } diff --git a/src/handler/proxy.rs b/src/handler/proxy.rs index 1e947c1..7303268 100644 --- a/src/handler/proxy.rs +++ b/src/handler/proxy.rs @@ -2,18 +2,15 @@ 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`. - +/// # Fields +/// * `new_path` - Path to use for the proxied request. +/// * `url` - Target upstream URL. pub struct ProxyHandler { new_path: String, url: String, @@ -22,55 +19,69 @@ pub struct ProxyHandler { 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().clone().unwrap() + 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, )); - if ctx.cache.is_some() { - let cache = ctx.cache.as_deref_mut().unwrap(); + + // 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() { - let path_match = ctx.request.path.strip_prefix(proxy_path); - if path_match.is_some() { - let new_path = path_match.unwrap().to_string(); - let url = &ctx.config.proxy_rules.get(proxy_path).unwrap().clone(); + 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() { - let proxy_url = &ctx.request.headers.get("host")?; + // 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(); + url.to_string() + }; + return Some(Box::new(ProxyHandler { url, new_path })); } } - return None; + + None } } diff --git a/src/main.rs b/src/main.rs index ca12108..c101fc3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,7 +53,7 @@ use hteapot::{Hteapot, HttpRequest, HttpResponse, HttpStatus}; use logger::{LogLevel, Logger}; use std::time::Instant; -use crate::handler::get_handler; +use crate::handler::HandlerEngine; use crate::utils::Context; /// Main entry point of the Hteapot server. @@ -174,6 +174,7 @@ fn main() { 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 @@ -215,7 +216,7 @@ fn main() { }, }; - let response = get_handler(&ctx); + let response = handlers.get_handler(&ctx); if response.is_none() { return HttpResponse::new(HttpStatus::InternalServerError, "content", None); } From d76139985a46ad347c4e0eac5c7e4aafab57d18d Mon Sep 17 00:00:00 2001 From: Alberto Ruiz <17555470+Az107@users.noreply.github.com> Date: Wed, 8 Oct 2025 08:17:09 +0200 Subject: [PATCH 6/6] Rename Headers struct to HttpHeaders and minor adjustments --- src/handler/file.rs | 12 +++++------- src/handler/proxy.rs | 2 +- src/hteapot/brew.rs | 4 ++-- src/hteapot/http/headers.rs | 28 ++++++++++++++++++---------- src/hteapot/http/methods.rs | 3 ++- src/hteapot/http/mod.rs | 2 +- src/hteapot/mod.rs | 6 +++--- src/hteapot/request.rs | 10 +++++----- src/hteapot/response.rs | 14 +++++++------- 9 files changed, 44 insertions(+), 37 deletions(-) diff --git a/src/handler/file.rs b/src/handler/file.rs index e2d921b..8bcfbaf 100644 --- a/src/handler/file.rs +++ b/src/handler/file.rs @@ -5,8 +5,7 @@ use std::{ use crate::{ handler::handler::{Handler, HandlerFactory}, - headers, - hteapot::{HttpResponse, HttpStatus}, + hteapot::{HttpHeaders, HttpResponse, HttpStatus}, utils::{Context, get_mime_tipe}, }; @@ -105,11 +104,10 @@ impl Handler for FileHandler { // Read file content match fs::read(&safe_path).ok() { Some(content) => { - let headers = headers!( - "Content-Type" => &mimetype, - "X-Content-Type-Options" => "nosniff" - ); - let response = HttpResponse::new(HttpStatus::OK, content, headers); + 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() { diff --git a/src/handler/proxy.rs b/src/handler/proxy.rs index 7303268..d6a7457 100644 --- a/src/handler/proxy.rs +++ b/src/handler/proxy.rs @@ -18,7 +18,7 @@ pub struct ProxyHandler { impl Handler for ProxyHandler { fn run(&self, ctx: &mut Context) -> Box { - let proxy_logger = &ctx.log.with_component("proxy"); + let _proxy_logger = &ctx.log.with_component("proxy"); // Return a tunnel response immediately for OPTIONS requests if ctx.request.method == HttpMethod::OPTIONS { 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 d005e2b..5c841a1 100644 --- a/src/hteapot/mod.rs +++ b/src/hteapot/mod.rs @@ -31,10 +31,10 @@ 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, HttpResponseCommon, StreamedResponse, TunnelResponse}; /// Crate version as set by `Cargo.toml`. @@ -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(),