fixed some components, mainly implemented safety, and implemented a great error page!
All checks were successful
Compile to Binary / build-linux (push) Successful in 55s
Compile to Binary / build-linux-musl (push) Successful in 1m9s
Compile to Binary / build-windows (push) Successful in 1m33s

This commit is contained in:
2026-03-17 17:05:21 +01:00
parent a671182b07
commit b6e8ae4ca8
6 changed files with 131 additions and 43 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
/target /target
Cargo.lock Cargo.lock
example example
*.diff

View File

@@ -7,8 +7,10 @@ edition = "2024"
[dependencies] [dependencies]
logger-rust = "0.2.12" logger-rust = "0.2.12"
mime_guess = "2.0.5" mime_guess = "2.0.5"
serde = { version = "1.0.228", features = ["derive"]}
threadpool = "1.8.1" threadpool = "1.8.1"
tiny_http = "0.12.0" tiny_http = "0.12.0"
tinytemplate = "1.2.1"
[profile.release] [profile.release]
opt-level = "z" opt-level = "z"

View File

@@ -1,11 +1,16 @@
/* /*
* src/response.rs
* Content-Type ; Mappings for the HTML headers, example 404 * Content-Type ; Mappings for the HTML headers, example 404
* rewritten partly / mostly 1 commit after hash: a671182b07da0c22ed29e66f87faa3738fbdca64
*/ */
use logger_rust::*; use std::io::Cursor;
use std::fs::File; use std::io::Read;
use std::io::{Cursor, Read};
use std::path::Path; 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<Cursor<Vec<u8>>>; type BoxedResponse = Response<Cursor<Vec<u8>>>;
@@ -13,6 +18,15 @@ fn content_type(value: &str) -> Header {
Header::from_bytes("Content-Type", value).unwrap() 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 { pub fn serve_file(path: &Path) -> BoxedResponse {
let mime = mime_guess::from_path(path) let mime = mime_guess::from_path(path)
.first_or_octet_stream() .first_or_octet_stream()
@@ -21,34 +35,62 @@ pub fn serve_file(path: &Path) -> BoxedResponse {
let mut file = match File::open(path) { let mut file = match File::open(path) {
Ok(f) => f, Ok(f) => f,
Err(e) => { Err(e) => {
log_error!("Serving a File failed with: {}", e); // made safer 1CA:a671182b07da0c22ed29e66f87faa3738fbdca64
return not_found(); // bad, we need to serve properly ... 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(); let mut content = Vec::new();
if file.read_to_end(&mut content).is_err() { if file.read_to_end(&mut content).is_err() {
return internal_error(); return internal_error("Failed to read file");
} }
Response::from_data(content) Response::from_data(content)
.with_header(content_type(&mime)) .with_header(content_type(&mime))
} }
pub fn not_found() -> BoxedResponse { fn render_error(code: u16, title: &str, message: &str) -> String {
Response::from_string("404 Not Found") let mut tt = TinyTemplate::new();
.with_status_code(404)
.with_header(content_type("text/plain")) // 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 { pub fn error(code: u16, title: &str, message: &str) -> BoxedResponse {
Response::from_string("403 Forbidden") let body = render_error(code, title, message);
.with_status_code(403)
.with_header(content_type("text/plain")) 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") /* error methods, which should be called if something goes wrong */
.with_status_code(500) pub fn forbidden(msg: &str) -> BoxedResponse {
.with_header(content_type("text/plain")) 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)
}

View File

@@ -1,34 +1,40 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
pub fn resolve(root: &Path, url_path: &str) -> Option<PathBuf> { // 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 = url_path.trim_start_matches('/');
let rel = if rel.is_empty() { "index.html" } else { rel }; 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 // if directory look for index
// CHANGE: 39bf977604 if joined.is_dir() {
let joined = if joined.is_dir() {
let html = joined.join("index.html"); let html = joined.join("index.html");
let htm = joined.join("index.htm"); let htm = joined.join("index.htm");
if html.exists() { if html.exists() {
html joined = html;
} else if htm.exists() { } else if htm.exists() {
htm joined = htm;
} else { } else {
return None; // directory is there, but no index file is found return ResolveResult::NoIndex; // no index found
}
} }
} else {
joined
};
// Prevent path traversal // nono no escape
let canonical_root = root.canonicalize().ok()?; let canonical_root = root.canonicalize().ok();
let canonical_file = joined.canonicalize().ok()?; let canonical_file = joined.canonicalize().ok();
if canonical_file.starts_with(&canonical_root) { match (canonical_root, canonical_file) {
Some(canonical_file) (Some(root), Some(file)) if file.starts_with(&root) => ResolveResult::Found(file),
} else { _ => ResolveResult::Traversal,
None
} }
} }

View File

@@ -12,8 +12,7 @@ pub fn run(
port: u16 port: u16
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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 server = Arc::new(Server::http(&addr)?);
let pool = ThreadPool::new(num_cpus()); let pool = ThreadPool::new(num_cpus());
@@ -27,10 +26,14 @@ pub fn run(
log_info!("{} {}", request.method(), url); log_info!("{} {}", request.method(), url);
let resp = match router::resolve(&root, &url) { let resp = match router::resolve(&root, &url) {
Some(path) => response::serve_file(&path), router::ResolveResult::Found(path) => response::serve_file(&path),
None => { 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); log_warn!("Rejected path traversal attempt: {}", url);
response::forbidden() response::forbidden("Access denied")
} }
}; };

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<body style="text-align:center;font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;">
<!-- HTTP-RS ERROR PAGE -->
<h1>{code} - {title}</h1>
<p>{message}</p>
<p>You've stumbled upon a error. Try again later!</p>
<hr>
<p id="http-rs">
<i>
<!-- link to project -->
<a href="https://rattatwinko.servecounterstrike.com/gitea/rattatwinko/http-rs">
http-rs
</a>
</i>@<span id="timestamp"></span></p>
<script type="text/javascript">
// Source - https://stackoverflow.com/a/12409344
// Posted by Aelios, modified by community. See post 'Timeline' for change history
// Retrieved 2026-03-17, License - CC BY-SA 4.0
const today = new Date();
const yyyy = today.getFullYear();
let mm = today.getMonth() + 1; // Months start at 0!
let dd = today.getDate();
if (dd < 10) dd = '0' + dd;
if (mm < 10) mm = '0' + mm;
const formattedToday = dd + '/' + mm + '/' + yyyy;
document.getElementById('timestamp').textContent = formattedToday;;
</script>
</body>
</html>