From 0bcce9fd6fbbaea403119b91aa0b7017b6612b90 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 17:58:33 -0500 Subject: [PATCH 01/11] Appease clippy Please my compiler is so angry ;-; --- src/types/body.rs | 2 +- src/types/document.rs | 9 +++++---- src/types/meta.rs | 2 +- src/types/request.rs | 4 ++-- src/types/response.rs | 6 +++--- src/types/response_header.rs | 4 ++-- src/types/status.rs | 5 ++--- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/types/body.rs b/src/types/body.rs index d1356cc..d2da102 100644 --- a/src/types/body.rs +++ b/src/types/body.rs @@ -11,7 +11,7 @@ pub enum Body { impl From for Body { fn from(document: Document) -> Self { - Body::from(document.to_string()) + Self::from(document.to_string()) } } diff --git a/src/types/document.rs b/src/types/document.rs index e322357..d71c851 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -183,7 +183,7 @@ impl Document { .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 = Link { uri: Box::new(uri), label: Some(label) }; let link = Item::Link(link); self.add_item(link); @@ -213,7 +213,7 @@ impl Document { .map(URIReference::into_owned) .or_else(|_| ".".try_into()).expect("Northstar BUG"); let link = Link { - uri, + uri: Box::new(uri), label: None, }; let link = Item::Link(link); @@ -391,6 +391,7 @@ impl fmt::Display for Document { } } +#[allow(clippy::enum_variant_names)] enum Item { Text(Text), Link(Link), @@ -414,7 +415,7 @@ impl Text { } struct Link { - uri: URIReference<'static>, + uri: Box>, label: Option, } @@ -424,7 +425,7 @@ impl LinkLabel { fn from_lossy(line: impl Cowy) -> Self { let line = strip_newlines(line); - LinkLabel(line) + Self(line) } } diff --git a/src/types/meta.rs b/src/types/meta.rs index ccc17ba..bfb36e5 100644 --- a/src/types/meta.rs +++ b/src/types/meta.rs @@ -12,7 +12,7 @@ impl Meta { /// Creates a new "Meta" string. /// Fails if `meta` contains `\n`. pub fn new(meta: impl Cowy) -> Result { - ensure!(!meta.as_ref().contains("\n"), "Meta must not contain newlines"); + 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); Ok(Self(meta.into())) diff --git a/src/types/request.rs b/src/types/request.rs index a99ea2f..76eace2 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -39,7 +39,7 @@ impl Request { }) } - pub fn uri(&self) -> &URIReference { + pub const fn uri(&self) -> &URIReference { &self.uri } @@ -60,7 +60,7 @@ impl Request { self.certificate = cert; } - pub fn certificate(&self) -> Option<&Certificate> { + pub const fn certificate(&self) -> Option<&Certificate> { self.certificate.as_ref() } } diff --git a/src/types/response.rs b/src/types/response.rs index 3e4a84a..991d511 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -12,7 +12,7 @@ pub struct Response { } impl Response { - pub fn new(header: ResponseHeader) -> Self { + pub const fn new(header: ResponseHeader) -> Self { Self { header, body: None, @@ -34,7 +34,7 @@ impl Response { } pub fn success(mime: &Mime) -> Self { - let header = ResponseHeader::success(&mime); + let header = ResponseHeader::success(mime); Self::new(header) } @@ -86,7 +86,7 @@ impl Response { self } - pub fn header(&self) -> &ResponseHeader { + pub const fn header(&self) -> &ResponseHeader { &self.header } diff --git a/src/types/response_header.rs b/src/types/response_header.rs index 56f2af3..b2b5e20 100644 --- a/src/types/response_header.rs +++ b/src/types/response_header.rs @@ -88,11 +88,11 @@ impl ResponseHeader { } } - pub fn status(&self) -> &Status { + pub const fn status(&self) -> &Status { &self.status } - pub fn meta(&self) -> &Meta { + pub const fn meta(&self) -> &Meta { &self.meta } } diff --git a/src/types/status.rs b/src/types/status.rs index a06e9f4..18c58a1 100644 --- a/src/types/status.rs +++ b/src/types/status.rs @@ -1,4 +1,3 @@ - #[derive(Debug,Copy,Clone,PartialEq,Eq)] pub struct Status(u8); @@ -22,7 +21,7 @@ impl Status { pub const CERTIFICATE_NOT_AUTHORIZED: Self = Self(61); pub const CERTIFICATE_NOT_VALID: Self = Self(62); - pub fn code(&self) -> u8 { + pub const fn code(&self) -> u8 { self.0 } @@ -30,7 +29,7 @@ impl Status { self.category().is_success() } - pub fn category(&self) -> StatusCategory { + pub const fn category(&self) -> StatusCategory { let class = self.0 / 10; match class { From 26e0fd2702a9f023823490fc957e34e0064b363a Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 21:37:02 -0500 Subject: [PATCH 02/11] Added some routing classes --- Cargo.toml | 1 + src/lib.rs | 6 ++- src/routing/mod.rs | 3 ++ src/routing/node.rs | 124 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 src/routing/mod.rs create mode 100644 src/routing/node.rs diff --git a/Cargo.toml b/Cargo.toml index 9ad991d..b1e09a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ documentation = "https://docs.rs/northstar" [features] default = ["serve_dir"] +routing = [] serve_dir = ["mime_guess", "tokio/fs"] [dependencies] diff --git a/src/lib.rs b/src/lib.rs index b8e00d6..2279027 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,8 @@ use crate::util::opt_timeout; pub mod types; pub mod util; +#[cfg(feature="routing")] +pub mod routing; pub use mime; pub use uriparse as uri; @@ -33,8 +35,8 @@ pub use types::*; pub const REQUEST_URI_MAX_LEN: usize = 1024; pub const GEMINI_PORT: u16 = 1965; -type Handler = Arc HandlerResponse + Send + Sync>; -pub (crate) type HandlerResponse = BoxFuture<'static, Result>; +pub type Handler = Arc HandlerResponse + Send + Sync>; +pub type HandlerResponse = BoxFuture<'static, Result>; #[derive(Clone)] pub struct Server { diff --git a/src/routing/mod.rs b/src/routing/mod.rs new file mode 100644 index 0000000..5a41d23 --- /dev/null +++ b/src/routing/mod.rs @@ -0,0 +1,3 @@ +//! Tools for adding routes to a [`Server`](crate::Server) +mod node; +pub use node::*; diff --git a/src/routing/node.rs b/src/routing/node.rs new file mode 100644 index 0000000..b8bce21 --- /dev/null +++ b/src/routing/node.rs @@ -0,0 +1,124 @@ +use uriparse::path::Path; + +use std::collections::HashMap; +use std::convert::TryInto; + +use crate::Handler; +use crate::types::Request; + +#[derive(Default)] +/// A node for routing requests +/// +/// Routing is processed by a tree, with each child being a single path segment. For +/// example, if a handler existed at "/trans/rights", then the root-level node would have +/// a child "trans", which would have a child "rights". "rights" would have no children, +/// but would have an attached handler. +/// +/// If one route is shorter than another, say "/trans/rights" and +/// "/trans/rights/r/human", then the longer route always matches first, so a request for +/// "/trans/rights/r/human/rights" would be routed to "/trans/rights/r/human", and +/// "/trans/rights/now" would route to "/trans/rights" +pub struct RoutingNode(Option, HashMap); + +impl RoutingNode { + /// Attempt to identify a handler based on path segments + /// + /// This searches the network of routing nodes attempting to match a specific request, + /// represented as a sequence of path segments. For example, "/dir/image.png?text" + /// should be represented as `&["dir", "image.png"]`. + /// + /// Routing is performed only on normalized paths, so if a route exists for + /// "/endpoint", "/endpoint/" will also match, and vice versa. Routes also match all + /// requests for which they are the base of, meaning a request of "/api/endpoint" will + /// match a route of "/api" if no route exists specifically for "/api/endpoint". + /// + /// Longer routes automatically match before shorter routes. + pub fn match_path(&self, path: I) -> Option<&Handler> + where + I: IntoIterator, + S: AsRef, + { + let mut node = self; + let mut path = path.into_iter(); + let mut last_seen_handler = None; + loop { + let Self(maybe_handler, map) = node; + + last_seen_handler = maybe_handler.as_ref().or(last_seen_handler); + + if let Some(segment) = path.next() { + if let Some(route) = map.get(segment.as_ref()) { + node = route; + } else { + return last_seen_handler; + } + } else { + return last_seen_handler; + } + } + } + + /// Attempt to identify a route for a given [`Request`] + /// + /// See [`match_path()`](Self::match_path()) for how matching works + pub fn match_request(&self, req: Request) -> Option<&Handler> { + let mut path = req.path().to_owned(); + path.normalize(false); + self.match_path(path.segments()) + } + + /// Add a route to the network + /// + /// This method wraps [`add_route_by_path()`](Self::add_route_by_path()) while + /// unwrapping any errors that might occur. For this reason, this method only takes + /// static strings. If you would like to add a string dynamically, please use + /// [`RoutingNode::add_route_by_path()`] in order to appropriately deal with any + /// errors that might arise. + pub fn add_route(&mut self, path: &'static str, handler: impl Into) { + let path: Path = path.try_into().expect("Malformed path route received"); + self.add_route_by_path(path, handler).unwrap(); + } + + /// Add a route to the network + /// + /// The path provided MUST be absolute. Callers should verify this before calling + /// this method. + /// + /// For information about how routes work, see [`RoutingNode::match_path()`] + pub fn add_route_by_path(&mut self, mut path: Path, handler: impl Into) -> Result<(), ConflictingRouteError>{ + debug_assert!(path.is_absolute()); + path.normalize(false); + + let mut node = self; + for segment in path.segments() { + node = node.1.entry(segment.to_string()).or_default(); + } + + if node.0.is_some() { + Err(ConflictingRouteError()) + } else { + node.0 = Some(handler.into()); + Ok(()) + } + } + + /// Recursively shrink maps to fit + pub fn shrink(&mut self) { + let mut to_shrink = vec![&mut self.1]; + while let Some(shrink) = to_shrink.pop() { + shrink.shrink_to_fit(); + to_shrink.extend(shrink.values_mut().map(|n| &mut n.1)); + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ConflictingRouteError(); + +impl std::error::Error for ConflictingRouteError { } + +impl std::fmt::Display for ConflictingRouteError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Attempted to create a route with the same matcher as an existing route") + } +} From e6c66ed9e79776827dd666db3dd031f572cd3128 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 21:45:41 -0500 Subject: [PATCH 03/11] Don't feature gate routing I really thought this was gonna be more complicated when I was planning it. Well, "planning" it. --- Cargo.toml | 1 - src/lib.rs | 1 - src/{routing/node.rs => routing.rs} | 16 +++++++++------- src/routing/mod.rs | 3 --- 4 files changed, 9 insertions(+), 12 deletions(-) rename src/{routing/node.rs => routing.rs} (89%) delete mode 100644 src/routing/mod.rs diff --git a/Cargo.toml b/Cargo.toml index b1e09a1..9ad991d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,6 @@ documentation = "https://docs.rs/northstar" [features] default = ["serve_dir"] -routing = [] serve_dir = ["mime_guess", "tokio/fs"] [dependencies] diff --git a/src/lib.rs b/src/lib.rs index 2279027..55df0b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,6 @@ use crate::util::opt_timeout; pub mod types; pub mod util; -#[cfg(feature="routing")] pub mod routing; pub use mime; diff --git a/src/routing/node.rs b/src/routing.rs similarity index 89% rename from src/routing/node.rs rename to src/routing.rs index b8bce21..3ba5aac 100644 --- a/src/routing/node.rs +++ b/src/routing.rs @@ -1,3 +1,7 @@ +//! Utilities for routing requests +//! +//! See [`RoutingNode`] for details on how routes are matched. + use uriparse::path::Path; use std::collections::HashMap; @@ -18,6 +22,9 @@ use crate::types::Request; /// "/trans/rights/r/human", then the longer route always matches first, so a request for /// "/trans/rights/r/human/rights" would be routed to "/trans/rights/r/human", and /// "/trans/rights/now" would route to "/trans/rights" +/// +/// Routing is only performed on normalized paths, so "/endpoint" and "/endpoint/" are +/// considered to be the same route. pub struct RoutingNode(Option, HashMap); impl RoutingNode { @@ -27,12 +34,7 @@ impl RoutingNode { /// represented as a sequence of path segments. For example, "/dir/image.png?text" /// should be represented as `&["dir", "image.png"]`. /// - /// Routing is performed only on normalized paths, so if a route exists for - /// "/endpoint", "/endpoint/" will also match, and vice versa. Routes also match all - /// requests for which they are the base of, meaning a request of "/api/endpoint" will - /// match a route of "/api" if no route exists specifically for "/api/endpoint". - /// - /// Longer routes automatically match before shorter routes. + /// See [`RoutingNode`] for details on how routes are matched. pub fn match_path(&self, path: I) -> Option<&Handler> where I: IntoIterator, @@ -60,7 +62,7 @@ impl RoutingNode { /// Attempt to identify a route for a given [`Request`] /// - /// See [`match_path()`](Self::match_path()) for how matching works + /// See [`RoutingNode`] for details on how routes are matched. pub fn match_request(&self, req: Request) -> Option<&Handler> { let mut path = req.path().to_owned(); path.normalize(false); diff --git a/src/routing/mod.rs b/src/routing/mod.rs deleted file mode 100644 index 5a41d23..0000000 --- a/src/routing/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Tools for adding routes to a [`Server`](crate::Server) -mod node; -pub use node::*; From 4a0d07c2ca271605f3e19d55f6acb862fb2e7202 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 22:11:31 -0500 Subject: [PATCH 04/11] Switched Builder & Server to routes from a single handler --- src/lib.rs | 52 +++++++++++++++++++++++++++++++++----------------- src/routing.rs | 2 +- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 55df0b3..fd68eb6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ use rustls::*; use anyhow::*; use lazy_static::lazy_static; use crate::util::opt_timeout; +use routing::RoutingNode; pub mod types; pub mod util; @@ -41,7 +42,7 @@ pub type HandlerResponse = BoxFuture<'static, Result>; pub struct Server { tls_acceptor: TlsAcceptor, listener: Arc, - handler: Handler, + routes: Arc, timeout: Duration, complex_timeout: Option, } @@ -94,19 +95,24 @@ impl Server { request.set_cert(client_cert); - let handler = (self.handler)(request); - let handler = AssertUnwindSafe(handler); + let response = if let Some(handler) = self.routes.match_request(&request) { - let response = util::HandlerCatchUnwind::new(handler).await - .unwrap_or_else(|_| Response::server_error("")) - .or_else(|err| { - error!("Handler failed: {:?}", err); - Response::server_error("") - }) - .context("Request handler failed")?; + let handler = (handler)(request); + let handler = AssertUnwindSafe(handler); - self.send_response(response, &mut stream).await - .context("Failed to send response")?; + util::HandlerCatchUnwind::new(handler).await + .unwrap_or_else(|_| Response::server_error("")) + .or_else(|err| { + error!("Handler failed: {:?}", err); + Response::server_error("") + }) + .context("Request handler failed")? + } else { + Response::not_found() + }; + + self.send_response(response, &mut stream).await + .context("Failed to send response")?; Ok(()) } @@ -164,6 +170,7 @@ pub struct Builder { key_path: PathBuf, timeout: Duration, complex_body_timeout_override: Option, + routes: RoutingNode, } impl Builder { @@ -174,6 +181,7 @@ impl Builder { complex_body_timeout_override: Some(Duration::from_secs(30)), cert_path: PathBuf::from("cert/cert.pem"), key_path: PathBuf::from("cert/key.pem"), + routes: RoutingNode::default(), } } @@ -276,20 +284,30 @@ impl Builder { self } - pub async fn serve(self, handler: F) -> Result<()> - where - F: Fn(Request) -> HandlerResponse + Send + Sync + 'static, - { + /// Add a handler for a route + /// + /// A route must be an absolute path, for example "/endpoint" or "/", but not + /// "endpoint". Entering a relative or malformed path will result in a panic. + /// + /// For more information about routing mechanics, see the docs for [`RoutingNode`]. + pub fn add_route(mut self, path: &'static str, handler: impl Into) -> Self { + self.routes.add_route(path, handler); + self + } + + pub async fn serve(mut self) -> Result<()> { let config = tls_config(&self.cert_path, &self.key_path) .context("Failed to create TLS config")?; let listener = TcpListener::bind(self.addr).await .context("Failed to create socket")?; + self.routes.shrink(); + let server = Server { tls_acceptor: TlsAcceptor::from(config), listener: Arc::new(listener), - handler: Arc::new(handler), + routes: Arc::new(self.routes), timeout: self.timeout, complex_timeout: self.complex_body_timeout_override, }; diff --git a/src/routing.rs b/src/routing.rs index 3ba5aac..2788af0 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -63,7 +63,7 @@ impl RoutingNode { /// Attempt to identify a route for a given [`Request`] /// /// See [`RoutingNode`] for details on how routes are matched. - pub fn match_request(&self, req: Request) -> Option<&Handler> { + pub fn match_request(&self, req: &Request) -> Option<&Handler> { let mut path = req.path().to_owned(); path.normalize(false); self.match_path(path.segments()) From dc18bf2d1cc88ee6713f4e2ea24de01262b123d1 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 22:33:44 -0500 Subject: [PATCH 05/11] Fix examples (& also bugs with args in lib.rs) I thought I was clever with Into :( --- examples/certificates.rs | 3 ++- examples/document.rs | 3 ++- examples/serve_dir.rs | 3 ++- src/lib.rs | 9 ++++++--- src/routing.rs | 6 +++--- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/examples/certificates.rs b/examples/certificates.rs index 541fbe5..143c71c 100644 --- a/examples/certificates.rs +++ b/examples/certificates.rs @@ -19,7 +19,8 @@ async fn main() -> Result<()> { let users = Arc::>>::default(); Server::bind(("0.0.0.0", GEMINI_PORT)) - .serve(move|req| handle_request(users.clone(), req)) + .add_route("/", move|req| handle_request(users.clone(), req)) + .serve() .await } diff --git a/examples/document.rs b/examples/document.rs index 8ff6bbb..cc889c6 100644 --- a/examples/document.rs +++ b/examples/document.rs @@ -12,7 +12,8 @@ async fn main() -> Result<()> { .init(); Server::bind(("localhost", GEMINI_PORT)) - .serve(handle_request) + .add_route("/",handle_request) + .serve() .await } diff --git a/examples/serve_dir.rs b/examples/serve_dir.rs index fd26ac4..de3e0b0 100644 --- a/examples/serve_dir.rs +++ b/examples/serve_dir.rs @@ -11,7 +11,8 @@ async fn main() -> Result<()> { .init(); Server::bind(("localhost", GEMINI_PORT)) - .serve(handle_request) + .add_route("/", handle_request) + .serve() .await } diff --git a/src/lib.rs b/src/lib.rs index fd68eb6..daf9a98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -290,12 +290,15 @@ impl Builder { /// "endpoint". Entering a relative or malformed path will result in a panic. /// /// For more information about routing mechanics, see the docs for [`RoutingNode`]. - pub fn add_route(mut self, path: &'static str, handler: impl Into) -> Self { - self.routes.add_route(path, handler); + pub fn add_route(mut self, path: &'static str, handler: H) -> Self + where + H: Fn(Request) -> HandlerResponse + Send + Sync + 'static, + { + self.routes.add_route(path, Arc::new(handler)); self } - pub async fn serve(mut self) -> Result<()> { + pub async fn serve(mut self) -> Result<()> { let config = tls_config(&self.cert_path, &self.key_path) .context("Failed to create TLS config")?; diff --git a/src/routing.rs b/src/routing.rs index 2788af0..225a58e 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -76,7 +76,7 @@ impl RoutingNode { /// static strings. If you would like to add a string dynamically, please use /// [`RoutingNode::add_route_by_path()`] in order to appropriately deal with any /// errors that might arise. - pub fn add_route(&mut self, path: &'static str, handler: impl Into) { + pub fn add_route(&mut self, path: &'static str, handler: Handler) { let path: Path = path.try_into().expect("Malformed path route received"); self.add_route_by_path(path, handler).unwrap(); } @@ -87,7 +87,7 @@ impl RoutingNode { /// this method. /// /// For information about how routes work, see [`RoutingNode::match_path()`] - pub fn add_route_by_path(&mut self, mut path: Path, handler: impl Into) -> Result<(), ConflictingRouteError>{ + pub fn add_route_by_path(&mut self, mut path: Path, handler: Handler) -> Result<(), ConflictingRouteError>{ debug_assert!(path.is_absolute()); path.normalize(false); @@ -99,7 +99,7 @@ impl RoutingNode { if node.0.is_some() { Err(ConflictingRouteError()) } else { - node.0 = Some(handler.into()); + node.0 = Some(handler); Ok(()) } } From 10c957aee5fcbe8c0d62040d80ac5f7e06599280 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 22:35:35 -0500 Subject: [PATCH 06/11] Re-restrict Handler & HandlerResponse --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index daf9a98..1812967 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,8 +35,8 @@ pub use types::*; pub const REQUEST_URI_MAX_LEN: usize = 1024; pub const GEMINI_PORT: u16 = 1965; -pub type Handler = Arc HandlerResponse + Send + Sync>; -pub type HandlerResponse = BoxFuture<'static, Result>; +pub (crate) type Handler = Arc HandlerResponse + Send + Sync>; +pub (crate) type HandlerResponse = BoxFuture<'static, Result>; #[derive(Clone)] pub struct Server { From c162bdd156299ec1d9b2b4e0ac789a1a123aa63f Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 22:37:31 -0500 Subject: [PATCH 07/11] Updated changelog to add routing API Look I remembered this time! --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47c5d1b..435e8ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `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) +### Changed +- Added route API [@Alch-Emi](https://github.com/Alch-Emi) ## [0.3.0] - 2020-11-14 ### Added From b085fa5836540f8ed8ebe3b04717b05f287562ef Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 22:53:50 -0500 Subject: [PATCH 08/11] Added routing example --- examples/routing.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 examples/routing.rs diff --git a/examples/routing.rs b/examples/routing.rs new file mode 100644 index 0000000..596e696 --- /dev/null +++ b/examples/routing.rs @@ -0,0 +1,53 @@ +use anyhow::*; +use futures_core::future::BoxFuture; +use futures_util::FutureExt; +use log::LevelFilter; +use northstar::{Document, document::HeadingLevel, Request, Response, GEMINI_PORT}; + +#[tokio::main] +async fn main() -> Result<()> { + env_logger::builder() + .filter_module("northstar", LevelFilter::Debug) + .init(); + + northstar::Server::bind(("localhost", GEMINI_PORT)) + .add_route("/", handle_base) + .add_route("/route", handle_short) + .add_route("/route/long", handle_long) + .serve() + .await +} + +fn handle_base(_: Request) -> BoxFuture<'static, Result> { + let doc = generate_doc("base"); + async move { + Ok(Response::document(doc)) + }.boxed() +} + +fn handle_short(_: Request) -> BoxFuture<'static, Result> { + let doc = generate_doc("short"); + async move { + Ok(Response::document(doc)) + }.boxed() +} + +fn handle_long(_: Request) -> BoxFuture<'static, Result> { + let doc = generate_doc("long"); + async move { + Ok(Response::document(doc)) + }.boxed() +} + +fn generate_doc(route_name: &str) -> Document { + let mut doc = Document::new(); + doc.add_heading(HeadingLevel::H1, "Routing Demo") + .add_text(&format!("You're currently on the {} route", route_name)) + .add_blank_line() + .add_text("Here's some links to try:") + .add_link_without_label("/") + .add_link_without_label("/route") + .add_link_without_label("/route/long") + .add_link_without_label("/route/not_real"); + doc +} From 54816e1f6730eb9b70bb5acef1befcd78fa4d3ef Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 23:34:45 -0500 Subject: [PATCH 09/11] Fixed bug where root handler was never hit for requests other than exact matches --- examples/routing.rs | 3 ++- src/routing.rs | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/routing.rs b/examples/routing.rs index 596e696..742a620 100644 --- a/examples/routing.rs +++ b/examples/routing.rs @@ -48,6 +48,7 @@ fn generate_doc(route_name: &str) -> Document { .add_link_without_label("/") .add_link_without_label("/route") .add_link_without_label("/route/long") - .add_link_without_label("/route/not_real"); + .add_link_without_label("/route/not_real") + .add_link_without_label("/rowte"); doc } diff --git a/src/routing.rs b/src/routing.rs index 225a58e..28966cb 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -93,7 +93,9 @@ impl RoutingNode { let mut node = self; for segment in path.segments() { - node = node.1.entry(segment.to_string()).or_default(); + if segment != "" { + node = node.1.entry(segment.to_string()).or_default(); + } } if node.0.is_some() { From 59e3222ce8513b591f43be0b2fac4b6502c6c27a Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Fri, 20 Nov 2020 13:22:34 -0500 Subject: [PATCH 10/11] Add trailing segments to request --- examples/routing.rs | 16 +++++++++------- src/lib.rs | 4 +++- src/routing.rs | 45 ++++++++++++++++++++++++++++++++++---------- src/types/request.rs | 31 ++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 18 deletions(-) diff --git a/examples/routing.rs b/examples/routing.rs index 742a620..04bded6 100644 --- a/examples/routing.rs +++ b/examples/routing.rs @@ -18,31 +18,33 @@ async fn main() -> Result<()> { .await } -fn handle_base(_: Request) -> BoxFuture<'static, Result> { - let doc = generate_doc("base"); +fn handle_base(req: Request) -> BoxFuture<'static, Result> { + let doc = generate_doc("base", &req); async move { Ok(Response::document(doc)) }.boxed() } -fn handle_short(_: Request) -> BoxFuture<'static, Result> { - let doc = generate_doc("short"); +fn handle_short(req: Request) -> BoxFuture<'static, Result> { + let doc = generate_doc("short", &req); async move { Ok(Response::document(doc)) }.boxed() } -fn handle_long(_: Request) -> BoxFuture<'static, Result> { - let doc = generate_doc("long"); +fn handle_long(req: Request) -> BoxFuture<'static, Result> { + let doc = generate_doc("long", &req); async move { Ok(Response::document(doc)) }.boxed() } -fn generate_doc(route_name: &str) -> Document { +fn generate_doc(route_name: &str, req: &Request) -> Document { + let trailing = req.trailing_segments().join("/"); let mut doc = Document::new(); doc.add_heading(HeadingLevel::H1, "Routing Demo") .add_text(&format!("You're currently on the {} route", route_name)) + .add_text(&format!("Trailing segments: /{}", trailing)) .add_blank_line() .add_text("Here's some links to try:") .add_link_without_label("/") diff --git a/src/lib.rs b/src/lib.rs index 1812967..2aa41e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -95,7 +95,9 @@ impl Server { request.set_cert(client_cert); - let response = if let Some(handler) = self.routes.match_request(&request) { + let response = if let Some((trailing, handler)) = self.routes.match_request(&request) { + + request.set_trailing(trailing); let handler = (handler)(request); let handler = AssertUnwindSafe(handler); diff --git a/src/routing.rs b/src/routing.rs index 28966cb..1179601 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -2,7 +2,7 @@ //! //! See [`RoutingNode`] for details on how routes are matched. -use uriparse::path::Path; +use uriparse::path::{Path, Segment}; use std::collections::HashMap; use std::convert::TryInto; @@ -34,39 +34,64 @@ impl RoutingNode { /// represented as a sequence of path segments. For example, "/dir/image.png?text" /// should be represented as `&["dir", "image.png"]`. /// + /// If a match is found, it is returned, along with the segments of the path trailing + /// the handler. For example, a route `/foo` recieving a request to `/foo/bar` would + /// receive `vec!["bar"]` + /// /// See [`RoutingNode`] for details on how routes are matched. - pub fn match_path(&self, path: I) -> Option<&Handler> + pub fn match_path(&self, path: I) -> Option<(Vec, &Handler)> where I: IntoIterator, S: AsRef, { let mut node = self; - let mut path = path.into_iter(); + let mut path = path.into_iter().filter(|seg| !seg.as_ref().is_empty()); let mut last_seen_handler = None; + let mut since_last_handler = Vec::new(); loop { let Self(maybe_handler, map) = node; - last_seen_handler = maybe_handler.as_ref().or(last_seen_handler); + if maybe_handler.is_some() { + last_seen_handler = maybe_handler.as_ref(); + since_last_handler.clear(); + } if let Some(segment) = path.next() { - if let Some(route) = map.get(segment.as_ref()) { + let maybe_route = map.get(segment.as_ref()); + since_last_handler.push(segment); + + if let Some(route) = maybe_route { node = route; } else { - return last_seen_handler; + break; } } else { - return last_seen_handler; + break; } + }; + + if let Some(handler) = last_seen_handler { + since_last_handler.extend(path); + Some((since_last_handler, handler)) + } else { + None } } /// Attempt to identify a route for a given [`Request`] /// - /// See [`RoutingNode`] for details on how routes are matched. - pub fn match_request(&self, req: &Request) -> Option<&Handler> { - let mut path = req.path().to_owned(); + /// See [`RoutingNode::match_path()`] for more information + pub fn match_request(&self, req: &Request) -> Option<(Vec, &Handler)> { + let mut path = req.path().to_borrowed(); path.normalize(false); self.match_path(path.segments()) + .map(|(segs, h)| ( + segs.into_iter() + .map(Segment::as_str) + .map(str::to_owned) + .collect(), + h, + )) } /// Add a route to the network diff --git a/src/types/request.rs b/src/types/request.rs index 76eace2..02d841b 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -8,6 +8,7 @@ pub struct Request { uri: URIReference<'static>, input: Option, certificate: Option, + trailing_segments: Option>, } impl Request { @@ -36,6 +37,7 @@ impl Request { uri, input, certificate, + trailing_segments: None, }) } @@ -43,6 +45,31 @@ impl Request { &self.uri } + #[allow(clippy::missing_const_for_fn)] + /// All of the path segments following the route to which this request was bound. + /// + /// For example, if this handler was bound to the `/api` route, and a request was + /// received to `/api/v1/endpoint`, then this value would be `["v1", "endpoint"]`. + /// This should not be confused with [`path_segments()`](Self::path_segments()), which + /// contains *all* of the segments, not just those trailing the route. + /// + /// If the trailing segments have not been set, this method will panic, but this + /// should only be possible if you are constructing the Request yourself. Requests + /// to handlers registered through [`add_route`](northstar::Builder::add_route()) will + /// always have trailing segments set. + pub fn trailing_segments(&self) -> &Vec { + self.trailing_segments.as_ref().unwrap() + } + + /// All of the segments in this path, percent decoded + /// + /// For example, for a request to `/api/v1/endpoint`, this would return `["api", "v1", + /// "endpoint"]`, no matter what route the handler that recieved this request was + /// bound to. This is not to be confused with + /// [`trailing_segments()`](Self::trailing_segments), which contains only the segments + /// following the bound route. + /// + /// Additionally, unlike `trailing_segments()`, this method percent decodes the path. pub fn path_segments(&self) -> Vec { self.uri() .path() @@ -60,6 +87,10 @@ impl Request { self.certificate = cert; } + pub fn set_trailing(&mut self, segments: Vec) { + self.trailing_segments = Some(segments); + } + pub const fn certificate(&self) -> Option<&Certificate> { self.certificate.as_ref() } From 536e404fdf2791615ac314c563ca02a7d927b5de Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Fri, 20 Nov 2020 13:54:24 -0500 Subject: [PATCH 11/11] Make RoutingNode generic --- src/lib.rs | 6 +++--- src/routing.rs | 36 ++++++++++++++++++++---------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2aa41e0..e957262 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,14 +35,14 @@ pub use types::*; pub const REQUEST_URI_MAX_LEN: usize = 1024; pub const GEMINI_PORT: u16 = 1965; -pub (crate) type Handler = Arc HandlerResponse + Send + Sync>; +type Handler = Arc HandlerResponse + Send + Sync>; pub (crate) type HandlerResponse = BoxFuture<'static, Result>; #[derive(Clone)] pub struct Server { tls_acceptor: TlsAcceptor, listener: Arc, - routes: Arc, + routes: Arc>, timeout: Duration, complex_timeout: Option, } @@ -172,7 +172,7 @@ pub struct Builder { key_path: PathBuf, timeout: Duration, complex_body_timeout_override: Option, - routes: RoutingNode, + routes: RoutingNode, } impl Builder { diff --git a/src/routing.rs b/src/routing.rs index 1179601..20708f7 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -7,16 +7,14 @@ use uriparse::path::{Path, Segment}; use std::collections::HashMap; use std::convert::TryInto; -use crate::Handler; use crate::types::Request; -#[derive(Default)] -/// A node for routing requests +/// A node for linking values to routes /// /// Routing is processed by a tree, with each child being a single path segment. For -/// example, if a handler existed at "/trans/rights", then the root-level node would have +/// example, if an entry existed at "/trans/rights", then the root-level node would have /// a child "trans", which would have a child "rights". "rights" would have no children, -/// but would have an attached handler. +/// but would have an attached entry. /// /// If one route is shorter than another, say "/trans/rights" and /// "/trans/rights/r/human", then the longer route always matches first, so a request for @@ -25,21 +23,21 @@ use crate::types::Request; /// /// Routing is only performed on normalized paths, so "/endpoint" and "/endpoint/" are /// considered to be the same route. -pub struct RoutingNode(Option, HashMap); +pub struct RoutingNode(Option, HashMap); -impl RoutingNode { - /// Attempt to identify a handler based on path segments +impl RoutingNode { + /// Attempt to find and entry based on path segments /// /// This searches the network of routing nodes attempting to match a specific request, /// represented as a sequence of path segments. For example, "/dir/image.png?text" /// should be represented as `&["dir", "image.png"]`. /// /// If a match is found, it is returned, along with the segments of the path trailing - /// the handler. For example, a route `/foo` recieving a request to `/foo/bar` would - /// receive `vec!["bar"]` + /// the subpath matcing the route. For example, a route `/foo` recieving a request to + /// `/foo/bar` would produce `vec!["bar"]` /// /// See [`RoutingNode`] for details on how routes are matched. - pub fn match_path(&self, path: I) -> Option<(Vec, &Handler)> + pub fn match_path(&self, path: I) -> Option<(Vec, &T)> where I: IntoIterator, S: AsRef, @@ -81,7 +79,7 @@ impl RoutingNode { /// Attempt to identify a route for a given [`Request`] /// /// See [`RoutingNode::match_path()`] for more information - pub fn match_request(&self, req: &Request) -> Option<(Vec, &Handler)> { + pub fn match_request(&self, req: &Request) -> Option<(Vec, &T)> { let mut path = req.path().to_borrowed(); path.normalize(false); self.match_path(path.segments()) @@ -101,9 +99,9 @@ impl RoutingNode { /// static strings. If you would like to add a string dynamically, please use /// [`RoutingNode::add_route_by_path()`] in order to appropriately deal with any /// errors that might arise. - pub fn add_route(&mut self, path: &'static str, handler: Handler) { + pub fn add_route(&mut self, path: &'static str, data: T) { let path: Path = path.try_into().expect("Malformed path route received"); - self.add_route_by_path(path, handler).unwrap(); + self.add_route_by_path(path, data).unwrap(); } /// Add a route to the network @@ -112,7 +110,7 @@ impl RoutingNode { /// this method. /// /// For information about how routes work, see [`RoutingNode::match_path()`] - pub fn add_route_by_path(&mut self, mut path: Path, handler: Handler) -> Result<(), ConflictingRouteError>{ + pub fn add_route_by_path(&mut self, mut path: Path, data: T) -> Result<(), ConflictingRouteError>{ debug_assert!(path.is_absolute()); path.normalize(false); @@ -126,7 +124,7 @@ impl RoutingNode { if node.0.is_some() { Err(ConflictingRouteError()) } else { - node.0 = Some(handler); + node.0 = Some(data); Ok(()) } } @@ -141,6 +139,12 @@ impl RoutingNode { } } +impl Default for RoutingNode { + fn default() -> Self { + Self(None, HashMap::default()) + } +} + #[derive(Debug, Clone, Copy)] pub struct ConflictingRouteError();