From ff6e841e0bf7a97d49e8d882906dcff48c2f73af Mon Sep 17 00:00:00 2001 From: _ <> Date: Tue, 10 Nov 2020 02:39:20 +0000 Subject: [PATCH] Markdown preview added to the standalone server, not linked in yet --- Cargo.toml | 2 + src/bin/ptth_file_server.rs | 6 +- src/server/file_server.rs | 148 ++++++++++++++++++++++++++++++------ test.md | 5 ++ 4 files changed, 135 insertions(+), 26 deletions(-) create mode 100644 test.md diff --git a/Cargo.toml b/Cargo.toml index 6cf0f5b..5bc3f40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ hyper = "0.13.8" lazy_static = "1.4.0" maplit = "1.0.2" percent-encoding = "2.1.0" +pulldown-cmark = "0.8.0" rand = "0.7.3" regex = "1.4.1" reqwest = { version = "0.10.8", features = ["stream"] } @@ -34,6 +35,7 @@ tracing-futures = "0.2.4" tracing-subscriber = "0.2.15" toml = "0.5.7" ulid = "0.4.1" +url = "2.2.0" always_equal = { path = "crates/always_equal" } diff --git a/src/bin/ptth_file_server.rs b/src/bin/ptth_file_server.rs index c6e263f..b287f05 100644 --- a/src/bin/ptth_file_server.rs +++ b/src/bin/ptth_file_server.rs @@ -47,13 +47,13 @@ async fn handle_all (req: Request , state: Arc >) { debug! ("req.uri () = {:?}", req.uri ()); - let path = req.uri ().path (); + let path_and_query = req.uri ().path_and_query ().map (|x| x.as_str ()).unwrap_or_else (|| req.uri ().path ()); - let path = path.into (); + let path_and_query = path_and_query.into (); let (parts, _) = req.into_parts (); - let ptth_req = match RequestParts::from_hyper (parts.method, path, parts.headers) { + let ptth_req = match RequestParts::from_hyper (parts.method, path_and_query, parts.headers) { Ok (x) => x, _ => return Ok (status_reply (StatusCode::BAD_REQUEST, "Bad request")), }; diff --git a/src/server/file_server.rs b/src/server/file_server.rs index cd8594f..c8d384c 100644 --- a/src/server/file_server.rs +++ b/src/server/file_server.rs @@ -381,6 +381,25 @@ fn serve_307 (location: String) -> Response { resp } +fn render_markdown (bytes: &[u8]) -> Result { + use pulldown_cmark::{Parser, Options, html}; + + let markdown_input = match std::str::from_utf8 (bytes) { + Err (_) => return Err (MarkdownError::FileIsNotUtf8), + Ok (x) => x, + }; + + let mut options = Options::empty (); + options.insert (Options::ENABLE_STRIKETHROUGH); + let parser = Parser::new_ext (markdown_input, options); + + // Write to String buffer. + let mut html_output = String::new (); + html::push_html (&mut html_output, parser); + + Ok (html_output) +} + // Sort of an internal API endpoint to make testing work better. // Eventually we could expose this as JSON or Msgpack or whatever. For now // it's just a Rust struct that we can test on without caring about @@ -400,10 +419,19 @@ struct ServeFileParams { file: AlwaysEqual , } +#[derive (Debug, PartialEq)] +enum MarkdownError { + FileIsTooBig, + FileIsNotMarkdown, + FileIsNotUtf8, +} + #[derive (Debug, PartialEq)] enum InternalResponse { Favicon, Forbidden, + InvalidUri, + InvalidQuery, MethodNotAllowed, NotFound, RangeNotSatisfiable (u64), @@ -412,8 +440,8 @@ enum InternalResponse { ServeDir (ServeDirParams), ServeFile (ServeFileParams), - MarkdownError, - ServeMarkdownPreview (String), + MarkdownErr (MarkdownError), + MarkdownPreview (String), } async fn internal_serve_all ( @@ -425,10 +453,16 @@ async fn internal_serve_all ( ) -> InternalResponse { + use std::str::FromStr; use InternalResponse::*; info! ("Client requested {}", uri); + let uri = match hyper::Uri::from_str (uri) { + Err (_) => return InvalidUri, + Ok (x) => x, + }; + let send_body = match &method { Method::Get => true, Method::Head => false, @@ -438,22 +472,22 @@ async fn internal_serve_all ( } }; - if uri == "/favicon.ico" { + if uri.path () == "/favicon.ico" { return Favicon; } - let uri = match prefix_match ("/files", uri) { + let path = match prefix_match ("/files", uri.path ()) { Some (x) => x, None => return Root, }; - if uri == "" { + if path == "" { return Redirect ("files/".to_string ()); } // TODO: There is totally a dir traversal attack in here somewhere - let encoded_path = &uri [1..]; + let encoded_path = &path [1..]; let path_s = percent_decode (encoded_path.as_bytes ()).decode_utf8 ().unwrap (); let path = Path::new (&*path_s); @@ -476,6 +510,10 @@ async fn internal_serve_all ( return Redirect (format! ("{}/", path.file_name ().unwrap ().to_str ().unwrap ())); } + if uri.query ().is_some () { + return InvalidQuery; + } + let dir = dir.into (); ServeDir (ServeDirParams { @@ -483,28 +521,54 @@ async fn internal_serve_all ( path: full_path, }) } - else if let Ok (file) = File::open (&full_path).await { + else if let Ok (mut file) = File::open (&full_path).await { let file_md = file.metadata ().await.unwrap (); let file_len = file_md.len (); let range_header = headers.get ("range").map (|v| std::str::from_utf8 (v).ok ()).flatten (); - let file = file.into (); - match check_range (range_header, file_len) { ParsedRange::RangeNotSatisfiable (file_len) => RangeNotSatisfiable (file_len), - ParsedRange::Ok (range) => ServeFile (ServeFileParams { - file, - send_body, - range, - range_requested: false, - }), - ParsedRange::PartialContent (range) => ServeFile (ServeFileParams { - file, - send_body, - range, - range_requested: true, - }), + ParsedRange::Ok (range) => { + if uri.query () == Some ("as_markdown") { + const MAX_BUF_SIZE: u32 = 1_000_000; + if file_len > MAX_BUF_SIZE.try_into ().unwrap () { + MarkdownErr (MarkdownError::FileIsTooBig) + } + else { + let mut buffer = vec! [0u8; MAX_BUF_SIZE.try_into ().unwrap ()]; + let bytes_read = file.read (&mut buffer).await.unwrap (); + buffer.truncate (bytes_read); + + MarkdownPreview (render_markdown (&buffer).unwrap ()) + } + } + else { + let file = file.into (); + + ServeFile (ServeFileParams { + file, + send_body, + range, + range_requested: false, + }) + } + }, + ParsedRange::PartialContent (range) => { + if uri.query ().is_some () { + InvalidQuery + } + else { + let file = file.into (); + + ServeFile (ServeFileParams { + file, + send_body, + range, + range_requested: true, + }) + } + }, } } else { @@ -529,6 +593,8 @@ pub async fn serve_all ( match internal_serve_all (root, method, uri, headers, hidden_path).await { Favicon => serve_error (StatusCode::NotFound, ""), Forbidden => serve_error (StatusCode::Forbidden, "403 Forbidden"), + InvalidUri => serve_error (StatusCode::BadRequest, "Invalid URI"), + InvalidQuery => serve_error (StatusCode::BadRequest, "Query is invalid for this object"), MethodNotAllowed => serve_error (StatusCode::MethodNotAllowed, "Unsupported method"), NotFound => serve_error (StatusCode::NotFound, "404 Not Found"), RangeNotSatisfiable (file_len) => { @@ -550,8 +616,12 @@ pub async fn serve_all ( range, range_requested, }) => serve_file (file.into_inner (), send_body, range, range_requested).await, - MarkdownError => serve_error (StatusCode::InternalServerError, "Error while rendering Markdown preview"), - ServeMarkdownPreview (s) => serve_html (s), + MarkdownErr (e) => match e { + MarkdownError::FileIsTooBig => serve_error (StatusCode::InternalServerError, "File is too big to preview as Markdown"), + MarkdownError::FileIsNotMarkdown => serve_error (StatusCode::BadRequest, "File is not Markdown"), + MarkdownError::FileIsNotUtf8 => serve_error (StatusCode::BadRequest, "File is not UTF-8"), + }, + MarkdownPreview (s) => serve_html (s), } } @@ -730,13 +800,23 @@ mod tests { for (uri_path, expected) in vec! [ ("/", Root), ("/files", Redirect ("files/".to_string ())), + ("/files/?", InvalidQuery), ("/files/src", Redirect ("src/".to_string ())), + ("/files/src/?", InvalidQuery), ("/files/src/bad_passwords.txt", ServeFile (ServeFileParams { send_body: true, range: 0..1_048_576, range_requested: false, file: AlwaysEqual::testing_blank (), })), + ("/files/test.md", ServeFile (ServeFileParams { + send_body: true, + range: 0..117, + range_requested: false, + file: AlwaysEqual::testing_blank (), + })), + ("/files/test.md?as_markdown", MarkdownPreview ("

Markdown test

\n

This is a test file for the Markdown previewing feature.

\n

Don\'t change it, it will break the tests.

\n".into ())), + ("/ ", InvalidUri), ].into_iter () { let resp = internal_serve_all ( &file_server_root, @@ -778,4 +858,26 @@ mod tests { } }); } + + #[test] + fn parse_uri () { + use hyper::Uri; + + assert! (Uri::from_maybe_shared ("/").is_ok ()); + } + + #[test] + fn markdown () { + use super::*; + + for (input, expected) in vec! [ + ("", ""), + ( + "Hello world, this is a ~~complicated~~ *very simple* example.", + "

Hello world, this is a complicated very simple example.

\n" + ), + ].into_iter () { + assert_eq! (expected, &render_markdown (input.as_bytes ()).unwrap ()); + } + } } diff --git a/test.md b/test.md new file mode 100644 index 0000000..120b141 --- /dev/null +++ b/test.md @@ -0,0 +1,5 @@ +# Markdown test + +This is a test file for the Markdown previewing feature. + +Don't change it, it will break the tests.