From 32453966822001a8a81b675e4cbe7d481896feef Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Tue, 17 Nov 2020 20:38:10 -0500 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] 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 0daf01fd3e9c68b143eff299b70d14a0cc52742b Mon Sep 17 00:00:00 2001 From: panicbit Date: Wed, 18 Nov 2020 21:16:14 +0100 Subject: [PATCH 05/11] 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 06/11] 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 07/11] 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 08/11] 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 09/11] 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 10/11] 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 11/11] 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