Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 81 additions & 26 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<PathBuf> {
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);
Expand All @@ -42,7 +61,11 @@ fn is_proxy(config: &Config, req: HttpRequest) -> Option<(String, HttpRequest)>
None
}

fn serve_file(path: &String) -> Option<Vec<u8>> {
// 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<Vec<u8>> {
let r = fs::read(path);
if r.is_ok() { Some(r.unwrap()) } else { None }
}
Expand Down Expand Up @@ -118,23 +141,21 @@ fn main() {
}
if proxy_only {
logger
.warn("WARNING: All requests are proxied to /. Local paths wont be used.".to_string());
.warn("WARNING: All requests are proxied to /. Local paths won't be used.".to_string());
}

// Create component loggers
let proxy_logger = logger.with_component("proxy");
let cache_logger = logger.with_component("cache");
let http_logger = logger.with_component("http");


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();


http_logger.info(format!("Request {} {}", req.method.to_str(), req.path));

let is_proxy = is_proxy(&config, req.clone());
Expand Down Expand Up @@ -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<Vec<u8>> = 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();
Expand All @@ -197,17 +248,21 @@ fn main() {
));
r
} else {
serve_file(&full_path)
serve_file(&safe_path)
};

let elapsed = start_time.elapsed();
http_logger.info(format!(
"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),
}
});
}
}
64 changes: 57 additions & 7 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>) -> HashMap<String, String> {}
//pub fn args_to_dict(list: Vec<String>) -> HashMap<String, String> {}