diff --git a/src/main.rs b/src/main.rs index ffb911b..7aa096a 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 +// 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("/")); + + if !requested_full_path.exists() { + return None; + } + + let canonical_path = requested_full_path.canonicalize().ok()?; + + 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,11 @@ fn is_proxy(config: &Config, req: HttpRequest) -> Option<(String, HttpRequest)> None } -fn serve_file(path: &String) -> Option> { +// 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 } } @@ -118,7 +141,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 +149,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 +156,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 +185,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 +248,7 @@ fn main() { )); r } else { - serve_file(&full_path) + serve_file(&safe_path) }; let elapsed = start_time.elapsed(); @@ -205,9 +256,13 @@ 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 headers = headers!("Content-Type" => mimetype, "X-Content-Type-Options" => "nosniff"); + HttpResponse::new(HttpStatus::OK, c, headers) + }, None => HttpResponse::new(HttpStatus::NotFound, "Not found", None), } }); -} +} \ No newline at end of file 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