diff --git a/.gitignore b/.gitignore index 690f90c..d4996e0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /ptth_server.toml /ptth_relay.toml /target +/test diff --git a/ptth_handlebars/file_server_dir.html b/ptth_handlebars/file_server_dir.html index 675d58f..552fb24 100644 --- a/ptth_handlebars/file_server_dir.html +++ b/ptth_handlebars/file_server_dir.html @@ -10,25 +10,30 @@ display: inline-block; padding: 10px; min-width: 50%; + text-decoration: none; } .entry_list div:nth-child(odd) { background-color: #ddd; } -{{path}} +{{path}} {{server_name}} +

{{server_name}}

+ +

{{path}}

+
-../ +📁 ../
{{#each entries}}
-{{this.file_name}}{{this.trailing_slash}} +{{this.icon}} {{this.file_name}}{{this.trailing_slash}}
{{/each}} diff --git a/ptth_handlebars/file_server_root.html b/ptth_handlebars/file_server_root.html new file mode 100644 index 0000000..00a88be --- /dev/null +++ b/ptth_handlebars/file_server_root.html @@ -0,0 +1,38 @@ + + + + + +{{server_name}} + + + +
+ +
+📁 ../ +
+ +
+ +📁 Files + +
+ +
+ + + diff --git a/ptth_handlebars/relay_root.html b/ptth_handlebars/relay_root.html new file mode 100644 index 0000000..79c7d30 --- /dev/null +++ b/ptth_handlebars/relay_root.html @@ -0,0 +1,33 @@ + + + + + +PTTH relay + + + +

PTTH relay

+ +
+ +
+Server list +
+ +
+ + + diff --git a/src/bin/ptth_file_server.rs b/src/bin/ptth_file_server.rs index 18b1865..2f638bc 100644 --- a/src/bin/ptth_file_server.rs +++ b/src/bin/ptth_file_server.rs @@ -23,7 +23,6 @@ use tracing::{ use ptth::{ http_serde::RequestParts, - prefix_match, server::file_server, }; @@ -46,54 +45,50 @@ fn status_reply > (status: StatusCode, b: B) async fn handle_all (req: Request , state: Arc >) -> Result , String> { - let path = req.uri ().path (); - //println! ("{}", path); + debug! ("req.uri () = {:?}", req.uri ()); - if let Some (path) = prefix_match (path, "/files") { - let path = path.into (); - - let (parts, _) = req.into_parts (); - - let ptth_req = match RequestParts::from_hyper (parts.method, path, parts.headers) { - Ok (x) => x, - _ => return Ok (status_reply (StatusCode::BAD_REQUEST, "Bad request")), - }; - - let default_root = PathBuf::from ("./"); - let file_server_root: &std::path::Path = state.config.file_server_root - .as_ref () - .unwrap_or (&default_root); - - let ptth_resp = file_server::serve_all ( - &state.handlebars, - file_server_root, - ptth_req.method, - &ptth_req.uri, - &ptth_req.headers, - None - ).await; - - let mut resp = Response::builder () - .status (StatusCode::from (ptth_resp.parts.status_code)); - - use std::str::FromStr; - - for (k, v) in ptth_resp.parts.headers.into_iter () { - resp = resp.header (hyper::header::HeaderName::from_str (&k).unwrap (), v); - } - - let body = ptth_resp.body - .map (Body::wrap_stream) - .unwrap_or_else (Body::empty) - ; - - let resp = resp.body (body).unwrap (); - - Ok (resp) - } - else { - Ok (status_reply (StatusCode::NOT_FOUND, "404 Not Found\n")) + let path = req.uri ().path (); + + let path = path.into (); + + let (parts, _) = req.into_parts (); + + let ptth_req = match RequestParts::from_hyper (parts.method, path, parts.headers) { + Ok (x) => x, + _ => return Ok (status_reply (StatusCode::BAD_REQUEST, "Bad request")), + }; + + let default_root = PathBuf::from ("./"); + let file_server_root: &std::path::Path = state.config.file_server_root + .as_ref () + .unwrap_or (&default_root); + + let ptth_resp = file_server::serve_all ( + &state.handlebars, + file_server_root, + ptth_req.method, + &ptth_req.uri, + &ptth_req.headers, + None + ).await; + + let mut resp = Response::builder () + .status (StatusCode::from (ptth_resp.parts.status_code)); + + use std::str::FromStr; + + for (k, v) in ptth_resp.parts.headers.into_iter () { + resp = resp.header (hyper::header::HeaderName::from_str (&k).unwrap (), v); } + + let body = ptth_resp.body + .map (Body::wrap_stream) + .unwrap_or_else (Body::empty) + ; + + let resp = resp.body (body).unwrap (); + + Ok (resp) } #[derive (Deserialize)] diff --git a/src/http_serde.rs b/src/http_serde.rs index 2ef2fc5..056ceb4 100644 --- a/src/http_serde.rs +++ b/src/http_serde.rs @@ -158,11 +158,17 @@ 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 (); }); self.body = Some (rx); + self } } diff --git a/src/relay/mod.rs b/src/relay/mod.rs index ef8aee9..6d017fb 100644 --- a/src/relay/mod.rs +++ b/src/relay/mod.rs @@ -523,6 +523,10 @@ async fn handle_all (req: Request , state: Arc ) error_reply (StatusCode::BAD_REQUEST, "Bad URI format") } } + else if path == "/" { + let s = state.handlebars.render ("relay_root", &()).unwrap (); + ok_reply (s) + } else if path == "/frontend/relay_up_check" { error_reply (StatusCode::OK, "Relay is up") } @@ -539,6 +543,7 @@ pub fn load_templates () for (k, v) in vec! [ ("relay_server_list", "relay_server_list.html"), + ("relay_root", "relay_root.html"), ].into_iter () { handlebars.register_template_file (k, format! ("ptth_handlebars/{}", v))?; } diff --git a/src/server/file_server.rs b/src/server/file_server.rs index e25b7e3..35d9ac2 100644 --- a/src/server/file_server.rs +++ b/src/server/file_server.rs @@ -10,8 +10,10 @@ use std::{ }; use handlebars::Handlebars; +use serde::Serialize; use tokio::{ fs::{ + DirEntry, File, read_dir, ReadDir, @@ -28,7 +30,43 @@ use tracing::{ use regex::Regex; -use crate::http_serde; +use crate::{ + http_serde, + prefix_match, +}; + +#[derive (Serialize)] +struct ServerInfo <'a> { + server_name: &'a str, +} + +#[derive (Serialize)] +struct TemplateDirEntry { + icon: &'static str, + trailing_slash: &'static str, + + // Unfortunately file_name will allocate as long as some platforms + // (Windows!) aren't UTF-8. Cause I don't want to write separate code + // for such a small problem. + + file_name: String, + + // This could be a Cow with file_name if no encoding was done but + // it's simpler to allocate. + + encoded_file_name: String, + + error: bool, +} + +#[derive (Serialize)] +struct TemplateDirPage <'a> { + #[serde (flatten)] + server_info: ServerInfo <'a>, + + path: Cow <'a, str>, + entries: Vec , +} fn parse_range_header (range_str: &str) -> (Option , Option ) { use lazy_static::*; @@ -52,47 +90,60 @@ fn parse_range_header (range_str: &str) -> (Option , Option ) { (start, end) } -use serde::Serialize; -use tokio::fs::DirEntry; - -// This could probably be done with borrows, if I owned the -// tokio::fs::DirEntry instead of consuming it - -#[derive (Serialize)] -struct TemplateDirEntry { - trailing_slash: &'static str, - file_name: String, - encoded_file_name: String, - error: bool, -} - async fn read_dir_entry (entry: DirEntry) -> TemplateDirEntry { - let trailing_slash = match entry.file_type ().await { - Ok (t) => if t.is_dir () { - "/" - } - else { - "" - }, - Err (_) => "", - }; - let file_name = match entry.file_name ().into_string () { Ok (x) => x, Err (_) => return TemplateDirEntry { + icon: "⚠️", trailing_slash: "", - file_name: "".into (), + file_name: "File / directory name is not UTF-8".into (), encoded_file_name: "".into (), error: true, }, }; + let (trailing_slash, icon) = match entry.file_type ().await { + Ok (t) => if t.is_dir () { + ("/", "📁") + } + else { + let icon = if file_name.ends_with (".mp4") { + "🎞️" + } + else if file_name.ends_with (".avi") { + "🎞️" + } + else if file_name.ends_with (".mkv") { + "🎞️" + } + else if file_name.ends_with (".jpg") { + "📷" + } + else if file_name.ends_with (".jpeg") { + "📷" + } + else if file_name.ends_with (".png") { + "📷" + } + else if file_name.ends_with (".bmp") { + "📷" + } + else { + "📄" + }; + + ("", icon) + }, + Err (_) => ("", "⚠️"), + }; + use percent_encoding::*; let encoded_file_name = utf8_percent_encode (&file_name, CONTROLS).to_string (); TemplateDirEntry { + icon, trailing_slash: &trailing_slash, file_name, encoded_file_name, @@ -100,6 +151,25 @@ async fn read_dir_entry (entry: DirEntry) -> TemplateDirEntry } } +async fn serve_root ( + handlebars: &Handlebars <'static>, +) -> http_serde::Response +{ + let server_info = ServerInfo { + server_name: "PTTH file server", + }; + + let s = handlebars.render ("file_server_root", &server_info).unwrap (); + let body = s.into_bytes (); + + let mut resp = http_serde::Response::default (); + resp + .header ("content-type".to_string (), "text/html".to_string ().into_bytes ()) + .body_bytes (body) + ; + resp +} + use std::borrow::Cow; #[instrument (level = "debug", skip (handlebars, dir))] @@ -109,6 +179,10 @@ async fn serve_dir ( mut dir: ReadDir ) -> http_serde::Response { + let server_info = ServerInfo { + server_name: "PTTH file server", + }; + let mut entries = vec! []; while let Ok (Some (entry)) = dir.next_entry ().await { @@ -117,23 +191,16 @@ async fn serve_dir ( entries.sort_unstable_by (|a, b| a.file_name.partial_cmp (&b.file_name).unwrap ()); - #[derive (Serialize)] - struct TemplateDirPage <'a> { - path: Cow <'a, str>, - entries: Vec , - } - let s = handlebars.render ("file_server_dir", &TemplateDirPage { path, entries, + server_info, }).unwrap (); let body = s.into_bytes (); let mut resp = http_serde::Response::default (); - resp.content_length = Some (body.len ().try_into ().unwrap ()); resp .header ("content-type".to_string (), "text/html".to_string ().into_bytes ()) - .header ("content-length".to_string (), body.len ().to_string ().into_bytes ()) .body_bytes (body) ; resp @@ -237,8 +304,8 @@ async fn serve_error ( -> http_serde::Response { let mut resp = http_serde::Response::default (); - resp.status_code (status_code) - .body_bytes (msg.into_bytes ()); + resp.status_code (status_code); + resp.body_bytes (msg.into_bytes ()); resp } @@ -254,31 +321,19 @@ pub async fn serve_all ( -> http_serde::Response { info! ("Client requested {}", uri); - let mut range_start = None; - let mut range_end = None; - - if let Some (v) = headers.get ("range") { - let v = std::str::from_utf8 (v).unwrap (); - - let (start, end) = parse_range_header (v); - range_start = start; - range_end = end; - } - - let should_send_body = match &method { - http_serde::Method::Get => true, - http_serde::Method::Head => false, - m => { - debug! ("Unsupported method {:?}", m); - return serve_error (http_serde::StatusCode::MethodNotAllowed, "Unsupported method".into ()).await; - } - }; use percent_encoding::*; + let uri = match prefix_match (uri, "/files/") { + Some (x) => x, + None => { + return serve_root (handlebars).await; + }, + }; + // TODO: There is totally a dir traversal attack in here somewhere - let encoded_path = &uri [1..]; + let encoded_path = &uri [..]; let path_s = percent_decode (encoded_path.as_bytes ()).decode_utf8 ().unwrap (); let path = Path::new (&*path_s); @@ -294,7 +349,10 @@ pub async fn serve_all ( } } - if let Ok (dir) = read_dir (&full_path).await { + if uri == "/" { + serve_root (handlebars).await + } + else if let Ok (dir) = read_dir (&full_path).await { serve_dir ( handlebars, full_path.to_string_lossy (), @@ -302,6 +360,26 @@ pub async fn serve_all ( ).await } else if let Ok (file) = File::open (&full_path).await { + let mut range_start = None; + let mut range_end = None; + + if let Some (v) = headers.get ("range") { + let v = std::str::from_utf8 (v).unwrap (); + + let (start, end) = parse_range_header (v); + range_start = start; + range_end = end; + } + + let should_send_body = match &method { + http_serde::Method::Get => true, + http_serde::Method::Head => false, + m => { + debug! ("Unsupported method {:?}", m); + return serve_error (http_serde::StatusCode::MethodNotAllowed, "Unsupported method".into ()).await; + } + }; + serve_file ( file, should_send_body, @@ -322,6 +400,7 @@ pub fn load_templates () for (k, v) in vec! [ ("file_server_dir", "file_server_dir.html"), + ("file_server_root", "file_server_root.html"), ].into_iter () { handlebars.register_template_file (k, format! ("ptth_handlebars/{}", v))?; } diff --git a/src/server/mod.rs b/src/server/mod.rs index aa3e041..6b9f06b 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -66,25 +66,19 @@ async fn handle_req_resp <'a> ( debug! ("Handling request {}", req_id); - let response = if let Some (uri) = prefix_match (&parts.uri, "/files") { - let default_root = PathBuf::from ("./"); - let file_server_root: &std::path::Path = state.config.file_server_root - .as_ref () - .unwrap_or (&default_root); - - file_server::serve_all ( - &state.handlebars, - file_server_root, - parts.method, - uri, - &parts.headers, - state.hidden_path.as_ref ().map (|p| p.as_path ()) - ).await - } - else { - debug! ("404 not found"); - status_reply (http_serde::StatusCode::NotFound, "404 Not Found") - }; + let default_root = PathBuf::from ("./"); + let file_server_root: &std::path::Path = state.config.file_server_root + .as_ref () + .unwrap_or (&default_root); + + let response = file_server::serve_all ( + &state.handlebars, + file_server_root, + parts.method, + &parts.uri, + &parts.headers, + state.hidden_path.as_ref ().map (|p| p.as_path ()) + ).await; let mut resp_req = state.client .post (&format! ("{}/http_response/{}", state.config.relay_url, req_id)) diff --git a/todo.md b/todo.md index b62398a..0064c5b 100644 --- a/todo.md +++ b/todo.md @@ -1,8 +1,7 @@ - Not working behind Nginx (Works okay behind Caddy) - Reduce idle memory use? -- Folder icons in dir list -- ".." from server to server list is broken +- Package templates into exe for release - Redirect to add trailing slashes - Add file size in directory listing - Allow spaces in server names