This commit is contained in:
2026-03-15 19:02:21 +01:00
commit c061763538
6 changed files with 150 additions and 0 deletions

26
src/main.rs Normal file
View File

@@ -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<String> = 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);
}

47
src/response.rs Normal file
View File

@@ -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<Cursor<Vec<u8>>>;
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"))
}

20
src/router.rs Normal file
View File

@@ -0,0 +1,20 @@
use std::path::{Path, PathBuf};
pub fn resolve(root: &Path, url_path: &str) -> Option<PathBuf> {
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
}
}

43
src/server.rs Normal file
View File

@@ -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<PathBuf>, 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)
}