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
\nThis is a test file for the Markdown previewing feature.
\nDon\'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.