From db970b745cf0b66962b99351c70d82dcb47a1550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LoboGuardian=20=F0=9F=90=BA?= Date: Sun, 13 Apr 2025 16:12:49 -0400 Subject: [PATCH 01/16] docs(request): document HttpRequest builder and structure Added doc comments to HttpRequest and HttpRequestBuilder Explained append() behavior and limits (header size, count) Documented known limitations (no chunked, no URI normalization) Improved inline comments for parsing logic and TODOs --- src/hteapot/request.rs | 47 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/src/hteapot/request.rs b/src/hteapot/request.rs index 4c7f6ef..4b8f939 100644 --- a/src/hteapot/request.rs +++ b/src/hteapot/request.rs @@ -1,7 +1,12 @@ // Written by Alberto Ruiz 2025-01-01 -// This module handles the request struct and a builder for it -// This implementation has some issues and fixes are required for security -// Refactor is recomended, but for now can work with the fixes +// +// This module defines the HTTP request structure and a streaming builder for parsing raw input. +// While the core functionality is usable, there are known limitations: +// - No support for chunked transfer encoding +// - Partial header validation +// - No URI normalization or encoding +// +// ⚠️ A full refactor is recommended before production use. use super::HttpMethod; use std::{cmp::min, collections::HashMap, net::TcpStream, str}; @@ -9,6 +14,9 @@ use std::{cmp::min, collections::HashMap, net::TcpStream, str}; const MAX_HEADER_SIZE: usize = 1024 * 16; const MAX_HEADER_COUNT: usize = 100; +/// Represents a parsed HTTP request. +/// +/// Contains method, path, optional query arguments, headers, body, and a stream (for low-level access). #[derive(Debug)] pub struct HttpRequest { pub method: HttpMethod, @@ -20,6 +28,7 @@ pub struct HttpRequest { } impl HttpRequest { + /// Creates a new HTTP request with the given method and path. pub fn new(method: HttpMethod, path: &str) -> Self { return HttpRequest { method, @@ -31,6 +40,7 @@ impl HttpRequest { }; } + /// Returns a blank default request (empty method/path/headers). pub fn default() -> Self { HttpRequest { method: HttpMethod::Other(String::new()), @@ -42,6 +52,7 @@ impl HttpRequest { } } + /// Returns a blank default request (empty method/path/headers). pub fn clone(&self) -> Self { return HttpRequest { method: self.method.clone(), @@ -53,10 +64,12 @@ impl HttpRequest { }; } + /// Attaches a raw TCP stream to this request. pub fn set_stream(&mut self, stream: TcpStream) { self.stream = Some(stream); } + /// Attempts to decode the body as UTF-8 and return it as text. pub fn text(&self) -> Option { if self.body.len() == 0 { return None; @@ -69,6 +82,9 @@ impl HttpRequest { } } +/// Builder for incrementally parsing a raw HTTP request. +/// +/// This is useful when reading from a stream (e.g., TCP) in chunks. pub struct HttpRequestBuilder { request: HttpRequest, buffer: Vec, @@ -79,6 +95,7 @@ pub struct HttpRequestBuilder { } impl HttpRequestBuilder { + /// Creates a new builder in the initial state. pub fn new() -> Self { return HttpRequestBuilder { request: HttpRequest { @@ -97,6 +114,7 @@ impl HttpRequestBuilder { }; } + /// Returns the built request if parsing is complete. pub fn get(&self) -> Option { if self.done { return Some(self.request.clone()); @@ -105,6 +123,7 @@ impl HttpRequestBuilder { } } + /// Reads bytes into the request body based on `Content-Length`. fn read_body_len(&mut self) -> Option<()> { let body_left = self.body_size.saturating_sub(self.request.body.len()); let to_take = min(body_left, self.buffer.len()); @@ -120,21 +139,28 @@ impl HttpRequestBuilder { } } - fn read_body_chunk(&mut self) -> Option<()> { + /// Placeholder for future support of chunked body parsing. + fn _read_body_chunk(&mut self) -> Option<()> { //TODO: this will support chunked body in the future todo!() } + /// Main entry point for reading the request body. fn read_body(&mut self) -> Option<()> { return self.read_body_len(); } + /// Feeds a chunk of bytes into the builder. + /// + /// This function may return an error if the header is too large or malformed. pub fn append(&mut self, chunk: Vec) -> Result<(), &'static str> { if !self.header_done && self.buffer.len() > MAX_HEADER_SIZE { return Err("Entity Too large"); } + let chunk_size = chunk.len(); self.buffer.extend(chunk); + if self.header_done { self.read_body(); return Ok(()); @@ -144,9 +170,10 @@ impl HttpRequestBuilder { return Err("Entity Too large"); } } + while let Some(pos) = self.buffer.windows(2).position(|w| w == b"\r\n") { let line = self.buffer.drain(..pos).collect::>(); - self.buffer.drain(..2); + self.buffer.drain(..2); // remove CRLF let line_str = match str::from_utf8(line.as_slice()) { Ok(v) => v.to_string(), @@ -154,6 +181,7 @@ impl HttpRequestBuilder { }; if self.request.path.is_empty() { + // This is the request line let parts: Vec<&str> = line_str.split_whitespace().collect(); if parts.len() < 2 { return Ok(()); @@ -162,6 +190,7 @@ impl HttpRequestBuilder { if parts.len() != 3 { return Err("Invalid method + path + version request"); } + self.request.method = HttpMethod::from_str(parts[0]); let path_parts: Vec<&str> = parts[1].split('?').collect(); self.request.path = path_parts[0].to_string(); @@ -180,14 +209,17 @@ impl HttpRequestBuilder { .collect(); } } else if !line_str.is_empty() { + // Header line if let Some((key, value)) = line_str.split_once(":") { //Check the number of headers, if the actual headers exceed that number //drop the connection if self.request.headers.len() > MAX_HEADER_COUNT { return Err("Header number exceed allowed"); } + let key = key.trim().to_lowercase(); let value = value.trim(); + if key == "content-length" { if self.request.headers.get("content-length").is_some() || self @@ -206,6 +238,7 @@ impl HttpRequestBuilder { .insert(key.to_string(), value.to_string()); } } else { + // Empty line = end of headers self.header_done = true; self.read_body(); return Ok(()); @@ -217,4 +250,6 @@ impl HttpRequestBuilder { #[cfg(test)] #[test] -fn basic_request() {} +fn basic_request() { + // Placeholder test — add real body/header parsing test here. +} From 41632a3f97ae7000992ab374791cf814be1c7c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LoboGuardian=20=F0=9F=90=BA?= Date: Sun, 13 Apr 2025 16:14:14 -0400 Subject: [PATCH 02/16] docs(response): document response types and iteration traits Added documentation to HttpResponse, StreamedResponse, ChunkSender Described chunked transfer encoding behavior Clarified use cases for EmptyHttpResponse and raw proxy responses Improved comments, and header serialization explanation --- src/hteapot/response.rs | 56 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/src/hteapot/response.rs b/src/hteapot/response.rs index dae9979..ee95a43 100644 --- a/src/hteapot/response.rs +++ b/src/hteapot/response.rs @@ -1,3 +1,12 @@ +//! HTTP response types for HTeaPot. +//! +//! Supports multiple types of responses: +//! - [`HttpResponse`] for standard fixed-size responses +//! - [`StreamedResponse`] for chunked transfer encoding +//! - [`EmptyHttpResponse`] as a sentinel or fallback +//! +//! All response types implement the [`HttpResponseCommon`] trait. + use super::HttpStatus; use super::{BUFFER_SIZE, VERSION}; use std::collections::{HashMap, VecDeque}; @@ -7,29 +16,35 @@ use std::sync::Arc; use std::thread; use std::thread::JoinHandle; +/// Basic HTTP status line + headers. pub struct BaseResponse { pub status: HttpStatus, pub headers: HashMap, } impl BaseResponse { + /// Converts the status + headers into a properly formatted HTTP header block. pub fn to_bytes(&mut self) -> Vec { let mut headers_text = String::new(); for (key, value) in self.headers.iter() { headers_text.push_str(&format!("{}: {}\r\n", key, value)); } + let response_header = format!( "HTTP/1.1 {} {}\r\n{}\r\n", self.status as u16, self.status.to_string(), headers_text ); + let mut response = Vec::new(); response.extend_from_slice(response_header.as_bytes()); response } } + +/// Represents a full HTTP response (headers + body). pub struct HttpResponse { base: BaseResponse, pub content: Vec, @@ -38,12 +53,19 @@ pub struct HttpResponse { index: usize, } +/// Trait shared by all response types (normal, streamed, etc.) pub trait HttpResponseCommon { + /// Returns a mutable reference to the base response (for status/headers). fn base(&mut self) -> &mut BaseResponse; + + /// Advances and returns the next chunk of the response body. fn next(&mut self) -> Result, IterError>; + + /// Advances and returns the next chunk of the response body. fn peek(&mut self) -> Result, IterError>; } +/// Error returned during response iteration. #[derive(Debug)] pub enum IterError { WouldBlock, @@ -51,6 +73,9 @@ pub enum IterError { } impl HttpResponse { + /// Creates a standard HTTP response with body and optional headers. + /// + /// Automatically sets `Content-Length` and `Server` headers. pub fn new>( status: HttpStatus, content: B, @@ -58,11 +83,13 @@ impl HttpResponse { ) -> Box { let mut headers = headers.unwrap_or(HashMap::new()); let content = content.as_ref(); + headers.insert("Content-Length".to_string(), content.len().to_string()); headers.insert( "Server".to_string(), format!("HTeaPot/{}", VERSION).to_string(), ); + Box::new(HttpResponse { base: BaseResponse { status, headers }, content: content.to_owned(), @@ -72,6 +99,7 @@ impl HttpResponse { }) } + /// Creates a raw response from raw bytes (used for proxy responses). pub fn new_raw(raw: Vec) -> Self { HttpResponse { base: BaseResponse { @@ -85,24 +113,29 @@ impl HttpResponse { } } + /// Returns true if this is a raw (proxy) response. pub fn is_raw(&self) -> bool { self.is_raw } + /// Serializes the entire response into a byte buffer. pub fn to_bytes(&mut self) -> Vec { if self.is_raw() { return self.raw.clone().unwrap(); } + let mut headers_text = String::new(); for (key, value) in self.base.headers.iter() { headers_text.push_str(&format!("{}: {}\r\n", key, value)); } + let response_header = format!( "HTTP/1.1 {} {}\r\n{}\r\n", self.base.status as u16, self.base.status.to_string(), headers_text ); + let mut response = Vec::new(); response.extend_from_slice(response_header.as_bytes()); response.append(&mut self.content); @@ -127,6 +160,7 @@ impl HttpResponseCommon for HttpResponse { if self.raw.is_none() { self.raw = Some(self.to_bytes()); } + let raw = self.raw.as_ref().unwrap(); let mut raw = raw.chunks(BUFFER_SIZE).skip(self.index); let byte_chunk = raw.next().ok_or(IterError::Finished)?.to_vec(); @@ -134,6 +168,7 @@ impl HttpResponseCommon for HttpResponse { } } +/// Dummy response used when nothing needs to be returned. pub struct EmptyHttpResponse {} impl EmptyHttpResponse {} @@ -141,6 +176,7 @@ impl HttpResponseCommon for EmptyHttpResponse { fn base(&mut self) -> &mut BaseResponse { panic!("Invalid state") } + fn next(&mut self) -> Result, IterError> { Err(IterError::Finished) } @@ -150,9 +186,14 @@ impl HttpResponseCommon for EmptyHttpResponse { } } +/// Sends response chunks in a `Transfer-Encoding: chunked` format. pub struct ChunkSender(Sender>); impl ChunkSender { + /// Sends a new chunk to the output stream. + /// + /// Prepends the size in hex followed by CRLF, then the chunk, then another CRLF. + // fn new(sender: Sender>) -> Self { // Self(sender) // } @@ -168,6 +209,9 @@ impl ChunkSender { // fn end(&self) -> Result<(), SendError>> {} } +/// Represents a streaming HTTP response using chunked transfer encoding. +/// +/// Runs the streaming action in a background thread. Chunks are sent via a channel. pub struct StreamedResponse { base: BaseResponse, receiver: Receiver>, @@ -177,22 +221,28 @@ pub struct StreamedResponse { } impl StreamedResponse { + /// Creates a new streamed response. The provided closure is run in a separate thread. + /// + /// The closure is given a `ChunkSender` to emit data. The response ends when the closure exits. pub fn new(action: impl Fn(ChunkSender) + Send + Sync + 'static) -> Box { let action = Arc::new(action); let (tx, rx) = mpsc::channel(); - let action_clon = action.clone(); + let mut base = BaseResponse { status: HttpStatus::OK, headers: HashMap::new(), }; + base.headers - .insert("Transfer-Encoding".to_string(), "chunked".to_string()); + .insert("Transfer-Encoding".to_string(), "chunked".to_string()); base.headers.insert( "Server".to_string(), format!("HTeaPot/{}", VERSION).to_string(), ); + let _ = tx.send(base.to_bytes()); let has_end = Arc::new(AtomicBool::new(false)); + let action_clon = action.clone(); let has_end_clone = has_end.clone(); let jh = thread::spawn(move || { @@ -201,6 +251,7 @@ impl StreamedResponse { let _ = tx.clone().send(b"0\r\n\r\n".to_vec()); has_end_clone.store(true, Ordering::SeqCst); }); + Box::new(StreamedResponse { base, has_end, @@ -219,6 +270,7 @@ impl HttpResponseCommon for StreamedResponse { fn base(&mut self) -> &mut BaseResponse { &mut self.base } + fn next(&mut self) -> Result, IterError> { self.peek() } From f654f60d9e94cd1639ca86381116bd75333050c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LoboGuardian=20=F0=9F=90=BA?= Date: Sun, 13 Apr 2025 16:15:57 -0400 Subject: [PATCH 03/16] docs(brew): document HTTP request composition and client behavior Added doc comments to HttpRequest methods and brew() helper Explained HTTP/1.1 serialization and TCP interaction Added inline comments for protocol stripping and query formatting Improved string handling and result propagation for clarity --- src/hteapot/brew.rs | 84 ++++++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/src/hteapot/brew.rs b/src/hteapot/brew.rs index 59b141b..a7a49ce 100644 --- a/src/hteapot/brew.rs +++ b/src/hteapot/brew.rs @@ -1,5 +1,8 @@ // Written by Alberto Ruiz 2024-04-08 -// This is the HTTP client module, it will handle the requests and responses +// +// This module provides basic HTTP client functionality. It defines +// methods to compose and send HTTP requests and parse the resulting +// responses using a `TcpStream`. use std::io::{Read, Write}; use std::net::{TcpStream, ToSocketAddrs}; @@ -11,71 +14,85 @@ use super::response::HttpResponse; // use std::net::{IpAddr, Ipv4Addr, SocketAddr}; impl HttpRequest { + /// Adds a query argument to the HTTP request. pub fn arg(&mut self, key: &str, value: &str) -> &mut HttpRequest { self.args.insert(key.to_string(), value.to_string()); - return self; + self } + /// Adds a header to the HTTP request. pub fn header(&mut self, key: &str, value: &str) -> &mut HttpRequest { self.headers.insert(key.to_string(), value.to_string()); - return self; + self } + /// Converts the request into a raw HTTP/1.1-compliant string. + /// + /// This includes method, path with optional query args, headers, and optional body. pub fn to_string(&self) -> String { + // Add query parameters to the path if needed let path = if self.args.is_empty() { self.path.clone() } else { - let mut path = self.path.clone(); - path.push('?'); - for (k, v) in self.args.iter() { - path.push_str(format!("{}={}&", k, v).as_str()); + let mut path = format!("{}?", self.path); + for (k, v) in &self.args { + path.push_str(&format!("{}={}&", k, v)); } if path.ends_with('&') { path.pop(); } path }; - let path = if path.is_empty() { - "/".to_string() - } else { - path - }; - let mut result: String = format!("{} {} HTTP/1.1\r\n", self.method.to_str(), path); - for (k, v) in self.headers.iter() { - result.push_str(format!("{}: {}\r\n", k, v).as_str()); - } - result.push_str(self.text().unwrap_or(String::new()).as_str()); + let path = if path.is_empty() { "/".to_string() } else { path }; + let mut result = format!("{} {} HTTP/1.1\r\n", self.method.to_str(), path); + for (k, v) in &self.headers { + result.push_str(&format!("{}: {}\r\n", k, v)); + } + + result.push_str(&self.text().unwrap_or_default()); result.push_str("\r\n\r\n"); + result } + /// Sends the request to a remote server and returns a parsed response. + /// + /// Supports only `http://` (not `https://`). Attempts to resolve the domain + /// and open a TCP connection. Times out after 5 seconds. pub fn brew(&self, addr: &str) -> Result, &'static str> { let mut addr = addr.to_string(); - if addr.starts_with("http://") { - addr = addr.strip_prefix("http://").unwrap().to_string(); + + // Strip protocol prefix + if let Some(stripped) = addr.strip_prefix("http://") { + addr = stripped.to_string(); } else if addr.starts_with("https://") { - return Err("Not implemented yet"); + return Err("HTTPS not implemented yet"); } + + // Add port if missing if !addr.contains(':') { - let _addr = format!("{}:80", addr.clone()); - addr = _addr + addr.push_str(":80"); } - let addr: Vec<_> = addr + + // Resolve address + let resolved_addrs: Vec<_> = addr .to_socket_addrs() - .expect("Unable to resolve domain") + .map_err(|_| "Unable to resolve domain")? .collect(); - let addr = addr.first().expect("Error parsing address"); - let stream = TcpStream::connect_timeout(addr, Duration::from_secs(5)); - if stream.is_err() { - return Err("Error connecting to server"); - } - let mut stream = stream.unwrap(); - let _ = stream.write(self.to_string().as_bytes()); + let socket_addr = resolved_addrs.first().ok_or("No address found")?; + + // Connect to server + let stream = TcpStream::connect_timeout(socket_addr, Duration::from_secs(5)) + .map_err(|_| "Error connecting to server")?; + + let mut stream = stream; + let _ = stream.write_all(self.to_string().as_bytes()); let _ = stream.flush(); let _ = stream.set_read_timeout(Some(Duration::from_secs(5))); + let mut raw: Vec = Vec::new(); let _ = stream.read_to_end(&mut raw); @@ -83,8 +100,11 @@ impl HttpRequest { } } +/// Alias to send a request via `request.brew()`. +/// +/// Useful for calling as a standalone function. pub fn brew(direction: &str, request: &mut HttpRequest) -> Result, &'static str> { - return request.brew(direction); + request.brew(direction) } // pub fn brew_url(url: &str) -> Result { From 9f062a427c6475f1d541401b9463d665ad30cd2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LoboGuardian=20=F0=9F=90=BA?= Date: Sun, 13 Apr 2025 16:19:15 -0400 Subject: [PATCH 04/16] docs(methods): document HttpMethod enum and string conversions Added doc comments to HttpMethod enum and its variants Explained from_str fallback to Other(String) Clarified behavior of to_str() with examples Preserved future Protocol enum for later HTTPS support --- src/hteapot/methods.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/hteapot/methods.rs b/src/hteapot/methods.rs index 6f43a4e..ddacee8 100644 --- a/src/hteapot/methods.rs +++ b/src/hteapot/methods.rs @@ -1,3 +1,7 @@ +/// Represents an HTTP method (verb). +/// +/// Includes standard HTTP/1.1 methods such as `GET`, `POST`, `PUT`, etc., +/// and a catch-all variant `Other(String)` for unknown or non-standard methods. #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub enum HttpMethod { GET, @@ -12,6 +16,21 @@ pub enum HttpMethod { Other(String), } impl HttpMethod { + /// Creates an `HttpMethod` from a raw string (case-sensitive). + /// + /// If the method is not one of the standard HTTP methods, + /// it will be returned as `HttpMethod::Other(method.to_string())`. + /// + /// # Examples + /// ``` + /// use your_crate::HttpMethod; + /// + /// let m = HttpMethod::from_str("GET"); + /// assert_eq!(m, HttpMethod::GET); + /// + /// let custom = HttpMethod::from_str("CUSTOM"); + /// assert_eq!(custom, HttpMethod::Other("CUSTOM".into())); + /// ``` pub fn from_str(method: &str) -> HttpMethod { match method { "GET" => HttpMethod::GET, @@ -26,6 +45,16 @@ impl HttpMethod { _ => Self::Other(method.to_string()), } } + + /// Returns the string representation of the HTTP method. + /// + /// If the method is non-standard (`Other`), it returns the inner string as-is. + /// + /// # Examples + /// ``` + /// let method = HttpMethod::GET; + /// assert_eq!(method.to_str(), "GET"); + /// ``` pub fn to_str(&self) -> &str { match self { HttpMethod::GET => "GET", From 74aff989550e93b630b1ac707e40e68c108bce83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LoboGuardian=20=F0=9F=90=BA?= Date: Sun, 13 Apr 2025 16:20:08 -0400 Subject: [PATCH 05/16] docs(status): document HttpStatus enum and reason phrase helpers Added module-level and function-level documentation Clarified purpose of from_u16 and to_string Fixed reason phrase for 302 (Found) Improved formatting for consistency and future maintainability --- src/hteapot/status.rs | 63 ++++++++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/src/hteapot/status.rs b/src/hteapot/status.rs index 4aec09a..f066a47 100644 --- a/src/hteapot/status.rs +++ b/src/hteapot/status.rs @@ -1,5 +1,19 @@ +//! HTTP status code definitions and helpers. +//! +//! This module defines the `HttpStatus` enum which maps common and uncommon +//! HTTP status codes to semantic Rust values. It includes conversions from +//! `u16` and methods to get the standard reason phrase. + +/// Represents an HTTP status code with a semantic variant name. +/// +/// This enum maps standard HTTP status codes (e.g., 200 OK, 404 Not Found) +/// to variants that make them easier to work with in Rust. +/// +/// Use [`HttpStatus::from_u16`] to convert from raw codes, +/// and [`HttpStatus::to_string`] to get the standard reason phrase. #[derive(Clone, Copy)] pub enum HttpStatus { + // 2xx Success OK = 200, Created = 201, Accepted = 202, @@ -8,15 +22,17 @@ pub enum HttpStatus { ResetContent = 205, PartialContent = 206, + // 3xx Redirection MultipleChoices = 300, MovedPermanently = 301, - Found = 302, // (Renombrado de MovedTemporarily) + Found = 302, // (Renamed from MovedTemporarily) SeeOther = 303, NotModified = 304, UseProxy = 305, TemporaryRedirect = 307, PermanentRedirect = 308, + // 4xx Client Error BadRequest = 400, Unauthorized = 401, PaymentRequired = 402, @@ -46,6 +62,7 @@ pub enum HttpStatus { TooManyRequests = 429, RequestHeaderFieldsTooLarge = 431, + // 5xx Server Error InternalServerError = 500, NotImplemented = 501, BadGateway = 502, @@ -60,6 +77,9 @@ pub enum HttpStatus { } impl HttpStatus { + /// Attempts to convert a `u16` status code into an `HttpStatus` enum variant. + /// + /// Returns an error if the code is not recognized. pub fn from_u16(status: u16) -> Result { match status { 200 => Ok(HttpStatus::OK), @@ -67,6 +87,9 @@ impl HttpStatus { 202 => Ok(HttpStatus::Accepted), 203 => Ok(HttpStatus::NonAuthoritativeInformation), 204 => Ok(HttpStatus::NoContent), + 205 => Ok(HttpStatus::ResetContent), + 206 => Ok(HttpStatus::PartialContent), + 300 => Ok(HttpStatus::MultipleChoices), 301 => Ok(HttpStatus::MovedPermanently), 302 => Ok(HttpStatus::Found), @@ -75,6 +98,7 @@ impl HttpStatus { 305 => Ok(HttpStatus::UseProxy), 307 => Ok(HttpStatus::TemporaryRedirect), 308 => Ok(HttpStatus::PermanentRedirect), + 400 => Ok(HttpStatus::BadRequest), 401 => Ok(HttpStatus::Unauthorized), 402 => Ok(HttpStatus::PaymentRequired), @@ -92,6 +116,7 @@ impl HttpStatus { 414 => Ok(HttpStatus::URITooLong), 415 => Ok(HttpStatus::UnsupportedMediaType), 416 => Ok(HttpStatus::RangeNotSatisfiable), + 417 => Ok(HttpStatus::ExpectationFailed), 418 => Ok(HttpStatus::IAmATeapot), 421 => Ok(HttpStatus::MisdirectedRequest), 422 => Ok(HttpStatus::UnprocessableEntity), @@ -102,6 +127,7 @@ impl HttpStatus { 428 => Ok(HttpStatus::PreconditionRequired), 429 => Ok(HttpStatus::TooManyRequests), 431 => Ok(HttpStatus::RequestHeaderFieldsTooLarge), + 500 => Ok(HttpStatus::InternalServerError), 501 => Ok(HttpStatus::NotImplemented), 502 => Ok(HttpStatus::BadGateway), @@ -113,37 +139,38 @@ impl HttpStatus { 508 => Ok(HttpStatus::LoopDetected), 510 => Ok(HttpStatus::NotExtended), 511 => Ok(HttpStatus::NetworkAuthenticationRequired), + _ => Err("Invalid HTTP status"), } } + /// Returns the standard reason phrase for this status code. + /// + /// For example: `HttpStatus::OK.to_string()` returns `"OK"`. pub fn to_string(&self) -> &str { match self { HttpStatus::OK => "OK", HttpStatus::Created => "Created", HttpStatus::Accepted => "Accepted", - HttpStatus::NoContent => "No Content", - HttpStatus::MovedPermanently => "Moved Permanently", - HttpStatus::Found => "Moved Temporarily", - HttpStatus::NotModified => "Not Modified", - HttpStatus::BadRequest => "Bad Request", - HttpStatus::Unauthorized => "Unauthorized", - HttpStatus::Forbidden => "Forbidden", - HttpStatus::NotFound => "Not Found", - HttpStatus::IAmATeapot => "I'm a teapot", - HttpStatus::InternalServerError => "Internal Server Error", - HttpStatus::NotImplemented => "Not Implemented", - HttpStatus::BadGateway => "Bad Gateway", - HttpStatus::ServiceUnavailable => "Service Unavailable", HttpStatus::NonAuthoritativeInformation => "Non Authoritative Information", + HttpStatus::NoContent => "No Content", HttpStatus::ResetContent => "Reset Content", - HttpStatus::PartialContent => "PartialContent", + HttpStatus::PartialContent => "Partial Content", + HttpStatus::MultipleChoices => "Multiple Choices", + HttpStatus::MovedPermanently => "Moved Permanently", + HttpStatus::Found => "Found", HttpStatus::SeeOther => "See Other", + HttpStatus::NotModified => "Not Modified", HttpStatus::UseProxy => "Use Proxy", HttpStatus::TemporaryRedirect => "Temporary Redirect", HttpStatus::PermanentRedirect => "Permanent Redirect", + + HttpStatus::BadRequest => "Bad Request", + HttpStatus::Unauthorized => "Unauthorized", HttpStatus::PaymentRequired => "Payment Required", + HttpStatus::Forbidden => "Forbidden", + HttpStatus::NotFound => "Not Found", HttpStatus::MethodNotAllowed => "Method Not Allowed", HttpStatus::NotAcceptable => "Not Acceptable", HttpStatus::ProxyAuthenticationRequired => "Proxy Authentication Required", @@ -157,6 +184,7 @@ impl HttpStatus { HttpStatus::UnsupportedMediaType => "Unsupported Media Type", HttpStatus::RangeNotSatisfiable => "Range Not Satisfiable", HttpStatus::ExpectationFailed => "Expectation Failed", + HttpStatus::IAmATeapot => "I'm a teapot", HttpStatus::MisdirectedRequest => "Misdirected Request", HttpStatus::UnprocessableEntity => "Unprocessable Entity", HttpStatus::Locked => "Locked", @@ -166,6 +194,11 @@ impl HttpStatus { HttpStatus::PreconditionRequired => "Precondition Required", HttpStatus::TooManyRequests => "Too Many Requests", HttpStatus::RequestHeaderFieldsTooLarge => "Request Header Fields Too Large", + + HttpStatus::InternalServerError => "Internal Server Error", + HttpStatus::NotImplemented => "Not Implemented", + HttpStatus::BadGateway => "Bad Gateway", + HttpStatus::ServiceUnavailable => "Service Unavailable", HttpStatus::GatewayTimeout => "Gateway Timeout", HttpStatus::HTTPVersionNotSupported => "HTTP Version Not Supported", HttpStatus::VariantAlsoNegotiates => "Variant Also Negotiates", From bc8fba7b4f6f7ab5fd25908cb319237afbbd657c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LoboGuardian=20=F0=9F=90=BA?= Date: Sun, 13 Apr 2025 16:22:57 -0400 Subject: [PATCH 06/16] docs(core): document mod.rs and HTTP server lifecycle Added module-level docs to hteapot core server Documented Hteapot struct, listen flow Explained internal socket thread pool and keep-alive TTL Provided example usage in docs.rs-ready format --- src/hteapot/mod.rs | 234 +++++++++++++++++++++++++-------------------- 1 file changed, 131 insertions(+), 103 deletions(-) diff --git a/src/hteapot/mod.rs b/src/hteapot/mod.rs index 87ef9c2..7e572f5 100644 --- a/src/hteapot/mod.rs +++ b/src/hteapot/mod.rs @@ -1,17 +1,33 @@ // Written by Alberto Ruiz 2024-03-08 +// // This is the HTTP server module, it will handle the requests and responses -// Also provide utilities to parse the requests and build the responses - -pub mod brew; -mod methods; -mod request; -mod response; -mod status; - -use self::response::EmptyHttpResponse; -use self::response::HttpResponseCommon; -use self::response::IterError; - +// Also provides utilities to parse the requests and build the response + +//! HTeaPot HTTP server core. +//! +//! This module provides a multithreaded HTTP/1.1 server built for performance and ease of use. +//! It handles request parsing, response building, connection lifecycle (keep-alive) +//! and hooks. +//! +//! Core types: +//! - [`Hteapot`] — the main server entry point +//! - [`HttpRequest`] and [`HttpResponse`] — re-exported from submodules +//! +//! Use [`Hteapot::listen`] to start a server with a request handler closure. +//! ``` + +/// Submodules for HTTP functionality. +pub mod brew; // HTTP client implementation +mod methods; // HTTP method and status enums +mod request; // Request parsing and builder +mod response; // Response types and streaming +mod status; // Status code mapping + +// Internal types used for connection management +use self::response::{EmptyHttpResponse, HttpResponseCommon, IterError}; +use std::sync::atomic::{AtomicBool, Ordering}; + +// Public API exposed by this module pub use self::methods::HttpMethod; pub use self::request::HttpRequest; use self::request::HttpRequestBuilder; @@ -25,10 +41,24 @@ use std::sync::{Arc, Condvar, Mutex}; use std::thread; use std::time::{Duration, Instant}; +/// Crate version as set by `Cargo.toml`. const VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Size of the buffer used for reading from the TCP stream. const BUFFER_SIZE: usize = 1024 * 2; + +/// Time-to-live for keep-alive connections. const KEEP_ALIVE_TTL: Duration = Duration::from_secs(10); +/// Helper macro to construct header maps. +/// +/// # Example +/// ```rust +/// let headers = headers! { +/// "Content-Type" => "text/html", +/// "X-Custom" => "value" +/// }; +/// ``` #[macro_export] macro_rules! headers { ( $($k:expr => $v:expr),*) => { @@ -47,6 +77,7 @@ pub struct Hteapot { threads: u16, } +/// Represents the state of a connection's lifecycle. struct SocketStatus { ttl: Instant, reading: bool, @@ -56,6 +87,7 @@ struct SocketStatus { index_writed: usize, } +/// Wraps a TCP stream and its associated state. struct SocketData { stream: TcpStream, status: Option, @@ -85,11 +117,10 @@ impl Hteapot { action: impl Fn(HttpRequest) -> Box + Send + Sync + 'static, ) { let addr = format!("{}:{}", self.address, self.port); - let listener = TcpListener::bind(addr); - let listener = match listener { + let listener = match TcpListener::bind(addr) { Ok(listener) => listener, Err(e) => { - eprintln!("Error L: {}", e); + eprintln!("Error binding to address: {}", e); return; } }; @@ -113,8 +144,8 @@ impl Hteapot { let mut pool = lock.lock().expect("Error locking pool"); if streams_to_handle.is_empty() { - pool = cvar - .wait_while(pool, |pool| pool.is_empty()) + // Store the returned guard back into pool + pool = cvar.wait_while(pool, |pool| pool.is_empty()) .expect("Error waiting on cvar"); } @@ -153,15 +184,19 @@ impl Hteapot { } loop { - let stream = listener.accept(); - if stream.is_err() { + let stream = match listener.accept() { + Ok((stream, _)) => stream, + Err(_) => continue, + }; + + if stream.set_nonblocking(true).is_err() { + eprintln!("Error setting non-blocking mode on stream"); + continue; + } + if stream.set_nodelay(true).is_err() { + eprintln!("Error setting no delay on stream"); continue; } - let (stream, _) = stream.unwrap(); - stream - .set_nonblocking(true) - .expect("Error setting non-blocking"); - stream.set_nodelay(true).expect("Error setting no delay"); { let (lock, cvar) = &*pool; @@ -183,23 +218,20 @@ impl Hteapot { // Fix by miky-rola 2025-04-08 // Check if the TTL (time-to-live) for the connection has expired. // If the connection is idle for longer than `KEEP_ALIVE_TTL` and no data is being written, - // the connection is gracefully shut down to free resources. + // the connection is gracefully shut down to free resources. if Instant::now().duration_since(status.ttl) > KEEP_ALIVE_TTL && !status.write { let _ = socket_data.stream.shutdown(Shutdown::Both); return None; } // If the request is not yet complete, read data from the stream into a buffer. // This ensures that the server can handle partial or chunked requests. + if !status.request.done { let mut buffer = [0; BUFFER_SIZE]; match socket_data.stream.read(&mut buffer) { Err(e) => match e.kind() { - io::ErrorKind::WouldBlock => { - return Some(()); - } - io::ErrorKind::ConnectionReset => { - return None; - } + io::ErrorKind::WouldBlock => return Some(()), + io::ErrorKind::ConnectionReset => return None, _ => { eprintln!("Read error: {:?}", e); return None; @@ -225,12 +257,7 @@ impl Hteapot { } } - let request = status.request.get(); - if request.is_none() { - return Some(()); - } - let request = request.unwrap(); - + let request = status.request.get()?; let keep_alive = request .headers .get("connection") //all headers are turn lowercase in the builder @@ -267,9 +294,7 @@ impl Hteapot { status.ttl = Instant::now(); let _ = status.response.next(); } - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { - return Some(()); - } + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => return Some(()), Err(e) => { eprintln!("Write error: {:?}", e); return None; @@ -297,68 +322,71 @@ impl Hteapot { } #[cfg(test)] -#[test] -fn test_http_response_maker() { - let mut response = HttpResponse::new(HttpStatus::IAmATeapot, "Hello, World!", None); - let response = String::from_utf8(response.to_bytes()).unwrap(); - let expected_response = format!( - "HTTP/1.1 418 I'm a teapot\r\nContent-Length: 13\r\nServer: HTeaPot/{}\r\n\r\nHello, World!\r\n", - VERSION - ); - let expected_response_list = expected_response.split("\r\n"); - for item in expected_response_list.into_iter() { - assert!(response.contains(item)); +mod tests { + use super::*; + + #[test] + fn test_http_response_maker() { + let mut response = HttpResponse::new(HttpStatus::IAmATeapot, "Hello, World!", None); + let response = String::from_utf8(response.to_bytes()).unwrap(); + let expected_response = format!( + "HTTP/1.1 418 I'm a teapot\r\nContent-Length: 13\r\nServer: HTeaPot/{}\r\n\r\nHello, World!\r\n", + VERSION + ); + let expected_response_list = expected_response.split("\r\n"); + for item in expected_response_list { + assert!(response.contains(item)); + } } -} -#[cfg(test)] -#[test] -fn test_keep_alive_connection() { - let mut response = HttpResponse::new( - HttpStatus::OK, - "Keep-Alive Test", - headers! { - "Connection" => "keep-alive", - "Content-Length" => "15" - }, - ); - - response.base().headers.insert( - "Keep-Alive".to_string(), - format!("timeout={}", KEEP_ALIVE_TTL.as_secs()), - ); - - let response_bytes = response.to_bytes(); - let response_str = String::from_utf8(response_bytes.clone()).unwrap(); - - assert!(response_str.contains("HTTP/1.1 200 OK")); - assert!(response_str.contains("Content-Length: 15")); - assert!(response_str.contains("Connection: keep-alive")); - assert!(response_str.contains("Keep-Alive: timeout=10")); - assert!(response_str.contains("Server: HTeaPot/")); - assert!(response_str.contains("Keep-Alive Test")); - - let mut second_response = HttpResponse::new( - HttpStatus::OK, - "Second Request", - headers! { - "Connection" => "keep-alive", - "Content-Length" => "14" // Length for "Second Request" - }, - ); - - second_response.base().headers.insert( - "Keep-Alive".to_string(), - format!("timeout={}", KEEP_ALIVE_TTL.as_secs()), - ); - - let second_response_bytes = second_response.to_bytes(); - let second_response_str = String::from_utf8(second_response_bytes.clone()).unwrap(); - - assert!(second_response_str.contains("HTTP/1.1 200 OK")); - assert!(second_response_str.contains("Content-Length: 14")); - assert!(second_response_str.contains("Connection: keep-alive")); - assert!(second_response_str.contains("Keep-Alive: timeout=10")); - assert!(second_response_str.contains("Server: HTeaPot/")); - assert!(second_response_str.contains("Second Request")); -} + #[test] + fn test_keep_alive_connection() { + let mut response = HttpResponse::new( + HttpStatus::OK, + "Keep-Alive Test", + headers! { + "Connection" => "keep-alive", + "Content-Length" => "15" + }, + ); + + response.base().headers.insert( + "Keep-Alive".to_string(), + format!("timeout={}", KEEP_ALIVE_TTL.as_secs()), + ); + + let response_bytes = response.to_bytes(); + let response_str = String::from_utf8(response_bytes.clone()).unwrap(); + + assert!(response_str.contains("HTTP/1.1 200 OK")); + assert!(response_str.contains("Content-Length: 15")); + assert!(response_str.contains("Connection: keep-alive")); + assert!(response_str.contains("Keep-Alive: timeout=10")); + assert!(response_str.contains("Server: HTeaPot/")); + assert!(response_str.contains("Keep-Alive Test")); + + let mut second_response = HttpResponse::new( + HttpStatus::OK, + "Second Request", + headers! { + "Connection" => "keep-alive", + "Content-Length" => "14" // Length for "Second Request" + }, + ); + + second_response.base().headers.insert( + "Keep-Alive".to_string(), + format!("timeout={}", KEEP_ALIVE_TTL.as_secs()), + ); + + let second_response_bytes = second_response.to_bytes(); + let second_response_str = String::from_utf8(second_response_bytes.clone()).unwrap(); + + assert!(second_response_str.contains("HTTP/1.1 200 OK")); + assert!(second_response_str.contains("Content-Length: 14")); + assert!(response_str.contains("Connection: keep-alive")); + assert!(response_str.contains("Keep-Alive: timeout=10")); + assert!(response_str.contains("Server: HTeaPot/")); + assert!(second_response_str.contains("Second Request")); + } +} \ No newline at end of file From b3ab116baac706d36c89b0373376c14659f8a638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LoboGuardian=20=F0=9F=90=BA?= Date: Sun, 13 Apr 2025 16:23:59 -0400 Subject: [PATCH 07/16] Reestructure Cargo.toml, and added no-readme to docs --- Cargo.toml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 495bbc9..9c789b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,17 @@ [package] -edition = "2024" name = "hteapot" version = "0.5.0" -exclude = ["config.toml", "demo/", "readme.md"] -license = "MIT" -keywords = ["HTTP", "HTTP-SERVER"] +edition = "2024" +authors = ["Alb Ruiz G. "] description = "HTeaPot is a lightweight HTTP server library designed to be easy to use and extend." +license = "MIT" +readme = "README.md" +documentation = "https://docs.rs/hteapot/" homepage = "https://github.com/az107/HTeaPot" repository = "https://github.com/az107/HTeaPot" -readme = "readme.md" -categories = ["network-programming", "web-programming"] -authors = ["Alb Ruiz G. "] +keywords = ["http", "server", "web", "lightweight", "rust"] +categories = ["network-programming", "web-programming", "command-line-utilities"] +exclude = ["config.toml", "demo/", "README.md"] [lib] name = "hteapot" @@ -18,3 +19,6 @@ path = "src/hteapot/mod.rs" [[bin]] name = "hteapot" + +[package.metadata.docs.rs] +no-readme = true \ No newline at end of file From 3c2fc7d6cae31bd47cc47f89393f9a6bc58ae86c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LoboGuardian=20=F0=9F=90=BA?= Date: Sun, 13 Apr 2025 16:24:26 -0400 Subject: [PATCH 08/16] REDO README.md changes --- README.md | 62 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 3137911..d1bcc20 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,19 @@ A high-performance, lightweight HTTP server and library built in Rust. HTeaPot is designed to deliver exceptional performance for modern web applications while maintaining a simple and intuitive API. -## Features +## 📚 Table of Contents + +- [Features](#--features) +- [Getting Started](#-getting-started) + - [Standalone Server](#standalone-server) + - [As a Library](#as-a-library) +- [Performance](#-performance) +- [Roadmap](#-roadmap) +- [Contributing](#-contributing) +- [License](#-license) +- [Acknowledgments](#-acknowledgments) + +## ✨ Features ### Exceptional Performance - **Threaded Architecture**: Powered by a custom-designed thread system that handles **70,000+ requests per second** @@ -34,9 +46,9 @@ A high-performance, lightweight HTTP server and library built in Rust. HTeaPot i - **Extensible Design**: Easily customize behavior for specific use cases - **Lightweight Footprint**: Zero dependencies and efficient resource usage -## Getting Started +## 🚀 Getting Started -### Installation +### 🔧 Installation ```bash # Install from crates.io @@ -48,11 +60,11 @@ cd hteapot cargo build --release ``` -### Standalone Server +### 🖥️ Running the Server -#### Using a configuration file: +#### Option 1: With Config -Create a `config.toml` file: +1. Create a `config.toml` file: ```toml [HTEAPOT] @@ -61,27 +73,27 @@ host = "localhost" # The host address to bind to root = "public" # The root directory to serve files from ``` -Run the server: +2. Run the server: ```bash hteapot ./config.toml ``` -#### Quick serve a directory: +#### Option 2: Quick Serve ```bash hteapot -s ./public/ ``` -### As a Library +### 🦀 Using as a Library -1. Add HTeaPot to your project: +1. Add HTeaPot to your ```Cargo.toml``` project: ```bash cargo add hteapot ``` -2. Implement in your code: +2. Implement in your code: ```example``` ```rust use hteapot::{HttpStatus, HttpResponse, Hteapot, HttpRequest}; @@ -97,16 +109,16 @@ fn main() { } ``` -## Performance +## 📊 Performance HTeaPot has been benchmarked against other popular HTTP servers, consistently demonstrating excellent metrics: -| Metric | HTeaPot | Industry Average | -|---------------|---------|------------------| -| Requests/sec | 70,000+ | 30,000-50,000 | -| Error rate | <0.1% | 0.5-2% | -| Latency (p99) | 5ms | 15-30ms | -| Memory usage | Low | Moderate | +| Metric | HTeaPot | Industry Average | +|---------------|---------------|------------------------| +| Requests/sec | 70,000+ req/s | 30,000 - 50,000 req/s | +| Error rate | < 0.1% | 0.5% - 2% | +| Latency (p99) | 5ms | 15ms - 30ms | +| Memory usage | Low | Moderate | ## Roadmap @@ -122,7 +134,19 @@ HTeaPot has been benchmarked against other popular HTTP servers, consistently de ## Contributing -We welcome contributions from the community! See our [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to get involved. +We welcome contributions from the community! To get started: + +```sh +# Format the code +cargo fmt + +# Lint for warnings +cargo clippy --all-targets --all-features + +# Run tests +cargo test +``` +See [CONTRIBUTING.md](https://github.com/Az107/HTeaPot/wiki/Contributing) for more details. ## License From 146a9a3f537221e995d37d1bc50b8d0c5ab7be39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LoboGuardian=20=F0=9F=90=BA?= Date: Sun, 13 Apr 2025 16:26:17 -0400 Subject: [PATCH 09/16] docs(cache): document and improve TTL-based cache module - Added doc comments to Cache struct and all methods - Explained TTL logic and usage with examples - Suggest simplified internal logic using idiomatic match patterns - Added inline comments for future maintainability --- src/cache.rs | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/cache.rs b/src/cache.rs index 53107bc..16f1c02 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,18 +1,38 @@ // Written by Alberto Ruiz, 2024-11-05 +// // Config module: handles application configuration setup and parsing. // This module defines structs and functions to load and validate // configuration settings from files, environment variables, or other sources. + use std::collections::HashMap; use std::time; use std::time::SystemTime; +/// A simple in-memory cache with TTL (time-to-live) support. +/// +/// This cache stores byte arrays (`Vec`) along with an expiration timestamp. +/// When a cached entry is fetched, the TTL is validated. If it's expired, the +/// item is removed and `None` is returned. +/// +/// Note: Currently not generic, but could be extended in the future to support +/// other data types. +/// +/// # Example +/// ``` +/// let mut cache = Cache::new(60); // 60 seconds TTL +/// cache.set("hello".into(), vec![1, 2, 3]); +/// let data = cache.get("hello".into()); +/// assert!(data.is_some()); +/// ``` pub struct Cache { - //TODO: consider make it generic + // TODO: consider make it generic + // The internal store: (data, expiration timestamp) data: HashMap, u64)>, max_ttl: u64, } impl Cache { + /// Creates a new `Cache` with the specified TTL in seconds. pub fn new(max_ttl: u64) -> Self { Cache { data: HashMap::new(), @@ -20,6 +40,7 @@ impl Cache { } } + /// Creates a new `Cache` with the specified TTL in seconds. fn validate_ttl(&self, ttl: u64) -> bool { let now = SystemTime::now(); let since_epoch = now @@ -29,6 +50,7 @@ impl Cache { secs < ttl } + /// Computes the expiration timestamp for a new cache entry. fn get_ttl(&self) -> u64 { let now = SystemTime::now(); let since_epoch = now @@ -38,10 +60,14 @@ impl Cache { secs + self.max_ttl } + /// Stores data in the cache with the given key and a TTL. pub fn set(&mut self, key: String, data: Vec) { self.data.insert(key, (data, self.get_ttl())); } + /// Retrieves data from the cache if it exists and hasn't expired. + /// + /// Removes and returns `None` if the TTL has expired. pub fn get(&mut self, key: String) -> Option> { let r = self.data.get(&key); if r.is_some() { @@ -55,5 +81,16 @@ impl Cache { } else { None } + + // Alternative implementation using pattern matching + // This is a more idiomatic way to handle the Option type in Rust. + // match self.data.get(&key) { + // Some((data, ttl)) if self.validate_ttl(*ttl) => Some(data.clone()), + // Some(_) => { + // self.data.remove(&key); + // None + // } + // None => None, + // } } } From 733620c8766b7a15c84c271f6759edf4fce7e0e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LoboGuardian=20=F0=9F=90=BA?= Date: Sun, 13 Apr 2025 16:30:19 -0400 Subject: [PATCH 10/16] docs(utils): document and improve get_mime_tipe function Added detailed doc comments explaining MIME type detection logic Suggest improved extension parsing with safer unwrap pattern Clarified default fallback to "application/octet-stream" Included example usage in doc comment for better discoverability --- src/utils.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/utils.rs b/src/utils.rs index 65561a4..8e56ca4 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,12 +1,35 @@ use std::path::Path; +/// Returns the MIME type based on the file extension of a given path. +/// +/// This function maps common file extensions to their appropriate +/// `Content-Type` values for HTTP responses. +/// +/// If the extension is unrecognized or missing, it defaults to +/// `"application/octet-stream"` for safe binary delivery. +/// +/// # Arguments +/// +/// * `path` - A file path as a `String` from which to extract the extension. +/// +/// # Examples +/// +/// ``` +/// let mime = get_mime_tipe(&"file.html".to_string()); +/// assert_eq!(mime, "text/html; charset=utf-8"); +/// ``` pub fn get_mime_tipe(path: &String) -> String { let extension = Path::new(path.as_str()) .extension() .map(|ext| ext.to_str().unwrap_or("")) .unwrap_or(""); + + // 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", "js" => "text/javascript", "mjs" => "text/javascript", From f75a595fc7cb0ce2b3c1ddcfda62983f975c77b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LoboGuardian=20=F0=9F=90=BA?= Date: Sun, 13 Apr 2025 16:33:34 -0400 Subject: [PATCH 11/16] docs(logger): document async logger, log levels, and timestamp formatting Added doc comments to Logger, LogLevel, and SimpleTime Described internal buffer strategy and log flushing behavior Clarified component-based logging and custom timestamp formatting Included usage test for multi-component logging --- src/logger.rs | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/logger.rs b/src/logger.rs index f61b19f..c7799ef 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -5,8 +5,6 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use std::fmt; use std::sync::Arc; - - /// Differnt log levels #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Copy)] #[allow(dead_code)] @@ -32,6 +30,7 @@ impl fmt::Display for LogLevel { } } +/// A helper struct for generating formatted timestamps in `YYYY-MM-DD HH:MM:SS.sss` format. struct SimpleTime; impl SimpleTime { fn epoch_to_ymdhms(seconds: u64, nanos: u32) -> (i32, u32, u32, u32, u32, u32, u32) { @@ -89,6 +88,8 @@ impl SimpleTime { (year, month as u32 + 1, day as u32, hour, minute, second, millis) } + + /// Returns a formatted timestamp string for the current system time. pub fn get_current_timestamp() -> String { let now = SystemTime::now(); let since_epoch = now.duration_since(UNIX_EPOCH).expect("Time went backwards"); @@ -103,7 +104,7 @@ impl SimpleTime { } } -// Log message with metadata +/// Internal message structure containing log metadata. struct LogMessage { timestamp: String, level: LogLevel, @@ -111,11 +112,16 @@ struct LogMessage { content: String, } +/// Core logger implementation running in a background thread. struct LoggerCore { tx: Sender, _thread: JoinHandle<()>, } +/// A non-blocking, multi-threaded logger with support for log levels and components. +/// +/// Logs are sent via a background thread to avoid blocking the main thread. +/// The logger buffers messages and flushes them periodically or based on buffer size. pub struct Logger { core: Arc, component: Arc, @@ -123,6 +129,7 @@ pub struct Logger { } impl Logger { + /// Creates a new logger with the given writer, minimum log level, and component name. pub fn new( mut writer: W, min_level: LogLevel, @@ -134,6 +141,7 @@ impl Logger { let mut buff = Vec::new(); let mut max_size = 100; let timeout = Duration::from_secs(1); + loop { let msg = rx.recv_timeout(timeout); match msg { @@ -148,6 +156,7 @@ impl Logger { Err(_) => break, } + // Flush if timeout or buffer threshold reached if last_flush.elapsed() >= timeout || buff.len() >= max_size { if !buff.is_empty() { if buff.len() >= max_size { @@ -178,7 +187,7 @@ impl Logger { } } - // New logger with different component but sharing same output + /// Creates a new logger instance with a different component name, but sharing same output. pub fn with_component(&self, component: &str) -> Logger { Logger { core: Arc::clone(&self.core), @@ -187,6 +196,7 @@ impl Logger { } } + /// Sends a log message with the given level and content. pub fn log(&self, level: LogLevel, content: String) { if level < self.min_level { return; @@ -198,10 +208,12 @@ impl Logger { component: (*self.component).clone(), content, }; + // Send the log message to the channel let _ = self.core.tx.send(log_msg); } + /// Logs a DEBUG-level message. pub fn debug(&self, content: String) { self.log(LogLevel::DEBUG, content); } @@ -233,6 +245,17 @@ impl Logger { } } +impl Clone for Logger { + fn clone(&self) -> Self { + Logger { + core: Arc::clone(&self.core), + component: Arc::clone(&self.component), + min_level: self.min_level, + } + } +} + + #[cfg(test)] mod tests { use super::*; From 10f6a028734cdaa862df772ef355b5097b80cf65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LoboGuardian=20=F0=9F=90=BA?= Date: Sun, 13 Apr 2025 16:35:05 -0400 Subject: [PATCH 12/16] Comment atomic::{AtomicBool, Ordering} --- src/hteapot/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hteapot/mod.rs b/src/hteapot/mod.rs index 7e572f5..96378e8 100644 --- a/src/hteapot/mod.rs +++ b/src/hteapot/mod.rs @@ -25,7 +25,7 @@ mod status; // Status code mapping // Internal types used for connection management use self::response::{EmptyHttpResponse, HttpResponseCommon, IterError}; -use std::sync::atomic::{AtomicBool, Ordering}; +// use std::sync::atomic::{AtomicBool, Ordering}; // Public API exposed by this module pub use self::methods::HttpMethod; From fe490778bc57f125c5e1b6531b08a411c5a7d4e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LoboGuardian=20=F0=9F=90=BA?= Date: Sun, 13 Apr 2025 16:36:09 -0400 Subject: [PATCH 13/16] docs(config): document TOML parser and configuration loading Added module-level comments and doc comments to Config and TOMLtype Explained custom schema system and value parsing logic Suggest improved clarity of get2 trait method with inline commentary Added panic descriptions for TOML parsing edge cases --- src/config.rs | 96 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 8 deletions(-) diff --git a/src/config.rs b/src/config.rs index fb689a5..42e68c1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,9 +1,15 @@ // Written by Alberto Ruiz 2024-04-07 (Happy 3th monthsary) -// This is the config module, it will load the configuration -// file and provide the settings +// +// This is the config module: responsible for loading application configuration +// from a file and providing structured access to settings. use std::{any::Any, collections::HashMap, fs}; +/// Dynamic TOML value representation. +/// +/// Each parsed TOML key is stored as a `TOMLtype`, which can be a string, +/// number, float, or boolean. This allows the custom parser to support basic +/// TOML configuration without external dependencies. #[derive(Clone, Debug)] pub enum TOMLtype { Text(String), @@ -12,8 +18,12 @@ pub enum TOMLtype { Boolean(bool), } +/// A section of the parsed TOML file, keyed by strings and holding `TOMLtype` values. type TOMLSchema = HashMap; + +/// Trait for safely extracting typed values from a `TOMLSchema`. trait Schema { + /// Attempts to retrieve a value of type `T` from the schema by key. fn get2(&self, key: &str) -> Option; } @@ -21,12 +31,16 @@ impl Schema for TOMLSchema { fn get2(&self, key: &str) -> Option { let value = self.get(key)?; let value = value.clone(); + + // Convert the TOMLtype to a dynamically typed value let any_value: Box = match value { TOMLtype::Text(d) => Box::new(d), TOMLtype::Number(d) => Box::new(d), TOMLtype::Float(d) => Box::new(d), TOMLtype::Boolean(d) => Box::new(d), }; + + // Try to downcast to the requested type let r = any_value.downcast_ref::().cloned(); if r.is_none() { println!("{} is none", key); @@ -35,22 +49,37 @@ impl Schema for TOMLSchema { } } +/// Parses a TOML-like string into a nested `HashMap` structure. +/// +/// This is a minimal, custom TOML parser that supports: +/// - Sections (e.g., `[HTEAPOT]`) +/// - Key-value pairs with types: string, bool, u16, f64 +/// - Ignores comments and blank lines +/// +/// # Panics +/// Panics if a numeric or float value fails to parse. pub fn toml_parser(content: &str) -> HashMap { let mut map = HashMap::new(); let mut submap = HashMap::new(); let mut title = "".to_string(); + let lines = content.split("\n"); for line in lines { if line.starts_with("#") || line.is_empty() { continue; } + + // Remove trailing inline comments let line = if line.contains('#') { let parts = line.split("#").collect::>(); parts[0].trim() } else { line.trim() }; + + // Skip empty lines if line.starts_with("[") && line.ends_with("]") { + // New section starts let key = line.trim_matches('[').trim_matches(']').trim(); if submap.len() != 0 && title.len() != 0 { map.insert(title.clone(), submap.clone()); @@ -59,10 +88,14 @@ pub fn toml_parser(content: &str) -> HashMap { submap = HashMap::new(); continue; } + + // Split key and value let parts = line.split("=").collect::>(); if parts.len() != 2 { continue; } + + // Remove leading and trailing whitespace let key = parts[0] .trim() .trim_end_matches('"') @@ -70,6 +103,8 @@ pub fn toml_parser(content: &str) -> HashMap { if key.is_empty() { continue; } + + // Remove leading and trailing whitespace let value = parts[1].trim(); let value = if value.contains('\'') || value.contains('"') { let value = value.trim_matches('"').trim(); @@ -90,23 +125,42 @@ pub fn toml_parser(content: &str) -> HashMap { } TOMLtype::Number(value.unwrap()) }; + + // Suggested alternative parsing logic + // let value = if value.contains('\'') || value.contains('"') { + // TOMLtype::Text(value.trim_matches('"').to_string()) + // } else if value.eq_ignore_ascii_case("true") || value.eq_ignore_ascii_case("false") { + // TOMLtype::Boolean(value.eq_ignore_ascii_case("true")) + // } else if value.contains('.') { + // TOMLtype::Float(value.parse().expect("Error parsing float")) + // } else { + // TOMLtype::Number(value.parse().expect("Error parsing number")) + // }; + + // Insert the key-value pair into the submap submap.insert(key.to_string(), value); } + + // Insert the last section if it exists map.insert(title, submap.clone()); map } +/// Configuration for the HTeaPot server. +/// +/// This struct holds the runtime settings for the server, +/// such as host, port, caching behavior, and proxy rules. #[derive(Debug)] pub struct Config { - pub port: u16, // Port number to listen - pub host: String, // Host name or IP - pub root: String, // Root directory to serve files + pub port: u16, // Port number to listen + pub host: String, // Host name or IP + pub root: String, // Root directory to serve files pub cache: bool, pub cache_ttl: u16, pub threads: u16, pub log_file: Option, - pub index: String, // Index file to serve by default - //pub error: String, // Error file to serve when a file is not found + pub index: String, // Index file to serve by default + // pub error: String, // Error file to serve when a file is not found pub proxy_rules: HashMap, } @@ -122,6 +176,7 @@ impl Config { // } // } + /// Returns a default configuration with sensible values. pub fn new_default() -> Config { Config { port: 8080, @@ -137,13 +192,21 @@ impl Config { } } + /// Loads configuration from a TOML file, returning defaults on failure. + /// + /// Expects the file to contain `[HTEAPOT]` and optionally `[proxy]` sections. + /// Supports type-safe extraction for each expected key. pub fn load_config(path: &str) -> Config { let content = fs::read_to_string(path); if content.is_err() { return Config::new_default(); } + + // Read the file content let content = content.unwrap(); let map = toml_parser(&content); + + // Extract proxy rules let mut proxy_rules: HashMap = HashMap::new(); let proxy_map = map.get("proxy"); if proxy_map.is_some() { @@ -151,7 +214,7 @@ impl Config { for k in proxy_map.keys() { let url = proxy_map.get2(k); if url.is_none() { - println!(); + println!("Missing or invalid proxy URL for key: {}", k); continue; } let url = url.unwrap(); @@ -159,7 +222,24 @@ impl Config { } } + // Suggested alternative parsing logic + // if let Some(proxy_map) = map.get("proxy") { + // for k in proxy_map.keys() { + // if let Some(url) = proxy_map.get2(k) { + // proxy_rules.insert(k.clone(), url); + // } else { + // println!("Missing or invalid proxy URL for key: {}", k); + // } + // } + // } + + // Extract main configuration let map = map.get("HTEAPOT").unwrap(); + + // Suggested alternative parsing logic (Not working) + // let map = map.get("HTEAPOT").unwrap_or(&TOMLSchema::new()); + + Config { port: map.get2("port").unwrap_or(8080), host: map.get2("host").unwrap_or("".to_string()), From fc5666afe4872e63569547871ed60650fecb08aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LoboGuardian=20=F0=9F=90=BA?= Date: Sun, 13 Apr 2025 17:13:35 -0400 Subject: [PATCH 14/16] docs(main): Improve readability of content response logic - Refactor match block to enhance clarity in handling `Some(c)` and `None` cases. - Add inline comments to explain the return values for both cases. - Use more consistent formatting and whitespace for better visual separation. --- src/main.rs | 230 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 189 insertions(+), 41 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7aa096a..cac0d5c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,39 @@ +//! # ☕ Hteapot Web Server +//! +//! **Hteapot** is a fast, lightweight, and highly extensible HTTP server written in idiomatic Rust. +//! Designed for simplicity and performance, it supports: +//! +//! - 🔁 **Reverse Proxying** — Forward requests to other servers based on custom routing rules +//! - 📁 **Static File Serving** — Serve local files from a configurable directory +//! - ⚡ **In-Memory Caching** — Reduce disk I/O with optional response caching +//! - 📜 **Structured Logging** — Toggle between file or console logging with fine-grained log levels +//! - 🧵 **Multithreading** — Handle requests concurrently using a configurable thread pool +//! +//! ## Use Cases +//! +//! - Local development server +//! - Lightweight reverse proxy +//! - Static site deployment +//! - Embedded use in tools or microservices +//! +//! ## Entry Point +//! +//! This crate's primary entry point is the `main.rs` module. It sets up configuration, +//! logging, caching, and request routing via the [`Hteapot`](crate::hteapot::Hteapot) engine. +//! +//! ## Example +//! +//! ```sh +//! $ hteapot ./config.toml +//! ``` +//! +//! Or serve a single file quickly: +//! +//! ```sh +//! $ hteapot --serve ./index.html +//! ``` +//! +//! See the [`config`](crate::config) module for configuration options and structure. mod cache; mod config; pub mod hteapot; @@ -18,10 +54,28 @@ use std::time::Instant; const VERSION: &str = env!("CARGO_PKG_VERSION"); -// Safely join paths and ensure the result is within the root directory -// Try to canonicalize to resolve any '..' components -// Ensure the canonicalized path is still within the root directory -// Check if the path exists before canonicalizing +/// Attempts to safely join a root directory and 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 +/// +/// This protects against directory traversal vulnerabilities, such as accessing +/// files outside of the intended root (e.g., `/etc/passwd`). +/// +/// # Arguments +/// * `root` - The root directory from which serving is allowed. +/// * `requested_path` - The path requested by the client (usually from the URL). +/// +/// # Returns +/// `Some(PathBuf)` if the resolved path exists and is within the root. `None` otherwise. +/// +/// # Example +/// ``` +/// let safe_path = safe_join_paths("/var/www", "/index.html"); +/// assert!(safe_path.unwrap().ends_with("index.html")); +/// ``` fn safe_join_paths(root: &str, requested_path: &str) -> Option { let root_path = Path::new(root).canonicalize().ok()?; let requested_full_path = root_path.join(requested_path.trim_start_matches("/")); @@ -39,6 +93,17 @@ fn safe_join_paths(root: &str, requested_path: &str) -> Option { } } +/// Determines whether a given HTTP request should be proxied based on the configuration. +/// +/// If a matching proxy rule is found in `config.proxy_rules`, the function rewrites the +/// request path and updates the `Host` header accordingly. +/// +/// # 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`. 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); @@ -61,22 +126,59 @@ fn is_proxy(config: &Config, req: HttpRequest) -> Option<(String, HttpRequest)> None } -// Change from &string to &PathBuf cos PathBuf explicitly represents a file system path as an owned buffer, -// making it clear that the data is intended to be a path rather than just any string. -// This reduces errors by enforcing the correct type for file system operations. -// Read more here: https://doc.rust-lang.org/std/path/index.html +/// 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) fn serve_file(path: &PathBuf) -> Option> { let r = fs::read(path); if r.is_ok() { Some(r.unwrap()) } else { None } } +// +// Suggest to use .ok()? instead of manual unwrap/if is_ok for more idiomatic error handling: +// fn serve_file(path: &PathBuf) -> Option> { + // fs::read(path).ok() +// } +// +// + + +/// Main entry point of the Hteapot server. +/// +/// Handles command-line interface, config file parsing, optional file-serving mode, +/// logger initialization, and server startup. Incoming requests are processed via +/// proxy rules or served from local files with optional caching. +/// +/// # CLI Usage +/// - `hteapot config.toml` – Start with a full configuration file. +/// - `hteapot --serve ./file.html` – Serve a single file. +/// - `hteapot --help` or `--version` – Show usage info. +/// +/// This function initializes core components: +/// - Configuration (`Config`) +/// - Logging (`Logger`) +/// - Optional response caching +/// - HTTP server via [`Hteapot::new_threaded`](crate::hteapot::Hteapot::new_threaded) fn main() { + // Parse CLI args and handle --help / --version / --serve flags let args = std::env::args().collect::>(); if args.len() == 1 { println!("Hteapot {}", VERSION); println!("usage: {} ", args[0]); return; } + + // Initialize logger based on config or default to stdout let config = match args[1].as_str() { "--help" | "-h" => { println!("Hteapot {}", VERSION); @@ -115,67 +217,90 @@ fn main() { _ => config::Config::load_config(&args[1]), }; + // Determine if the server should proxy all requests let proxy_only = config.proxy_rules.get("/").is_some(); + + // Initialize the logger based on the config or default to stdout if the log file can't be created let logger = match config.log_file.clone() { Some(file_name) => { - let file = fs::File::create(file_name.clone()); - match file { - Ok(file) => Logger::new(file, LogLevel::INFO, "main"), + let file = fs::File::create(file_name.clone()); // Attempt to create the log file + match file { // If creating the file fails, log to stdout instead + Ok(file) => Logger::new(file, LogLevel::INFO, "main"), // If successful, use the file Err(e) => { println!("Failed to create log file: {:?}. Using stdout instead.", e); - Logger::new(io::stdout(), LogLevel::INFO, "main") + Logger::new(io::stdout(), LogLevel::INFO, "main") // Log to stdout } } } - None => Logger::new(io::stdout(), LogLevel::INFO, "main"), + None => Logger::new(io::stdout(), LogLevel::INFO, "main"), // If no log file is specified, use stdout }; - let cache: Mutex = Mutex::new(Cache::new(config.cache_ttl as u64)); + // 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 = 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 let server = Hteapot::new_threaded(config.host.as_str(), config.port, config.threads); + logger.info(format!( "Server started at http://{}:{}", config.host, config.port - )); + )); // Log that the server has started + + // Log whether the cache is enabled based on the config setting if config.cache { logger.info("Cache Enabled".to_string()); } + + // If proxy-only mode is enabled, issue a warning that local paths won't be used if proxy_only { logger .warn("WARNING: All requests are proxied to /. Local paths won't be used.".to_string()); } - // Create component loggers + // 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"); + // Start listening for HTTP requests server.listen(move |req| { - // SERVER CORE - // for each request - let start_time = Instant::now(); - let req_method = req.method.to_str(); - let req_path = req.path.clone(); + // 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 - http_logger.info(format!("Request {} {}", req.method.to_str(), req.path)); + // Log the incoming request method and path + http_logger.info(format!("Request {} {}", req_method, req.path)); let is_proxy = is_proxy(&config, req.clone()); + // 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()); if proxy_only || is_proxy.is_some() { - let (host, proxy_req) = is_proxy.unwrap(); + // If proxying is enabled or this request matches a proxy rule, handle it + 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(); + 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.info(format!( "Proxy request processed in {:.6}ms", - elapsed.as_secs_f64() * 1000.0 + 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, @@ -185,84 +310,107 @@ fn main() { } } - // Safely resolve the requested path + // If the request is not a proxy request, resolve the requested path safely let safe_path_result = if req.path == "/" { - // Handle root path specially + // 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) + Some(index_path) // If index exists, return its path } else { - None + None // If no index exists, return None } } else { - None + 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 directory paths + // 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 + 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 + 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> = if config.cache { + // Lock the cache to ensure thread-safe access let mut cachee = cache.lock().expect("Error locking cache"); - let cache_start = Instant::now(); - let cache_key = req.path.clone(); - let mut r = cachee.get(cache_key.clone()); + let cache_start = Instant::now(); // Track cache operation time + let cache_key = req.path.clone(); // Use the request path as the cache key + let mut r = cachee.get(cache_key.clone()); // Try to get the content from cache if r.is_none() { + // If cache miss, read the file from disk and store it in cache cache_logger.debug(format!("cache miss for {}", cache_key)); r = serve_file(&safe_path); if r.is_some() { + // If the file is read successfully, add it to the cache cache_logger.info(format!("Adding {} to cache", cache_key)); cachee.set(cache_key, r.clone().unwrap()); } } else { + // If cache hit, log it cache_logger.debug(format!("cache hit for {}", cache_key)); } + // Log how long the cache operation took let cache_elapsed = cache_start.elapsed(); cache_logger.debug(format!( "Cache operation completed in {:.6}µs", cache_elapsed.as_micros() )); - r + r // Return the cached content (or None if not found) } else { + // If cache is disabled, read the file from disk serve_file(&safe_path) }; + // Log how long the request took to process let elapsed = start_time.elapsed(); http_logger.info(format!( "Request processed in {:.6}ms", - elapsed.as_secs_f64() * 1000.0 + elapsed.as_secs_f64() * 1000.0 // Log the time taken in milliseconds )); - + + // If content was found, return it with the appropriate headers, otherwise return a 404 match content { Some(c) => { - let headers = headers!("Content-Type" => mimetype, "X-Content-Type-Options" => "nosniff"); + // 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 => HttpResponse::new(HttpStatus::NotFound, "Not found", None), + None => { + // If no content is found, return a 404 Not Found response + HttpResponse::new(HttpStatus::NotFound, "Not found", None) + }, } }); } \ No newline at end of file From bf112b7d712acb032f74101f5edf03f46ed6e488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LoboGuardian=20=F0=9F=90=BA?= Date: Sun, 13 Apr 2025 17:14:56 -0400 Subject: [PATCH 15/16] Fix a typo --- src/main.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index cac0d5c..4b41751 100644 --- a/src/main.rs +++ b/src/main.rs @@ -275,8 +275,6 @@ fn main() { // Log the incoming request method and path http_logger.info(format!("Request {} {}", req_method, req.path)); - let is_proxy = is_proxy(&config, req.clone()); - // 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()); if proxy_only || is_proxy.is_some() { From 154dac695a72b4ebfae95e6bb3318125410e0f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LoboGuardian=20=F0=9F=90=BA?= Date: Sun, 13 Apr 2025 17:23:26 -0400 Subject: [PATCH 16/16] Fixed import on examples --- src/hteapot/methods.rs | 4 +++- src/hteapot/mod.rs | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/hteapot/methods.rs b/src/hteapot/methods.rs index ddacee8..78592f6 100644 --- a/src/hteapot/methods.rs +++ b/src/hteapot/methods.rs @@ -23,7 +23,7 @@ impl HttpMethod { /// /// # Examples /// ``` - /// use your_crate::HttpMethod; + /// use hteapot::HttpMethod; /// /// let m = HttpMethod::from_str("GET"); /// assert_eq!(m, HttpMethod::GET); @@ -52,6 +52,8 @@ impl HttpMethod { /// /// # Examples /// ``` + /// use hteapot::HttpMethod; + /// /// let method = HttpMethod::GET; /// assert_eq!(method.to_str(), "GET"); /// ``` diff --git a/src/hteapot/mod.rs b/src/hteapot/mod.rs index 96378e8..c03acd3 100644 --- a/src/hteapot/mod.rs +++ b/src/hteapot/mod.rs @@ -54,6 +54,7 @@ const KEEP_ALIVE_TTL: Duration = Duration::from_secs(10); /// /// # Example /// ```rust +/// use hteapot::headers; /// let headers = headers! { /// "Content-Type" => "text/html", /// "X-Custom" => "value"