From 0c05d6d16259e6cee7cbb200a417a9c6dc9dfe77 Mon Sep 17 00:00:00 2001 From: panicbit Date: Sat, 14 Nov 2020 09:55:21 +0100 Subject: [PATCH 01/38] implement document API --- examples/document.rs | 50 ++++++ examples/northstar_logo.txt | 5 + src/types.rs | 4 + src/types/body.rs | 8 + src/types/document.rs | 332 ++++++++++++++++++++++++++++++++++++ src/types/response.rs | 7 +- src/util.rs | 29 ++-- 7 files changed, 418 insertions(+), 17 deletions(-) create mode 100644 examples/document.rs create mode 100644 examples/northstar_logo.txt create mode 100644 src/types/document.rs diff --git a/examples/document.rs b/examples/document.rs new file mode 100644 index 0000000..8b096f3 --- /dev/null +++ b/examples/document.rs @@ -0,0 +1,50 @@ +use anyhow::*; +use futures::{future::BoxFuture, FutureExt}; +use log::LevelFilter; +use northstar::{Server, Request, Response, GEMINI_PORT, Document}; +use northstar::document::HeadingLevel::*; + +#[tokio::main] +async fn main() -> Result<()> { + env_logger::builder() + .filter_module("northstar", LevelFilter::Debug) + .init(); + + Server::bind(("localhost", GEMINI_PORT)) + .serve(handle_request) + .await +} + +fn handle_request(_request: Request) -> BoxFuture<'static, Result> { + async move { + let mut document = Document::new(); + + document + .add_preformatted(include_str!("northstar_logo.txt")) + .add_blank_line() + .add_link("https://docs.rs/northstar", "Documentation") + .add_link("https://github.com/panicbit/northstar", "GitHub") + .add_blank_line() + .add_heading(H1, "Usage") + .add_blank_line() + .add_text("Add the latest version of northstar to your `Cargo.toml`.") + .add_blank_line() + .add_heading(H2, "Manually") + .add_blank_line() + .add_preformatted_with_alt("alt", r#"northstar = "0.3.0" # check crates.io for the latest version"#) + .add_blank_line() + .add_heading(H2, "Automatically") + .add_blank_line() + .add_preformatted_with_alt("sh", "cargo add northstar") + .add_blank_line() + .add_heading(H1, "Generating a key & certificate") + .add_blank_line() + .add_preformatted_with_alt("sh", concat!( + "mkdir cert && cd cert\n", + "openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365", + )); + + Ok(Response::document(document)) + } + .boxed() +} diff --git a/examples/northstar_logo.txt b/examples/northstar_logo.txt new file mode 100644 index 0000000..9fe390c --- /dev/null +++ b/examples/northstar_logo.txt @@ -0,0 +1,5 @@ + __ __ __ + ____ ____ _____/ /_/ /_ _____/ /_____ ______ + / __ \/ __ \/ ___/ __/ __ \/ ___/ __/ __ `/ ___/ + / / / / /_/ / / / /_/ / / (__ ) /_/ /_/ / / +/_/ /_/\____/_/ \__/_/ /_/____/\__/\__,_/_/ \ No newline at end of file diff --git a/src/types.rs b/src/types.rs index b52d0da..b376427 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,5 +1,6 @@ pub use ::mime::Mime; pub use rustls::Certificate; +pub use uriparse::URIReference; mod meta; pub use self::meta::Meta; @@ -18,3 +19,6 @@ pub use response::Response; mod body; pub use body::Body; + +pub mod document; +pub use document::Document; diff --git a/src/types/body.rs b/src/types/body.rs index 7e697b5..dfeb8ca 100644 --- a/src/types/body.rs +++ b/src/types/body.rs @@ -1,10 +1,18 @@ use tokio::{io::AsyncRead, fs::File}; +use crate::types::Document; + pub enum Body { Bytes(Vec), Reader(Box), } +impl From for Body { + fn from(document: Document) -> Self { + Body::from(document.to_string()) + } +} + impl From> for Body { fn from(bytes: Vec) -> Self { Self::Bytes(bytes) diff --git a/src/types/document.rs b/src/types/document.rs new file mode 100644 index 0000000..e62eecb --- /dev/null +++ b/src/types/document.rs @@ -0,0 +1,332 @@ +use std::convert::TryInto; +use std::fmt; + +use itertools::Itertools; +use crate::types::URIReference; + +#[derive(Default)] +pub struct Document { + items: Vec, +} + +impl Document { + pub fn new() -> Self { + Self::default() + } + + pub fn add_item(&mut self, item: Item) -> &mut Self { + self.items.push(item); + self + } + + pub fn add_items(&mut self, items: I) -> &mut Self + where + I: IntoIterator, + { + self.items.extend(items); + self + } + + pub fn add_blank_line(&mut self) -> &mut Self { + self.add_item(Item::Text(Text::blank())) + } + + pub fn add_text(&mut self, text: &str) -> &mut Self { + let text = text + .lines() + .map(Text::new_lossy) + .map(Item::Text); + + self.add_items(text); + + self + } + + pub fn add_link<'a, U>(&mut self, uri: U, label: impl AsRef + Into) -> &mut Self + where + U: TryInto>, + { + let uri = uri + .try_into() + .map(URIReference::into_owned) + .or_else(|_| ".".try_into()).expect("Northstar BUG"); + let label = LinkLabel::from_lossy(label); + let link = Link { uri, label: Some(label) }; + let link = Item::Link(link); + + self.add_item(link); + + self + } + + pub fn add_link_without_label(&mut self, uri: URIReference<'static>) -> &mut Self { + let link = Link { + uri, + label: None, + }; + let link = Item::Link(link); + + self.add_item(link); + + self + } + + pub fn add_preformatted(&mut self, preformatted_text: &str) -> &mut Self { + self.add_preformatted_with_alt("", preformatted_text) + } + + pub fn add_preformatted_with_alt(&mut self, alt: &str, preformatted_text: &str) -> &mut Self { + let alt = AltText::new_lossy(alt); + let lines = preformatted_text + .lines() + .map(PreformattedText::new_lossy) + .collect(); + let preformatted = Preformatted { + alt, + lines, + }; + let preformatted = Item::Preformatted(preformatted); + + self.add_item(preformatted); + + self + } + + pub fn add_heading(&mut self, level: HeadingLevel, text: impl AsRef + Into) -> &mut Self { + let text = HeadingText::new_lossy(text); + let heading = Heading { + level, + text, + }; + let heading = Item::Heading(heading); + + self.add_item(heading); + + self + } + + pub fn add_unordered_list_item(&mut self, text: &str) -> &mut Self { + let item = UnorderedListItem::new_lossy(text); + let item = Item::UnorderedListItem(item); + + self.add_item(item); + + self + } + + pub fn add_quote(&mut self, text: &str) -> &mut Self { + let quote = text + .lines() + .map(Quote::new_lossy) + .map(Item::Quote); + + self.add_items(quote); + + self + } +} + +impl fmt::Display for Document { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for item in &self.items { + match item { + Item::Text(text) => writeln!(f, "{}", text.0)?, + Item::Link(link) => { + let separator = if link.label.is_some() {" "} else {""}; + let label = link.label.as_ref().map(|label| label.0.as_str()) + .unwrap_or(""); + + writeln!(f, "=>{}{}{}", link.uri, separator, label)?; + } + Item::Preformatted(preformatted) => { + writeln!(f, "```{}", preformatted.alt.0)?; + + for line in &preformatted.lines { + writeln!(f, "{}", line.0)?; + } + + writeln!(f, "```")? + } + Item::Heading(heading) => { + let level = match heading.level { + HeadingLevel::H1 => "#", + HeadingLevel::H2 => "##", + HeadingLevel::H3 => "###", + }; + + writeln!(f, "{} {}", level, heading.text.0)?; + } + Item::UnorderedListItem(item) => writeln!(f, "* {}", item.0)?, + Item::Quote(quote) => writeln!(f, "> {}", quote.0)?, + } + } + + Ok(()) + } +} + +pub enum Item { + Text(Text), + Link(Link), + Preformatted(Preformatted), + Heading(Heading), + UnorderedListItem(UnorderedListItem), + Quote(Quote), +} + +#[derive(Default)] +pub struct Text(String); + +impl Text { + pub fn blank() -> Self { + Self::default() + } + + pub fn new_lossy(line: impl AsRef + Into) -> Self { + Self(lossy_escaped_line(line, SPECIAL_STARTS)) + } +} + +pub struct Link { + pub uri: URIReference<'static>, + pub label: Option, +} + +pub struct LinkLabel(String); + +impl LinkLabel { + pub fn from_lossy(line: impl AsRef + Into) -> Self { + let line = strip_newlines(line); + + LinkLabel(line) + } +} + +pub struct Preformatted { + pub alt: AltText, + pub lines: Vec, +} + +pub struct PreformattedText(String); + +impl PreformattedText { + pub fn new_lossy(line: impl AsRef + Into) -> Self { + Self(lossy_escaped_line(line, &[PREFORMATTED_TOGGLE_START])) + } +} + +pub struct AltText(String); + +impl AltText { + pub fn new_lossy(alt: &str) -> Self { + let alt = strip_newlines(alt); + + Self(alt) + } +} + +pub struct Heading { + pub level: HeadingLevel, + pub text: HeadingText, +} + +pub enum HeadingLevel { + H1, + H2, + H3, +} + +impl Heading { + pub fn new_lossy(level: HeadingLevel, line: &str) -> Self { + Self { + level, + text: HeadingText::new_lossy(line), + } + } +} + +pub struct HeadingText(String); + +impl HeadingText { + pub fn new_lossy(line: impl AsRef + Into) -> Self { + let line = strip_newlines(line); + + Self(lossy_escaped_line(line, &[HEADING_START])) + } +} + +pub struct UnorderedListItem(String); + +impl UnorderedListItem { + pub fn new_lossy(text: &str) -> Self { + let text = strip_newlines(text); + + Self(text) + } +} + +pub struct Quote(String); + +impl Quote { + pub fn new_lossy(text: &str) -> Self { + Self(lossy_escaped_line(text, &[QUOTE_START])) + } +} + + +const LINK_START: &str = "=>"; +const PREFORMATTED_TOGGLE_START: &str = "```"; +const HEADING_START: &str = "#"; +const UNORDERED_LIST_ITEM_START: &str = "*"; +const QUOTE_START: &str = ">"; + +const SPECIAL_STARTS: &[&str] = &[ + LINK_START, + PREFORMATTED_TOGGLE_START, + HEADING_START, + UNORDERED_LIST_ITEM_START, + QUOTE_START, +]; + +fn starts_with_any(s: &str, starts: &[&str]) -> bool { + for start in starts { + if s.starts_with(start) { + return true; + } + } + + false +} + +fn lossy_escaped_line(line: impl AsRef + Into, escape_starts: &[&str]) -> String { + let line_ref = line.as_ref(); + let contains_newline = line_ref.contains('\n'); + let has_special_start = starts_with_any(line_ref, escape_starts); + + if !contains_newline && !has_special_start { + return line.into(); + } + + let mut line = String::new(); + + if has_special_start { + line.push(' '); + } + + if let Some(line_ref) = line_ref.split('\n').next() { + line.push_str(line_ref); + } + + line +} + +fn strip_newlines(text: impl AsRef + Into) -> String { + if !text.as_ref().contains(&['\r', '\n'][..]) { + return text.into(); + } + + text.as_ref() + .lines() + .filter(|part| !part.is_empty()) + .join(" ") +} diff --git a/src/types/response.rs b/src/types/response.rs index f2389a0..6a24de7 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -1,5 +1,6 @@ use anyhow::*; -use crate::types::{ResponseHeader, Body, Mime}; +use crate::types::{ResponseHeader, Body, Mime, Document}; +use crate::GEMINI_MIME; pub struct Response { header: ResponseHeader, @@ -14,6 +15,10 @@ impl Response { } } + pub fn document(document: Document) -> Self { + Self::success(&GEMINI_MIME).with_body(document) + } + pub fn input(prompt: impl AsRef + Into) -> Result { let header = ResponseHeader::input(prompt)?; Ok(Self::new(header)) diff --git a/src/util.rs b/src/util.rs index c1265e4..bd7a28c 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,12 +1,12 @@ use std::path::Path; use mime::Mime; -use percent_encoding::utf8_percent_encode; use anyhow::*; use tokio::{ fs::{self, File}, io, }; -use crate::{GEMINI_MIME, GEMINI_MIME_STR, Response}; +use crate::GEMINI_MIME_STR; +use crate::types::{Response, Document, document::HeadingLevel::*}; use itertools::Itertools; pub async fn serve_file>(path: P, mime: &Mime) -> Result { @@ -49,8 +49,6 @@ pub async fn serve_dir, P: AsRef>(dir: D, virtual_path: &[P } async fn serve_dir_listing, B: AsRef>(path: P, virtual_path: &[B]) -> Result { - use std::fmt::Write; - let mut dir = match fs::read_dir(path).await { Ok(dir) => dir, Err(err) => match err.kind() { @@ -60,13 +58,13 @@ async fn serve_dir_listing, B: AsRef>(path: P, virtual_path }; let breadcrumbs = virtual_path.iter().map(|segment| segment.as_ref().display()).join("/"); - let mut listing = String::new(); - - writeln!(listing, "# Index of /{}", breadcrumbs)?; - writeln!(listing)?; + let mut document = Document::new(); + + document.add_heading(H1, format!("Index of /{}", breadcrumbs)); + document.add_blank_line(); if virtual_path.get(0).map(<_>::as_ref) != Some(Path::new("")) { - writeln!(listing, "=> .. 📁 ../")?; + document.add_link("..", "📁 ../"); } while let Some(entry) = dir.next_entry().await.context("Failed to list directory")? { @@ -75,18 +73,17 @@ async fn serve_dir_listing, B: AsRef>(path: P, virtual_path let is_dir = entry.file_type().await .with_context(|| format!("Failed to get file type of `{}`", entry.path().display()))? .is_dir(); + let trailing_slash = if is_dir { "/" } else { "" }; + let uri = format!("./{}{}", file_name, trailing_slash); - writeln!( - listing, - "=> {link}{trailing_slash} {icon} {name}{trailing_slash}", + document.add_link(uri.as_str(), format!("{icon} {name}{trailing_slash}", icon = if is_dir { '📁' } else { '📄' }, - link = utf8_percent_encode(&file_name, percent_encoding::NON_ALPHANUMERIC), - trailing_slash = if is_dir { "/" } else { "" }, name = file_name, - )?; + trailing_slash = trailing_slash + )); } - Ok(Response::success(&GEMINI_MIME).with_body(listing)) + Ok(Response::document(document)) } pub fn guess_mime_from_path>(path: P) -> Mime { From 2fc015fb89bd67ef39069466ab121912dca0c091 Mon Sep 17 00:00:00 2001 From: panicbit Date: Sat, 14 Nov 2020 10:00:48 +0100 Subject: [PATCH 02/38] fix typo --- examples/document.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/document.rs b/examples/document.rs index 8b096f3..5986896 100644 --- a/examples/document.rs +++ b/examples/document.rs @@ -31,7 +31,7 @@ fn handle_request(_request: Request) -> BoxFuture<'static, Result> { .add_blank_line() .add_heading(H2, "Manually") .add_blank_line() - .add_preformatted_with_alt("alt", r#"northstar = "0.3.0" # check crates.io for the latest version"#) + .add_preformatted_with_alt("toml", r#"northstar = "0.3.0" # check crates.io for the latest version"#) .add_blank_line() .add_heading(H2, "Automatically") .add_blank_line() From 0425bf2cf3acfeb4b82c1a8d8d1742a4bfa79f08 Mon Sep 17 00:00:00 2001 From: panicbit Date: Sat, 14 Nov 2020 22:46:29 +0100 Subject: [PATCH 03/38] add Cowy util trait --- src/types/document.rs | 17 +++++++++-------- src/types/meta.rs | 8 +++++--- src/types/response.rs | 7 ++++--- src/types/response_header.rs | 11 ++++++----- src/util.rs | 16 ++++++++++++++++ 5 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/types/document.rs b/src/types/document.rs index e62eecb..3d65194 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -3,6 +3,7 @@ use std::fmt; use itertools::Itertools; use crate::types::URIReference; +use crate::util::Cowy; #[derive(Default)] pub struct Document { @@ -42,7 +43,7 @@ impl Document { self } - pub fn add_link<'a, U>(&mut self, uri: U, label: impl AsRef + Into) -> &mut Self + pub fn add_link<'a, U>(&mut self, uri: U, label: impl Cowy) -> &mut Self where U: TryInto>, { @@ -92,7 +93,7 @@ impl Document { self } - pub fn add_heading(&mut self, level: HeadingLevel, text: impl AsRef + Into) -> &mut Self { + pub fn add_heading(&mut self, level: HeadingLevel, text: impl Cowy) -> &mut Self { let text = HeadingText::new_lossy(text); let heading = Heading { level, @@ -182,7 +183,7 @@ impl Text { Self::default() } - pub fn new_lossy(line: impl AsRef + Into) -> Self { + pub fn new_lossy(line: impl Cowy) -> Self { Self(lossy_escaped_line(line, SPECIAL_STARTS)) } } @@ -195,7 +196,7 @@ pub struct Link { pub struct LinkLabel(String); impl LinkLabel { - pub fn from_lossy(line: impl AsRef + Into) -> Self { + pub fn from_lossy(line: impl Cowy) -> Self { let line = strip_newlines(line); LinkLabel(line) @@ -210,7 +211,7 @@ pub struct Preformatted { pub struct PreformattedText(String); impl PreformattedText { - pub fn new_lossy(line: impl AsRef + Into) -> Self { + pub fn new_lossy(line: impl Cowy) -> Self { Self(lossy_escaped_line(line, &[PREFORMATTED_TOGGLE_START])) } } @@ -248,7 +249,7 @@ impl Heading { pub struct HeadingText(String); impl HeadingText { - pub fn new_lossy(line: impl AsRef + Into) -> Self { + pub fn new_lossy(line: impl Cowy) -> Self { let line = strip_newlines(line); Self(lossy_escaped_line(line, &[HEADING_START])) @@ -298,7 +299,7 @@ fn starts_with_any(s: &str, starts: &[&str]) -> bool { false } -fn lossy_escaped_line(line: impl AsRef + Into, escape_starts: &[&str]) -> String { +fn lossy_escaped_line(line: impl Cowy, escape_starts: &[&str]) -> String { let line_ref = line.as_ref(); let contains_newline = line_ref.contains('\n'); let has_special_start = starts_with_any(line_ref, escape_starts); @@ -320,7 +321,7 @@ fn lossy_escaped_line(line: impl AsRef + Into, escape_starts: &[&st line } -fn strip_newlines(text: impl AsRef + Into) -> String { +fn strip_newlines(text: impl Cowy) -> String { if !text.as_ref().contains(&['\r', '\n'][..]) { return text.into(); } diff --git a/src/types/meta.rs b/src/types/meta.rs index 144fc13..ccc17ba 100644 --- a/src/types/meta.rs +++ b/src/types/meta.rs @@ -1,5 +1,7 @@ use anyhow::*; -use mime::Mime; +use crate::Mime; +use crate::util::Cowy; + #[derive(Debug,Clone,PartialEq,Eq,Default)] pub struct Meta(String); @@ -9,7 +11,7 @@ impl Meta { /// Creates a new "Meta" string. /// Fails if `meta` contains `\n`. - pub fn new(meta: impl AsRef + Into) -> Result { + pub fn new(meta: impl Cowy) -> Result { ensure!(!meta.as_ref().contains("\n"), "Meta must not contain newlines"); ensure!(meta.as_ref().len() <= Self::MAX_LEN, "Meta must not exceed {} bytes", Self::MAX_LEN); @@ -20,7 +22,7 @@ impl Meta { /// Truncates `meta` to before: /// - the first occurrence of `\n` /// - the character that makes `meta` exceed `Meta::MAX_LEN` - pub fn new_lossy(meta: impl AsRef + Into) -> Self { + pub fn new_lossy(meta: impl Cowy) -> Self { let meta = meta.as_ref(); let truncate_pos = meta.char_indices().position(|(i, ch)| { let is_newline = ch == '\n'; diff --git a/src/types/response.rs b/src/types/response.rs index 6a24de7..a76d6a1 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -1,5 +1,6 @@ use anyhow::*; use crate::types::{ResponseHeader, Body, Mime, Document}; +use crate::util::Cowy; use crate::GEMINI_MIME; pub struct Response { @@ -19,12 +20,12 @@ impl Response { Self::success(&GEMINI_MIME).with_body(document) } - pub fn input(prompt: impl AsRef + Into) -> Result { + pub fn input(prompt: impl Cowy) -> Result { let header = ResponseHeader::input(prompt)?; Ok(Self::new(header)) } - pub fn input_lossy(prompt: impl AsRef + Into) -> Self { + pub fn input_lossy(prompt: impl Cowy) -> Self { let header = ResponseHeader::input_lossy(prompt); Self::new(header) } @@ -34,7 +35,7 @@ impl Response { Self::new(header) } - pub fn server_error(reason: impl AsRef + Into) -> Result { + pub fn server_error(reason: impl Cowy) -> Result { let header = ResponseHeader::server_error(reason)?; Ok(Self::new(header)) } diff --git a/src/types/response_header.rs b/src/types/response_header.rs index 8307707..824401e 100644 --- a/src/types/response_header.rs +++ b/src/types/response_header.rs @@ -1,5 +1,6 @@ use anyhow::*; -use mime::Mime; +use crate::Mime; +use crate::util::Cowy; use crate::types::{Status, Meta}; #[derive(Debug,Clone)] @@ -9,14 +10,14 @@ pub struct ResponseHeader { } impl ResponseHeader { - pub fn input(prompt: impl AsRef + Into) -> Result { + pub fn input(prompt: impl Cowy) -> Result { Ok(Self { status: Status::INPUT, meta: Meta::new(prompt).context("Invalid input prompt")?, }) } - pub fn input_lossy(prompt: impl AsRef + Into) -> Self { + pub fn input_lossy(prompt: impl Cowy) -> Self { Self { status: Status::INPUT, meta: Meta::new_lossy(prompt), @@ -30,14 +31,14 @@ impl ResponseHeader { } } - pub fn server_error(reason: impl AsRef + Into) -> Result { + pub fn server_error(reason: impl Cowy) -> Result { Ok(Self { status: Status::PERMANENT_FAILURE, meta: Meta::new(reason).context("Invalid server error reason")?, }) } - pub fn server_error_lossy(reason: impl AsRef + Into) -> Self { + pub fn server_error_lossy(reason: impl Cowy) -> Self { Self { status: Status::PERMANENT_FAILURE, meta: Meta::new_lossy(reason), diff --git a/src/util.rs b/src/util.rs index bd7a28c..4382723 100644 --- a/src/util.rs +++ b/src/util.rs @@ -102,3 +102,19 @@ pub fn guess_mime_from_path>(path: P) -> Mime { mime.parse::().unwrap_or(mime::APPLICATION_OCTET_STREAM) } + +/// A convenience trait alias for `AsRef + Into`, +/// most commonly used to accept `&str` or `String`: +/// +/// `Cowy` ⇔ `AsRef + Into` +pub trait Cowy +where + Self: AsRef + Into, + T: ToOwned + ?Sized, +{} + +impl Cowy for C +where + C: AsRef + Into, + T: ToOwned + ?Sized, +{} From 46077739b3243466cb49683c79253339e2b34fde Mon Sep 17 00:00:00 2001 From: panicbit Date: Sun, 15 Nov 2020 07:01:38 +0100 Subject: [PATCH 04/38] document some Document methods --- src/types/document.rs | 102 +++++++++++++++++++++++++++++++++++++++--- src/util.rs | 2 +- 2 files changed, 97 insertions(+), 7 deletions(-) diff --git a/src/types/document.rs b/src/types/document.rs index 3d65194..8a455fc 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -11,15 +11,61 @@ pub struct Document { } impl Document { + /// Creates an empty Gemini `Document`. + /// + /// # Examples + /// + /// ``` + /// let document = northstar::Document::new(); + /// + /// assert_eq!(document.to_string(), ""); + /// ``` pub fn new() -> Self { Self::default() } + /// Adds an `item` to the document. + /// + /// An `item` usually corresponds to a single line, + /// except in the case of preformatted text. + /// + /// # Examples + /// + /// ``` + /// use northstar::document::{Document, Item, Text}; + /// + /// let mut document = Document::new(); + /// let text = Text::new_lossy("foo"); + /// let item = Item::Text(text); + /// + /// document.add_item(item); + /// + /// assert_eq!(document.to_string(), "foo\n"); + /// ``` pub fn add_item(&mut self, item: Item) -> &mut Self { self.items.push(item); self } + /// Adds multiple `items` to the document. + /// + /// This is a convenience wrapper around `add_item`. + /// + /// # Examples + /// + /// ``` + /// use northstar::document::{Document, Item, Text}; + /// + /// let mut document = Document::new(); + /// let items = vec!["foo", "bar", "baz"] + /// .into_iter() + /// .map(Text::new_lossy) + /// .map(Item::Text); + /// + /// document.add_items(items); + /// + /// assert_eq!(document.to_string(), "foo\nbar\nbaz\n"); + /// ``` pub fn add_items(&mut self, items: I) -> &mut Self where I: IntoIterator, @@ -28,10 +74,38 @@ impl Document { self } + /// Adds a blank line to the document. + /// + /// # Examples + /// + /// ``` + /// let mut document = northstar::Document::new(); + /// + /// document.add_blank_line(); + /// + /// assert_eq!(document.to_string(), "\n"); + /// ``` pub fn add_blank_line(&mut self) -> &mut Self { self.add_item(Item::Text(Text::blank())) } + /// Adds plain text to the document. + /// + /// This function allows adding multiple lines at once. + /// + /// It inserts a whitespace at the beginning of a line + /// if it starts with a character sequence that + /// would make it a non-plain text line (e.g. link, heading etc). + /// + /// # Examples + /// + /// ``` + /// let mut document = northstar::Document::new(); + /// + /// document.add_text("hello\n* world!"); + /// + /// assert_eq!(document.to_string(), "hello\n * world!\n"); + /// ``` pub fn add_text(&mut self, text: &str) -> &mut Self { let text = text .lines() @@ -43,6 +117,22 @@ impl Document { self } + /// Adds a link to the document. + /// + /// `uri`s that fail to parse are substituted with `.`. + /// + /// Consecutive newlines in `label` will be replaced + /// with a single whitespace. + /// + /// # Examples + /// + /// ``` + /// let mut document = northstar::Document::new(); + /// + /// document.add_link("https://wikipedia.org", "Wiki\n\nWiki"); + /// + /// assert_eq!(document.to_string(), "=> https://wikipedia.org/ Wiki Wiki\n"); + /// ``` pub fn add_link<'a, U>(&mut self, uri: U, label: impl Cowy) -> &mut Self where U: TryInto>, @@ -68,7 +158,7 @@ impl Document { let link = Item::Link(link); self.add_item(link); - + self } @@ -120,7 +210,7 @@ impl Document { .lines() .map(Quote::new_lossy) .map(Item::Quote); - + self.add_items(quote); self @@ -137,7 +227,7 @@ impl fmt::Display for Document { let label = link.label.as_ref().map(|label| label.0.as_str()) .unwrap_or(""); - writeln!(f, "=>{}{}{}", link.uri, separator, label)?; + writeln!(f, "=> {}{}{}", link.uri, separator, label)?; } Item::Preformatted(preformatted) => { writeln!(f, "```{}", preformatted.alt.0)?; @@ -198,7 +288,7 @@ pub struct LinkLabel(String); impl LinkLabel { pub fn from_lossy(line: impl Cowy) -> Self { let line = strip_newlines(line); - + LinkLabel(line) } } @@ -221,7 +311,7 @@ pub struct AltText(String); impl AltText { pub fn new_lossy(alt: &str) -> Self { let alt = strip_newlines(alt); - + Self(alt) } } @@ -261,7 +351,7 @@ pub struct UnorderedListItem(String); impl UnorderedListItem { pub fn new_lossy(text: &str) -> Self { let text = strip_newlines(text); - + Self(text) } } diff --git a/src/util.rs b/src/util.rs index 4382723..8af22d9 100644 --- a/src/util.rs +++ b/src/util.rs @@ -99,7 +99,7 @@ pub fn guess_mime_from_path>(path: P) -> Mime { }, None => "application/octet-stream", }; - + mime.parse::().unwrap_or(mime::APPLICATION_OCTET_STREAM) } From 45c808d0d04c4a613d7b2e4f73f9488de2a8bf31 Mon Sep 17 00:00:00 2001 From: panicbit Date: Sun, 15 Nov 2020 21:14:41 +0100 Subject: [PATCH 05/38] document more Document methods --- src/types/document.rs | 96 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/src/types/document.rs b/src/types/document.rs index 8a455fc..ae6cbb1 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -150,7 +150,27 @@ impl Document { self } - pub fn add_link_without_label(&mut self, uri: URIReference<'static>) -> &mut Self { + /// Adds a link to the document, but without a label. + /// + /// See `add_link` for details. + /// + /// # Examples + /// + /// ``` + /// let mut document = northstar::Document::new(); + /// + /// document.add_link_without_label("https://wikipedia.org"); + /// + /// assert_eq!(document.to_string(), "=> https://wikipedia.org/\n"); + /// ``` + pub fn add_link_without_label<'a, U>(&mut self, uri: U) -> &mut Self + where + U: TryInto>, + { + let uri = uri + .try_into() + .map(URIReference::into_owned) + .or_else(|_| ".".try_into()).expect("Northstar BUG"); let link = Link { uri, label: None, @@ -162,10 +182,40 @@ impl Document { self } + /// Adds a block of preformatted text. + /// + /// Lines that start with ` ``` ` will be prependend with a whitespace. + /// + /// # Examples + /// + /// ``` + /// let mut document = northstar::Document::new(); + /// + /// document.add_preformatted("a\n b\n c"); + /// + /// assert_eq!(document.to_string(), "```\na\n b\n c\n```\n"); + /// ``` pub fn add_preformatted(&mut self, preformatted_text: &str) -> &mut Self { self.add_preformatted_with_alt("", preformatted_text) } + /// Adds a block of preformatted text with an alt text. + /// + /// Consecutive newlines in `alt` will be replaced + /// with a single whitespace. + /// + /// `preformatted_text` lines that start with ` ``` ` + /// will be prependend with a whitespace. + /// + /// # Examples + /// + /// ``` + /// let mut document = northstar::Document::new(); + /// + /// document.add_preformatted_with_alt("rust", "fn main() {\n}\n"); + /// + /// assert_eq!(document.to_string(), "```rust\nfn main() {\n}\n```\n"); + /// ``` pub fn add_preformatted_with_alt(&mut self, alt: &str, preformatted_text: &str) -> &mut Self { let alt = AltText::new_lossy(alt); let lines = preformatted_text @@ -183,6 +233,22 @@ impl Document { self } + /// Adds a heading. + /// + /// Consecutive newlines in `text` will be replaced + /// with a single whitespace. + /// + /// # Examples + /// + /// ``` + /// use northstar::document::HeadingLevel::H1; + /// + /// let mut document = northstar::Document::new(); + /// + /// document.add_heading(H1, "Welcome!"); + /// + /// assert_eq!(document.to_string(), "# Welcome!\n"); + /// ``` pub fn add_heading(&mut self, level: HeadingLevel, text: impl Cowy) -> &mut Self { let text = HeadingText::new_lossy(text); let heading = Heading { @@ -196,6 +262,21 @@ impl Document { self } + /// Adds an unordered list item. + /// + /// Consecutive newlines in `text` will be replaced + /// with a single whitespace. + /// + /// # Examples + /// + /// ``` + /// let mut document = northstar::Document::new(); + /// + /// document.add_unordered_list_item("milk"); + /// document.add_unordered_list_item("eggs"); + /// + /// assert_eq!(document.to_string(), "* milk\n* eggs\n"); + /// ``` pub fn add_unordered_list_item(&mut self, text: &str) -> &mut Self { let item = UnorderedListItem::new_lossy(text); let item = Item::UnorderedListItem(item); @@ -205,6 +286,19 @@ impl Document { self } + /// Adds a quote. + /// + /// This function allows adding multiple quote lines at once. + /// + /// # Examples + /// + /// ``` + /// let mut document = northstar::Document::new(); + /// + /// document.add_quote("I think,\ntherefore I am"); + /// + /// assert_eq!(document.to_string(), "> I think,\n> therefore I am\n"); + /// ``` pub fn add_quote(&mut self, text: &str) -> &mut Self { let quote = text .lines() From ae803a55d22b9ec032a5a38fea9d2ed1fc79aa0a Mon Sep 17 00:00:00 2001 From: panicbit Date: Sun, 15 Nov 2020 21:14:53 +0100 Subject: [PATCH 06/38] do not escape heading text --- src/types/document.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/document.rs b/src/types/document.rs index ae6cbb1..9934c3c 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -436,7 +436,7 @@ impl HeadingText { pub fn new_lossy(line: impl Cowy) -> Self { let line = strip_newlines(line); - Self(lossy_escaped_line(line, &[HEADING_START])) + Self(line) } } From ec6a0af782e3aa15f570071b6ec8bde69824bd93 Mon Sep 17 00:00:00 2001 From: panicbit Date: Sun, 15 Nov 2020 21:17:22 +0100 Subject: [PATCH 07/38] keep document internals private --- src/types/document.rs | 56 +++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/types/document.rs b/src/types/document.rs index 9934c3c..cd0efd8 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -42,7 +42,7 @@ impl Document { /// /// assert_eq!(document.to_string(), "foo\n"); /// ``` - pub fn add_item(&mut self, item: Item) -> &mut Self { + fn add_item(&mut self, item: Item) -> &mut Self { self.items.push(item); self } @@ -66,7 +66,7 @@ impl Document { /// /// assert_eq!(document.to_string(), "foo\nbar\nbaz\n"); /// ``` - pub fn add_items(&mut self, items: I) -> &mut Self + fn add_items(&mut self, items: I) -> &mut Self where I: IntoIterator, { @@ -350,7 +350,7 @@ impl fmt::Display for Document { } } -pub enum Item { +enum Item { Text(Text), Link(Link), Preformatted(Preformatted), @@ -360,59 +360,59 @@ pub enum Item { } #[derive(Default)] -pub struct Text(String); +struct Text(String); impl Text { - pub fn blank() -> Self { + fn blank() -> Self { Self::default() } - pub fn new_lossy(line: impl Cowy) -> Self { + fn new_lossy(line: impl Cowy) -> Self { Self(lossy_escaped_line(line, SPECIAL_STARTS)) } } -pub struct Link { - pub uri: URIReference<'static>, - pub label: Option, +struct Link { + uri: URIReference<'static>, + label: Option, } -pub struct LinkLabel(String); +struct LinkLabel(String); impl LinkLabel { - pub fn from_lossy(line: impl Cowy) -> Self { + fn from_lossy(line: impl Cowy) -> Self { let line = strip_newlines(line); LinkLabel(line) } } -pub struct Preformatted { - pub alt: AltText, - pub lines: Vec, +struct Preformatted { + alt: AltText, + lines: Vec, } -pub struct PreformattedText(String); +struct PreformattedText(String); impl PreformattedText { - pub fn new_lossy(line: impl Cowy) -> Self { + fn new_lossy(line: impl Cowy) -> Self { Self(lossy_escaped_line(line, &[PREFORMATTED_TOGGLE_START])) } } -pub struct AltText(String); +struct AltText(String); impl AltText { - pub fn new_lossy(alt: &str) -> Self { + fn new_lossy(alt: &str) -> Self { let alt = strip_newlines(alt); Self(alt) } } -pub struct Heading { - pub level: HeadingLevel, - pub text: HeadingText, +struct Heading { + level: HeadingLevel, + text: HeadingText, } pub enum HeadingLevel { @@ -422,7 +422,7 @@ pub enum HeadingLevel { } impl Heading { - pub fn new_lossy(level: HeadingLevel, line: &str) -> Self { + fn new_lossy(level: HeadingLevel, line: &str) -> Self { Self { level, text: HeadingText::new_lossy(line), @@ -430,30 +430,30 @@ impl Heading { } } -pub struct HeadingText(String); +struct HeadingText(String); impl HeadingText { - pub fn new_lossy(line: impl Cowy) -> Self { + fn new_lossy(line: impl Cowy) -> Self { let line = strip_newlines(line); Self(line) } } -pub struct UnorderedListItem(String); +struct UnorderedListItem(String); impl UnorderedListItem { - pub fn new_lossy(text: &str) -> Self { + fn new_lossy(text: &str) -> Self { let text = strip_newlines(text); Self(text) } } -pub struct Quote(String); +struct Quote(String); impl Quote { - pub fn new_lossy(text: &str) -> Self { + fn new_lossy(text: &str) -> Self { Self(lossy_escaped_line(text, &[QUOTE_START])) } } From bbeb697bb1f98103c3119ea8595137a1608ea83f Mon Sep 17 00:00:00 2001 From: panicbit Date: Sun, 15 Nov 2020 21:40:16 +0100 Subject: [PATCH 08/38] complete document documentation --- src/types/document.rs | 50 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/types/document.rs b/src/types/document.rs index cd0efd8..3780843 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -1,3 +1,41 @@ +//! Provides types for creating Gemini Documents. +//! +//! The module is centered around the `Document` type, +//! which provides all the necessary methods for programatically +//! creation of Gemini documents. +//! +//! # Examples +//! +//! ``` +//! use northstar::document::HeadingLevel::*; +//! +//! let mut document = northstar::Document::new(); +//! +//! document.add_heading(H1, "Heading 1"); +//! document.add_heading(H2, "Heading 2"); +//! document.add_heading(H3, "Heading 3"); +//! document.add_blank_line(); +//! document.add_text("text"); +//! document.add_link("gemini://gemini.circumlunar.space", "Project Gemini"); +//! document.add_unordered_list_item("list item"); +//! document.add_quote("quote"); +//! document.add_preformatted("preformatted"); +//! +//! assert_eq!(document.to_string(), "\ +//! ## Heading 1\n\ +//! ### Heading 2\n\ +//! #### Heading 3\n\ +//! \n\ +//! text\n\ +//! => gemini://gemini.circumlunar.space/ Project Gemini\n\ +//! * list item\n\ +//! > quote\n\ +//! ```\n\ +//! preformatted\n\ +//! ```\n\ +//! "); +//! ``` +#![warn(missing_docs)] use std::convert::TryInto; use std::fmt; @@ -6,6 +44,10 @@ use crate::types::URIReference; use crate::util::Cowy; #[derive(Default)] +/// Represents a Gemini document. +/// +/// Provides convenient methods for programatically +/// creation of Gemini documents. pub struct Document { items: Vec, } @@ -31,7 +73,7 @@ impl Document { /// /// # Examples /// - /// ``` + /// ```compile_fail /// use northstar::document::{Document, Item, Text}; /// /// let mut document = Document::new(); @@ -53,7 +95,7 @@ impl Document { /// /// # Examples /// - /// ``` + /// ```compile_fail /// use northstar::document::{Document, Item, Text}; /// /// let mut document = Document::new(); @@ -415,9 +457,13 @@ struct Heading { text: HeadingText, } +/// The level of a heading. pub enum HeadingLevel { + /// Heading level 1 (`#`) H1, + /// Heading level 2 (`##`) H2, + /// Heading level 3 (`###`) H3, } From add2f30ca63f3847a2f494634976e80f6357768c Mon Sep 17 00:00:00 2001 From: panicbit Date: Sun, 15 Nov 2020 21:42:13 +0100 Subject: [PATCH 09/38] remove unused function --- src/types/document.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/types/document.rs b/src/types/document.rs index 3780843..f6fc5aa 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -467,15 +467,6 @@ pub enum HeadingLevel { H3, } -impl Heading { - fn new_lossy(level: HeadingLevel, line: &str) -> Self { - Self { - level, - text: HeadingText::new_lossy(line), - } - } -} - struct HeadingText(String); impl HeadingText { From 09f78666530b371d75aff16bd679f85a124fe8a3 Mon Sep 17 00:00:00 2001 From: panicbit Date: Sun, 15 Nov 2020 21:46:10 +0100 Subject: [PATCH 10/38] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf5f82e..daade96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- `document` API for creating Gemini documents ## [0.3.0] - 2020-11-14 ### Added From 987621cca9fa8423a9c19c5f3c777c16a35fe1c0 Mon Sep 17 00:00:00 2001 From: panicbit Date: Sun, 15 Nov 2020 22:02:57 +0100 Subject: [PATCH 11/38] support gmi extension when guessing mime types Closes #12 --- src/util.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.rs b/src/util.rs index 8af22d9..2b1358a 100644 --- a/src/util.rs +++ b/src/util.rs @@ -91,7 +91,7 @@ pub fn guess_mime_from_path>(path: P) -> Mime { let extension = path.extension().and_then(|s| s.to_str()); let mime = match extension { Some(extension) => match extension { - "gemini" => GEMINI_MIME_STR, + "gemini" | "gmi" => GEMINI_MIME_STR, "txt" => "text/plain", "jpeg" | "jpg" | "jpe" => "image/jpeg", "png" => "image/png", From 387ca06611d794786c1dfd271d0ad476e8af1949 Mon Sep 17 00:00:00 2001 From: panicbit Date: Tue, 17 Nov 2020 01:17:47 +0100 Subject: [PATCH 12/38] fix visibility of URIReference in crate root --- src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index e3b1bc0..8717bc0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,6 @@ use rustls::ClientCertVerifier; use tokio_rustls::{rustls, TlsAcceptor}; use rustls::*; use anyhow::*; -use uri::URIReference; use lazy_static::lazy_static; pub mod types; From 32453966822001a8a81b675e4cbe7d481896feef Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Tue, 17 Nov 2020 20:38:10 -0500 Subject: [PATCH 13/38] Add basic timeout with hardcoded durations --- src/lib.rs | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8717bc0..ec4b027 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,18 @@ #[macro_use] extern crate log; -use std::{panic::AssertUnwindSafe, convert::TryFrom, io::BufReader, sync::Arc}; +use std::{ + panic::AssertUnwindSafe, + convert::TryFrom, + io::BufReader, + sync::Arc, + time::Duration, +}; use futures::{future::BoxFuture, FutureExt}; use tokio::{ prelude::*, io::{self, BufStream}, net::{TcpStream, ToSocketAddrs}, + time::timeout, }; use tokio::net::TcpListener; use rustls::ClientCertVerifier; @@ -54,12 +61,22 @@ impl Server { } async fn serve_client(self, stream: TcpStream) -> Result<()> { - let stream = self.tls_acceptor.accept(stream).await - .context("Failed to establish TLS session")?; - let mut stream = BufStream::new(stream); + let fut_accept_request = async { + let stream = self.tls_acceptor.accept(stream).await + .context("Failed to establish TLS session")?; + let mut stream = BufStream::new(stream); + + let request = receive_request(&mut stream).await + .context("Failed to receive request")?; + + Result::<_, anyhow::Error>::Ok((request, stream)) + }; + + // Use a timeout for interacting with the client + let fut_accept_request = timeout(Duration::from_secs(5), fut_accept_request); + let (mut request, mut stream) = fut_accept_request.await + .context("Client timed out while waiting for response")??; - let mut request = receive_request(&mut stream).await - .context("Failed to receive request")?; debug!("Client requested: {}", request.uri()); // Identify the client certificate from the tls stream. This is the first @@ -83,11 +100,18 @@ impl Server { }) .context("Request handler failed")?; - send_response(response, &mut stream).await - .context("Failed to send response")?; + // Use a timeout for sending the response + let fut_send_and_flush = async { + send_response(response, &mut stream).await + .context("Failed to send response")?; - stream.flush().await - .context("Failed to flush response data")?; + stream.flush() + .await + .context("Failed to flush response data") + }; + timeout(Duration::from_millis(1000), fut_send_and_flush) + .await + .context("Client timed out receiving response data")??; Ok(()) } From 71cf4d33f2c568a5595587160b00d5b098c060fb Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Tue, 17 Nov 2020 20:45:57 -0500 Subject: [PATCH 14/38] Added ability to set the timeout in the Server builder --- src/lib.rs | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ec4b027..a75d88a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,6 +39,7 @@ pub struct Server { tls_acceptor: TlsAcceptor, listener: Arc, handler: Handler, + timeout: Duration, } impl Server { @@ -73,7 +74,7 @@ impl Server { }; // Use a timeout for interacting with the client - let fut_accept_request = timeout(Duration::from_secs(5), fut_accept_request); + let fut_accept_request = timeout(self.timeout, fut_accept_request); let (mut request, mut stream) = fut_accept_request.await .context("Client timed out while waiting for response")??; @@ -109,7 +110,7 @@ impl Server { .await .context("Failed to flush response data") }; - timeout(Duration::from_millis(1000), fut_send_and_flush) + timeout(self.timeout, fut_send_and_flush) .await .context("Client timed out receiving response data")??; @@ -119,11 +120,29 @@ impl Server { pub struct Builder { addr: A, + timeout: Duration, } impl Builder { fn bind(addr: A) -> Self { - Self { addr } + Self { addr, timeout: Duration::from_secs(1) } + } + + /// Set the timeout on incoming requests + /// + /// Note that this timeout is applied twice, once for the delivery of the request, and + /// once for sending the client's response. This means that for a 1 second timeout, + /// the client will have 1 second to complete the TLS handshake and deliver a request + /// header, then your API will have as much time as it needs to handle the request, + /// before the client has another second to receive the response. + /// + /// If you would like a timeout for your code itself, please use + /// ['tokio::time::Timeout`] to implement it internally. + /// + /// The default timeout is 1 second. + pub fn set_timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self } pub async fn serve(self, handler: F) -> Result<()> @@ -140,6 +159,7 @@ impl Builder { tls_acceptor: TlsAcceptor::from(config), listener: Arc::new(listener), handler: Arc::new(handler), + timeout: self.timeout, }; server.serve().await From aa2dbbf67ade4d538d8c7893109dc43399e2bc99 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Tue, 17 Nov 2020 21:01:54 -0500 Subject: [PATCH 15/38] Fixed typo in timeout docs --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index a75d88a..2dadbc4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -137,7 +137,7 @@ impl Builder { /// before the client has another second to receive the response. /// /// If you would like a timeout for your code itself, please use - /// ['tokio::time::Timeout`] to implement it internally. + /// [`tokio::time::Timeout`] to implement it internally. /// /// The default timeout is 1 second. pub fn set_timeout(mut self, timeout: Duration) -> Self { From 6a78b2f31ae96578edf02cfc6aa6d6b78371c8a0 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Tue, 17 Nov 2020 21:41:18 -0500 Subject: [PATCH 16/38] Added Response::success_with_body --- examples/certificates.rs | 14 +++++++++----- src/types/response.rs | 15 ++++++++++++++- src/util.rs | 2 +- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/examples/certificates.rs b/examples/certificates.rs index 2da1e68..f0f98b3 100644 --- a/examples/certificates.rs +++ b/examples/certificates.rs @@ -37,8 +37,10 @@ fn handle_request(users: Arc>>, request: Reque if let Some(user) = users_read.get(cert_bytes) { // The user has already registered Ok( - Response::success(&GEMINI_MIME) - .with_body(format!("Welcome {}!", user)) + Response::success_with_body( + &GEMINI_MIME, + format!("Welcome {}!", user) + ) ) } else { // The user still needs to register @@ -49,11 +51,13 @@ fn handle_request(users: Arc>>, request: Reque let mut users_write = users.write().await; users_write.insert(cert_bytes.clone(), username.to_owned()); Ok( - Response::success(&GEMINI_MIME) - .with_body(format!( + Response::success_with_body( + &GEMINI_MIME, + format!( "Your account has been created {}! Welcome!", username - )) + ) + ) ) } else { // The user didn't provide input, and should be prompted diff --git a/src/types/response.rs b/src/types/response.rs index a76d6a1..74e36a3 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -17,7 +17,7 @@ impl Response { } pub fn document(document: Document) -> Self { - Self::success(&GEMINI_MIME).with_body(document) + Self::success_with_body(&GEMINI_MIME, document) } pub fn input(prompt: impl Cowy) -> Result { @@ -35,6 +35,19 @@ impl Response { Self::new(header) } + /// Create a successful response with a preconfigured body + /// + /// This is equivilent to: + /// + /// ```norun + /// Response::success(mime) + /// .with_body(body) + /// ``` + pub fn success_with_body(mime: &Mime, body: impl Into) -> Self { + Self::success(mime) + .with_body(body) + } + pub fn server_error(reason: impl Cowy) -> Result { let header = ResponseHeader::server_error(reason)?; Ok(Self::new(header)) diff --git a/src/util.rs b/src/util.rs index 2b1358a..9db296f 100644 --- a/src/util.rs +++ b/src/util.rs @@ -20,7 +20,7 @@ pub async fn serve_file>(path: P, mime: &Mime) -> Result, P: AsRef>(dir: D, virtual_path: &[P]) -> Result { From 343de637c4246b91672e11021eaf5dde69a34cf0 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Wed, 18 Nov 2020 09:21:54 -0500 Subject: [PATCH 17/38] Added ability to load from custom key and certificate paths --- src/lib.rs | 78 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 14 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8717bc0..2e434c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,12 @@ #[macro_use] extern crate log; -use std::{panic::AssertUnwindSafe, convert::TryFrom, io::BufReader, sync::Arc}; +use std::{ + panic::AssertUnwindSafe, + convert::TryFrom, + io::BufReader, + sync::Arc, + path::PathBuf, +}; use futures::{future::BoxFuture, FutureExt}; use tokio::{ prelude::*, @@ -95,18 +101,64 @@ impl Server { pub struct Builder { addr: A, + cert_path: PathBuf, + key_path: PathBuf, } impl Builder { fn bind(addr: A) -> Self { - Self { addr } + Self { + addr, + cert_path: PathBuf::from("cert/cert.pem"), + key_path: PathBuf::from("cert/key.pem"), + } + } + + /// Sets the directory that northstar should look for TLS certs and keys into + /// + /// Northstar will look for files called `cert.pem` and `key.pem` in the provided + /// directory. + /// + /// This does not need to be set if both [`set_cert()`](Self::set_cert()) and + /// [`set_key()`](Self::set_key()) have been called. + /// + /// If not set, the default is `cert/` + pub fn set_tls_dir(self, dir: impl Into) -> Self { + let dir = dir.into(); + self.set_cert(dir.join("cert.pem")) + .set_key(dir.join("key.pem")) + } + + /// Set the path to the TLS certificate northstar will use + /// + /// This defaults to `cert/cert.pem`. + /// + /// This does not need to be called it [`set_tls_dir()`](Self::set_tls_dir()) has been + /// called. + pub fn set_cert(mut self, cert_path: impl Into) -> Self { + self.cert_path = cert_path.into(); + self + } + + /// Set the path to the ertificate key northstar will use + /// + /// This defaults to `cert/key.pem`. + /// + /// This does not need to be called it [`set_tls_dir()`](Self::set_tls_dir()) has been + /// called. + /// + /// This should of course correspond to the key set in + /// [`set_cert()`](Self::set_cert()) + pub fn set_key(mut self, key_path: impl Into) -> Self { + self.key_path = key_path.into(); + self } pub async fn serve(self, handler: F) -> Result<()> where F: Fn(Request) -> HandlerResponse + Send + Sync + 'static, { - let config = tls_config() + let config = tls_config(&self.cert_path, &self.key_path) .context("Failed to create TLS config")?; let listener = TcpListener::bind(self.addr).await @@ -183,12 +235,12 @@ async fn send_response_body(body: Body, stream: &mut (impl AsyncWrite + Unpin)) Ok(()) } -fn tls_config() -> Result> { +fn tls_config(cert_path: &PathBuf, key_path: &PathBuf) -> Result> { let mut config = ServerConfig::new(AllowAnonOrSelfsignedClient::new()); - let cert_chain = load_cert_chain() + let cert_chain = load_cert_chain(cert_path) .context("Failed to load TLS certificate")?; - let key = load_key() + let key = load_key(key_path) .context("Failed to load TLS key")?; config.set_single_cert(cert_chain, key) .context("Failed to use loaded TLS certificate")?; @@ -196,24 +248,22 @@ fn tls_config() -> Result> { Ok(config.into()) } -fn load_cert_chain() -> Result> { - let cert_path = "cert/cert.pem"; +fn load_cert_chain(cert_path: &PathBuf) -> Result> { let certs = std::fs::File::open(cert_path) - .with_context(|| format!("Failed to open `{}`", cert_path))?; + .with_context(|| format!("Failed to open `{:?}`", cert_path))?; let mut certs = BufReader::new(certs); let certs = rustls::internal::pemfile::certs(&mut certs) - .map_err(|_| anyhow!("failed to load certs `{}`", cert_path))?; + .map_err(|_| anyhow!("failed to load certs `{:?}`", cert_path))?; Ok(certs) } -fn load_key() -> Result { - let key_path = "cert/key.pem"; +fn load_key(key_path: &PathBuf) -> Result { let keys = std::fs::File::open(key_path) - .with_context(|| format!("Failed to open `{}`", key_path))?; + .with_context(|| format!("Failed to open `{:?}`", key_path))?; let mut keys = BufReader::new(keys); let mut keys = rustls::internal::pemfile::pkcs8_private_keys(&mut keys) - .map_err(|_| anyhow!("failed to load key `{}`", key_path))?; + .map_err(|_| anyhow!("failed to load key `{:?}`", key_path))?; ensure!(!keys.is_empty(), "no key found"); From 0daf01fd3e9c68b143eff299b70d14a0cc52742b Mon Sep 17 00:00:00 2001 From: panicbit Date: Wed, 18 Nov 2020 21:16:14 +0100 Subject: [PATCH 18/38] update changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daade96..12db361 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - `document` API for creating Gemini documents +- add preliminary timeout API by [@Alch-Emi](https://github.com/Alch-Emi) +- `Response::success_with_body` by [@Alch-Emi](https://github.com/Alch-Emi) ## [0.3.0] - 2020-11-14 ### Added @@ -25,4 +27,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.2.0] - 2020-11-14 ### Added -- Access to client certificates by [@Alch-Emi](https://github.com/Alch-Emi) \ No newline at end of file +- Access to client certificates by [@Alch-Emi](https://github.com/Alch-Emi) From 931c3fbbc238c487d31acda29ef6f70f7476b9d3 Mon Sep 17 00:00:00 2001 From: panicbit Date: Wed, 18 Nov 2020 21:26:27 +0100 Subject: [PATCH 19/38] add temporary redirects and bad requests --- src/types/response.rs | 13 +++++++++++++ src/types/response_header.rs | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/types/response.rs b/src/types/response.rs index 74e36a3..c883148 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -1,4 +1,7 @@ +use std::convert::TryInto; + use anyhow::*; +use uriparse::URIReference; use crate::types::{ResponseHeader, Body, Mime, Document}; use crate::util::Cowy; use crate::GEMINI_MIME; @@ -35,6 +38,11 @@ impl Response { Self::new(header) } + pub fn redirect_temporary_lossy<'a>(location: impl TryInto>) -> Self { + let header = ResponseHeader::redirect_temporary_lossy(location); + Self::new(header) + } + /// Create a successful response with a preconfigured body /// /// This is equivilent to: @@ -58,6 +66,11 @@ impl Response { Self::new(header) } + pub fn bad_request_lossy(reason: impl Cowy) -> Self { + let header = ResponseHeader::bad_request_lossy(reason); + Self::new(header) + } + pub fn client_certificate_required() -> Self { let header = ResponseHeader::client_certificate_required(); Self::new(header) diff --git a/src/types/response_header.rs b/src/types/response_header.rs index 824401e..56f2af3 100644 --- a/src/types/response_header.rs +++ b/src/types/response_header.rs @@ -1,4 +1,7 @@ +use std::convert::TryInto; + use anyhow::*; +use uriparse::URIReference; use crate::Mime; use crate::util::Cowy; use crate::types::{Status, Meta}; @@ -31,6 +34,18 @@ impl ResponseHeader { } } + pub fn redirect_temporary_lossy<'a>(location: impl TryInto>) -> Self { + let location = match location.try_into() { + Ok(location) => location, + Err(_) => return Self::bad_request_lossy("Invalid redirect location"), + }; + + Self { + status: Status::REDIRECT_TEMPORARY, + meta: Meta::new_lossy(location.to_string()), + } + } + pub fn server_error(reason: impl Cowy) -> Result { Ok(Self { status: Status::PERMANENT_FAILURE, @@ -52,6 +67,13 @@ impl ResponseHeader { } } + pub fn bad_request_lossy(reason: impl Cowy) -> Self { + Self { + status: Status::BAD_REQUEST, + meta: Meta::new_lossy(reason), + } + } + pub fn client_certificate_required() -> Self { Self { status: Status::CLIENT_CERTIFICATE_REQUIRED, From 65b35a48d71ca011f16b8814297fe339c057fa83 Mon Sep 17 00:00:00 2001 From: panicbit Date: Wed, 18 Nov 2020 21:29:21 +0100 Subject: [PATCH 20/38] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12db361..90aaf79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `document` API for creating Gemini documents - add preliminary timeout API by [@Alch-Emi](https://github.com/Alch-Emi) - `Response::success_with_body` by [@Alch-Emi](https://github.com/Alch-Emi) +- `redirect_temporary_lossy` for `Response` and `ResponseHeader` +- `bad_request_lossy` for `Response` and `ResponseHeader` ## [0.3.0] - 2020-11-14 ### Added From fffada14e36418471159320c60cbcb0692a6a6de Mon Sep 17 00:00:00 2001 From: panicbit Date: Wed, 18 Nov 2020 22:46:18 +0100 Subject: [PATCH 21/38] increase default timeout from 1 to 30 seconds --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2dadbc4..066ce28 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -125,7 +125,7 @@ pub struct Builder { impl Builder { fn bind(addr: A) -> Self { - Self { addr, timeout: Duration::from_secs(1) } + Self { addr, timeout: Duration::from_secs(30) } } /// Set the timeout on incoming requests @@ -139,7 +139,7 @@ impl Builder { /// If you would like a timeout for your code itself, please use /// [`tokio::time::Timeout`] to implement it internally. /// - /// The default timeout is 1 second. + /// The default timeout is 30 seconds. pub fn set_timeout(mut self, timeout: Duration) -> Self { self.timeout = timeout; self From 5a92d8d28f4ded4f48f20dd4a81f76339d8ee5ac Mon Sep 17 00:00:00 2001 From: panicbit Date: Wed, 18 Nov 2020 22:58:23 +0100 Subject: [PATCH 22/38] use mime_guess for guessing mime --- Cargo.toml | 1 + src/util.rs | 19 ++++++++----------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index abd66e1..dd12927 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ itertools = "0.9.0" log = "0.4.11" webpki = "0.21.0" lazy_static = "1.4.0" +mime_guess = "2.0.3" [dev-dependencies] env_logger = "0.8.1" diff --git a/src/util.rs b/src/util.rs index 9db296f..1ba11af 100644 --- a/src/util.rs +++ b/src/util.rs @@ -5,7 +5,6 @@ use tokio::{ fs::{self, File}, io, }; -use crate::GEMINI_MIME_STR; use crate::types::{Response, Document, document::HeadingLevel::*}; use itertools::Itertools; @@ -89,18 +88,16 @@ async fn serve_dir_listing, B: AsRef>(path: P, virtual_path pub fn guess_mime_from_path>(path: P) -> Mime { let path = path.as_ref(); let extension = path.extension().and_then(|s| s.to_str()); - let mime = match extension { - Some(extension) => match extension { - "gemini" | "gmi" => GEMINI_MIME_STR, - "txt" => "text/plain", - "jpeg" | "jpg" | "jpe" => "image/jpeg", - "png" => "image/png", - _ => "application/octet-stream", - }, - None => "application/octet-stream", + let extension = match extension { + Some(extension) => extension, + None => return mime::APPLICATION_OCTET_STREAM, }; - mime.parse::().unwrap_or(mime::APPLICATION_OCTET_STREAM) + if let "gemini" | "gmi" = extension { + return crate::GEMINI_MIME.clone(); + } + + mime_guess::from_ext(extension).first_or_octet_stream() } /// A convenience trait alias for `AsRef + Into`, From f2b27665c2f696311cb844dea12b36d3dda0f2bb Mon Sep 17 00:00:00 2001 From: panicbit Date: Wed, 18 Nov 2020 23:00:49 +0100 Subject: [PATCH 23/38] correctly ignore doctest --- src/types/response.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/response.rs b/src/types/response.rs index c883148..3e4a84a 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -47,7 +47,7 @@ impl Response { /// /// This is equivilent to: /// - /// ```norun + /// ```ignore /// Response::success(mime) /// .with_body(body) /// ``` From bbf034cf47ddc552257bc02caa55052ce2c5d2b7 Mon Sep 17 00:00:00 2001 From: panicbit Date: Wed, 18 Nov 2020 23:18:50 +0100 Subject: [PATCH 24/38] update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90aaf79..aa5c95c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - `document` API for creating Gemini documents -- add preliminary timeout API by [@Alch-Emi](https://github.com/Alch-Emi) +- preliminary timeout API by [@Alch-Emi](https://github.com/Alch-Emi) - `Response::success_with_body` by [@Alch-Emi](https://github.com/Alch-Emi) - `redirect_temporary_lossy` for `Response` and `ResponseHeader` - `bad_request_lossy` for `Response` and `ResponseHeader` +- support for a lot more mime-types in `guess_mime_from_path`, backed by the `mime_guess` crate ## [0.3.0] - 2020-11-14 ### Added From a7787741898e0aef9c7ce4324e615634169c44b5 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Wed, 18 Nov 2020 23:29:00 -0500 Subject: [PATCH 25/38] Added note about short timeouts to set_timeout docs --- src/lib.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 066ce28..96acd69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -139,7 +139,12 @@ impl Builder { /// If you would like a timeout for your code itself, please use /// [`tokio::time::Timeout`] to implement it internally. /// - /// The default timeout is 30 seconds. + /// **The default timeout is 30 seconds.** If you are considering changing this, keep + /// in mind that some clients, when recieving a file type not supported for display, + /// will prompt the user how they would like to proceed. While this occurs, the + /// request hangs open. Setting a short timeout may close the prompt before user has + /// a chance to respond. If you are only serving `text/plain` and `text/gemini`, this + /// should not be a problem. pub fn set_timeout(mut self, timeout: Duration) -> Self { self.timeout = timeout; self From 94d7a5ab4fd966d014fcf210055551b6cebf76d3 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 01:25:34 -0500 Subject: [PATCH 26/38] Added complex timeout override option to builder --- src/lib.rs | 160 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 136 insertions(+), 24 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 96acd69..9bd962d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,7 @@ pub struct Server { listener: Arc, handler: Handler, timeout: Duration, + complex_timeout: Option, } impl Server { @@ -101,31 +102,116 @@ impl Server { }) .context("Request handler failed")?; - // Use a timeout for sending the response - let fut_send_and_flush = async { - send_response(response, &mut stream).await + self.send_response(response, &mut stream).await .context("Failed to send response")?; + Ok(()) + } + + async fn send_response(&self, mut response: Response, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> { + let maybe_body = response.take_body(); + let header = response.header(); + + // Okay, I know this method looks really complicated, but I promise it's not. + // There's really only three things this method does: + // + // * Send the response header + // * Send the response body + // * Flush the stream + // + // All the other code is doing one of two things. Either it's + // + // * code to add and handle timeouts (that's what all the async blocks and calls + // to tokio::time::timeout are), or + // * logic to decide whether to use the special case timeout handling (seperate + // timeouts for the header and the body) vs the normal timeout handling (header, + // body, and flush all as one timeout) + // + // The split between the two cases happens at this very first if block. + // Everything in this deep chain of if's and if-let's is for the special case. If + // any one of the ifs fails, the code after the big if block is run, and that's + // the normal case. + // + // Hope this helps! Emi <3 + + if header.status == Status::SUCCESS && maybe_body.is_some() { + // aaaa let me have if-let chaining ;_; + if let "text/plain"|"text/gemini" = header.meta.as_str() { + if let Some(cplx_timeout) = self.complex_timeout { + + + ////////////// Use the special case timeout override ///////////////////////////// + + // Send the header & flush + let fut_send_header = async { + send_response_header(response.header(), stream).await + .context("Failed to write response header")?; + + stream.flush() + .await + .context("Failed to flush response header") + }; + tokio::time::timeout(self.timeout, fut_send_header) + .await + .context("Timed out while sending response header")??; + + // Send the body & flush + let fut_send_body = async { + send_response_body(maybe_body.unwrap(), stream).await + .context("Failed to write response body")?; + + stream.flush() + .await + .context("Failed to flush response body") + }; + tokio::time::timeout(cplx_timeout, fut_send_body) + .await + .context("Timed out while sending response body")??; + + return Ok(()) + } + } + } + + + ///////////// Use the normal timeout ///////////////////////////////////////////// + + let fut_send_response = async { + send_response_header(response.header(), stream).await + .context("Failed to write response header")?; + + if let Some(body) = maybe_body { + send_response_body(body, stream).await + .context("Failed to write response body")?; + } + stream.flush() .await .context("Failed to flush response data") }; - timeout(self.timeout, fut_send_and_flush) + tokio::time::timeout(self.timeout, fut_send_response) .await - .context("Client timed out receiving response data")??; + .context("Timed out while sending response data")??; Ok(()) + + ////////////////////////////////////////////////////////////////////////////////// } } pub struct Builder { addr: A, timeout: Duration, + complex_body_timeout_override: Option, } impl Builder { fn bind(addr: A) -> Self { - Self { addr, timeout: Duration::from_secs(30) } + Self { + addr, + timeout: Duration::from_secs(1), + complex_body_timeout_override: Some(Duration::from_secs(30)), + } } /// Set the timeout on incoming requests @@ -139,17 +225,54 @@ impl Builder { /// If you would like a timeout for your code itself, please use /// [`tokio::time::Timeout`] to implement it internally. /// - /// **The default timeout is 30 seconds.** If you are considering changing this, keep - /// in mind that some clients, when recieving a file type not supported for display, - /// will prompt the user how they would like to proceed. While this occurs, the - /// request hangs open. Setting a short timeout may close the prompt before user has - /// a chance to respond. If you are only serving `text/plain` and `text/gemini`, this - /// should not be a problem. + /// **The default timeout is 1 second.** As somewhat of a workaround for + /// shortcomings of the specification, this timeout, and any timeout set using this + /// method, is overridden in special cases, specifically for MIME types outside of + /// `text/plain` and `text/gemini`, to be 30 seconds. If you would like to change or + /// prevent this, please see + /// [`override_complex_body_timeout`](Self::override_complex_body_timeout()). pub fn set_timeout(mut self, timeout: Duration) -> Self { self.timeout = timeout; self } + /// Override the timeout for complex body types + /// + /// Many clients choose to handle body types which cannot be displayed by prompting + /// the user if they would like to download or open the request body. However, since + /// this prompt occurs in the middle of receiving a request, often the connection + /// times out before the end user is able to respond to the prompt. + /// + /// As a workaround, it is possible to set an override on the request timeout in + /// specific conditions: + /// + /// 1. **Only override the timeout for receiving the body of the request.** This will + /// not override the timeout on sending the request header, nor on receiving the + /// response header. + /// 2. **Only override the timeout for successful responses.** The only bodies which + /// have bodies are successful ones. In all other cases, there's no body to + /// timeout for + /// 3. **Only override the timeout for complex body types.** Almost all clients are + /// able to display `text/plain` and `text/gemini` responses, and will not prompt + /// the user for these response types. This means that there is no reason to + /// expect a client to have a human-length response time for these MIME types. + /// Because of this, responses of this type will not be overridden. + /// + /// This method is used to override the timeout for responses meeting these specific + /// criteria. All other stages of the connection will use the timeout specified in + /// [`set_timeout()`](Self::set_timeout()). + /// + /// If this is set to [`None`], then the client will have the default amount of time + /// to both receive the header and the body. If this is set to [`Some`], the client + /// will have the default amount of time to recieve the header, and an *additional* + /// alotment of time to recieve the body. + /// + /// The default timeout for this is 30 seconds. + pub fn override_complex_body_timeout(mut self, timeout: Option) -> Self { + self.complex_body_timeout_override = timeout; + self + } + pub async fn serve(self, handler: F) -> Result<()> where F: Fn(Request) -> HandlerResponse + Send + Sync + 'static, @@ -165,6 +288,7 @@ impl Builder { listener: Arc::new(listener), handler: Arc::new(handler), timeout: self.timeout, + complex_timeout: self.complex_body_timeout_override, }; server.serve().await @@ -199,18 +323,6 @@ async fn receive_request(stream: &mut (impl AsyncBufRead + Unpin)) -> Result Result<()> { - send_response_header(response.header(), stream).await - .context("Failed to send response header")?; - - if let Some(body) = response.take_body() { - send_response_body(body, stream).await - .context("Failed to send response body")?; - } - - Ok(()) -} - async fn send_response_header(header: &ResponseHeader, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> { let header = format!( "{status} {meta}\r\n", From 25d575bee7e0a6ca083b4e1e12892924ea96dba7 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 01:30:08 -0500 Subject: [PATCH 27/38] Shorten references to tokio::time::timeout because I forgot I used an import for that woops sorry --- src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9bd962d..d57fc93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -122,7 +122,7 @@ impl Server { // All the other code is doing one of two things. Either it's // // * code to add and handle timeouts (that's what all the async blocks and calls - // to tokio::time::timeout are), or + // to timeout are), or // * logic to decide whether to use the special case timeout handling (seperate // timeouts for the header and the body) vs the normal timeout handling (header, // body, and flush all as one timeout) @@ -151,7 +151,7 @@ impl Server { .await .context("Failed to flush response header") }; - tokio::time::timeout(self.timeout, fut_send_header) + timeout(self.timeout, fut_send_header) .await .context("Timed out while sending response header")??; @@ -164,7 +164,7 @@ impl Server { .await .context("Failed to flush response body") }; - tokio::time::timeout(cplx_timeout, fut_send_body) + timeout(cplx_timeout, fut_send_body) .await .context("Timed out while sending response body")??; @@ -189,7 +189,7 @@ impl Server { .await .context("Failed to flush response data") }; - tokio::time::timeout(self.timeout, fut_send_response) + timeout(self.timeout, fut_send_response) .await .context("Timed out while sending response data")??; From df362d1bc351b5c82897899cf004a6f1fc69c9df Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 01:43:57 -0500 Subject: [PATCH 28/38] Fixed bug where incorrect timeout was used. I got the mime types backwards haha pretend you didnt see that --- src/lib.rs | 59 +++++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d57fc93..bb4ced9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -128,48 +128,49 @@ impl Server { // body, and flush all as one timeout) // // The split between the two cases happens at this very first if block. - // Everything in this deep chain of if's and if-let's is for the special case. If - // any one of the ifs fails, the code after the big if block is run, and that's - // the normal case. + // Everything in this if is for the special case. If any one of the ifs fails, + // the code after the big if block is run, and that's the normal case. // // Hope this helps! Emi <3 - if header.status == Status::SUCCESS && maybe_body.is_some() { - // aaaa let me have if-let chaining ;_; - if let "text/plain"|"text/gemini" = header.meta.as_str() { - if let Some(cplx_timeout) = self.complex_timeout { + if header.status == Status::SUCCESS && + maybe_body.is_some() && + header.meta.as_str() != "text/plain" && + header.meta.as_str() != "text/gemini" + { + if let Some(cplx_timeout) = self.complex_timeout { ////////////// Use the special case timeout override ///////////////////////////// - // Send the header & flush - let fut_send_header = async { - send_response_header(response.header(), stream).await - .context("Failed to write response header")?; + // Send the header & flush + let fut_send_header = async { + send_response_header(response.header(), stream).await + .context("Failed to write response header")?; - stream.flush() - .await - .context("Failed to flush response header") - }; - timeout(self.timeout, fut_send_header) + stream.flush() .await - .context("Timed out while sending response header")??; + .context("Failed to flush response header") + }; + timeout(self.timeout, fut_send_header) + .await + .context("Timed out while sending response header")??; - // Send the body & flush - let fut_send_body = async { - send_response_body(maybe_body.unwrap(), stream).await - .context("Failed to write response body")?; + // Send the body & flush + let fut_send_body = async { + send_response_body(maybe_body.unwrap(), stream).await + .context("Failed to write response body")?; - stream.flush() - .await - .context("Failed to flush response body") - }; - timeout(cplx_timeout, fut_send_body) + stream.flush() .await - .context("Timed out while sending response body")??; + .context("Failed to flush response body") + }; + timeout(cplx_timeout, fut_send_body) + .await + .context("Timed out while sending response body")??; - return Ok(()) - } + + return Ok(()) } } From 753ecf708d6d943ce8da777044c37f86fb778e09 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 02:02:11 -0500 Subject: [PATCH 29/38] Isolate directory serving methods behind feature, incl mime_guess. Remove itertools --- Cargo.toml | 7 +++++-- src/types/document.rs | 2 +- src/util.rs | 16 ++++++++++++---- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dd12927..a2de13d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,10 @@ description = "Gemini server implementation" repository = "https://github.com/panicbit/northstar" documentation = "https://docs.rs/northstar" +[features] +default = ["serve_dir"] +serve_dir = ["mime_guess"] + [dependencies] anyhow = "1.0.33" rustls = { version = "0.18.1", features = ["dangerous_configuration"] } @@ -17,11 +21,10 @@ mime = "0.3.16" uriparse = "0.6.3" percent-encoding = "2.1.0" futures = "0.3.7" -itertools = "0.9.0" log = "0.4.11" webpki = "0.21.0" lazy_static = "1.4.0" -mime_guess = "2.0.3" +mime_guess = { version = "2.0.3", optional = true } [dev-dependencies] env_logger = "0.8.1" diff --git a/src/types/document.rs b/src/types/document.rs index f6fc5aa..e322357 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -39,7 +39,6 @@ use std::convert::TryInto; use std::fmt; -use itertools::Itertools; use crate::types::URIReference; use crate::util::Cowy; @@ -550,5 +549,6 @@ fn strip_newlines(text: impl Cowy) -> String { text.as_ref() .lines() .filter(|part| !part.is_empty()) + .collect::>() .join(" ") } diff --git a/src/util.rs b/src/util.rs index 1ba11af..ac774a2 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,13 +1,18 @@ -use std::path::Path; +#[cfg(feature="serve_dir")] +use std::path::{Path, PathBuf}; +#[cfg(feature="serve_dir")] use mime::Mime; +#[cfg(feature="serve_dir")] use anyhow::*; +#[cfg(feature="serve_dir")] use tokio::{ fs::{self, File}, io, }; +#[cfg(feature="serve_dir")] use crate::types::{Response, Document, document::HeadingLevel::*}; -use itertools::Itertools; +#[cfg(feature="serve_dir")] pub async fn serve_file>(path: P, mime: &Mime) -> Result { let path = path.as_ref(); @@ -22,6 +27,7 @@ pub async fn serve_file>(path: P, mime: &Mime) -> Result, P: AsRef>(dir: D, virtual_path: &[P]) -> Result { debug!("Dir: {}", dir.as_ref().display()); let dir = dir.as_ref().canonicalize() @@ -47,6 +53,7 @@ pub async fn serve_dir, P: AsRef>(dir: D, virtual_path: &[P serve_dir_listing(path, virtual_path).await } +#[cfg(feature="serve_dir")] async fn serve_dir_listing, B: AsRef>(path: P, virtual_path: &[B]) -> Result { let mut dir = match fs::read_dir(path).await { Ok(dir) => dir, @@ -56,10 +63,10 @@ async fn serve_dir_listing, B: AsRef>(path: P, virtual_path } }; - let breadcrumbs = virtual_path.iter().map(|segment| segment.as_ref().display()).join("/"); + let breadcrumbs: PathBuf = virtual_path.iter().collect(); let mut document = Document::new(); - document.add_heading(H1, format!("Index of /{}", breadcrumbs)); + document.add_heading(H1, format!("Index of /{}", breadcrumbs.display())); document.add_blank_line(); if virtual_path.get(0).map(<_>::as_ref) != Some(Path::new("")) { @@ -85,6 +92,7 @@ async fn serve_dir_listing, B: AsRef>(path: P, virtual_path Ok(Response::document(document)) } +#[cfg(feature="serve_dir")] pub fn guess_mime_from_path>(path: P) -> Mime { let path = path.as_ref(); let extension = path.extension().and_then(|s| s.to_str()); From e0abe6344bc9ac93b03898ee8c8f0453e6017c76 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 02:54:45 -0500 Subject: [PATCH 30/38] Reduced dependency on futures-rs --- Cargo.toml | 3 ++- examples/certificates.rs | 3 ++- examples/document.rs | 3 ++- examples/serve_dir.rs | 3 ++- src/lib.rs | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a2de13d..1ef35dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,8 @@ tokio = { version = "0.3.1", features = ["full"] } mime = "0.3.16" uriparse = "0.6.3" percent-encoding = "2.1.0" -futures = "0.3.7" +futures-core = "0.3.7" +futures-util = "0.3.7" log = "0.4.11" webpki = "0.21.0" lazy_static = "1.4.0" diff --git a/examples/certificates.rs b/examples/certificates.rs index f0f98b3..541fbe5 100644 --- a/examples/certificates.rs +++ b/examples/certificates.rs @@ -1,5 +1,6 @@ use anyhow::*; -use futures::{future::BoxFuture, FutureExt}; +use futures_core::future::BoxFuture; +use futures_util::FutureExt; use log::LevelFilter; use tokio::sync::RwLock; use northstar::{Certificate, GEMINI_MIME, GEMINI_PORT, Request, Response, Server}; diff --git a/examples/document.rs b/examples/document.rs index 5986896..8ff6bbb 100644 --- a/examples/document.rs +++ b/examples/document.rs @@ -1,5 +1,6 @@ use anyhow::*; -use futures::{future::BoxFuture, FutureExt}; +use futures_core::future::BoxFuture; +use futures_util::FutureExt; use log::LevelFilter; use northstar::{Server, Request, Response, GEMINI_PORT, Document}; use northstar::document::HeadingLevel::*; diff --git a/examples/serve_dir.rs b/examples/serve_dir.rs index 47145e1..fd26ac4 100644 --- a/examples/serve_dir.rs +++ b/examples/serve_dir.rs @@ -1,5 +1,6 @@ use anyhow::*; -use futures::{future::BoxFuture, FutureExt}; +use futures_core::future::BoxFuture; +use futures_util::FutureExt; use log::LevelFilter; use northstar::{Server, Request, Response, GEMINI_PORT}; diff --git a/src/lib.rs b/src/lib.rs index 066ce28..2083e0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,8 @@ use std::{ sync::Arc, time::Duration, }; -use futures::{future::BoxFuture, FutureExt}; +use futures_core::future::BoxFuture; +use futures_util::future::FutureExt; use tokio::{ prelude::*, io::{self, BufStream}, From 3da18ca5306241c54a269b2806d3735d810b992a Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 10:21:32 -0500 Subject: [PATCH 31/38] Reduced tokio featureset --- Cargo.toml | 4 ++-- src/types/body.rs | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1ef35dc..ae965a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,13 @@ documentation = "https://docs.rs/northstar" [features] default = ["serve_dir"] -serve_dir = ["mime_guess"] +serve_dir = ["mime_guess", "tokio/fs"] [dependencies] anyhow = "1.0.33" rustls = { version = "0.18.1", features = ["dangerous_configuration"] } tokio-rustls = "0.20.0" -tokio = { version = "0.3.1", features = ["full"] } +tokio = { version = "0.3.1", features = ["io-util","net","time", "rt"] } mime = "0.3.16" uriparse = "0.6.3" percent-encoding = "2.1.0" diff --git a/src/types/body.rs b/src/types/body.rs index dfeb8ca..d1356cc 100644 --- a/src/types/body.rs +++ b/src/types/body.rs @@ -1,4 +1,6 @@ -use tokio::{io::AsyncRead, fs::File}; +use tokio::io::AsyncRead; +#[cfg(feature="serve_dir")] +use tokio::fs::File; use crate::types::Document; @@ -37,6 +39,7 @@ impl<'a> From<&'a str> for Body { } } +#[cfg(feature="serve_dir")] impl From for Body { fn from(file: File) -> Self { Self::Reader(Box::new(file)) From 87d71cb207d8bd669b5125c5b58c9de57076b797 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 11:08:20 -0500 Subject: [PATCH 32/38] Fixed examples --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index ae965a5..62743e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,5 @@ mime_guess = { version = "2.0.3", optional = true } [dev-dependencies] env_logger = "0.8.1" +futures-util = "0.3.7" +tokio = { version = "0.3.1", features = ["macros", "rt-multi-thread", "sync"] } From 475db6af797fa80996fe2a8f50ce006c95c3b2a2 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 11:09:53 -0500 Subject: [PATCH 33/38] Adapted a type from futures-util allowing us to drop the dep and remove a big chunk of the dep tree --- Cargo.toml | 1 - src/lib.rs | 5 ++--- src/util.rs | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 62743e8..9ad991d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,6 @@ mime = "0.3.16" uriparse = "0.6.3" percent-encoding = "2.1.0" futures-core = "0.3.7" -futures-util = "0.3.7" log = "0.4.11" webpki = "0.21.0" lazy_static = "1.4.0" diff --git a/src/lib.rs b/src/lib.rs index 2083e0f..9b1ad5c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,6 @@ use std::{ time::Duration, }; use futures_core::future::BoxFuture; -use futures_util::future::FutureExt; use tokio::{ prelude::*, io::{self, BufStream}, @@ -33,7 +32,7 @@ pub const REQUEST_URI_MAX_LEN: usize = 1024; pub const GEMINI_PORT: u16 = 1965; type Handler = Arc HandlerResponse + Send + Sync>; -type HandlerResponse = BoxFuture<'static, Result>; +pub (crate) type HandlerResponse = BoxFuture<'static, Result>; #[derive(Clone)] pub struct Server { @@ -94,7 +93,7 @@ impl Server { let handler = (self.handler)(request); let handler = AssertUnwindSafe(handler); - let response = handler.catch_unwind().await + let response = util::HandlerCatchUnwind::new(handler).await .unwrap_or_else(|_| Response::server_error("")) .or_else(|err| { error!("Handler failed: {:?}", err); diff --git a/src/util.rs b/src/util.rs index ac774a2..628b018 100644 --- a/src/util.rs +++ b/src/util.rs @@ -11,6 +11,9 @@ use tokio::{ }; #[cfg(feature="serve_dir")] use crate::types::{Response, Document, document::HeadingLevel::*}; +use std::panic::{catch_unwind, AssertUnwindSafe}; +use std::task::Poll; +use futures_core::future::Future; #[cfg(feature="serve_dir")] pub async fn serve_file>(path: P, mime: &Mime) -> Result { @@ -123,3 +126,32 @@ where C: AsRef + Into, T: ToOwned + ?Sized, {} + +/// A utility for catching unwinds on Futures. +/// +/// This is adapted from the futures-rs CatchUnwind, in an effort to reduce the large +/// amount of dependencies tied into the feature that provides this simple struct. +#[must_use = "futures do nothing unless polled"] +pub (crate) struct HandlerCatchUnwind { + future: AssertUnwindSafe, +} + +impl HandlerCatchUnwind { + pub(super) fn new(future: AssertUnwindSafe) -> Self { + Self { future } + } +} + +impl Future for HandlerCatchUnwind { + type Output = Result, Box>; + + fn poll( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context + ) -> Poll { + match catch_unwind(AssertUnwindSafe(|| self.future.as_mut().poll(cx))) { + Ok(res) => res.map(Ok), + Err(e) => Poll::Ready(Err(e)) + } + } +} From c69cf49d995a2c341fe0e68e0fc8cd83f14640d0 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 11:29:11 -0500 Subject: [PATCH 34/38] Removed over-zeleous feature gating --- src/util.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util.rs b/src/util.rs index 628b018..c48b8bf 100644 --- a/src/util.rs +++ b/src/util.rs @@ -2,7 +2,6 @@ use std::path::{Path, PathBuf}; #[cfg(feature="serve_dir")] use mime::Mime; -#[cfg(feature="serve_dir")] use anyhow::*; #[cfg(feature="serve_dir")] use tokio::{ @@ -10,7 +9,8 @@ use tokio::{ io, }; #[cfg(feature="serve_dir")] -use crate::types::{Response, Document, document::HeadingLevel::*}; +use crate::types::{Document, document::HeadingLevel::*}; +use crate::types::Response; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::task::Poll; use futures_core::future::Future; From a3882b76de5f1ad7d9a2cad5805de607e6713899 Mon Sep 17 00:00:00 2001 From: panicbit Date: Thu, 19 Nov 2020 18:18:29 +0100 Subject: [PATCH 35/38] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa5c95c..63c75f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `redirect_temporary_lossy` for `Response` and `ResponseHeader` - `bad_request_lossy` for `Response` and `ResponseHeader` - support for a lot more mime-types in `guess_mime_from_path`, backed by the `mime_guess` crate +- customizable TLS cert & key paths by [@Alch-Emi](https://github.com/Alch-Emi) ## [0.3.0] - 2020-11-14 ### Added From 381bccf36f85ce5eeba83a205ce68bef44e3c185 Mon Sep 17 00:00:00 2001 From: panicbit Date: Thu, 19 Nov 2020 18:35:06 +0100 Subject: [PATCH 36/38] update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63c75f1..f1d7f18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `bad_request_lossy` for `Response` and `ResponseHeader` - support for a lot more mime-types in `guess_mime_from_path`, backed by the `mime_guess` crate - customizable TLS cert & key paths by [@Alch-Emi](https://github.com/Alch-Emi) +- `server_dir` default feature for serve_dir utils [@Alch-Emi](https://github.com/Alch-Emi) +### Improved +- build time and size by [@Alch-Emi](https://github.com/Alch-Emi) ## [0.3.0] - 2020-11-14 ### Added From aeeee86aae4e37534899e0d0f73198e494f9a817 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 12:37:41 -0500 Subject: [PATCH 37/38] Updated changelog for complex mime timeout override --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1d7f18..47c5d1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - `document` API for creating Gemini documents -- preliminary timeout API by [@Alch-Emi](https://github.com/Alch-Emi) +- preliminary timeout API, incl a special case for complex MIMEs by [@Alch-Emi](https://github.com/Alch-Emi) - `Response::success_with_body` by [@Alch-Emi](https://github.com/Alch-Emi) - `redirect_temporary_lossy` for `Response` and `ResponseHeader` - `bad_request_lossy` for `Response` and `ResponseHeader` From 46ab84ba04ceba67ac7c06cfcbced8f933b146b4 Mon Sep 17 00:00:00 2001 From: panicbit Date: Thu, 19 Nov 2020 19:29:17 +0100 Subject: [PATCH 38/38] streamline send_response --- src/lib.rs | 115 +++++++++++++++++++--------------------------------- src/util.rs | 8 ++++ 2 files changed, 49 insertions(+), 74 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b8ed975..b8e00d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ use tokio_rustls::{rustls, TlsAcceptor}; use rustls::*; use anyhow::*; use lazy_static::lazy_static; +use crate::util::opt_timeout; pub mod types; pub mod util; @@ -113,91 +114,46 @@ impl Server { let maybe_body = response.take_body(); let header = response.header(); - // Okay, I know this method looks really complicated, but I promise it's not. - // There's really only three things this method does: - // - // * Send the response header - // * Send the response body - // * Flush the stream - // - // All the other code is doing one of two things. Either it's - // - // * code to add and handle timeouts (that's what all the async blocks and calls - // to timeout are), or - // * logic to decide whether to use the special case timeout handling (seperate - // timeouts for the header and the body) vs the normal timeout handling (header, - // body, and flush all as one timeout) - // - // The split between the two cases happens at this very first if block. - // Everything in this if is for the special case. If any one of the ifs fails, - // the code after the big if block is run, and that's the normal case. - // - // Hope this helps! Emi <3 - - if header.status == Status::SUCCESS && + let use_complex_timeout = + header.status.is_success() && maybe_body.is_some() && header.meta.as_str() != "text/plain" && - header.meta.as_str() != "text/gemini" - { - if let Some(cplx_timeout) = self.complex_timeout { + header.meta.as_str() != "text/gemini" && + self.complex_timeout.is_some(); + let send_general_timeout; + let send_header_timeout; + let send_body_timeout; - ////////////// Use the special case timeout override ///////////////////////////// - - // Send the header & flush - let fut_send_header = async { - send_response_header(response.header(), stream).await - .context("Failed to write response header")?; - - stream.flush() - .await - .context("Failed to flush response header") - }; - timeout(self.timeout, fut_send_header) - .await - .context("Timed out while sending response header")??; - - // Send the body & flush - let fut_send_body = async { - send_response_body(maybe_body.unwrap(), stream).await - .context("Failed to write response body")?; - - stream.flush() - .await - .context("Failed to flush response body") - }; - timeout(cplx_timeout, fut_send_body) - .await - .context("Timed out while sending response body")??; - - - return Ok(()) - } + if use_complex_timeout { + send_general_timeout = None; + send_header_timeout = Some(self.timeout); + send_body_timeout = self.complex_timeout; + } else { + send_general_timeout = Some(self.timeout); + send_header_timeout = None; + send_body_timeout = None; } - - ///////////// Use the normal timeout ///////////////////////////////////////////// - - let fut_send_response = async { - send_response_header(response.header(), stream).await + opt_timeout(send_general_timeout, async { + // Send the header + opt_timeout(send_header_timeout, send_response_header(response.header(), stream)) + .await + .context("Timed out while sending response header")? .context("Failed to write response header")?; - if let Some(body) = maybe_body { - send_response_body(body, stream).await - .context("Failed to write response body")?; - } - - stream.flush() + // Send the body + opt_timeout(send_body_timeout, maybe_send_response_body(maybe_body, stream)) .await - .context("Failed to flush response data") - }; - timeout(self.timeout, fut_send_response) - .await - .context("Timed out while sending response data")??; + .context("Timed out while sending response body")? + .context("Failed to write response body")?; + + Ok::<_,Error>(()) + }) + .await + .context("Timed out while sending response data")??; Ok(()) - - ////////////////////////////////////////////////////////////////////////////////// } } @@ -377,6 +333,15 @@ async fn send_response_header(header: &ResponseHeader, stream: &mut (impl AsyncW ); stream.write_all(header.as_bytes()).await?; + stream.flush().await?; + + Ok(()) +} + +async fn maybe_send_response_body(maybe_body: Option, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> { + if let Some(body) = maybe_body { + send_response_body(body, stream).await?; + } Ok(()) } @@ -387,6 +352,8 @@ async fn send_response_body(body: Body, stream: &mut (impl AsyncWrite + Unpin)) Body::Reader(mut reader) => { io::copy(&mut reader, stream).await?; }, } + stream.flush().await?; + Ok(()) } diff --git a/src/util.rs b/src/util.rs index c48b8bf..5c623aa 100644 --- a/src/util.rs +++ b/src/util.rs @@ -14,6 +14,7 @@ use crate::types::Response; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::task::Poll; use futures_core::future::Future; +use tokio::time; #[cfg(feature="serve_dir")] pub async fn serve_file>(path: P, mime: &Mime) -> Result { @@ -155,3 +156,10 @@ impl Future for HandlerCatchUnwind { } } } + +pub(crate) async fn opt_timeout(duration: Option, future: impl Future) -> Result { + match duration { + Some(duration) => time::timeout(duration, future).await, + None => Ok(future.await), + } +}