From 3b4345742af04f139b477c0544a9e8874f93705e Mon Sep 17 00:00:00 2001 From: miky-rola Date: Thu, 10 Apr 2025 12:35:41 +0000 Subject: [PATCH 1/5] Fix path sanitization --- src/main.rs | 106 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 80 insertions(+), 26 deletions(-) diff --git a/src/main.rs b/src/main.rs index ffb911b..b51be6e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,10 +4,8 @@ pub mod hteapot; mod logger; mod utils; -use std::{fs, io}; - +use std::{fs, io, path::PathBuf}; use std::path::Path; - use std::sync::Mutex; use cache::Cache; @@ -20,6 +18,27 @@ use std::time::Instant; const VERSION: &str = env!("CARGO_PKG_VERSION"); +// Safely join paths and ensure the result is within the root directory +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("/")); + + // Check if the path exists before canonicalizing + if !requested_full_path.exists() { + return None; + } + + // Try to canonicalize to resolve any '..' components + let canonical_path = requested_full_path.canonicalize().ok()?; + + // Ensure the canonicalized path is still within the root directory + if canonical_path.starts_with(&root_path) { + Some(canonical_path) + } else { + 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); @@ -42,7 +61,7 @@ fn is_proxy(config: &Config, req: HttpRequest) -> Option<(String, HttpRequest)> None } -fn serve_file(path: &String) -> Option> { +fn serve_file(path: &PathBuf) -> Option> { let r = fs::read(path); if r.is_ok() { Some(r.unwrap()) } else { None } } @@ -118,7 +137,7 @@ fn main() { } if proxy_only { logger - .warn("WARNING: All requests are proxied to /. Local paths won’t be used.".to_string()); + .warn("WARNING: All requests are proxied to /. Local paths won't be used.".to_string()); } // Create component loggers @@ -126,7 +145,6 @@ fn main() { let cache_logger = logger.with_component("cache"); let http_logger = logger.with_component("http"); - server.listen(move |req| { // SERVER CORE // for each request @@ -134,7 +152,6 @@ fn main() { let req_method = req.method.to_str(); let req_path = req.path.clone(); - http_logger.info(format!("Request {} {}", req.method.to_str(), req.path)); let is_proxy = is_proxy(&config, req.clone()); @@ -164,30 +181,60 @@ fn main() { } } - let mut full_path = format!("{}{}", config.root, req.path.clone()); - if Path::new(full_path.as_str()).is_dir() { - let separator = if full_path.ends_with('/') { "" } else { "/" }; - full_path = format!("{}{}{}", full_path, separator, config.index); - } + // Safely resolve the requested path + let safe_path_result = if req.path == "/" { + // Handle root path specially + let root_path = Path::new(&config.root).canonicalize(); + if root_path.is_ok() { + let index_path = root_path.unwrap().join(&config.index); + if index_path.exists() { + Some(index_path) + } else { + None + } + } else { + None + } + } else { + safe_join_paths(&config.root, &req.path) + }; - if !Path::new(full_path.as_str()).exists() { - http_logger.warn(format!("Path {} does not exist", req.path)); - return HttpResponse::new(HttpStatus::NotFound, "Not found", None); - } - let mimetype = get_mime_tipe(&full_path); + // Handle directory paths + let safe_path = match safe_path_result { + Some(path) => { + if path.is_dir() { + let index_path = path.join(&config.index); + if index_path.exists() { + index_path + } else { + http_logger.warn(format!("Index file not found in directory: {}", req.path)); + return HttpResponse::new(HttpStatus::NotFound, "Index not found", None); + } + } else { + path + } + }, + None => { + http_logger.warn(format!("Path not found or access denied: {}", req.path)); + return HttpResponse::new(HttpStatus::NotFound, "Not found", None); + } + }; + + let mimetype = get_mime_tipe(&safe_path.to_string_lossy().to_string()); let content: Option> = if config.cache { let mut cachee = cache.lock().expect("Error locking cache"); let cache_start = Instant::now(); - let mut r = cachee.get(req.path.clone()); + let cache_key = req.path.clone(); + let mut r = cachee.get(cache_key.clone()); if r.is_none() { - cache_logger.debug(format!("cache miss for {}", req.path)); - r = serve_file(&full_path); + cache_logger.debug(format!("cache miss for {}", cache_key)); + r = serve_file(&safe_path); if r.is_some() { - cache_logger.info(format!("Adding {} to cache", req.path)); - cachee.set(req.path.clone(), r.clone().unwrap()); + cache_logger.info(format!("Adding {} to cache", cache_key)); + cachee.set(cache_key, r.clone().unwrap()); } } else { - cache_logger.debug(format!("cache hit for {}", req.path)); + cache_logger.debug(format!("cache hit for {}", cache_key)); } let cache_elapsed = cache_start.elapsed(); @@ -197,7 +244,7 @@ fn main() { )); r } else { - serve_file(&full_path) + serve_file(&safe_path) }; let elapsed = start_time.elapsed(); @@ -205,9 +252,16 @@ fn main() { "Request processed in {:.6}ms", elapsed.as_secs_f64() * 1000.0 )); + match content { - Some(c) => HttpResponse::new(HttpStatus::OK, c, headers!("Content-Type" => mimetype)), + Some(c) => { + let mut headers = headers!("Content-Type" => mimetype); + if let Some(ref mut map) = headers { // Unwrap the Option safely + map.insert("X-Content-Type-Options".to_string(), "nosniff".to_string()); + } + HttpResponse::new(HttpStatus::OK, c, headers) // Pass the Option directly + }, None => HttpResponse::new(HttpStatus::NotFound, "Not found", None), } }); -} +} \ No newline at end of file From 958242851feb4e4ec092b2cb3a8eb055724c96ff Mon Sep 17 00:00:00 2001 From: miky-rola Date: Thu, 10 Apr 2025 12:48:50 +0000 Subject: [PATCH 2/5] Add comments to changes --- src/main.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index b51be6e..2037111 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,19 +19,20 @@ 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 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("/")); - // Check if the path exists before canonicalizing if !requested_full_path.exists() { return None; } - // Try to canonicalize to resolve any '..' components + let canonical_path = requested_full_path.canonicalize().ok()?; - // Ensure the canonicalized path is still within the root directory if canonical_path.starts_with(&root_path) { Some(canonical_path) } else { @@ -61,6 +62,9 @@ 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. fn serve_file(path: &PathBuf) -> Option> { let r = fs::read(path); if r.is_ok() { Some(r.unwrap()) } else { None } From 7aa68d49a7f2f1a60e025fea2d16b2b804af9e0b Mon Sep 17 00:00:00 2001 From: miky-rola Date: Thu, 10 Apr 2025 12:50:49 +0000 Subject: [PATCH 3/5] Added support for many more file types --- src/utils.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index a007fc9..65561a4 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,20 +3,70 @@ use std::path::Path; pub fn get_mime_tipe(path: &String) -> String { let extension = Path::new(path.as_str()) .extension() - .unwrap() - .to_str() - .unwrap(); + .map(|ext| ext.to_str().unwrap_or("")) + .unwrap_or(""); + let mimetipe = match extension { + "html" | "htm" => "text/html; charset=utf-8", "js" => "text/javascript", - "json" => "application/json", + "mjs" => "text/javascript", "css" => "text/css", - "html" => "text/html; charset=utf-8", + "json" => "application/json", + "xml" => "application/xml", + "txt" => "text/plain", + "md" => "text/markdown", + "csv" => "text/csv", + + // Images "ico" => "image/x-icon", - _ => "text/plain", + "png" => "image/png", + "jpg" | "jpeg" => "image/jpeg", + "gif" => "image/gif", + "svg" => "image/svg+xml", + "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", + "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "xls" => "application/vnd.ms-excel", + "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", }; mimetipe.to_string() } //TODO: make a parser args to config -//pub fn args_to_dict(list: Vec) -> HashMap {} +//pub fn args_to_dict(list: Vec) -> HashMap {} \ No newline at end of file From 46d24e77fceb776f2b8cfbcbc881c35a27d44d85 Mon Sep 17 00:00:00 2001 From: miky-rola Date: Thu, 10 Apr 2025 12:55:35 +0000 Subject: [PATCH 4/5] Add reference to the Pathbuf --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index 2037111..5b1ca2d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,6 +65,7 @@ fn is_proxy(config: &Config, req: HttpRequest) -> Option<(String, HttpRequest)> // 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 fn serve_file(path: &PathBuf) -> Option> { let r = fs::read(path); if r.is_ok() { Some(r.unwrap()) } else { None } From 9f2cf669cbb2fd393c0ed9c4bb676cc04417313d Mon Sep 17 00:00:00 2001 From: miky-rola Date: Thu, 10 Apr 2025 13:21:30 +0000 Subject: [PATCH 5/5] Handle headers with macros --- src/main.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5b1ca2d..7aa096a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,7 +30,6 @@ fn safe_join_paths(root: &str, requested_path: &str) -> Option { return None; } - let canonical_path = requested_full_path.canonicalize().ok()?; if canonical_path.starts_with(&root_path) { @@ -260,11 +259,8 @@ fn main() { match content { Some(c) => { - let mut headers = headers!("Content-Type" => mimetype); - if let Some(ref mut map) = headers { // Unwrap the Option safely - map.insert("X-Content-Type-Options".to_string(), "nosniff".to_string()); - } - HttpResponse::new(HttpStatus::OK, c, headers) // Pass the Option directly + 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), }