diff --git a/.gitignore b/.gitignore index fa7c88d..a008caf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /target Cargo.lock example - +*.diff diff --git a/Cargo.toml b/Cargo.toml index d1526e7..6de3a12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,10 @@ edition = "2024" [dependencies] logger-rust = "0.2.12" mime_guess = "2.0.5" +serde = { version = "1.0.228", features = ["derive"]} threadpool = "1.8.1" tiny_http = "0.12.0" +tinytemplate = "1.2.1" [profile.release] opt-level = "z" diff --git a/src/response.rs b/src/response.rs index 97a6cb0..d171dce 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,11 +1,16 @@ /* +* src/response.rs * Content-Type ; Mappings for the HTML headers, example 404 +* rewritten partly / mostly 1 commit after hash: a671182b07da0c22ed29e66f87faa3738fbdca64 */ -use logger_rust::*; -use std::fs::File; -use std::io::{Cursor, Read}; +use std::io::Cursor; +use std::io::Read; use std::path::Path; -use tiny_http::{Header, Response}; +use std::fs::File; +use logger_rust::*; +use tiny_http::{Header, Response, StatusCode}; +use tinytemplate::TinyTemplate; +use serde::Serialize; type BoxedResponse = Response>>; @@ -13,6 +18,15 @@ fn content_type(value: &str) -> Header { Header::from_bytes("Content-Type", value).unwrap() } +#[derive(Serialize)] +struct ErrorContext { + code: u16, + title: String, + message: String, +} + +const ERROR_TEMPLATE: &str = include_str!("./templates/response_fail.html"); + pub fn serve_file(path: &Path) -> BoxedResponse { let mime = mime_guess::from_path(path) .first_or_octet_stream() @@ -21,34 +35,62 @@ pub fn serve_file(path: &Path) -> BoxedResponse { let mut file = match File::open(path) { Ok(f) => f, Err(e) => { - log_error!("Serving a File failed with: {}", e); - return not_found(); // bad, we need to serve properly ... + // made safer 1CA:a671182b07da0c22ed29e66f87faa3738fbdca64 + return match e.kind() { + std::io::ErrorKind::NotFound => not_found("File does not exist"), + std::io::ErrorKind::PermissionDenied => forbidden("Access denied"), + _ => {log_error!("Failed to open File!"); internal_error("Failed to load file")} + } } + }; let mut content = Vec::new(); if file.read_to_end(&mut content).is_err() { - return internal_error(); + return internal_error("Failed to read file"); } Response::from_data(content) .with_header(content_type(&mime)) } -pub fn not_found() -> BoxedResponse { - Response::from_string("404 Not Found") - .with_status_code(404) - .with_header(content_type("text/plain")) +fn render_error(code: u16, title: &str, message: &str) -> String { + let mut tt = TinyTemplate::new(); + + // load template + tt.add_template("error", ERROR_TEMPLATE) + .expect("template load failed"); + + // fill in template + let ctx = ErrorContext { + code, + title: title.into(), + message: message.into(), + }; + + tt.render("error", &ctx) + .unwrap_or_else(|_| format!("{} {}", code, title)) } -pub fn forbidden() -> BoxedResponse { - Response::from_string("403 Forbidden") - .with_status_code(403) - .with_header(content_type("text/plain")) +pub fn error(code: u16, title: &str, message: &str) -> BoxedResponse { + let body = render_error(code, title, message); + + Response::from_string(body) + .with_status_code(StatusCode(code)) + .with_header(content_type("text/html")) // cause text/plain would be just pissing in the wind } -pub fn internal_error() -> BoxedResponse { - Response::from_string("500 Internal Server Error") - .with_status_code(500) - .with_header(content_type("text/plain")) + +/* error methods, which should be called if something goes wrong */ +pub fn forbidden(msg: &str) -> BoxedResponse { + error(403, "Forbidden", msg) } + +pub fn not_found(msg: &str) -> BoxedResponse { + error(404, "Not Found", msg) +} + +pub fn internal_error(msg: &str) -> BoxedResponse { + error(500, "Internal Server Error", msg) +} + diff --git a/src/router.rs b/src/router.rs index 48c90d1..ed4a45d 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,34 +1,40 @@ use std::path::{Path, PathBuf}; -pub fn resolve(root: &Path, url_path: &str) -> Option { +// Fixed from version a671182b07da0c22ed29e66f87faa3738fbdca64 +// onwards + +pub enum ResolveResult { + Found(PathBuf), + NoIndex, + Traversal, +} + +pub fn resolve(root: &Path, url_path: &str) -> ResolveResult { let rel = url_path.trim_start_matches('/'); let rel = if rel.is_empty() { "index.html" } else { rel }; - let joined = root.join(rel); + // join once + let mut joined = root.join(rel); - // directory, look for index file - // CHANGE: 39bf977604 - let joined = if joined.is_dir() { + // if directory look for index + if joined.is_dir() { let html = joined.join("index.html"); let htm = joined.join("index.htm"); if html.exists() { - html + joined = html; } else if htm.exists() { - htm + joined = htm; } else { - return None; // directory is there, but no index file is found + return ResolveResult::NoIndex; // no index found } - } else { - joined - }; + } - // Prevent path traversal - let canonical_root = root.canonicalize().ok()?; - let canonical_file = joined.canonicalize().ok()?; + // nono no escape + let canonical_root = root.canonicalize().ok(); + let canonical_file = joined.canonicalize().ok(); - if canonical_file.starts_with(&canonical_root) { - Some(canonical_file) - } else { - None + match (canonical_root, canonical_file) { + (Some(root), Some(file)) if file.starts_with(&root) => ResolveResult::Found(file), + _ => ResolveResult::Traversal, } } diff --git a/src/server.rs b/src/server.rs index 22e8140..53dda15 100644 --- a/src/server.rs +++ b/src/server.rs @@ -12,8 +12,7 @@ pub fn run( port: u16 ) -> Result<(), Box> { - let addr = format!("0.0.0.0:{}", port); // serve localhost - + let addr = format!("0.0.0.0:{}", port); let server = Arc::new(Server::http(&addr)?); let pool = ThreadPool::new(num_cpus()); @@ -27,10 +26,14 @@ pub fn run( log_info!("{} {}", request.method(), url); let resp = match router::resolve(&root, &url) { - Some(path) => response::serve_file(&path), - None => { + router::ResolveResult::Found(path) => response::serve_file(&path), + router::ResolveResult::NoIndex => { + log_warn!("No index file for: {}", url); + response::not_found("No index file found") + } + router::ResolveResult::Traversal => { log_warn!("Rejected path traversal attempt: {}", url); - response::forbidden() + response::forbidden("Access denied") } }; diff --git a/src/templates/response_fail.html b/src/templates/response_fail.html new file mode 100644 index 0000000..d114219 --- /dev/null +++ b/src/templates/response_fail.html @@ -0,0 +1,35 @@ + + + + +

{code} - {title}

+

{message}

+

You've stumbled upon a error. Try again later!

+
+

+ + + + http-rs + + @

+ + + +