commit c06176353827a9ed156e7ecf4292673729643d28 Author: rattatwinko Date: Sun Mar 15 19:02:21 2026 +0100 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa7c88d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +Cargo.lock +example + diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..899486c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "http-rs" +version = "0.1.0" +edition = "2024" + +[dependencies] +logger-rust = "0.2.12" +mime_guess = "2.0.5" +threadpool = "1.8.1" +tiny_http = "0.12.0" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e039161 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,26 @@ +mod server; +mod router; +mod response; + +use std::path::PathBuf; +use std::sync::Arc; +use logger_rust::*; + +fn main() { + let args: Vec = std::env::args().collect(); + + let port: u16 = args.get(1) + .and_then(|p| p.parse().ok()) + .unwrap_or(8080); + + let root = Arc::new( + PathBuf::from(args.get(2).map(|s| s.as_str()).unwrap_or(".")) + ); + + if !root.exists() || !root.is_dir() { + log_error!("Root {} isnt a valid directory", root.display()); + std::process::exit(1); + } + + server::run(root, port); +} diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 0000000..91a6d3b --- /dev/null +++ b/src/response.rs @@ -0,0 +1,47 @@ +use std::fs::File; +use std::io::{Cursor, Read}; +use std::path::Path; +use tiny_http::{Header, Response}; + +type BoxedResponse = Response>>; + +fn content_type(value: &str) -> Header { + Header::from_bytes("Content-Type", value).unwrap() +} + +pub fn serve_file(path: &Path) -> BoxedResponse { + let mime = mime_guess::from_path(path) + .first_or_octet_stream() + .to_string(); + + let mut file = match File::open(path) { + Ok(f) => f, + Err(_) => return not_found(), + }; + + let mut content = Vec::new(); + if file.read_to_end(&mut content).is_err() { + return internal_error(); + } + + 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")) +} + +pub fn forbidden() -> BoxedResponse { + Response::from_string("403 Forbidden") + .with_status_code(403) + .with_header(content_type("text/plain")) +} + +pub fn internal_error() -> BoxedResponse { + Response::from_string("500 Internal Server Error") + .with_status_code(500) + .with_header(content_type("text/plain")) +} diff --git a/src/router.rs b/src/router.rs new file mode 100644 index 0000000..ac6b73b --- /dev/null +++ b/src/router.rs @@ -0,0 +1,20 @@ +use std::path::{Path, PathBuf}; + +pub fn resolve(root: &Path, url_path: &str) -> Option { + let rel = url_path.trim_start_matches('/'); + let rel = if rel.is_empty() { "index.html" } else { rel }; // default to index.html in . dir + + //let rel = rel.split('?').next().unwrap_or(rel); + + let joined = root.join(rel); + + // prevent escape essentially + let canonical_root = root.canonicalize().ok()?; + let canonical_file = joined.canonicalize().ok()?; + + if canonical_file.starts_with(&canonical_root) { + Some(canonical_file) + } else { + None + } +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..833faf7 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,43 @@ +use std::path::PathBuf; +use std::sync::Arc; +use logger_rust::*; +use threadpool::ThreadPool; +use tiny_http::Server; + +use crate::router; +use crate::response; + +pub fn run(root: Arc, port: u16) { + let addr = format!("0.0.0.0:{}", port); + let server = Arc::new(Server::http(&addr).expect("Failed to start server")); + let pool = ThreadPool::new(num_cpus()); + + log_info!("Serving '{}' on http://{}", root.display(), addr); + + for request in server.incoming_requests() { + let root = Arc::clone(&root); + + pool.execute(move || { + let url = request.url().to_owned(); + log_info!("{} {}", request.method(), url); + + let resp = match router::resolve(&root, &url) { + Some(path) => response::serve_file(&path), + None => { + log_warn!("Rejected path traversal attempt: {}", url); + response::forbidden() + } + }; + + if let Err(e) = request.respond(resp) { + log_error!("Failed to send response: {}", e); + } + }); + } +} + +fn num_cpus() -> usize { + std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(4) +}