diff --git a/examples/subdomain.rs b/examples/subdomain.rs new file mode 100644 index 000000000..c49f054a9 --- /dev/null +++ b/examples/subdomain.rs @@ -0,0 +1,29 @@ +#[async_std::main] +async fn main() -> Result<(), std::io::Error> { + tide::log::start(); + let mut app = tide::new(); + app.at("/") + .get(|_| async { Ok("Welcome to my landing page") }); + app.subdomain("blog") + .at("/") + .get(|_| async { Ok("Welcome to my blog") }); + app.subdomain(":user") + .at("/") + .get(|req: tide::Request<()>| async move { + let user = req.param("user").unwrap(); + Ok(format!("Welcome user {}", user)) + }); + + // to be able to use this example, please note some domains down inside of + // your /etc/hosts file. Add the following: + // 127.0.0.1 example.local + // 127.0.0.1 blog.example.local + // 127.0.0.1 tom.example.local + + // After adding the urls. Test it inside of your browser. Try: + // - example.local:8080 + // - blog.example.local:8080 + // - tom.example.local:8080 + app.listen("http://example.local:8080").await?; + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index f47041964..2a1bf5ffa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,11 +64,14 @@ mod cookies; mod endpoint; mod fs; mod middleware; +mod namespace; mod redirect; mod request; mod response; mod response_builder; mod route; +mod subdomain; +mod subdomain_router; #[cfg(not(feature = "__internal__bench"))] mod router; diff --git a/src/namespace.rs b/src/namespace.rs new file mode 100644 index 000000000..426a2e6e8 --- /dev/null +++ b/src/namespace.rs @@ -0,0 +1,86 @@ +use std::{collections::BTreeMap, sync::Arc}; + +use crate::{ + router::Selection, subdomain::Subdomain, subdomain_router::router::SubdomainRouter, Middleware, +}; + +/// The routing for subdomains used by `Server` +pub struct Namespace { + router: SubdomainRouter>, +} + +/// The result of routing a subdomain and a URL +pub struct NamespaceSelection<'a, State> { + pub(crate) selection: Selection<'a, State>, + pub(crate) middleware: Vec>>, + pub(crate) params: BTreeMap<&'a String, String>, +} + +impl<'a, State> NamespaceSelection<'a, State> { + pub fn subdomain_params(&self) -> route_recognizer::Params { + let mut params = route_recognizer::Params::new(); + for (key, value) in &self.params { + params.insert(key.to_string(), value.to_owned()); + } + params + } +} + +impl Namespace { + pub fn new() -> Self { + Self { + router: SubdomainRouter::new(), + } + } + + pub fn add(&mut self, subdomain: String, router: Subdomain) -> &mut Subdomain { + self.router.add(&subdomain, router) + } + + pub fn route( + &self, + domain: &str, + path: &str, + method: http_types::Method, + global_middleware: &[Arc>], + ) -> NamespaceSelection<'_, State> { + let subdomains = domain.split('.').rev().skip(2).collect::>(); + let domain = if subdomains.len() == 0 { + "".to_owned() + } else { + subdomains + .iter() + .rev() + .fold(String::new(), |sub, part| sub + "." + part)[1..] + .to_owned() + }; + + match self.router.recognize(&domain) { + Some(data) => { + let subdomain = data.data; + let params = data.params; + let selection = subdomain.route(path, method); + let subdomain_middleware = subdomain.middleware().as_slice(); + let global_middleware = global_middleware; + let mut middleware = vec![]; + middleware.extend_from_slice(global_middleware); + middleware.extend_from_slice(subdomain_middleware); + NamespaceSelection { + selection, + middleware, + params, + } + } + None => { + let selection = Selection::not_found_endpoint(); + let mut middleware = vec![]; + middleware.extend_from_slice(global_middleware); + NamespaceSelection { + selection, + middleware, + params: BTreeMap::new(), + } + } + } + } +} diff --git a/src/router.rs b/src/router.rs index b3673ee7d..6daa80730 100644 --- a/src/router.rs +++ b/src/router.rs @@ -21,6 +21,25 @@ pub struct Selection<'a, State> { pub(crate) params: Params, } +impl<'a, State> Selection<'a, State> +where + State: Clone + Send + Sync + 'static, +{ + pub fn not_found_endpoint() -> Selection<'a, State> { + Selection { + endpoint: ¬_found_endpoint, + params: Params::new(), + } + } + + pub fn method_not_allowed() -> Selection<'a, State> { + Selection { + endpoint: &method_not_allowed, + params: Params::new(), + } + } +} + impl Router { pub fn new() -> Self { Router { @@ -68,15 +87,9 @@ impl Router { { // If this `path` can be handled by a callback registered with a different HTTP method // should return 405 Method Not Allowed - Selection { - endpoint: &method_not_allowed, - params: Params::new(), - } + Selection::method_not_allowed() } else { - Selection { - endpoint: ¬_found_endpoint, - params: Params::new(), - } + Selection::not_found_endpoint() } } } diff --git a/src/server.rs b/src/server.rs index 3f0d21811..8cd262373 100644 --- a/src/server.rs +++ b/src/server.rs @@ -3,11 +3,13 @@ use async_std::io; use async_std::sync::Arc; -use crate::cookies; -use crate::listener::{Listener, ToListener}; use crate::log; use crate::middleware::{Middleware, Next}; -use crate::router::{Router, Selection}; +use crate::{cookies, namespace::Namespace}; +use crate::{ + listener::{Listener, ToListener}, + subdomain::Subdomain, +}; use crate::{Endpoint, Request, Route}; /// An HTTP server. @@ -26,7 +28,7 @@ use crate::{Endpoint, Request, Route}; /// response processing, such as compression, default headers, or logging. To /// add middleware to an app, use the [`Server::middleware`] method. pub struct Server { - router: Arc>, + router: Arc>, state: State, /// Holds the middleware stack. /// @@ -101,7 +103,7 @@ impl Server { /// ``` pub fn with_state(state: State) -> Self { let mut server = Self { - router: Arc::new(Router::new()), + router: Arc::new(Namespace::new()), middleware: Arc::new(vec![]), state, }; @@ -111,6 +113,52 @@ impl Server { server } + /// Add a new subdomain route given a `subdomain`, relative to the apex domain. + /// + /// Routing subdomains only works if you are listening for an apex domain. + /// Routing works by putting all subdomains into a list and looping over all + /// of them until the correct route has been found. Be sure to place routes + /// that require parameters at the bottom of your routing. After a subdomain + /// has been picked you can use whatever you like. An example of subdomain + /// routing would look like: + /// + /// ```rust,no_run + /// let mut app = tide::Server::new(); + /// app.subdomain("blog").at("/").get(|_| async { Ok("Hello blogger")}); + /// ``` + /// + /// A subdomain is comprised of zero or more non-empty string segments that + /// are separated by '.'. Like `Route` there are two kinds of segments: + /// concrete and wildcard. A concrete segment is used to exactly match the + /// respective part of the subdomain of the incoming request. A wildcard + /// segment on the other hand extracts and parses the respective part of the + /// subdomain of the incoming request to pass it along to the endpoint as an + /// argument. A wildcard segment is written as `:user`, which creates an + /// endpoint parameter called `user`. Something to remember is that this + /// parameter feature is also used inside of path routing so if you use a + /// wildcard for your subdomain and path that share the same key name, it + /// will replace the subdomain value with the paths value. + /// + /// Alternatively a wildcard definition can only be a `*`, for example + /// `blog.*`, which means that the wildcard will match any subdomain from + /// the first part. + /// + /// Here are some examples omitting the path routing selection: + /// + /// ```rust,no_run + /// # let mut app = tide::Server::new(); + /// app.subdomain(""); + /// app.subdomain("blog"); + /// app.subdomain(":user.blog"); + /// app.subdomain(":user.*"); + /// app.subdomain(":context.:.api"); + /// ``` + pub fn subdomain<'a>(&'a mut self, subdomain: &str) -> &'a mut Subdomain { + let namespace = Arc::get_mut(&mut self.router) + .expect("Registering namespaces is not possible after the server has started"); + Subdomain::new(namespace, subdomain) + } + /// Add a new route at the given `path`, relative to root. /// /// Routing means mapping an HTTP request to an endpoint. Here Tide applies @@ -158,9 +206,8 @@ impl Server { /// match or not, which means that the order of adding resources has no /// effect. pub fn at<'a>(&'a mut self, path: &str) -> Route<'a, State> { - let router = Arc::get_mut(&mut self.router) - .expect("Registering routes is not possible after the Server has started"); - Route::new(router, path.to_owned()) + let subdomain = self.subdomain(""); + subdomain.at(path) } /// Add middleware to an application. @@ -222,14 +269,19 @@ impl Server { middleware, } = self.clone(); + let path = req.url().path(); let method = req.method().to_owned(); - let Selection { endpoint, params } = router.route(&req.url().path(), method); - let route_params = vec![params]; + let domain = req.host().unwrap_or(""); + + let namespace = router.route(domain, &path, method, &middleware); + let mut route_params = vec![]; + route_params.push(namespace.subdomain_params()); + route_params.push(namespace.selection.params); let req = Request::new(state, req, route_params); let next = Next { - endpoint, - next_middleware: &middleware, + endpoint: namespace.selection.endpoint, + next_middleware: &namespace.middleware, }; let res = next.run(req).await; @@ -279,19 +331,22 @@ impl { + subdomain: String, + router: Router, + middleware: Vec>>, +} + +impl Subdomain { + pub(crate) fn new<'a>( + namespace: &'a mut Namespace, + subdomain: &str, + ) -> &'a mut Subdomain { + let router = Self { + subdomain: subdomain.to_owned(), + router: Router::new(), + middleware: Vec::new(), + }; + namespace.add(router.subdomain.clone(), router) + } + + pub(crate) fn route<'a>(&self, path: &str, method: http_types::Method) -> Selection<'_, State> { + self.router.route(path, method) + } + + pub(crate) fn middleware(&self) -> &Vec>> { + &self.middleware + } + + /// Create a route on the given subdomain + pub fn at<'b>(&'b mut self, path: &str) -> Route<'b, State> { + Route::new(&mut self.router, path.to_owned()) + } + + /// Apply the given middleware to the current route + pub fn with(&mut self, middleware: M) -> &mut Self + where + M: Middleware, + { + log::trace!( + "Adding middleware {} to subdomain {:?}", + middleware.name(), + self.subdomain + ); + self.middleware.push(Arc::new(middleware)); + self + } +} diff --git a/src/subdomain_router/holder.rs b/src/subdomain_router/holder.rs new file mode 100644 index 000000000..d0fec6cad --- /dev/null +++ b/src/subdomain_router/holder.rs @@ -0,0 +1,56 @@ +use std::collections::BTreeMap; + +use super::{Match, SubdomainParams}; + +pub struct Holder { + data: T, + map: Vec, +} + +impl Holder { + pub fn new(domain: &str, data: T) -> Holder { + let map = domain + .split('.') + .rev() + .map(|p| { + if p.starts_with(":") { + SubdomainParams::Param(p[1..].to_owned()) + } else { + SubdomainParams::String(p.to_owned()) + } + }) + .collect(); + Holder { data, map } + } + + /// Compare a subdomain that has been split into parts to the subdomain + /// that the holder implements + pub fn compare(&self, parts: &Vec<&str>) -> Option> { + if self.map.len() != parts.len() { + return None; + } + let mut m = Match { + data: &self.data, + params: BTreeMap::new(), + }; + for (url_part, subdomain_part) in parts.iter().zip(&self.map) { + match subdomain_part { + SubdomainParams::Param(param_name) => { + m.params.insert(param_name, url_part.to_string()); + } + SubdomainParams::String(exact_name) => { + if exact_name == "*" { + continue; + } else if url_part != exact_name { + return None; + } + } + } + } + return Some(m); + } + + pub fn data(&mut self) -> &mut T { + &mut self.data + } +} diff --git a/src/subdomain_router/mod.rs b/src/subdomain_router/mod.rs new file mode 100644 index 000000000..45eaf303c --- /dev/null +++ b/src/subdomain_router/mod.rs @@ -0,0 +1,68 @@ +use std::{cmp::Ordering, collections::BTreeMap}; + +mod holder; +pub mod router; + +pub enum SubdomainParams { + Param(String), + String(String), +} + +pub struct Match<'a, T> { + pub(crate) data: &'a T, + pub(crate) params: BTreeMap<&'a String, String>, +} + +#[derive(Eq)] +pub enum SubdomainType { + Static(String), + Parametrized(String), +} + +impl SubdomainType { + pub fn new(subdomain: &str) -> SubdomainType { + let parts = subdomain.split('.').rev(); + for part in parts { + if part.starts_with(":") { + return SubdomainType::Parametrized(subdomain.to_owned()); + } + } + SubdomainType::Static(subdomain.to_owned()) + } +} + +impl Ord for SubdomainType { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self { + SubdomainType::Static(me) => match other { + SubdomainType::Static(you) => me.cmp(you), + SubdomainType::Parametrized(_) => Ordering::Less, + }, + SubdomainType::Parametrized(me) => match other { + SubdomainType::Static(_) => Ordering::Greater, + SubdomainType::Parametrized(you) => me.cmp(you), + }, + } + } +} + +impl PartialOrd for SubdomainType { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for SubdomainType { + fn eq(&self, other: &Self) -> bool { + match self { + SubdomainType::Static(me) => match other { + SubdomainType::Static(you) => me == you, + SubdomainType::Parametrized(_) => false, + }, + SubdomainType::Parametrized(me) => match other { + SubdomainType::Static(_) => false, + SubdomainType::Parametrized(you) => me == you, + }, + } + } +} diff --git a/src/subdomain_router/router.rs b/src/subdomain_router/router.rs new file mode 100644 index 000000000..1ff078aa8 --- /dev/null +++ b/src/subdomain_router/router.rs @@ -0,0 +1,33 @@ +use super::{holder::Holder, Match, SubdomainType}; +use std::collections::BTreeMap; + +/// A router made for routing subdomain strings to a resource +pub struct SubdomainRouter { + subdomains: BTreeMap>, +} + +impl SubdomainRouter { + pub fn new() -> Self { + Self { + subdomains: BTreeMap::new(), + } + } + + pub fn add(&mut self, subdomain: &str, element: T) -> &mut T { + let subdomain_type = SubdomainType::new(subdomain); + self.subdomains + .entry(subdomain_type) + .or_insert_with(|| Holder::new(subdomain, element)) + .data() + } + + pub fn recognize(&self, domain: &str) -> Option> { + let domain = domain.split('.').rev().collect::>(); + for (_, value) in &self.subdomains { + if let Some(subdomain) = value.compare(&domain) { + return Some(subdomain); + } + } + None + } +} diff --git a/tests/namespaces.rs b/tests/namespaces.rs new file mode 100644 index 000000000..ea1a1275e --- /dev/null +++ b/tests/namespaces.rs @@ -0,0 +1,233 @@ +use std::{future::Future, pin::Pin}; + +use http::Url; +use tide::http::{self, Method}; + +async fn success(_: tide::Request<()>) -> Result { + let mut res = tide::Response::new(200); + res.set_body("success"); + Ok(res) +} + +async fn echo_path(req: tide::Request<()>) -> Result { + match req.param("path") { + Ok(path) => Ok(path.to_owned()), + Err(err) => Err(tide::Error::from_str(tide::StatusCode::BadRequest, err)), + } +} + +async fn multiple_echo_path(req: tide::Request<()>) -> Result { + let err = |err| tide::Error::from_str(tide::StatusCode::BadRequest, err); + let path = req.param("path").map_err(err)?; + let user = req.param("user").map_err(err)?; + Ok(format!("{} {}", path, user)) +} + +fn test_middleware<'a>( + _: tide::Request<()>, + _: tide::Next<'a, ()>, +) -> Pin + Send + 'a>> { + Box::pin(async { + let mut res = tide::Response::new(200); + res.set_body("middleware return"); + Ok(res) + }) +} + +#[async_std::test] +async fn subdomain() { + let mut app = tide::Server::new(); + app.subdomain("api").at("/").get(success); + let url: Url = "http://api.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "success"); +} + +#[async_std::test] +async fn multiple_subdomain() { + let mut app = tide::Server::new(); + app.subdomain("this.for.subdomain.length") + .at("/") + .get(success); + let url: Url = "http://this.for.subdomain.length.example.com/" + .parse() + .unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "success"); +} + +#[async_std::test] +async fn subdomain_with_path_params() { + let mut app = tide::Server::new(); + app.subdomain("api").at("/:path").get(echo_path); + let url: Url = "http://api.example.com/subdomain-work".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "subdomain-work"); +} + +#[async_std::test] +async fn multiple_registered_subdomains() { + let mut app = tide::Server::new(); + app.subdomain("blog").at("/").get(success); + app.subdomain("api").at("/:path").get(echo_path); + + let url: Url = "http://blog.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "success"); + + let url: Url = "http://api.example.com/subdomain-work".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "subdomain-work"); +} + +#[async_std::test] +async fn subdomain_with_middleware() { + let mut app = tide::Server::new(); + app.subdomain("api") + .with(test_middleware) + .at("/") + .get(success); + + let url: Url = "http://api.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "middleware return"); +} + +#[async_std::test] +async fn subdomain_params() { + let mut app = tide::Server::new(); + app.subdomain(":path").at("/").get(echo_path); + let url: Url = "http://example.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "example"); +} + +#[async_std::test] +async fn subdomain_multiple_params() { + let mut app = tide::Server::new(); + app.subdomain(":path.:user").at("/").get(multiple_echo_path); + let url: Url = "http://example.tommy.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "example tommy"); +} + +#[async_std::test] +async fn subdomain_wildcard() { + let mut app = tide::Server::new(); + app.subdomain("*").at("/").get(success); + + let url: Url = "http://example.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "success"); + + let url: Url = "http://user.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "success"); + + let url: Url = "http://example.user.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 404); +} + +#[async_std::test] +async fn subdomain_routing() { + let mut app = tide::Server::new(); + // setup + app.at("/").get(|_| async { Ok("landing page") }); + app.subdomain("blog") + .at("/") + .get(|_| async { Ok("my blog") }); + app.subdomain(":user") + .at("/") + .get(|req: tide::Request<()>| async move { + let user = req.param("user").unwrap(); + Ok(format!("user {}", user)) + }); + + // testing + let url: Url = "http://example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "landing page"); + + let url: Url = "http://blog.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "my blog"); + + let url: Url = "http://tom.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "user tom"); + + let url: Url = "http://user.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "user user"); +} + +#[async_std::test] +async fn subdomain_routing_wildcard() { + let mut app = tide::Server::new(); + // setup + app.subdomain(":user") + .at("/") + .get(|req: tide::Request<()>| async move { + let user = req.param("user").unwrap(); + Ok(format!("user {}", user)) + }); + app.at("/").get(|_| async { Ok("landing page") }); + app.subdomain("blog") + .at("/") + .get(|_| async { Ok("my blog") }); + + // testing + let url: Url = "http://example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "landing page"); + + let url: Url = "http://blog.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "my blog"); + + let url: Url = "http://tom.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "user tom"); + + let url: Url = "http://user.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "user user"); +}