diff options
| author | Fuwn <[email protected]> | 2022-03-26 07:45:22 +0000 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2022-03-26 07:45:22 +0000 |
| commit | 09baf75b01339a133818143fd2f3997d5ab00ab5 (patch) | |
| tree | 67d173095fc52e3ae79f36198345fa6711cceed0 | |
| parent | feat(utilities): queries_from_url (diff) | |
| download | windmark-09baf75b01339a133818143fd2f3997d5ab00ab5.tar.xz windmark-09baf75b01339a133818143fd2f3997d5ab00ab5.zip | |
feat(mount): dynamic parameters
| -rw-r--r-- | Cargo.toml | 5 | ||||
| -rw-r--r-- | examples/windmark.rs | 21 | ||||
| -rw-r--r-- | src/lib.rs | 169 |
3 files changed, 152 insertions, 43 deletions
@@ -23,8 +23,11 @@ openssl = "0.10.38" # tokio = { version = "1.17.0", features = ["full"] } # tokio-openssl = "0.5.0" # tokio-uds = "0.2.7" -url = "2.2.2" # Logging pretty_env_logger = { version = "0.4.0", optional = true } log = { version = "0.4.16", optional = true } + +# URL +url = "2.2.2" +regex = "1.5.5" diff --git a/examples/windmark.rs b/examples/windmark.rs index bd31a5b..aa86ebc 100644 --- a/examples/windmark.rs +++ b/examples/windmark.rs @@ -16,6 +16,8 @@ // Copyright (C) 2022-2022 Fuwn <[email protected]> // SPDX-License-Identifier: GPL-3.0-only +//! `cargo run --example windmark --features logger` + #[macro_use] extern crate log; @@ -24,7 +26,7 @@ fn main() -> std::io::Result<()> { .set_private_key_file("windmark_private.pem") .set_certificate_chain_file("windmark_pair.pem") .enable_default_logger(true) - .set_error_handler(|_, _| "error...".to_string()) + .set_error_handler(|_, _, _| "error...".to_string()) .set_pre_route_callback(|stream, url| { info!( "accepted connection from {} to {}", @@ -38,25 +40,28 @@ fn main() -> std::io::Result<()> { stream.peer_addr().unwrap().ip() ) }) - .set_header(|_, _| "```\nART IS COOL\n```".to_string()) - .set_footer(|_, _| "Copyright 2022".to_string()) - .mount("/", |_, _| { + .set_header(|_, _, _| "```\nART IS COOL\n```".to_string()) + .set_footer(|_, _, _| "Copyright 2022".to_string()) + .mount("/", |_, _, _| { "# INDEX\n\nWelcome!\n\n=> /test Test Page\n=> /time Unix Epoch\n" .to_string() }) - .mount("/ip", |stream, _| { + .mount("/ip", |stream, _, _| { { format!("Hello, {}", stream.peer_addr().unwrap().ip()) }.into() }) - .mount("/test", |_, _| "hi there\n=> / back".to_string()) - .mount("/time", |_, _| { + .mount("/test", |_, _, _| "hi there\n=> / back".to_string()) + .mount("/time", |_, _, _| { std::time::UNIX_EPOCH .elapsed() .unwrap() .as_nanos() .to_string() }) - .mount("/query", |_, url| { + .mount("/query", |_, url, _| { format!("queries: {:?}", windmark::utilities::queries_from_url(&url)) }) + .mount("/param/:lang", |_, _url, dynamic_parameter| { + format!("Parameter lang is {:?}", dynamic_parameter) + }) .run() } @@ -16,6 +16,7 @@ // Copyright (C) 2022-2022 Fuwn <[email protected]> // SPDX-License-Identifier: GPL-3.0-only +#![feature(once_cell)] #![deny( warnings, nonstandard_style, @@ -35,24 +36,53 @@ pub mod utilities; #[macro_use] extern crate log; -use std::{collections::HashMap, net::TcpStream, sync::Arc}; +use std::{collections::HashMap, lazy::SyncLazy, net::TcpStream, sync::Arc}; use openssl::ssl::{self, SslAcceptor, SslMethod}; +use regex::Regex; use url::Url; +static DYNAMIC_PARAMETER_REGEX: SyncLazy<Regex> = + SyncLazy::new(|| Regex::new(r":[a-zA-Z][0-9a-zA-Z_-]*").unwrap()); + +type RouteResponseHandler = fn(&TcpStream, &Url, Option<String>) -> String; +type CallbackHandler = fn(&TcpStream, &Url); + +#[allow(unused)] +#[derive(Clone)] +struct RouteResponse { + is_dynamic: bool, + dynamics_parameters: Vec<String>, + handler: RouteResponseHandler, +} +impl RouteResponse { + pub fn new( + is_dynamic: bool, + handler: RouteResponseHandler, + dynamics_parameters: Vec<String>, + ) -> Self { + Self { + is_dynamic, + dynamics_parameters, + handler, + } + } +} + #[derive(Clone)] pub struct Router { - routes: HashMap<String, fn(&TcpStream, &Url) -> String>, - error_handler: fn(&TcpStream, &Url) -> String, + routes: HashMap<String, RouteResponse>, + error_handler: RouteResponseHandler, private_key_file_name: String, certificate_chain_file_name: String, - header: fn(&TcpStream, &Url) -> String, - footer: fn(&TcpStream, &Url) -> String, + header: RouteResponseHandler, + footer: RouteResponseHandler, ssl_acceptor: Arc<SslAcceptor>, #[cfg(feature = "logger")] default_logger: bool, - pre_route_callback: fn(&TcpStream, &Url), - post_route_callback: fn(&TcpStream, &Url), + pre_route_callback: CallbackHandler, + post_route_callback: CallbackHandler, + drop_trailing_slash: bool, } impl Router { /// Create a new `Router` @@ -107,15 +137,32 @@ impl Router { /// /// ```rust /// windmark::Router::new() - /// .mount("/", |_, _| "This is the index page!".into()) - /// .mount("/test", |_, _| "This is a test page!".into()); + /// .mount("/", |_, _, _| "This is the index page!".into()) + /// .mount("/test", |_, _, _| "This is a test page!".into()); /// ``` pub fn mount( &mut self, route: &str, - handler: fn(&TcpStream, &Url) -> String, + handler: RouteResponseHandler, ) -> &mut Self { - self.routes.insert(route.to_string(), handler); + let mut fixed_route = route.to_string(); + let mut is_dynamic = false; + let dynamic_parameters = DYNAMIC_PARAMETER_REGEX + .find_iter(route) + .map(|m| m.as_str().to_string()) + .collect::<Vec<String>>(); + + if let Some(dynamic_parameter) = dynamic_parameters.get(0) { + fixed_route = route.replace(dynamic_parameter, ""); + is_dynamic = true; + } + + if !fixed_route.is_empty() { + self.routes.insert( + fixed_route, + RouteResponse::new(is_dynamic, handler, dynamic_parameters), + ); + } self } @@ -126,11 +173,11 @@ impl Router { /// /// ```rust /// windmark::Router::new() - /// .set_error_handler(|_, _| "You have encountered an error!".into()); + /// .set_error_handler(|_, _, _| "You have encountered an error!".into()); /// ``` pub fn set_error_handler( &mut self, - handler: fn(&TcpStream, &Url) -> String, + handler: RouteResponseHandler, ) -> &mut Self { self.error_handler = handler; @@ -142,14 +189,11 @@ impl Router { /// # Examples /// /// ```rust - /// windmark::Router::new().set_header(|_, _| { + /// windmark::Router::new().set_header(|_, _, _| { /// "This will be displayed on every route! (at the top)".into() /// }); /// ``` - pub fn set_header( - &mut self, - handler: fn(&TcpStream, &Url) -> String, - ) -> &mut Self { + pub fn set_header(&mut self, handler: RouteResponseHandler) -> &mut Self { self.header = handler; self @@ -160,14 +204,11 @@ impl Router { /// # Examples /// /// ```rust - /// windmark::Router::new().set_footer(|_, _| { + /// windmark::Router::new().set_footer(|_, _, _| { /// "This will be displayed on every route! (at the bottom)".into() /// }); /// ``` - pub fn set_footer( - &mut self, - handler: fn(&TcpStream, &Url) -> String, - ) -> &mut Self { + pub fn set_footer(&mut self, handler: RouteResponseHandler) -> &mut Self { self.footer = handler; self @@ -224,6 +265,7 @@ impl Router { fn handle(&self, stream: &mut ssl::SslStream<std::net::TcpStream>) { let mut buffer = [0u8; 1024]; let mut url = Url::parse("gemini://fuwn.me/").unwrap(); + let fixed_url_path; while let Ok(size) = stream.ssl_read(&mut buffer) { let content = String::from_utf8(buffer[0..size].to_vec()).unwrap(); @@ -242,7 +284,7 @@ impl Router { format!( "20 text/gemini; charset=utf-8\r\n{}{}{}", { - let header = (self.header)(stream.get_ref(), &url); + let header = (self.header)(stream.get_ref(), &url, None); if header.is_empty() { "".to_string() @@ -250,12 +292,52 @@ impl Router { format!("{}\n", header) } }, - self.routes.get(url.path()).unwrap_or(&self.error_handler)( - stream.get_ref(), - &url - ), { - let footer = (self.footer)(stream.get_ref(), &url); + if self.drop_trailing_slash + && url.path().ends_with('/') + && url.path() != "/" + { + fixed_url_path = url.path().trim_end_matches('/'); + } else { + fixed_url_path = url.path(); + } + + #[allow(clippy::option_if_let_else)] + if let Some(route) = self.routes.get(fixed_url_path) { + println!("non dynamic"); + + (route.handler)(stream.get_ref(), &url, None) + } else { + let matched_dynamics = self + .routes + .iter() + .filter(|(path, _)| url.path().contains(&(*path).clone())) + .map(|(path, _)| path.clone()) + .filter(|path| path.matches('/').count() == 2) + .collect::<Vec<String>>(); + + if matched_dynamics.is_empty() { + (self.error_handler)(stream.get_ref(), &url, None) + } else { + (self + .routes + .get(matched_dynamics[0].as_str()) + .unwrap() + .handler)(stream.get_ref(), &url, { + let raw_dynamic = + url.path().replace(&matched_dynamics[0], ""); + + if raw_dynamic.is_empty() { + None + } else { + Some(raw_dynamic) + } + }) + } + } + }, + { + let footer = (self.footer)(stream.get_ref(), &url, None); if footer.is_empty() { "".to_string() @@ -343,7 +425,7 @@ impl Router { /// ``` pub fn set_pre_route_callback( &mut self, - callback: fn(&TcpStream, &Url), + callback: CallbackHandler, ) -> &mut Self { self.pre_route_callback = callback; @@ -366,24 +448,42 @@ impl Router { /// ``` pub fn set_post_route_callback( &mut self, - callback: fn(&TcpStream, &Url), + callback: CallbackHandler, ) -> &mut Self { self.post_route_callback = callback; self } + + /// Drop the trailing slash on requests. + /// + /// Defaults to `true`. + /// + /// # Examples + /// + /// ```rust + /// // A request to `gemini://fuwn.me/test/` will be interpreted as + /// // `gemini://fuwn.me/test`. This is the recommended behaviour. + /// + /// windmark::Router::new().set_drop_trailing_slash(true); + /// ``` + pub fn set_drop_trailing_slash(&mut self, drop: bool) -> &mut Self { + self.drop_trailing_slash = drop; + + self + } } impl Default for Router { fn default() -> Self { Self { routes: HashMap::default(), - error_handler: |_, _| { + error_handler: |_, _, _| { "This capsule has not implemented an error handler...".to_string() }, private_key_file_name: "".to_string(), certificate_chain_file_name: "".to_string(), - header: |_, _| "".to_string(), - footer: |_, _| "".to_string(), + header: |_, _, _| "".to_string(), + footer: |_, _, _| "".to_string(), ssl_acceptor: Arc::new( SslAcceptor::mozilla_intermediate(SslMethod::tls()) .unwrap() @@ -393,6 +493,7 @@ impl Default for Router { default_logger: false, pre_route_callback: |_, _| {}, post_route_callback: |_, _| {}, + drop_trailing_slash: true, } } } |