Markdown preview added to the standalone server, not linked in yet
parent
13b816fd6e
commit
ff6e841e0b
|
@ -22,6 +22,7 @@ hyper = "0.13.8"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
maplit = "1.0.2"
|
maplit = "1.0.2"
|
||||||
percent-encoding = "2.1.0"
|
percent-encoding = "2.1.0"
|
||||||
|
pulldown-cmark = "0.8.0"
|
||||||
rand = "0.7.3"
|
rand = "0.7.3"
|
||||||
regex = "1.4.1"
|
regex = "1.4.1"
|
||||||
reqwest = { version = "0.10.8", features = ["stream"] }
|
reqwest = { version = "0.10.8", features = ["stream"] }
|
||||||
|
@ -34,6 +35,7 @@ tracing-futures = "0.2.4"
|
||||||
tracing-subscriber = "0.2.15"
|
tracing-subscriber = "0.2.15"
|
||||||
toml = "0.5.7"
|
toml = "0.5.7"
|
||||||
ulid = "0.4.1"
|
ulid = "0.4.1"
|
||||||
|
url = "2.2.0"
|
||||||
|
|
||||||
always_equal = { path = "crates/always_equal" }
|
always_equal = { path = "crates/always_equal" }
|
||||||
|
|
||||||
|
|
|
@ -47,13 +47,13 @@ async fn handle_all (req: Request <Body>, state: Arc <ServerState <'static>>)
|
||||||
{
|
{
|
||||||
debug! ("req.uri () = {:?}", req.uri ());
|
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 (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,
|
Ok (x) => x,
|
||||||
_ => return Ok (status_reply (StatusCode::BAD_REQUEST, "Bad request")),
|
_ => return Ok (status_reply (StatusCode::BAD_REQUEST, "Bad request")),
|
||||||
};
|
};
|
||||||
|
|
|
@ -381,6 +381,25 @@ fn serve_307 (location: String) -> Response {
|
||||||
resp
|
resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_markdown (bytes: &[u8]) -> Result <String, MarkdownError> {
|
||||||
|
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.
|
// Sort of an internal API endpoint to make testing work better.
|
||||||
// Eventually we could expose this as JSON or Msgpack or whatever. For now
|
// 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
|
// it's just a Rust struct that we can test on without caring about
|
||||||
|
@ -400,10 +419,19 @@ struct ServeFileParams {
|
||||||
file: AlwaysEqual <File>,
|
file: AlwaysEqual <File>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive (Debug, PartialEq)]
|
||||||
|
enum MarkdownError {
|
||||||
|
FileIsTooBig,
|
||||||
|
FileIsNotMarkdown,
|
||||||
|
FileIsNotUtf8,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive (Debug, PartialEq)]
|
#[derive (Debug, PartialEq)]
|
||||||
enum InternalResponse {
|
enum InternalResponse {
|
||||||
Favicon,
|
Favicon,
|
||||||
Forbidden,
|
Forbidden,
|
||||||
|
InvalidUri,
|
||||||
|
InvalidQuery,
|
||||||
MethodNotAllowed,
|
MethodNotAllowed,
|
||||||
NotFound,
|
NotFound,
|
||||||
RangeNotSatisfiable (u64),
|
RangeNotSatisfiable (u64),
|
||||||
|
@ -412,8 +440,8 @@ enum InternalResponse {
|
||||||
ServeDir (ServeDirParams),
|
ServeDir (ServeDirParams),
|
||||||
ServeFile (ServeFileParams),
|
ServeFile (ServeFileParams),
|
||||||
|
|
||||||
MarkdownError,
|
MarkdownErr (MarkdownError),
|
||||||
ServeMarkdownPreview (String),
|
MarkdownPreview (String),
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn internal_serve_all (
|
async fn internal_serve_all (
|
||||||
|
@ -425,10 +453,16 @@ async fn internal_serve_all (
|
||||||
)
|
)
|
||||||
-> InternalResponse
|
-> InternalResponse
|
||||||
{
|
{
|
||||||
|
use std::str::FromStr;
|
||||||
use InternalResponse::*;
|
use InternalResponse::*;
|
||||||
|
|
||||||
info! ("Client requested {}", uri);
|
info! ("Client requested {}", uri);
|
||||||
|
|
||||||
|
let uri = match hyper::Uri::from_str (uri) {
|
||||||
|
Err (_) => return InvalidUri,
|
||||||
|
Ok (x) => x,
|
||||||
|
};
|
||||||
|
|
||||||
let send_body = match &method {
|
let send_body = match &method {
|
||||||
Method::Get => true,
|
Method::Get => true,
|
||||||
Method::Head => false,
|
Method::Head => false,
|
||||||
|
@ -438,22 +472,22 @@ async fn internal_serve_all (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if uri == "/favicon.ico" {
|
if uri.path () == "/favicon.ico" {
|
||||||
return Favicon;
|
return Favicon;
|
||||||
}
|
}
|
||||||
|
|
||||||
let uri = match prefix_match ("/files", uri) {
|
let path = match prefix_match ("/files", uri.path ()) {
|
||||||
Some (x) => x,
|
Some (x) => x,
|
||||||
None => return Root,
|
None => return Root,
|
||||||
};
|
};
|
||||||
|
|
||||||
if uri == "" {
|
if path == "" {
|
||||||
return Redirect ("files/".to_string ());
|
return Redirect ("files/".to_string ());
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: There is totally a dir traversal attack in here somewhere
|
// 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_s = percent_decode (encoded_path.as_bytes ()).decode_utf8 ().unwrap ();
|
||||||
let path = Path::new (&*path_s);
|
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 ()));
|
return Redirect (format! ("{}/", path.file_name ().unwrap ().to_str ().unwrap ()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if uri.query ().is_some () {
|
||||||
|
return InvalidQuery;
|
||||||
|
}
|
||||||
|
|
||||||
let dir = dir.into ();
|
let dir = dir.into ();
|
||||||
|
|
||||||
ServeDir (ServeDirParams {
|
ServeDir (ServeDirParams {
|
||||||
|
@ -483,28 +521,54 @@ async fn internal_serve_all (
|
||||||
path: full_path,
|
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_md = file.metadata ().await.unwrap ();
|
||||||
let file_len = file_md.len ();
|
let file_len = file_md.len ();
|
||||||
|
|
||||||
let range_header = headers.get ("range").map (|v| std::str::from_utf8 (v).ok ()).flatten ();
|
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) {
|
match check_range (range_header, file_len) {
|
||||||
ParsedRange::RangeNotSatisfiable (file_len) => RangeNotSatisfiable (file_len),
|
ParsedRange::RangeNotSatisfiable (file_len) => RangeNotSatisfiable (file_len),
|
||||||
ParsedRange::Ok (range) => ServeFile (ServeFileParams {
|
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,
|
file,
|
||||||
send_body,
|
send_body,
|
||||||
range,
|
range,
|
||||||
range_requested: false,
|
range_requested: false,
|
||||||
}),
|
})
|
||||||
ParsedRange::PartialContent (range) => ServeFile (ServeFileParams {
|
}
|
||||||
|
},
|
||||||
|
ParsedRange::PartialContent (range) => {
|
||||||
|
if uri.query ().is_some () {
|
||||||
|
InvalidQuery
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let file = file.into ();
|
||||||
|
|
||||||
|
ServeFile (ServeFileParams {
|
||||||
file,
|
file,
|
||||||
send_body,
|
send_body,
|
||||||
range,
|
range,
|
||||||
range_requested: true,
|
range_requested: true,
|
||||||
}),
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -529,6 +593,8 @@ pub async fn serve_all (
|
||||||
match internal_serve_all (root, method, uri, headers, hidden_path).await {
|
match internal_serve_all (root, method, uri, headers, hidden_path).await {
|
||||||
Favicon => serve_error (StatusCode::NotFound, ""),
|
Favicon => serve_error (StatusCode::NotFound, ""),
|
||||||
Forbidden => serve_error (StatusCode::Forbidden, "403 Forbidden"),
|
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"),
|
MethodNotAllowed => serve_error (StatusCode::MethodNotAllowed, "Unsupported method"),
|
||||||
NotFound => serve_error (StatusCode::NotFound, "404 Not Found"),
|
NotFound => serve_error (StatusCode::NotFound, "404 Not Found"),
|
||||||
RangeNotSatisfiable (file_len) => {
|
RangeNotSatisfiable (file_len) => {
|
||||||
|
@ -550,8 +616,12 @@ pub async fn serve_all (
|
||||||
range,
|
range,
|
||||||
range_requested,
|
range_requested,
|
||||||
}) => serve_file (file.into_inner (), send_body, range, range_requested).await,
|
}) => serve_file (file.into_inner (), send_body, range, range_requested).await,
|
||||||
MarkdownError => serve_error (StatusCode::InternalServerError, "Error while rendering Markdown preview"),
|
MarkdownErr (e) => match e {
|
||||||
ServeMarkdownPreview (s) => serve_html (s),
|
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! [
|
for (uri_path, expected) in vec! [
|
||||||
("/", Root),
|
("/", Root),
|
||||||
("/files", Redirect ("files/".to_string ())),
|
("/files", Redirect ("files/".to_string ())),
|
||||||
|
("/files/?", InvalidQuery),
|
||||||
("/files/src", Redirect ("src/".to_string ())),
|
("/files/src", Redirect ("src/".to_string ())),
|
||||||
|
("/files/src/?", InvalidQuery),
|
||||||
("/files/src/bad_passwords.txt", ServeFile (ServeFileParams {
|
("/files/src/bad_passwords.txt", ServeFile (ServeFileParams {
|
||||||
send_body: true,
|
send_body: true,
|
||||||
range: 0..1_048_576,
|
range: 0..1_048_576,
|
||||||
range_requested: false,
|
range_requested: false,
|
||||||
file: AlwaysEqual::testing_blank (),
|
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 ("<h1>Markdown test</h1>\n<p>This is a test file for the Markdown previewing feature.</p>\n<p>Don\'t change it, it will break the tests.</p>\n".into ())),
|
||||||
|
("/ ", InvalidUri),
|
||||||
].into_iter () {
|
].into_iter () {
|
||||||
let resp = internal_serve_all (
|
let resp = internal_serve_all (
|
||||||
&file_server_root,
|
&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.",
|
||||||
|
"<p>Hello world, this is a <del>complicated</del> <em>very simple</em> example.</p>\n"
|
||||||
|
),
|
||||||
|
].into_iter () {
|
||||||
|
assert_eq! (expected, &render_markdown (input.as_bytes ()).unwrap ());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue