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
|
/target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
example
|
example
|
||||||
|
*.diff
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
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