fixed some components, mainly implemented safety, and implemented a great error page!
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
example
|
||||
|
||||
*.diff
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<Cursor<Vec<u8>>>;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,40 @@
|
||||
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 = 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ pub fn run(
|
||||
port: u16
|
||||
) -> 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 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")
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
35
src/templates/response_fail.html
Normal file
35
src/templates/response_fail.html
Normal 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>
|
||||
Reference in New Issue
Block a user