aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2022-06-01 01:04:43 +0000
committerFuwn <[email protected]>2022-06-01 01:04:43 +0000
commitc36a6a2b07a5a13fae2d3b8d404c9c99c95bdfcf (patch)
tree02de9953f5828e635653aba7751a59c6248ced82
parentfix(lib.rs): response content no leading whitespace (diff)
downloadwindmark-c36a6a2b07a5a13fae2d3b8d404c9c99c95bdfcf.tar.xz
windmark-c36a6a2b07a5a13fae2d3b8d404c9c99c95bdfcf.zip
refactor(router): move router to seperate file
-rw-r--r--Makefile.toml4
-rw-r--r--src/lib.rs752
-rw-r--r--src/router.rs769
3 files changed, 773 insertions, 752 deletions
diff --git a/Makefile.toml b/Makefile.toml
index e0190bf..ef78a0e 100644
--- a/Makefile.toml
+++ b/Makefile.toml
@@ -54,6 +54,6 @@ toolchain = "nightly"
command = "cargo"
args = ["doc", "--open", "--no-deps"]
-[tasks.run]
+[tasks.example]
command = "cargo"
-args = ["run", "--example", "windmark", "--features", "auto-deduce-mime,logger"]
+args = ["run", "--example", "${@}", "--all-features"]
diff --git a/src/lib.rs b/src/lib.rs
index 19cb235..b2b31c0 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -111,761 +111,13 @@ pub mod handler;
pub mod module;
pub mod response;
pub mod returnable;
+pub mod router;
pub mod utilities;
#[macro_use]
extern crate log;
-use std::{
- error::Error,
- sync::{Arc, Mutex},
-};
-
pub use module::Module;
-use openssl::ssl::{self, SslAcceptor, SslMethod};
pub use response::Response;
+pub use router::Router;
pub use tokio::main;
-use tokio::{
- io::{AsyncReadExt, AsyncWriteExt},
- stream::StreamExt,
-};
-use url::Url;
-
-use crate::{
- handler::{Callback, ErrorResponse, Partial, RouteResponse},
- response::to_value_set_status,
- returnable::{CallbackContext, ErrorContext, RouteContext},
-};
-
-macro_rules! or_error {
- ($stream:ident, $operation:expr, $error_format:literal) => {
- match $operation {
- Ok(u) => u,
- Err(e) => {
- $stream
- .write_all(format!($error_format, e).as_bytes())
- .await?;
-
- $stream.shutdown().await?;
-
- return Ok(());
- }
- }
- };
-}
-
-/// A router which takes care of all tasks a Windmark server should handle:
-/// response generation, panics, logging, and more.
-#[derive(Clone)]
-pub struct Router {
- routes: matchit::Router<Arc<Mutex<RouteResponse>>>,
- error_handler: Arc<Mutex<ErrorResponse>>,
- private_key_file_name: String,
- ca_file_name: String,
- headers: Arc<Mutex<Vec<Partial>>>,
- footers: Arc<Mutex<Vec<Partial>>>,
- ssl_acceptor: Arc<SslAcceptor>,
- #[cfg(feature = "logger")]
- default_logger: bool,
- pre_route_callback: Arc<Mutex<Callback>>,
- post_route_callback: Arc<Mutex<Callback>>,
- charset: String,
- language: String,
- port: i32,
- modules: Arc<Mutex<Vec<Box<dyn Module + Send>>>>,
- fix_path: bool,
-}
-impl Router {
- /// Create a new `Router`
- ///
- /// # Examples
- ///
- /// ```rust
- /// let _router = windmark::Router::new();
- /// ```
- ///
- /// # Panics
- ///
- /// if a default `SslAcceptor` could not be built.
- #[must_use]
- pub fn new() -> Self { Self::default() }
-
- /// Set the filename of the private key file.
- ///
- /// # Examples
- ///
- /// ```rust
- /// windmark::Router::new().set_private_key_file("windmark_private.pem");
- /// ```
- pub fn set_private_key_file(
- &mut self,
- private_key_file_name: &str,
- ) -> &mut Self {
- self.private_key_file_name = private_key_file_name.to_string();
-
- self
- }
-
- /// Set the filename of the certificate chain file.
- ///
- /// # Examples
- ///
- /// ```rust
- /// windmark::Router::new().set_certificate_file("windmark_public.pem");
- /// ```
- pub fn set_certificate_file(&mut self, certificate_name: &str) -> &mut Self {
- self.ca_file_name = certificate_name.to_string();
-
- self
- }
-
- /// Map routes to URL paths
- ///
- /// # Examples
- ///
- /// ```rust
- /// use windmark::Response;
- ///
- /// windmark::Router::new()
- /// .mount(
- /// "/",
- /// Box::new(|_| Response::Success("This is the index page!".into())),
- /// )
- /// .mount(
- /// "/test",
- /// Box::new(|_| Response::Success("This is a test page!".into())),
- /// );
- /// ```
- ///
- /// # Panics
- ///
- /// if the route cannot be mounted.
- pub fn mount(&mut self, route: &str, handler: RouteResponse) -> &mut Self {
- self
- .routes
- .insert(route, Arc::new(Mutex::new(handler)))
- .unwrap();
-
- self
- }
-
- /// Create an error handler which will be displayed on any error.
- ///
- /// # Examples
- ///
- /// ```rust
- /// windmark::Router::new().set_error_handler(Box::new(|_| {
- /// windmark::Response::Success("You have encountered an error!".into())
- /// }));
- /// ```
- pub fn set_error_handler(&mut self, handler: ErrorResponse) -> &mut Self {
- self.error_handler = Arc::new(Mutex::new(handler));
-
- self
- }
-
- /// Add a header for the `Router` which should be displayed on every route.
- ///
- /// # Panics
- ///
- /// May panic if the header cannot be added.
- ///
- /// # Examples
- ///
- /// ```rust
- /// windmark::Router::new().add_header(Box::new(|context| {
- /// format!("This is displayed at the top of {}!", context.url.path())
- /// }));
- /// ```
- pub fn add_header(&mut self, handler: Partial) -> &mut Self {
- (*self.headers.lock().unwrap()).push(handler);
-
- self
- }
-
- /// Add a footer for the `Router` which should be displayed on every route.
- ///
- /// # Panics
- ///
- /// May panic if the header cannot be added.
- ///
- /// # Examples
- ///
- /// ```rust
- /// windmark::Router::new().add_footer(Box::new(|context| {
- /// format!("This is displayed at the bottom of {}!", context.url.path())
- /// }));
- /// ```
- pub fn add_footer(&mut self, handler: Partial) -> &mut Self {
- (*self.footers.lock().unwrap()).push(handler);
-
- self
- }
-
- /// Run the `Router` and wait for requests
- ///
- /// # Examples
- ///
- /// ```rust
- /// windmark::Router::new().run();
- /// ```
- ///
- /// # Panics
- ///
- /// if the client could not be accepted.
- ///
- /// # Errors
- ///
- /// if the `TcpListener` could not be bound.
- pub async fn run(&mut self) -> Result<(), Box<dyn Error>> {
- self.create_acceptor()?;
-
- #[cfg(feature = "logger")]
- if self.default_logger {
- pretty_env_logger::init();
- }
-
- let acceptor = self.ssl_acceptor.clone();
- let mut listener =
- tokio::net::TcpListener::bind(format!("0.0.0.0:{}", self.port)).await?;
-
- #[cfg(feature = "logger")]
- info!("windmark is listening for connections");
-
- while let Some(stream) = listener.incoming().next().await {
- match stream {
- Ok(stream) => {
- let acceptor = acceptor.clone();
- let mut self_clone = self.clone();
-
- tokio::spawn(async move {
- match tokio_openssl::accept(&acceptor, stream).await {
- Ok(mut stream) => {
- if let Err(e) = self_clone.handle(&mut stream).await {
- error!("handle error: {}", e);
- }
- }
- Err(e) => error!("ssl error: {:?}", e),
- }
- });
- }
- Err(e) => error!("tcp error: {:?}", e),
- }
- }
-
- Ok(())
- }
-
- #[allow(clippy::too_many_lines)]
- async fn handle(
- &mut self,
- stream: &mut tokio_openssl::SslStream<tokio::net::TcpStream>,
- ) -> Result<(), Box<dyn std::error::Error>> {
- let mut buffer = [0u8; 1024];
- let mut url = Url::parse("gemini://fuwn.me/")?;
- let mut response_status = 0;
- #[cfg(not(feature = "auto-deduce-mime"))]
- let mut response_mime_type = "".to_string();
- let mut footer = String::new();
- let mut header = String::new();
-
- while let Ok(size) = stream.read(&mut buffer).await {
- let request = or_error!(
- stream,
- String::from_utf8(buffer[0..size].to_vec()),
- "59 The server (Windmark) received a bad request: {}"
- );
- url = or_error!(
- stream,
- url::Url::parse(&request.replace("\r\n", "")),
- "59 The server (Windmark) received a bad request: {}"
- );
-
- if request.contains("\r\n") {
- break;
- }
- }
-
- let fixed_path = if self.fix_path {
- self
- .routes
- .fix_path(if url.path().is_empty() {
- "/"
- } else {
- url.path()
- })
- .unwrap_or_else(|| url.path().to_string())
- } else {
- url.path().to_string()
- };
- let route = &mut self.routes.at(&fixed_path);
-
- for module in &mut *self.modules.lock().unwrap() {
- module.on_pre_route(CallbackContext::new(stream.get_ref(), &url, {
- if let Ok(route) = &route {
- Some(&route.params)
- } else {
- None
- }
- }));
- }
-
- (*self.pre_route_callback).lock().unwrap().call_mut((
- stream.get_ref(),
- &url,
- {
- if let Ok(route) = &route {
- Some(&route.params)
- } else {
- None
- }
- },
- ));
-
- let content = if let Ok(ref route) = route {
- let footers_length = (*self.footers.lock().unwrap()).len();
-
- for partial_header in &mut *self.headers.lock().unwrap() {
- header.push_str(&format!(
- "{}\n",
- partial_header(RouteContext::new(
- stream.get_ref(),
- &url,
- &route.params,
- )),
- ));
- }
- for (i, partial_footer) in
- (&mut *self.footers.lock().unwrap()).iter_mut().enumerate()
- {
- footer.push_str(&format!(
- "{}{}",
- partial_footer(RouteContext::new(
- stream.get_ref(),
- &url,
- &route.params,
- )),
- if footers_length > 1 && i != footers_length - 1 {
- "\n"
- } else {
- ""
- },
- ));
- }
- to_value_set_status(
- (*route.value).lock().unwrap().call_mut((RouteContext::new(
- stream.get_ref(),
- &url,
- &route.params,
- ),)),
- &mut response_status,
- #[cfg(not(feature = "auto-deduce-mime"))]
- &mut response_mime_type,
- )
- } else {
- to_value_set_status(
- (*self.error_handler)
- .lock()
- .unwrap()
- .call_mut((ErrorContext::new(stream.get_ref(), &url),)),
- &mut response_status,
- #[cfg(not(feature = "auto-deduce-mime"))]
- &mut response_mime_type,
- )
- };
-
- stream
- .write_all(
- format!(
- "{}{}\r\n{}",
- if response_status == 21 {
- 20
- } else {
- response_status
- },
- match response_status {
- 20 =>
- format!(
- " text/gemini; charset={}; lang={}",
- self.charset, self.language
- ),
- #[cfg(feature = "auto-deduce-mime")]
- 21 => format!(" {}", tree_magic::from_u8(&*content.as_bytes())),
- #[cfg(not(feature = "auto-deduce-mime"))]
- 21 => response_mime_type,
- _ => format!(" {}", content),
- },
- match response_status {
- 20 => format!("{}{}\n{}", header, content, footer),
- 21 => content.to_string(),
- _ => "".to_string(),
- }
- )
- .as_bytes(),
- )
- .await?;
-
- for module in &mut *self.modules.lock().unwrap() {
- module.on_post_route(CallbackContext::new(stream.get_ref(), &url, {
- if let Ok(route) = &route {
- Some(&route.params)
- } else {
- None
- }
- }));
- }
-
- (*self.post_route_callback).lock().unwrap().call_mut((
- stream.get_ref(),
- &url,
- {
- if let Ok(route) = &route {
- Some(&route.params)
- } else {
- None
- }
- },
- ));
-
- stream.shutdown().await?;
-
- Ok(())
- }
-
- fn create_acceptor(&mut self) -> Result<(), Box<dyn Error>> {
- let mut builder = SslAcceptor::mozilla_intermediate(ssl::SslMethod::tls())?;
-
- builder.set_private_key_file(
- &self.private_key_file_name,
- ssl::SslFiletype::PEM,
- )?;
- builder.set_certificate_file(
- &self.ca_file_name,
- openssl::ssl::SslFiletype::PEM,
- )?;
- builder.check_private_key()?;
-
- self.ssl_acceptor = Arc::new(builder.build());
-
- Ok(())
- }
-
- /// Use a self-made `SslAcceptor`
- ///
- /// # Examples
- ///
- /// ```rust
- /// use openssl::ssl;
- ///
- /// windmark::Router::new().set_ssl_acceptor({
- /// let mut builder =
- /// ssl::SslAcceptor::mozilla_intermediate(ssl::SslMethod::tls()).unwrap();
- ///
- /// builder
- /// .set_private_key_file("windmark_private.pem", ssl::SslFiletype::PEM)
- /// .unwrap();
- /// builder
- /// .set_certificate_file(
- /// "windmark_public.pem",
- /// openssl::ssl::SslFiletype::PEM,
- /// )
- /// .unwrap();
- /// builder.check_private_key().unwrap();
- ///
- /// builder.build()
- /// });
- /// ```
- pub fn set_ssl_acceptor(&mut self, ssl_acceptor: SslAcceptor) -> &mut Self {
- self.ssl_acceptor = Arc::new(ssl_acceptor);
-
- self
- }
-
- /// Enabled the default logger (the
- /// [`pretty_env_logger`](https://crates.io/crates/pretty_env_logger) and
- /// [`log`](https://crates.io/crates/log) crates).
- #[cfg(feature = "logger")]
- pub fn enable_default_logger(&mut self, enable: bool) -> &mut Self {
- self.default_logger = enable;
- std::env::set_var("RUST_LOG", "windmark=trace");
-
- self
- }
-
- /// Set the default logger's log level.
- ///
- /// If you enable Windmark's default logger with `enable_default_logger`,
- /// Windmark will only log, logs from itself. By setting a log level with
- /// this method, you are overriding the default log level, so you must choose
- /// to enable logs from Windmark with the `log_windmark` parameter.
- ///
- /// Log level "language" is detailed
- /// [here](https://docs.rs/env_logger/0.9.0/env_logger/#enabling-logging).
- ///
- /// # Examples
- ///
- /// ```rust
- /// windmark::Router::new()
- /// .enable_default_logger(true)
- /// .set_log_level("your_crate_name=trace", true);
- /// // If you would only like to log, logs from your crate:
- /// // .set_log_level("your_crate_name=trace", false);
- /// ```
- #[cfg(feature = "logger")]
- pub fn set_log_level(
- &mut self,
- log_level: &str,
- log_windmark: bool,
- ) -> &mut Self {
- std::env::set_var(
- "RUST_LOG",
- format!(
- "{}{}",
- if log_windmark { "windmark," } else { "" },
- log_level
- ),
- );
-
- self
- }
-
- /// Set a callback to run before a client response is delivered
- ///
- /// # Examples
- ///
- /// ```rust
- /// use log::info;
- ///
- /// windmark::Router::new().set_pre_route_callback(Box::new(
- /// |stream, _url, _| {
- /// info!(
- /// "accepted connection from {}",
- /// stream.peer_addr().unwrap().ip(),
- /// )
- /// },
- /// ));
- /// ```
- pub fn set_pre_route_callback(&mut self, callback: Callback) -> &mut Self {
- self.pre_route_callback = Arc::new(Mutex::new(callback));
-
- self
- }
-
- /// Set a callback to run after a client response is delivered
- ///
- /// # Examples
- ///
- /// ```rust
- /// use log::info;
- ///
- /// windmark::Router::new().set_post_route_callback(Box::new(
- /// |stream, _url, _| {
- /// info!(
- /// "closed connection from {}",
- /// stream.peer_addr().unwrap().ip(),
- /// )
- /// },
- /// ));
- /// ```
- pub fn set_post_route_callback(&mut self, callback: Callback) -> &mut Self {
- self.post_route_callback = Arc::new(Mutex::new(callback));
-
- self
- }
-
- /// Attach a stateless module to a `Router`.
- ///
- /// A module is an extension or middleware to a `Router`. Modules get full
- /// access to the `Router`, but can be extended by a third party.
- ///
- /// # Examples
- ///
- /// ## Integrated Module
- ///
- /// ```rust
- /// use windmark::Response;
- ///
- /// windmark::Router::new().attach_stateless(|r| {
- /// r.mount(
- /// "/module",
- /// Box::new(|_| Response::Success("This is a module!".into())),
- /// );
- /// r.set_error_handler(Box::new(|_| {
- /// Response::NotFound(
- /// "This error handler has been implemented by a module!".into(),
- /// )
- /// }));
- /// });
- /// ```
- ///
- /// ## External Module
- ///
- /// ```rust
- /// use windmark::Response;
- ///
- /// mod windmark_example {
- /// pub fn module(router: &mut windmark::Router) {
- /// router.mount(
- /// "/module",
- /// Box::new(|_| windmark::Response::Success("This is a module!".into())),
- /// );
- /// }
- /// }
- ///
- /// windmark::Router::new().attach_stateless(windmark_example::module);
- /// ```
- pub fn attach_stateless<F>(&mut self, mut module: F) -> &mut Self
- where F: FnMut(&mut Self) {
- module(self);
-
- self
- }
-
- /// Attach a stateful module to a `Router`.
- ///
- /// Like a stateless module is an extension or middleware to a `Router`.
- /// Modules get full access to the `Router` and can be extended by a third
- /// party, but also, can create hooks will be executed through various parts
- /// of a routes' lifecycle. Stateful modules also have state, so variables can
- /// be stored for further access.
- ///
- /// # Panics
- ///
- /// May panic if the stateful module cannot be attached.
- ///
- /// # Examples
- ///
- /// ```rust
- /// use log::info;
- /// use windmark::{returnable::CallbackContext, Response, Router};
- ///
- /// #[derive(Default)]
- /// struct Clicker {
- /// clicks: isize,
- /// }
- /// impl windmark::Module for Clicker {
- /// fn on_attach(&mut self, _: &mut Router) {
- /// info!("clicker has been attached!");
- /// }
- ///
- /// fn on_pre_route(&mut self, context: CallbackContext<'_>) {
- /// self.clicks += 1;
- ///
- /// info!(
- /// "clicker has been called pre-route on {} with {} clicks!",
- /// context.url.path(),
- /// self.clicks
- /// );
- /// }
- ///
- /// fn on_post_route(&mut self, context: CallbackContext<'_>) {
- /// info!(
- /// "clicker has been called post-route on {} with {} clicks!",
- /// context.url.path(),
- /// self.clicks
- /// );
- /// }
- /// }
- ///
- /// Router::new().attach(Clicker::default());
- /// ```
- pub fn attach(
- &mut self,
- mut module: impl Module + 'static + Send,
- ) -> &mut Self {
- module.on_attach(self);
-
- (*self.modules.lock().unwrap()).push(Box::new(module));
-
- self
- }
-
- /// Specify a custom character set.
- ///
- /// Defaults to `"utf-8"`.
- ///
- /// # Examples
- ///
- /// ```rust
- /// windmark::Router::new().set_charset("utf-8");
- /// ```
- pub fn set_charset(&mut self, charset: &str) -> &mut Self {
- self.charset = charset.to_string();
-
- self
- }
-
- /// Specify a custom language.
- ///
- /// Defaults to `"en"`.
- ///
- /// # Examples
- ///
- /// ```rust
- /// windmark::Router::new().set_language("en");
- /// ```
- pub fn set_language(&mut self, language: &str) -> &mut Self {
- self.language = language.to_string();
-
- self
- }
-
- /// Specify a custom port.
- ///
- /// Defaults to `1965`.
- ///
- /// # Examples
- ///
- /// ```rust
- /// windmark::Router::new().set_port(1965);
- /// ```
- pub fn set_port(&mut self, port: i32) -> &mut Self {
- self.port = port;
-
- self
- }
-
- /// Performs a case-insensitive lookup of routes, using the case corrected
- /// path if successful. Missing/ extra trailing slashes are also corrected.
- ///
- /// # Examples
- ///
- /// ```rust
- /// windmark::Router::new().set_fix_path(true);
- /// ```
- pub fn set_fix_path(&mut self, fix_path: bool) -> &mut Self {
- self.fix_path = fix_path;
-
- self
- }
-}
-impl Default for Router {
- fn default() -> Self {
- Self {
- routes: matchit::Router::new(),
- error_handler: Arc::new(Mutex::new(Box::new(|_| {
- Response::NotFound(
- "This capsule has not implemented an error handler...".to_string(),
- )
- }))),
- private_key_file_name: "".to_string(),
- ca_file_name: "".to_string(),
- headers: Arc::new(Mutex::new(vec![])),
- footers: Arc::new(Mutex::new(vec![])),
- ssl_acceptor: Arc::new(
- SslAcceptor::mozilla_intermediate(SslMethod::tls())
- .unwrap()
- .build(),
- ),
- #[cfg(feature = "logger")]
- default_logger: false,
- pre_route_callback: Arc::new(Mutex::new(Box::new(|_, _, _| {}))),
- post_route_callback: Arc::new(Mutex::new(Box::new(|_, _, _| {}))),
- charset: "utf-8".to_string(),
- language: "en".to_string(),
- port: 1965,
- modules: Arc::new(Mutex::new(vec![])),
- fix_path: false,
- }
- }
-}
diff --git a/src/router.rs b/src/router.rs
new file mode 100644
index 0000000..da1a724
--- /dev/null
+++ b/src/router.rs
@@ -0,0 +1,769 @@
+// This file is part of Windmark <https://github.com/gemrest/windmark>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+use std::{
+ error::Error,
+ sync::{Arc, Mutex},
+};
+
+use openssl::ssl::{self, SslAcceptor, SslMethod};
+use tokio::{
+ io::{AsyncReadExt, AsyncWriteExt},
+ stream::StreamExt,
+};
+use url::Url;
+
+use crate::{
+ handler::{Callback, ErrorResponse, Partial, RouteResponse},
+ module::Module,
+ response::{to_value_set_status, Response},
+ returnable::{CallbackContext, ErrorContext, RouteContext},
+};
+
+macro_rules! or_error {
+ ($stream:ident, $operation:expr, $error_format:literal) => {
+ match $operation {
+ Ok(u) => u,
+ Err(e) => {
+ $stream
+ .write_all(format!($error_format, e).as_bytes())
+ .await?;
+
+ $stream.shutdown().await?;
+
+ return Ok(());
+ }
+ }
+ };
+}
+
+/// A router which takes care of all tasks a Windmark server should handle:
+/// response generation, panics, logging, and more.
+#[derive(Clone)]
+pub struct Router {
+ routes: matchit::Router<Arc<Mutex<RouteResponse>>>,
+ error_handler: Arc<Mutex<ErrorResponse>>,
+ private_key_file_name: String,
+ ca_file_name: String,
+ headers: Arc<Mutex<Vec<Partial>>>,
+ footers: Arc<Mutex<Vec<Partial>>>,
+ ssl_acceptor: Arc<SslAcceptor>,
+ #[cfg(feature = "logger")]
+ default_logger: bool,
+ pre_route_callback: Arc<Mutex<Callback>>,
+ post_route_callback: Arc<Mutex<Callback>>,
+ charset: String,
+ language: String,
+ port: i32,
+ modules: Arc<Mutex<Vec<Box<dyn Module + Send>>>>,
+ fix_path: bool,
+}
+impl Router {
+ /// Create a new `Router`
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// let _router = windmark::Router::new();
+ /// ```
+ ///
+ /// # Panics
+ ///
+ /// if a default `SslAcceptor` could not be built.
+ #[must_use]
+ pub fn new() -> Self { Self::default() }
+
+ /// Set the filename of the private key file.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// windmark::Router::new().set_private_key_file("windmark_private.pem");
+ /// ```
+ pub fn set_private_key_file(
+ &mut self,
+ private_key_file_name: &str,
+ ) -> &mut Self {
+ self.private_key_file_name = private_key_file_name.to_string();
+
+ self
+ }
+
+ /// Set the filename of the certificate chain file.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// windmark::Router::new().set_certificate_file("windmark_public.pem");
+ /// ```
+ pub fn set_certificate_file(&mut self, certificate_name: &str) -> &mut Self {
+ self.ca_file_name = certificate_name.to_string();
+
+ self
+ }
+
+ /// Map routes to URL paths
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use windmark::Response;
+ ///
+ /// windmark::Router::new()
+ /// .mount(
+ /// "/",
+ /// Box::new(|_| Response::Success("This is the index page!".into())),
+ /// )
+ /// .mount(
+ /// "/test",
+ /// Box::new(|_| Response::Success("This is a test page!".into())),
+ /// );
+ /// ```
+ ///
+ /// # Panics
+ ///
+ /// if the route cannot be mounted.
+ pub fn mount(&mut self, route: &str, handler: RouteResponse) -> &mut Self {
+ self
+ .routes
+ .insert(route, Arc::new(Mutex::new(handler)))
+ .unwrap();
+
+ self
+ }
+
+ /// Create an error handler which will be displayed on any error.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// windmark::Router::new().set_error_handler(Box::new(|_| {
+ /// windmark::Response::Success("You have encountered an error!".into())
+ /// }));
+ /// ```
+ pub fn set_error_handler(&mut self, handler: ErrorResponse) -> &mut Self {
+ self.error_handler = Arc::new(Mutex::new(handler));
+
+ self
+ }
+
+ /// Add a header for the `Router` which should be displayed on every route.
+ ///
+ /// # Panics
+ ///
+ /// May panic if the header cannot be added.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// windmark::Router::new().add_header(Box::new(|context| {
+ /// format!("This is displayed at the top of {}!", context.url.path())
+ /// }));
+ /// ```
+ pub fn add_header(&mut self, handler: Partial) -> &mut Self {
+ (*self.headers.lock().unwrap()).push(handler);
+
+ self
+ }
+
+ /// Add a footer for the `Router` which should be displayed on every route.
+ ///
+ /// # Panics
+ ///
+ /// May panic if the header cannot be added.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// windmark::Router::new().add_footer(Box::new(|context| {
+ /// format!("This is displayed at the bottom of {}!", context.url.path())
+ /// }));
+ /// ```
+ pub fn add_footer(&mut self, handler: Partial) -> &mut Self {
+ (*self.footers.lock().unwrap()).push(handler);
+
+ self
+ }
+
+ /// Run the `Router` and wait for requests
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// windmark::Router::new().run();
+ /// ```
+ ///
+ /// # Panics
+ ///
+ /// if the client could not be accepted.
+ ///
+ /// # Errors
+ ///
+ /// if the `TcpListener` could not be bound.
+ pub async fn run(&mut self) -> Result<(), Box<dyn Error>> {
+ self.create_acceptor()?;
+
+ #[cfg(feature = "logger")]
+ if self.default_logger {
+ pretty_env_logger::init();
+ }
+
+ let acceptor = self.ssl_acceptor.clone();
+ let mut listener =
+ tokio::net::TcpListener::bind(format!("0.0.0.0:{}", self.port)).await?;
+
+ #[cfg(feature = "logger")]
+ info!("windmark is listening for connections");
+
+ while let Some(stream) = listener.incoming().next().await {
+ match stream {
+ Ok(stream) => {
+ let acceptor = acceptor.clone();
+ let mut self_clone = self.clone();
+
+ tokio::spawn(async move {
+ match tokio_openssl::accept(&acceptor, stream).await {
+ Ok(mut stream) => {
+ if let Err(e) = self_clone.handle(&mut stream).await {
+ error!("handle error: {}", e);
+ }
+ }
+ Err(e) => error!("ssl error: {:?}", e),
+ }
+ });
+ }
+ Err(e) => error!("tcp error: {:?}", e),
+ }
+ }
+
+ Ok(())
+ }
+
+ #[allow(clippy::too_many_lines)]
+ async fn handle(
+ &mut self,
+ stream: &mut tokio_openssl::SslStream<tokio::net::TcpStream>,
+ ) -> Result<(), Box<dyn std::error::Error>> {
+ let mut buffer = [0u8; 1024];
+ let mut url = Url::parse("gemini://fuwn.me/")?;
+ let mut response_status = 0;
+ #[cfg(not(feature = "auto-deduce-mime"))]
+ let mut response_mime_type = "".to_string();
+ let mut footer = String::new();
+ let mut header = String::new();
+
+ while let Ok(size) = stream.read(&mut buffer).await {
+ let request = or_error!(
+ stream,
+ String::from_utf8(buffer[0..size].to_vec()),
+ "59 The server (Windmark) received a bad request: {}"
+ );
+ url = or_error!(
+ stream,
+ url::Url::parse(&request.replace("\r\n", "")),
+ "59 The server (Windmark) received a bad request: {}"
+ );
+
+ if request.contains("\r\n") {
+ break;
+ }
+ }
+
+ let fixed_path = if self.fix_path {
+ self
+ .routes
+ .fix_path(if url.path().is_empty() {
+ "/"
+ } else {
+ url.path()
+ })
+ .unwrap_or_else(|| url.path().to_string())
+ } else {
+ url.path().to_string()
+ };
+ let route = &mut self.routes.at(&fixed_path);
+
+ for module in &mut *self.modules.lock().unwrap() {
+ module.on_pre_route(CallbackContext::new(stream.get_ref(), &url, {
+ if let Ok(route) = &route {
+ Some(&route.params)
+ } else {
+ None
+ }
+ }));
+ }
+
+ (*self.pre_route_callback).lock().unwrap().call_mut((
+ stream.get_ref(),
+ &url,
+ {
+ if let Ok(route) = &route {
+ Some(&route.params)
+ } else {
+ None
+ }
+ },
+ ));
+
+ let content = if let Ok(ref route) = route {
+ let footers_length = (*self.footers.lock().unwrap()).len();
+
+ for partial_header in &mut *self.headers.lock().unwrap() {
+ header.push_str(&format!(
+ "{}\n",
+ partial_header(RouteContext::new(
+ stream.get_ref(),
+ &url,
+ &route.params,
+ )),
+ ));
+ }
+ for (i, partial_footer) in
+ (&mut *self.footers.lock().unwrap()).iter_mut().enumerate()
+ {
+ footer.push_str(&format!(
+ "{}{}",
+ partial_footer(RouteContext::new(
+ stream.get_ref(),
+ &url,
+ &route.params,
+ )),
+ if footers_length > 1 && i != footers_length - 1 {
+ "\n"
+ } else {
+ ""
+ },
+ ));
+ }
+ to_value_set_status(
+ (*route.value).lock().unwrap().call_mut((RouteContext::new(
+ stream.get_ref(),
+ &url,
+ &route.params,
+ ),)),
+ &mut response_status,
+ #[cfg(not(feature = "auto-deduce-mime"))]
+ &mut response_mime_type,
+ )
+ } else {
+ to_value_set_status(
+ (*self.error_handler)
+ .lock()
+ .unwrap()
+ .call_mut((ErrorContext::new(stream.get_ref(), &url),)),
+ &mut response_status,
+ #[cfg(not(feature = "auto-deduce-mime"))]
+ &mut response_mime_type,
+ )
+ };
+
+ stream
+ .write_all(
+ format!(
+ "{}{}\r\n{}",
+ if response_status == 21 {
+ 20
+ } else {
+ response_status
+ },
+ match response_status {
+ 20 =>
+ format!(
+ " text/gemini; charset={}; lang={}",
+ self.charset, self.language
+ ),
+ #[cfg(feature = "auto-deduce-mime")]
+ 21 => format!(" {}", tree_magic::from_u8(&*content.as_bytes())),
+ #[cfg(not(feature = "auto-deduce-mime"))]
+ 21 => response_mime_type,
+ _ => format!(" {}", content),
+ },
+ match response_status {
+ 20 => format!("{}{}\n{}", header, content, footer),
+ 21 => content.to_string(),
+ _ => "".to_string(),
+ }
+ )
+ .as_bytes(),
+ )
+ .await?;
+
+ for module in &mut *self.modules.lock().unwrap() {
+ module.on_post_route(CallbackContext::new(stream.get_ref(), &url, {
+ if let Ok(route) = &route {
+ Some(&route.params)
+ } else {
+ None
+ }
+ }));
+ }
+
+ (*self.post_route_callback).lock().unwrap().call_mut((
+ stream.get_ref(),
+ &url,
+ {
+ if let Ok(route) = &route {
+ Some(&route.params)
+ } else {
+ None
+ }
+ },
+ ));
+
+ stream.shutdown().await?;
+
+ Ok(())
+ }
+
+ fn create_acceptor(&mut self) -> Result<(), Box<dyn Error>> {
+ let mut builder = SslAcceptor::mozilla_intermediate(ssl::SslMethod::tls())?;
+
+ builder.set_private_key_file(
+ &self.private_key_file_name,
+ ssl::SslFiletype::PEM,
+ )?;
+ builder.set_certificate_file(
+ &self.ca_file_name,
+ openssl::ssl::SslFiletype::PEM,
+ )?;
+ builder.check_private_key()?;
+
+ self.ssl_acceptor = Arc::new(builder.build());
+
+ Ok(())
+ }
+
+ /// Use a self-made `SslAcceptor`
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use openssl::ssl;
+ ///
+ /// windmark::Router::new().set_ssl_acceptor({
+ /// let mut builder =
+ /// ssl::SslAcceptor::mozilla_intermediate(ssl::SslMethod::tls()).unwrap();
+ ///
+ /// builder
+ /// .set_private_key_file("windmark_private.pem", ssl::SslFiletype::PEM)
+ /// .unwrap();
+ /// builder
+ /// .set_certificate_file(
+ /// "windmark_public.pem",
+ /// openssl::ssl::SslFiletype::PEM,
+ /// )
+ /// .unwrap();
+ /// builder.check_private_key().unwrap();
+ ///
+ /// builder.build()
+ /// });
+ /// ```
+ pub fn set_ssl_acceptor(&mut self, ssl_acceptor: SslAcceptor) -> &mut Self {
+ self.ssl_acceptor = Arc::new(ssl_acceptor);
+
+ self
+ }
+
+ /// Enabled the default logger (the
+ /// [`pretty_env_logger`](https://crates.io/crates/pretty_env_logger) and
+ /// [`log`](https://crates.io/crates/log) crates).
+ #[cfg(feature = "logger")]
+ pub fn enable_default_logger(&mut self, enable: bool) -> &mut Self {
+ self.default_logger = enable;
+ std::env::set_var("RUST_LOG", "windmark=trace");
+
+ self
+ }
+
+ /// Set the default logger's log level.
+ ///
+ /// If you enable Windmark's default logger with `enable_default_logger`,
+ /// Windmark will only log, logs from itself. By setting a log level with
+ /// this method, you are overriding the default log level, so you must choose
+ /// to enable logs from Windmark with the `log_windmark` parameter.
+ ///
+ /// Log level "language" is detailed
+ /// [here](https://docs.rs/env_logger/0.9.0/env_logger/#enabling-logging).
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// windmark::Router::new()
+ /// .enable_default_logger(true)
+ /// .set_log_level("your_crate_name=trace", true);
+ /// // If you would only like to log, logs from your crate:
+ /// // .set_log_level("your_crate_name=trace", false);
+ /// ```
+ #[cfg(feature = "logger")]
+ pub fn set_log_level(
+ &mut self,
+ log_level: &str,
+ log_windmark: bool,
+ ) -> &mut Self {
+ std::env::set_var(
+ "RUST_LOG",
+ format!(
+ "{}{}",
+ if log_windmark { "windmark," } else { "" },
+ log_level
+ ),
+ );
+
+ self
+ }
+
+ /// Set a callback to run before a client response is delivered
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use log::info;
+ ///
+ /// windmark::Router::new().set_pre_route_callback(Box::new(
+ /// |stream, _url, _| {
+ /// info!(
+ /// "accepted connection from {}",
+ /// stream.peer_addr().unwrap().ip(),
+ /// )
+ /// },
+ /// ));
+ /// ```
+ pub fn set_pre_route_callback(&mut self, callback: Callback) -> &mut Self {
+ self.pre_route_callback = Arc::new(Mutex::new(callback));
+
+ self
+ }
+
+ /// Set a callback to run after a client response is delivered
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use log::info;
+ ///
+ /// windmark::Router::new().set_post_route_callback(Box::new(
+ /// |stream, _url, _| {
+ /// info!(
+ /// "closed connection from {}",
+ /// stream.peer_addr().unwrap().ip(),
+ /// )
+ /// },
+ /// ));
+ /// ```
+ pub fn set_post_route_callback(&mut self, callback: Callback) -> &mut Self {
+ self.post_route_callback = Arc::new(Mutex::new(callback));
+
+ self
+ }
+
+ /// Attach a stateless module to a `Router`.
+ ///
+ /// A module is an extension or middleware to a `Router`. Modules get full
+ /// access to the `Router`, but can be extended by a third party.
+ ///
+ /// # Examples
+ ///
+ /// ## Integrated Module
+ ///
+ /// ```rust
+ /// use windmark::Response;
+ ///
+ /// windmark::Router::new().attach_stateless(|r| {
+ /// r.mount(
+ /// "/module",
+ /// Box::new(|_| Response::Success("This is a module!".into())),
+ /// );
+ /// r.set_error_handler(Box::new(|_| {
+ /// Response::NotFound(
+ /// "This error handler has been implemented by a module!".into(),
+ /// )
+ /// }));
+ /// });
+ /// ```
+ ///
+ /// ## External Module
+ ///
+ /// ```rust
+ /// use windmark::Response;
+ ///
+ /// mod windmark_example {
+ /// pub fn module(router: &mut windmark::Router) {
+ /// router.mount(
+ /// "/module",
+ /// Box::new(|_| windmark::Response::Success("This is a module!".into())),
+ /// );
+ /// }
+ /// }
+ ///
+ /// windmark::Router::new().attach_stateless(windmark_example::module);
+ /// ```
+ pub fn attach_stateless<F>(&mut self, mut module: F) -> &mut Self
+ where F: FnMut(&mut Self) {
+ module(self);
+
+ self
+ }
+
+ /// Attach a stateful module to a `Router`.
+ ///
+ /// Like a stateless module is an extension or middleware to a `Router`.
+ /// Modules get full access to the `Router` and can be extended by a third
+ /// party, but also, can create hooks will be executed through various parts
+ /// of a routes' lifecycle. Stateful modules also have state, so variables can
+ /// be stored for further access.
+ ///
+ /// # Panics
+ ///
+ /// May panic if the stateful module cannot be attached.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use log::info;
+ /// use windmark::{returnable::CallbackContext, Response, Router};
+ ///
+ /// #[derive(Default)]
+ /// struct Clicker {
+ /// clicks: isize,
+ /// }
+ /// impl windmark::Module for Clicker {
+ /// fn on_attach(&mut self, _: &mut Router) {
+ /// info!("clicker has been attached!");
+ /// }
+ ///
+ /// fn on_pre_route(&mut self, context: CallbackContext<'_>) {
+ /// self.clicks += 1;
+ ///
+ /// info!(
+ /// "clicker has been called pre-route on {} with {} clicks!",
+ /// context.url.path(),
+ /// self.clicks
+ /// );
+ /// }
+ ///
+ /// fn on_post_route(&mut self, context: CallbackContext<'_>) {
+ /// info!(
+ /// "clicker has been called post-route on {} with {} clicks!",
+ /// context.url.path(),
+ /// self.clicks
+ /// );
+ /// }
+ /// }
+ ///
+ /// Router::new().attach(Clicker::default());
+ /// ```
+ pub fn attach(
+ &mut self,
+ mut module: impl Module + 'static + Send,
+ ) -> &mut Self {
+ module.on_attach(self);
+
+ (*self.modules.lock().unwrap()).push(Box::new(module));
+
+ self
+ }
+
+ /// Specify a custom character set.
+ ///
+ /// Defaults to `"utf-8"`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// windmark::Router::new().set_charset("utf-8");
+ /// ```
+ pub fn set_charset(&mut self, charset: &str) -> &mut Self {
+ self.charset = charset.to_string();
+
+ self
+ }
+
+ /// Specify a custom language.
+ ///
+ /// Defaults to `"en"`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// windmark::Router::new().set_language("en");
+ /// ```
+ pub fn set_language(&mut self, language: &str) -> &mut Self {
+ self.language = language.to_string();
+
+ self
+ }
+
+ /// Specify a custom port.
+ ///
+ /// Defaults to `1965`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// windmark::Router::new().set_port(1965);
+ /// ```
+ pub fn set_port(&mut self, port: i32) -> &mut Self {
+ self.port = port;
+
+ self
+ }
+
+ /// Performs a case-insensitive lookup of routes, using the case corrected
+ /// path if successful. Missing/ extra trailing slashes are also corrected.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// windmark::Router::new().set_fix_path(true);
+ /// ```
+ pub fn set_fix_path(&mut self, fix_path: bool) -> &mut Self {
+ self.fix_path = fix_path;
+
+ self
+ }
+}
+impl Default for Router {
+ fn default() -> Self {
+ Self {
+ routes: matchit::Router::new(),
+ error_handler: Arc::new(Mutex::new(Box::new(|_| {
+ Response::NotFound(
+ "This capsule has not implemented an error handler...".to_string(),
+ )
+ }))),
+ private_key_file_name: "".to_string(),
+ ca_file_name: "".to_string(),
+ headers: Arc::new(Mutex::new(vec![])),
+ footers: Arc::new(Mutex::new(vec![])),
+ ssl_acceptor: Arc::new(
+ SslAcceptor::mozilla_intermediate(SslMethod::tls())
+ .unwrap()
+ .build(),
+ ),
+ #[cfg(feature = "logger")]
+ default_logger: false,
+ pre_route_callback: Arc::new(Mutex::new(Box::new(|_, _, _| {}))),
+ post_route_callback: Arc::new(Mutex::new(Box::new(|_, _, _| {}))),
+ charset: "utf-8".to_string(),
+ language: "en".to_string(),
+ port: 1965,
+ modules: Arc::new(Mutex::new(vec![])),
+ fix_path: false,
+ }
+ }
+}