From 02da0ff0fc067c0292a3bdfb71833559f479ce9b Mon Sep 17 00:00:00 2001 From: _ <> Date: Sun, 8 Nov 2020 17:58:14 +0000 Subject: [PATCH] :bug: Redirect to add trailing slashes for directories --- ptth_handlebars/file_server_dir.html | 39 +++-- ptth_handlebars/file_server_root.html | 4 +- src/bin/ptth_file_server.rs | 8 +- src/http_serde.rs | 30 +++- src/lib.rs | 8 +- src/server/file_server.rs | 198 ++++++++++++++++++++------ todo.md | 24 +++- 7 files changed, 238 insertions(+), 73 deletions(-) diff --git a/ptth_handlebars/file_server_dir.html b/ptth_handlebars/file_server_dir.html index 552fb24..4d4cdc0 100644 --- a/ptth_handlebars/file_server_dir.html +++ b/ptth_handlebars/file_server_dir.html @@ -9,10 +9,16 @@ .entry { display: inline-block; padding: 10px; - min-width: 50%; + width: 100%; text-decoration: none; } - .entry_list div:nth-child(odd) { + .grey { + color: #888; + } + .entry_list { + width: 100%; + } + .entry_list tr:nth-child(even) { background-color: #ddd; } @@ -24,21 +30,30 @@

{{path}}

-
+ + + + + + + + -
-📁 ../ -
+ + + + {{#each entries}} -
- -{{this.icon}} {{this.file_name}}{{this.trailing_slash}} - -
+ + + + {{/each}} - + +
NameSize
📁 ../
+{{this.icon}} {{this.file_name}}{{this.trailing_slash}}{{this.size}}
diff --git a/ptth_handlebars/file_server_root.html b/ptth_handlebars/file_server_root.html index 00a88be..a234e11 100644 --- a/ptth_handlebars/file_server_root.html +++ b/ptth_handlebars/file_server_root.html @@ -9,10 +9,10 @@ .entry { display: inline-block; padding: 10px; - min-width: 50%; + width: 100%; text-decoration: none; } - .entry_list div:nth-child(odd) { + .entry_list div:nth-child(even) { background-color: #ddd; } diff --git a/src/bin/ptth_file_server.rs b/src/bin/ptth_file_server.rs index 8291428..184c762 100644 --- a/src/bin/ptth_file_server.rs +++ b/src/bin/ptth_file_server.rs @@ -32,6 +32,7 @@ pub struct Config { struct ServerState <'a> { config: Config, handlebars: handlebars::Handlebars <'a>, + hidden_path: Option , } fn status_reply > (status: StatusCode, b: B) @@ -67,7 +68,7 @@ async fn handle_all (req: Request , state: Arc >) ptth_req.method, &ptth_req.uri, &ptth_req.headers, - None + state.hidden_path.as_ref ().map (|p| p.as_path ()) ).await; let mut resp = Response::builder () @@ -97,7 +98,9 @@ pub struct ConfigFile { #[tokio::main] async fn main () -> Result <(), Box > { tracing_subscriber::fmt::init (); - let config_file: ConfigFile = ptth::load_toml::load ("config/ptth_server.toml"); + + let path = PathBuf::from ("./config/ptth_server.toml"); + let config_file: ConfigFile = ptth::load_toml::load (&path); info! ("file_server_root: {:?}", config_file.file_server_root); let addr = SocketAddr::from(([0, 0, 0, 0], 4000)); @@ -109,6 +112,7 @@ async fn main () -> Result <(), Box > { file_server_root: config_file.file_server_root, }, handlebars, + hidden_path: Some (path), }); let make_svc = make_service_fn (|_conn| { diff --git a/src/http_serde.rs b/src/http_serde.rs index 056ceb4..52aae59 100644 --- a/src/http_serde.rs +++ b/src/http_serde.rs @@ -1,6 +1,6 @@ use std::{ collections::*, - convert::{TryFrom}, + convert::{TryFrom, TryInto}, }; use serde::{Deserialize, Serialize}; @@ -85,11 +85,13 @@ pub struct WrappedRequest { pub req: RequestParts, } -#[derive (Debug, Deserialize, Serialize)] +#[derive (Debug, Deserialize, Serialize, PartialEq)] pub enum StatusCode { Ok, // 200 PartialContent, // 206 + TemporaryRedirect, // 307 + BadRequest, // 400 Forbidden, // 403 NotFound, // 404 @@ -108,6 +110,8 @@ impl From for hyper::StatusCode { StatusCode::Ok => Self::OK, StatusCode::PartialContent => Self::PARTIAL_CONTENT, + StatusCode::TemporaryRedirect => Self::TEMPORARY_REDIRECT, + StatusCode::BadRequest => Self::BAD_REQUEST, StatusCode::Forbidden => Self::FORBIDDEN, StatusCode::NotFound => Self::NOT_FOUND, @@ -142,6 +146,24 @@ pub struct Response { } impl Response { + pub async fn into_bytes (self) -> Vec { + let mut body = match self.body { + None => return Vec::new (), + Some (x) => x, + }; + + let mut result = match self.content_length { + None => Vec::new (), + Some (x) => Vec::with_capacity (x.try_into ().unwrap ()), + }; + + while let Some (Ok (mut chunk)) = body.recv ().await { + result.append (&mut chunk); + } + + result + } + pub fn status_code (&mut self, c: StatusCode) -> &mut Self { self.parts.status_code = c; self @@ -158,14 +180,12 @@ impl Response { } pub fn body_bytes (&mut self, b: Vec ) -> &mut Self { - use std::convert::TryInto; - self.content_length = b.len ().try_into ().ok (); self.header ("content-length".to_string (), b.len ().to_string ().into_bytes ()); let (mut tx, rx) = tokio::sync::mpsc::channel (1); tokio::spawn (async move { - tx.send (Ok (b)).await.unwrap (); + tx.send (Ok (b)).await.ok (); }); self.body = Some (rx); diff --git a/src/lib.rs b/src/lib.rs index 435bae3..7270684 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -86,12 +86,10 @@ mod tests { use reqwest::Client; use tracing::{info}; - // This should be the first line of the `tracing` - // crate documentation. Their docs are awful, but you - // didn't hear it from me. - - tracing_subscriber::fmt::init (); + // Prefer this form for tests, since all tests share one process + // and we don't care if another test already installed a subscriber. + tracing_subscriber::fmt ().try_init ().ok (); let mut rt = Runtime::new ().unwrap (); // Spawn the root task diff --git a/src/server/file_server.rs b/src/server/file_server.rs index ec1b136..65f18ed 100644 --- a/src/server/file_server.rs +++ b/src/server/file_server.rs @@ -1,6 +1,7 @@ // Static file server that can plug into the PTTH reverse server use std::{ + borrow::Cow, cmp::{min, max}, collections::*, convert::{Infallible, TryInto}, @@ -28,7 +29,11 @@ use tracing::instrument; use regex::Regex; use crate::{ - http_serde, + http_serde::{ + Method, + Response, + StatusCode, + }, prelude::*, prefix_match, }; @@ -54,6 +59,8 @@ struct TemplateDirEntry { encoded_file_name: String, + size: Cow <'static, str>, + error: bool, } @@ -97,13 +104,28 @@ async fn read_dir_entry (entry: DirEntry) -> TemplateDirEntry trailing_slash: "", file_name: "File / directory name is not UTF-8".into (), encoded_file_name: "".into (), + size: "".into (), error: true, }, }; - let (trailing_slash, icon) = match entry.file_type ().await { - Ok (t) => if t.is_dir () { - ("/", "📁") + let metadata = match entry.metadata ().await { + Ok (x) => x, + Err (_) => return TemplateDirEntry { + icon: "⚠️", + trailing_slash: "", + file_name: "Could not fetch metadata".into (), + encoded_file_name: "".into (), + size: "".into (), + error: true, + }, + }; + + let (trailing_slash, icon, size) = { + let t = metadata.file_type (); + + if t.is_dir () { + ("/", "📁", "".into ()) } else { let icon = if file_name.ends_with (".mp4") { @@ -131,9 +153,8 @@ async fn read_dir_entry (entry: DirEntry) -> TemplateDirEntry "📄" }; - ("", icon) - }, - Err (_) => ("", "⚠️"), + ("", icon, pretty_print_bytes (metadata.len ()).into ()) + } }; use percent_encoding::*; @@ -145,13 +166,14 @@ async fn read_dir_entry (entry: DirEntry) -> TemplateDirEntry trailing_slash: &trailing_slash, file_name, encoded_file_name, + size, error: false, } } async fn serve_root ( handlebars: &Handlebars <'static>, -) -> http_serde::Response +) -> Response { let server_info = ServerInfo { server_name: "PTTH file server", @@ -160,7 +182,7 @@ async fn serve_root ( let s = handlebars.render ("file_server_root", &server_info).unwrap (); let body = s.into_bytes (); - let mut resp = http_serde::Response::default (); + let mut resp = Response::default (); resp .header ("content-type".to_string (), "text/html".to_string ().into_bytes ()) .body_bytes (body) @@ -168,14 +190,12 @@ async fn serve_root ( resp } -use std::borrow::Cow; - #[instrument (level = "debug", skip (handlebars, dir))] async fn serve_dir ( handlebars: &Handlebars <'static>, path: Cow <'_, str>, mut dir: ReadDir -) -> http_serde::Response +) -> Response { let server_info = ServerInfo { server_name: "PTTH file server", @@ -196,7 +216,7 @@ async fn serve_dir ( }).unwrap (); let body = s.into_bytes (); - let mut resp = http_serde::Response::default (); + let mut resp = Response::default (); resp .header ("content-type".to_string (), "text/html".to_string ().into_bytes ()) .body_bytes (body) @@ -210,7 +230,9 @@ async fn serve_file ( should_send_body: bool, range_start: Option , range_end: Option -) -> http_serde::Response { +) + -> Response +{ let (tx, rx) = channel (1); let body = if should_send_body { Some (rx) @@ -271,17 +293,17 @@ async fn serve_file ( }); } - let mut response = http_serde::Response::default (); + let mut response = Response::default (); response.header (String::from ("accept-ranges"), b"bytes".to_vec ()); if should_send_body { if range_start.is_none () && range_end.is_none () { - response.status_code (http_serde::StatusCode::Ok); + response.status_code (StatusCode::Ok); response.header (String::from ("content-length"), end.to_string ().into_bytes ()); } else { - response.status_code (http_serde::StatusCode::PartialContent); + response.status_code (StatusCode::PartialContent); response.header (String::from ("content-range"), format! ("bytes {}-{}/{}", start, end - 1, end).into_bytes ()); } @@ -295,15 +317,23 @@ async fn serve_file ( response } -async fn serve_error ( - status_code: http_serde::StatusCode, - msg: String +fn serve_error ( + status_code: StatusCode, + msg: &str ) --> http_serde::Response +-> Response { - let mut resp = http_serde::Response::default (); + let mut resp = Response::default (); resp.status_code (status_code); - resp.body_bytes (msg.into_bytes ()); + resp.body_bytes (msg.as_bytes ().to_vec ()); + resp +} + +fn serve_307 (location: String) -> Response { + let mut resp = Response::default (); + resp.status_code (StatusCode::TemporaryRedirect); + resp.header ("location".to_string (), location.into_bytes ()); + resp.body_bytes (b"Redirecting...".to_vec ()); resp } @@ -311,18 +341,22 @@ async fn serve_error ( pub async fn serve_all ( handlebars: &Handlebars <'static>, root: &Path, - method: http_serde::Method, + method: Method, uri: &str, headers: &HashMap >, hidden_path: Option <&Path> ) --> http_serde::Response +-> Response { info! ("Client requested {}", uri); use percent_encoding::*; - let uri = match prefix_match (uri, "/files/") { + if uri == "/favicon.ico" { + return serve_error (StatusCode::NotFound, ""); + } + + let uri = match prefix_match (uri, "/files") { Some (x) => x, None => { return serve_root (handlebars).await; @@ -331,7 +365,7 @@ pub async fn serve_all ( // TODO: There is totally a dir traversal attack in here somewhere - let encoded_path = &uri [..]; + let encoded_path = &uri [1..]; let path_s = percent_decode (encoded_path.as_bytes ()).decode_utf8 ().unwrap (); let path = Path::new (&*path_s); @@ -343,14 +377,17 @@ pub async fn serve_all ( if let Some (hidden_path) = hidden_path { if full_path == hidden_path { - return serve_error (http_serde::StatusCode::Forbidden, "403 Forbidden".into ()).await; + return serve_error (StatusCode::Forbidden, "403 Forbidden"); } } - if uri == "/" { - serve_root (handlebars).await - } - else if let Ok (dir) = read_dir (&full_path).await { + let has_trailing_slash = path_s.is_empty () || path_s.ends_with ("/"); + + if let Ok (dir) = read_dir (&full_path).await { + if ! has_trailing_slash { + return serve_307 (format! ("{}/", path.file_name ().unwrap ().to_str ().unwrap ())); + } + serve_dir ( handlebars, full_path.to_string_lossy (), @@ -370,11 +407,11 @@ pub async fn serve_all ( } let should_send_body = match &method { - http_serde::Method::Get => true, - http_serde::Method::Head => false, + Method::Get => true, + Method::Head => false, m => { debug! ("Unsupported method {:?}", m); - return serve_error (http_serde::StatusCode::MethodNotAllowed, "Unsupported method".into ()).await; + return serve_error (StatusCode::MethodNotAllowed, "Unsupported method"); } }; @@ -386,7 +423,7 @@ pub async fn serve_all ( ).await } else { - serve_error (http_serde::StatusCode::NotFound, "404 Not Found".into ()).await + serve_error (StatusCode::NotFound, "404 Not Found") } } @@ -406,15 +443,60 @@ pub fn load_templates () Ok (handlebars) } +fn pretty_print_bytes (b: u64) -> String { + if b < 1024 { + format! ("{} B", b) + } + else if (b + 512) < 1024 * 1024 { + format! ("{} KiB", (b + 512) / 1024) + } + else if (b + 512 * 1024) < 1024 * 1024 * 1024 { + format! ("{} MiB", (b + 512 * 1024) / 1024 / 1024) + } + else { + format! ("{} GiB", (b + 512 * 1024 * 1024) / 1024 / 1024 / 1024) + } +} + #[cfg (test)] mod tests { + use std::{ + ffi::OsStr, + path::{ + Component, + Path, + PathBuf + }, + }; + + use tokio::runtime::Runtime; + + use crate::http_serde::{ + StatusCode, + }; + + #[test] + fn pretty_print_bytes () { + for (input_after, expected_before, expected_after) in vec! [ + (1, "0 B", "1 B"), + (1024, "1023 B", "1 KiB"), + (1024 + 512, "1 KiB", "2 KiB"), + (1023 * 1024 + 512, "1023 KiB", "1 MiB"), + ((1024 + 512) * 1024, "1 MiB", "2 MiB"), + (1023 * 1024 * 1024 + 512 * 1024, "1023 MiB", "1 GiB"), + ((1024 + 512) * 1024 * 1024, "1 GiB", "2 GiB"), + + ].into_iter () { + let actual = super::pretty_print_bytes (input_after - 1); + assert_eq! (&actual, expected_before); + + let actual = super::pretty_print_bytes (input_after); + assert_eq! (&actual, expected_after); + } + } + #[test] fn i_hate_paths () { - use std::{ - ffi::OsStr, - path::{Component, Path} - }; - let mut components = Path::new ("/home/user").components (); assert_eq! (components.next (), Some (Component::RootDir)); @@ -434,4 +516,38 @@ mod tests { assert_eq! (components.next (), Some (Component::CurDir)); assert_eq! (components.next (), None); } + + #[test] + fn file_server () { + use crate::{ + http_serde::Method, + prelude::*, + }; + + tracing_subscriber::fmt ().try_init ().ok (); + let mut rt = Runtime::new ().unwrap (); + + rt.block_on (async { + let handlebars = super::load_templates ().unwrap (); + let file_server_root = PathBuf::from ("./"); + let headers = Default::default (); + + for (uri_path, expected_status) in vec! [ + ("/", StatusCode::Ok), + ("/files/src", StatusCode::TemporaryRedirect), + ("/files/src/", StatusCode::Ok), + ].into_iter () { + let resp = super::serve_all ( + &handlebars, + &file_server_root, + Method::Get, + uri_path, + &headers, + None + ).await; + + assert_eq! (resp.parts.status_code, expected_status); + } + }); + } } diff --git a/todo.md b/todo.md index 398278c..5f4fe46 100644 --- a/todo.md +++ b/todo.md @@ -1,10 +1,6 @@ - Not working behind Nginx (Works okay behind Caddy) - Reduce idle memory use? -- Compress bad passwords file -- Package templates into exe for release -- Redirect to add trailing slashes -- Add file size in directory listing - Allow spaces in server names - Deny unused HTTP methods for endpoints - ETag cache based on mtime @@ -17,11 +13,13 @@ - Reverse proxy to other local servers -Off-project stuff: +# Off-project stuff: - Benchmark directory entry sorting -Known issues: +# Known issues: + +## Graceful shutdown Relay can't shut down gracefully if Firefox is connected to it, e.g. if Firefox kept a connection open while watching a video. @@ -31,3 +29,17 @@ forced shutdown timer. Sometimes I get the turtle icon in Firefox's network debugger. But this happens even with Caddy running a static file server, so I can't prove that it's on my side. The VPS is cheap, and the datacenter is far away. + +## Embedded asssets + +The bad_passwords file is huge. Since it's static, it should only be in physical +RAM when the server first launches, and then the kernel will let it be paged +out. + +Rust has some open issues with compiling assets into the exe, so I'm not +going to push on this for now, for neither bad_passwords nor the HTML assets: + +https://github.com/rust-lang/rust/issues/65818 + +I also considered compressing the passwords file, but I couldn't even get +brotli to give it a decent ratio.