From 0e2f8d5f624142d8f44c20cd5802e8b3c1a0dce0 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Mon, 16 Nov 2020 01:13:16 -0500 Subject: [PATCH 001/113] Added user management API [WIP] --- Cargo.toml | 13 +++ examples/user_management.rs | 65 +++++++++++ src/lib.rs | 98 +++++++++++----- src/types/request.rs | 38 +++++- src/user_management/manager.rs | 121 ++++++++++++++++++++ src/user_management/mod.rs | 71 ++++++++++++ src/user_management/user.rs | 203 +++++++++++++++++++++++++++++++++ 7 files changed, 575 insertions(+), 34 deletions(-) create mode 100644 examples/user_management.rs create mode 100644 src/user_management/manager.rs create mode 100644 src/user_management/mod.rs create mode 100644 src/user_management/user.rs diff --git a/Cargo.toml b/Cargo.toml index abd66e1..de183b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,10 @@ description = "Gemini server implementation" repository = "https://github.com/panicbit/northstar" documentation = "https://docs.rs/northstar" +[features] +# default = ["user_management"] +user_management = ["sled", "bincode", "serde/derive", "bcrypt", "crc32fast"] + [dependencies] anyhow = "1.0.33" rustls = { version = "0.18.1", features = ["dangerous_configuration"] } @@ -21,6 +25,15 @@ itertools = "0.9.0" log = "0.4.11" webpki = "0.21.0" lazy_static = "1.4.0" +sled = { version = "0.34.6", optional = true } +bincode = { version = "1.3.1", optional = true } +serde = { version = "1.0", optional = true } +bcrypt = { version = "0.9", optional = true } +crc32fast = { version = "1.2.1", optional = true } [dev-dependencies] env_logger = "0.8.1" + +[[example]] +name = "user_management" +required-features = ["user_management"] diff --git a/examples/user_management.rs b/examples/user_management.rs new file mode 100644 index 0000000..55e9e04 --- /dev/null +++ b/examples/user_management.rs @@ -0,0 +1,65 @@ +use anyhow::*; +use futures::{future::BoxFuture, FutureExt}; +use log::LevelFilter; +use northstar::{ + GEMINI_MIME, + GEMINI_PORT, + Request, + Response, + Server, + user_management::{User, UserManagerError}, +}; + +#[tokio::main] +async fn main() -> Result<()> { + env_logger::builder() + .filter_module("northstar", LevelFilter::Debug) + .init(); + + Server::bind(("0.0.0.0", GEMINI_PORT)) + .serve(handle_request) + .await +} + +/// An ultra-simple demonstration of simple authentication. +/// +/// If the user attempts to connect, they will be prompted to create a client certificate. +/// Once they've made one, they'll be given the opportunity to create an account by +/// selecting a username. They'll then get a message confirming their account creation. +/// Any time this user visits the site in the future, they'll get a personalized welcome +/// message. +fn handle_request(request: Request) -> BoxFuture<'static, Result> { + async move { + Ok(match request.user::()? { + User::Unauthenticated => { + Response::client_certificate_required() + }, + User::NotSignedIn(user) => { + if let Some(username) = request.input() { + match user.register::(username.to_owned()) { + Ok(_user) => Response::success(&GEMINI_MIME) + .with_body("Your account has been created!\n=>/ Begin"), + Err(UserManagerError::UsernameNotUnique) => + Response::input_lossy("That username is taken. Try again"), + Err(e) => panic!("Unexpected error: {}", e), + } + } else { + Response::input_lossy("Please pick a username") + } + }, + User::SignedIn(mut user) => { + if request.path_segments()[0].eq("push") { // User connecting to /push + if let Some(push) = request.input() { + user.as_mut().push_str(push); + user.save(); + } else { + return Ok(Response::input_lossy("Enter a string to push")); + } + } + + Response::success(&GEMINI_MIME) + .with_body(format!("Your current string: {}\n=> /push Push", user.as_ref())) + } + }) + }.boxed() +} diff --git a/src/lib.rs b/src/lib.rs index e3b1bc0..41f4f75 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ #[macro_use] extern crate log; -use std::{panic::AssertUnwindSafe, convert::TryFrom, io::BufReader, sync::Arc}; +use std::{panic::AssertUnwindSafe, convert::TryFrom, io::BufReader, sync::Arc, path::PathBuf}; use futures::{future::BoxFuture, FutureExt}; use tokio::{ prelude::*, @@ -17,6 +17,11 @@ use lazy_static::lazy_static; pub mod types; pub mod util; +#[cfg(feature = "user_management")] +pub mod user_management; + +#[cfg(feature="user_management")] +use user_management::UserManager; pub use mime; pub use uriparse as uri; @@ -33,6 +38,8 @@ pub struct Server { tls_acceptor: TlsAcceptor, listener: Arc, handler: Handler, + #[cfg(feature="user_management")] + manager: UserManager, } impl Server { @@ -59,8 +66,13 @@ impl Server { .context("Failed to establish TLS session")?; let mut stream = BufStream::new(stream); - let mut request = receive_request(&mut stream).await + #[cfg(feature="user_management")] + let mut request = self.receive_request(&mut stream).await .context("Failed to receive request")?; + #[cfg(not(feature="user_management"))] + let mut request = Self::receive_request(&mut stream).await + .context("Failed to receive request")?; + debug!("Client requested: {}", request.uri()); // Identify the client certificate from the tls stream. This is the first @@ -92,15 +104,65 @@ impl Server { Ok(()) } + + async fn receive_request( + #[cfg(feature="user_management")] + &self, + stream: &mut (impl AsyncBufRead + Unpin) + ) -> Result { + let limit = REQUEST_URI_MAX_LEN + "\r\n".len(); + let mut stream = stream.take(limit as u64); + let mut uri = Vec::new(); + + stream.read_until(b'\n', &mut uri).await?; + + if !uri.ends_with(b"\r\n") { + if uri.len() < REQUEST_URI_MAX_LEN { + bail!("Request header not terminated with CRLF") + } else { + bail!("Request URI too long") + } + } + + // Strip CRLF + uri.pop(); + uri.pop(); + + let uri = URIReference::try_from(&*uri) + .context("Request URI is invalid")? + .into_owned(); + let request = Request::from_uri( + uri, + #[cfg(feature="user_management")] + self.manager.clone(), + ) .context("Failed to create request from URI")?; + + Ok(request) + } } pub struct Builder { addr: A, + #[cfg(feature="user_management")] + data_dir: PathBuf, } impl Builder { fn bind(addr: A) -> Self { - Self { addr } + Self { + addr, + #[cfg(feature="user_management")] + data_dir: "data".into(), + } + } + + #[cfg(feature="user_management")] + /// Sets the directory to store user data in + /// + /// Defaults to `./data` if not specified + pub fn set_database_dir(mut self, path: impl Into) -> Self { + self.data_dir = path.into(); + self } pub async fn serve(self, handler: F) -> Result<()> @@ -117,40 +179,14 @@ impl Builder { tls_acceptor: TlsAcceptor::from(config), listener: Arc::new(listener), handler: Arc::new(handler), + #[cfg(feature="user_management")] + manager: UserManager::new(self.data_dir)?, }; server.serve().await } } -async fn receive_request(stream: &mut (impl AsyncBufRead + Unpin)) -> Result { - let limit = REQUEST_URI_MAX_LEN + "\r\n".len(); - let mut stream = stream.take(limit as u64); - let mut uri = Vec::new(); - - stream.read_until(b'\n', &mut uri).await?; - - if !uri.ends_with(b"\r\n") { - if uri.len() < REQUEST_URI_MAX_LEN { - bail!("Request header not terminated with CRLF") - } else { - bail!("Request URI too long") - } - } - - // Strip CRLF - uri.pop(); - uri.pop(); - - let uri = URIReference::try_from(&*uri) - .context("Request URI is invalid")? - .into_owned(); - let request = Request::from_uri(uri) - .context("Failed to create request from URI")?; - - Ok(request) -} - async fn send_response(mut response: Response, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> { send_response_header(response.header(), stream).await .context("Failed to send response header")?; diff --git a/src/types/request.rs b/src/types/request.rs index a99ea2f..4d4e678 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -3,21 +3,39 @@ use anyhow::*; use percent_encoding::percent_decode_str; use uriparse::URIReference; use rustls::Certificate; +#[cfg(feature="user_management")] +use serde::{Serialize, de::DeserializeOwned}; + +#[cfg(feature="user_management")] +use crate::user_management::{UserManager, User}; pub struct Request { uri: URIReference<'static>, input: Option, certificate: Option, + #[cfg(feature="user_management")] + manager: UserManager, } impl Request { - pub fn from_uri(uri: URIReference<'static>) -> Result { - Self::with_certificate(uri, None) + pub fn from_uri( + uri: URIReference<'static>, + #[cfg(feature="user_management")] + manager: UserManager, + ) -> Result { + Self::with_certificate( + uri, + None, + #[cfg(feature="user_management")] + manager + ) } pub fn with_certificate( mut uri: URIReference<'static>, - certificate: Option + certificate: Option, + #[cfg(feature="user_management")] + manager: UserManager, ) -> Result { uri.normalize(); @@ -36,6 +54,8 @@ impl Request { uri, input, certificate, + #[cfg(feature="user_management")] + manager, }) } @@ -63,6 +83,18 @@ impl Request { pub fn certificate(&self) -> Option<&Certificate> { self.certificate.as_ref() } + + #[cfg(feature="user_management")] + /// Attempt to determine the user who sent this request + /// + /// May return a variant depending on if the client used a client certificate, and if + /// they've registered as a user yet. + pub fn user(&self) -> Result> + where + UserData: Serialize + DeserializeOwned + { + Ok(self.manager.get_user(self.certificate())?) + } } impl ops::Deref for Request { diff --git a/src/user_management/manager.rs b/src/user_management/manager.rs new file mode 100644 index 0000000..3ccd150 --- /dev/null +++ b/src/user_management/manager.rs @@ -0,0 +1,121 @@ +use rustls::Certificate; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; + +use std::path::Path; + +use crate::user_management::{User, Result}; +use crate::user_management::user::{SignedInUser, NotSignedInUser, UserInner}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +/// Data stored in the certificate tree about a certain certificate +pub struct CertificateData { + #[serde(with = "CertificateDef")] + pub certificate: Certificate, + pub owner_username: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(remote = "Certificate")] +struct CertificateDef(Vec); + +#[derive(Debug, Clone)] +/// A struct containing information for managing users. +/// +/// Wraps a [`sled::Db`] +pub struct UserManager { + db: sled::Db, + pub (crate) users: sled::Tree, // user_id:String maps to data:UserData + pub (crate) certificates: sled::Tree, // certificate:u64 maps to data:CertificateData +} + +impl UserManager { + + /// Create or open a new UserManager + /// + /// The `dir` argument is the path to a data directory, to be populated using sled. + /// This will be created if it does not exist. + pub fn new(dir: impl AsRef) -> Result { + let db = sled::open(dir)?; + Ok(Self { + users: db.open_tree("users")?, + certificates: db.open_tree("certificates")?, + db, + }) + } + + /// Produce a u32 hash from a certificate, used for [`lookup_certficate()`] + pub fn hash_certificate(cert: &Certificate) -> u32 { + let mut hasher = crc32fast::Hasher::new(); + hasher.update(cert.0.as_ref()); + hasher.finalize() + } + + /// Lookup information about a certificate based on it's u32 hash + /// + /// # Errors + /// An error is thrown if there is an error reading from the database or if data + /// recieved from the database is corrupt + pub fn lookup_certificate(&self, cert: u32) -> Result> { + if let Some(bytes) = self.certificates.get(cert.to_le_bytes())? { + Ok(Some(bincode::deserialize(&bytes)?)) + } else { + Ok(None) + } + } + + /// Lookup information about a user by username + /// + /// # Errors + /// An error is thrown if there is an error reading from the database or if data + /// recieved from the database is corrupt + pub fn lookup_user<'de, UserData>( + &self, + username: impl AsRef<[u8]> + ) -> Result>> + where + UserData: Serialize + DeserializeOwned + { + if let Some(bytes) = self.users.get(username)? { + Ok(Some(bincode::deserialize_from(bytes.as_ref())?)) + } else { + Ok(None) + } + } + + /// Attempt to determine the user who sent a request based on the certificate. + /// + /// # Errors + /// An error is thrown if there is an error reading from the database or if data + /// recieved from the database is corrupt + /// + /// # Panics + /// Pancis if the database is corrupt + pub fn get_user<'de, UserData>( + &self, + cert: Option<&Certificate> + ) -> Result> + where + UserData: Serialize + DeserializeOwned + { + if let Some(certificate) = cert { + let cert_hash = Self::hash_certificate(certificate); + if let Some(certificate_data) = self.lookup_certificate(cert_hash)? { + let user_inner = self.lookup_user(&certificate_data.owner_username)? + .expect("Database corruption: Certificate data refers to non-existant user"); + Ok(User::SignedIn(SignedInUser { + username: certificate_data.owner_username, + active_certificate: certificate_data.certificate, + manager: self.clone(), + inner: user_inner, + })) + } else { + Ok(User::NotSignedIn(NotSignedInUser { + certificate: certificate.clone(), + manager: self.clone(), + })) + } + } else { + Ok(User::Unauthenticated) + } + } +} diff --git a/src/user_management/mod.rs b/src/user_management/mod.rs new file mode 100644 index 0000000..8569b2b --- /dev/null +++ b/src/user_management/mod.rs @@ -0,0 +1,71 @@ +pub mod user; +mod manager; +pub use manager::UserManager; +pub use user::User; + +#[derive(Debug)] +pub enum UserManagerError { + UsernameNotUnique, + PasswordNotSet, + DatabaseError(sled::Error), + DatabaseTransactionError(sled::transaction::TransactionError), + DeserializeError(bincode::Error), + BcryptError(bcrypt::BcryptError), +} + +impl From for UserManagerError { + fn from(error: sled::Error) -> Self { + Self::DatabaseError(error) + } +} + +impl From for UserManagerError { + fn from(error: sled::transaction::TransactionError) -> Self { + Self::DatabaseTransactionError(error) + } +} + +impl From for UserManagerError { + fn from(error: bincode::Error) -> Self { + Self::DeserializeError(error) + } +} + +impl From for UserManagerError { + fn from(error: bcrypt::BcryptError) -> Self { + Self::BcryptError(error) + } +} + +impl std::error::Error for UserManagerError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::DatabaseError(e) => Some(e), + Self::DatabaseTransactionError(e) => Some(e), + Self::DeserializeError(e) => Some(e), + Self::BcryptError(e) => Some(e), + _ => None + } + } +} + +impl std::fmt::Display for UserManagerError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> { + match self { + Self::UsernameNotUnique => + write!(f, "Attempted to create a user using a username that's already been taken"), + Self::PasswordNotSet => + write!(f, "Attempted to check the password of a user who has not set one yet"), + Self::DatabaseError(e) => + write!(f, "Error accessing the user database: {}", e), + Self::DatabaseTransactionError(e) => + write!(f, "Error accessing the user database: {}", e), + Self::DeserializeError(e) => + write!(f, "Recieved messy data from database, possible corruption: {}", e), + Self::BcryptError(e) => + write!(f, "Bcrypt Error, likely malformed password hash, possible database corruption: {}", e), + } + } +} + +pub type Result = std::result::Result; diff --git a/src/user_management/user.rs b/src/user_management/user.rs new file mode 100644 index 0000000..205071c --- /dev/null +++ b/src/user_management/user.rs @@ -0,0 +1,203 @@ +use rustls::Certificate; +use serde::{Deserialize, Serialize}; +use sled::Transactional; + +use crate::user_management::UserManager; +use crate::user_management::Result; +use crate::user_management::manager::CertificateData; + +/// An struct corresponding to the data stored in the user tree +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UserInner { + data: UserData, + certificates: Vec, + pass_hash: Option, +} + +/// Any information about the connecting user +#[derive(Clone, Debug)] +pub enum User { + /// A user who is connected without using a client certificate. + /// + /// This could be a user who has an account but just isn't presenting a certificate at + /// the minute, a user whose client does not support client certificates, or a user + /// who has not yet created a certificate for the site + Unauthenticated, + + /// A user who is connecting with a certificate that isn't connected to an account + /// + /// This is typically a new user who hasn't set up an account yet, or a user + /// connecting with a new certificate that needs to be added to an existing account. + NotSignedIn(NotSignedInUser), + + /// A user connecting with an identified account + SignedIn(SignedInUser), +} + +#[derive(Clone, Debug)] +/// Data about a user with a certificate not associated with an account +/// +/// For more information about the user lifecycle and sign-in stages, see [`User`] +pub struct NotSignedInUser { + pub (crate) certificate: Certificate, + pub (crate) manager: UserManager, +} + +impl NotSignedInUser { + /// Register a new user with this certificate + /// + /// # Errors + /// The provided username must be unique, or else an error will be raised. + /// + /// Additional errors might occur if there is a problem writing to the database + pub fn register( + self, + username: String, + ) -> Result> + where + UserData: Serialize + Default + { + if self.manager.users.contains_key(username.as_str())? { + Err(super::UserManagerError::UsernameNotUnique) + } else { + let cert_hash = UserManager::hash_certificate(&self.certificate); + + let newser = SignedInUser { + inner: UserInner { + data: UserData::default(), + certificates: vec![cert_hash], + pass_hash: None, + }, + username: username.clone(), + active_certificate: self.certificate.clone(), + manager: self.manager, + }; + + let cert_info = CertificateData { + certificate: self.certificate, + owner_username: username, + }; + + let newser_serialized = bincode::serialize(&newser.inner)?; + let cert_info_serialized = bincode::serialize(&cert_info)?; + + (&newser.manager.users, &newser.manager.certificates) + .transaction(|(tx_usr, tx_crt)| { + tx_usr.insert( + newser.username.as_str(), + newser_serialized.clone(), + )?; + tx_crt.insert( + cert_hash.to_le_bytes().as_ref(), + cert_info_serialized.clone(), + )?; + Ok(()) + })?; //TODO + + Ok(newser) + } + } + + /// Attach this certificate to an existing user + /// + /// Try to add this certificate to another user using a username and password + /// + /// # Errors + /// This will error if the username and password are incorrect, or if the user has yet + /// to set a password. + /// + /// Additional errors might occur if an error occurs during database lookup and + /// deserialization + pub fn attach<'de, UserData>( + username: impl AsRef<[u8]>, + password: impl AsRef<[u8]>, + ) -> Result> + where + UserData: Serialize + Deserialize<'de> + { + todo!() + } +} + +#[derive(Clone, Debug)] +/// Data about a logged in user +/// +/// For more information about the user lifecycle and sign-in stages, see [`User`] +pub struct SignedInUser { + pub (crate) username: String, + pub (crate) active_certificate: Certificate, + pub (crate) manager: UserManager, + pub (crate) inner: UserInner, +} + +impl SignedInUser { + /// Get the [`Certificate`] that the user is currently using to connect. + pub const fn active_certificate(&self) -> &Certificate { + &self.active_certificate + } + + /// Produce a list of all [`Certificate`]s registered to this account + pub fn all_certificates(&self) -> Vec { + self.inner.certificates + .iter() + .map( + |cid| self.manager.lookup_certificate(*cid) + .expect("Database corruption: User refers to non-existant certificate") + .expect("Error accessing database") + .certificate + ) + .collect() + } + + /// Get the user's current username. + /// + /// NOTE: This is not guaranteed not to change. + pub const fn username(&self) -> &String { + &self.username + } + + /// Check a password against the user's password hash + /// + /// # Errors + /// An error is raised if the user has yet to set a password, or if the user's + /// password hash is somehow malformed. + pub fn check_password( + &self, + try_password: impl AsRef<[u8]> + ) -> Result { + if let Some(hash) = &self.inner.pass_hash { + Ok(bcrypt::verify(try_password, hash.as_str())?) + } else { + Err(super::UserManagerError::PasswordNotSet) + } + } + + /// Write any updates to the user to the database. + /// + /// Updates caused by calling methods directly on the user do not need to be saved. + /// This is only for changes made to the UserData. + pub fn save(&self) -> Result<()> + where + UserData: Serialize + { + self.manager.users.insert( + &self.username, + bincode::serialize(&self.inner)?, + )?; + Ok(()) + } +} + +impl AsRef for SignedInUser { + fn as_ref(&self) -> &UserData { + &self.inner.data + } +} + +impl AsMut for SignedInUser { + /// NOTE: Changes made to the user data won't be persisted until SignedInUser::save + /// is called + fn as_mut(&mut self) -> &mut UserData { + &mut self.inner.data + } +} From 0a67c1d6722b33fbcc8b12e56728187a36b64688 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Mon, 16 Nov 2020 09:21:24 -0500 Subject: [PATCH 002/113] Rename UserInner -> PartialUser, expose for access outside of crate --- src/user_management/manager.rs | 7 +++- src/user_management/user.rs | 68 ++++++++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/src/user_management/manager.rs b/src/user_management/manager.rs index 3ccd150..2dacd35 100644 --- a/src/user_management/manager.rs +++ b/src/user_management/manager.rs @@ -4,13 +4,16 @@ use serde::{Deserialize, Serialize, de::DeserializeOwned}; use std::path::Path; use crate::user_management::{User, Result}; -use crate::user_management::user::{SignedInUser, NotSignedInUser, UserInner}; +use crate::user_management::user::{SignedInUser, NotSignedInUser, PartialUser}; #[derive(Debug, Clone, Deserialize, Serialize)] /// Data stored in the certificate tree about a certain certificate pub struct CertificateData { #[serde(with = "CertificateDef")] + /// The certificate in question pub certificate: Certificate, + + /// The username of the user to which this certificate is registered pub owner_username: String, } @@ -71,7 +74,7 @@ impl UserManager { pub fn lookup_user<'de, UserData>( &self, username: impl AsRef<[u8]> - ) -> Result>> + ) -> Result>> where UserData: Serialize + DeserializeOwned { diff --git a/src/user_management/user.rs b/src/user_management/user.rs index 205071c..cb2d036 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -7,11 +7,61 @@ use crate::user_management::Result; use crate::user_management::manager::CertificateData; /// An struct corresponding to the data stored in the user tree +/// +/// In order to generate a full user obj, you need to perform a lookup with a specific +/// certificate. #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct UserInner { - data: UserData, - certificates: Vec, - pass_hash: Option, +pub struct PartialUser { + pub data: UserData, + pub certificates: Vec, + pub pass_hash: Option, +} + +impl PartialUser { + + /// A list of certificate hashes registered to this user + /// + /// Can be looked up using [`UserManager::lookup_certficate`] to get full information + pub fn certificates(&self) -> &Vec { + &self.certificates + } + + /// The bcrypt hash of the user's password + pub fn pass_hash(&self) -> Option<&str> { + self.pass_hash.as_ref().map(|s| s.as_str()) + } + + /// Write user data to the database + /// + /// This MUST be called if the user data is modified using the AsMut trait, or else + /// changes will not be written to the database + pub fn store(&self, tree: &sled::Tree, username: impl AsRef<[u8]>) -> Result<()> + where + UserData: Serialize + { + tree.insert( + &username, + bincode::serialize(&self)?, + )?; + Ok(()) + } +} + +impl AsRef for PartialUser { + /// Access any data the application has stored for the user. + fn as_ref(&self) -> &UserData { + &self.data + } +} + +impl AsMut for PartialUser { + /// Modify the data stored for a user + /// + /// IMPORTANT: Changes will not be written to the database until + /// [`PartialUser::store()`] is called + fn as_mut(&mut self) -> &mut UserData { + &mut self.data + } } /// Any information about the connecting user @@ -63,7 +113,7 @@ impl NotSignedInUser { let cert_hash = UserManager::hash_certificate(&self.certificate); let newser = SignedInUser { - inner: UserInner { + inner: PartialUser { data: UserData::default(), certificates: vec![cert_hash], pass_hash: None, @@ -127,7 +177,7 @@ pub struct SignedInUser { pub (crate) username: String, pub (crate) active_certificate: Certificate, pub (crate) manager: UserManager, - pub (crate) inner: UserInner, + pub (crate) inner: PartialUser, } impl SignedInUser { @@ -180,11 +230,7 @@ impl SignedInUser { where UserData: Serialize { - self.manager.users.insert( - &self.username, - bincode::serialize(&self.inner)?, - )?; - Ok(()) + self.inner.store(&self.manager.users, &self.username) } } From e7cf782a60ee832a77649258b7e663980ffd8312 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 13:20:14 -0500 Subject: [PATCH 003/113] Improve user management docs --- Cargo.toml | 1 - src/user_management/manager.rs | 2 +- src/user_management/mod.rs | 23 ++++++++++++++++++++++ src/user_management/user.rs | 36 +++++++++++++++++++++++++++++++--- 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index de183b5..5a1d4d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ repository = "https://github.com/panicbit/northstar" documentation = "https://docs.rs/northstar" [features] -# default = ["user_management"] user_management = ["sled", "bincode", "serde/derive", "bcrypt", "crc32fast"] [dependencies] diff --git a/src/user_management/manager.rs b/src/user_management/manager.rs index 2dacd35..4f93e5e 100644 --- a/src/user_management/manager.rs +++ b/src/user_management/manager.rs @@ -46,7 +46,7 @@ impl UserManager { }) } - /// Produce a u32 hash from a certificate, used for [`lookup_certficate()`] + /// Produce a u32 hash from a certificate, used for [`lookup_certificate()`](Self::lookup_certificate()) pub fn hash_certificate(cert: &Certificate) -> u32 { let mut hasher = crc32fast::Hasher::new(); hasher.update(cert.0.as_ref()); diff --git a/src/user_management/mod.rs b/src/user_management/mod.rs index 8569b2b..4384355 100644 --- a/src/user_management/mod.rs +++ b/src/user_management/mod.rs @@ -1,7 +1,30 @@ +//! Tools for registering users & persisting arbitrary user data +//! +//! Many Gemini applications use some form of a login method in order to allow users to +//! persist personal data, authenticate themselves, and login from multiple devices using +//! multiple certificates. +//! +//! This module contains tools to help you build a system like this without stress. A +//! typical workflow looks something like this: +//! +//! * Call [`Request::user()`] to retrieve information about a user +//! * Direct any users without a certificate to create a certificate +//! * Ask users with a certificate not yet linked to an account to create an account using +//! [`NotSignedInUser::register()`] or link their certificate to an existing account +//! with a password using [`NotSignedInUser::attach()`]. +//! * You should now have a [`SignedInUser`] either from registering/attaching a +//! [`NotSignedInUser`] or because the user was already registered +//! * Access and modify user data using [`SignedInUser::as_mut()`], changes are +//! automatically persisted to the database (on user drop). +//! +//! Use of this module requires the `user_management` feature to be enabled pub mod user; mod manager; pub use manager::UserManager; pub use user::User; +pub use manager::CertificateData; +use crate::types::Request; +use user::{NotSignedInUser, SignedInUser}; #[derive(Debug)] pub enum UserManagerError { diff --git a/src/user_management/user.rs b/src/user_management/user.rs index cb2d036..ca61472 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -1,3 +1,23 @@ +//! Several structs representing data about users +//! +//! This module contains any structs needed to store and retrieve data about users. The +//! different varieties have different purposes and come from different places. +//! +//! [`User`] is the most common for of user struct, and typically comes from calling +//! [`Request::user()`](crate::types::Request::user()). This is an enum with several +//! variants, and can be specialized into a [`NotSignedInUser`] or a [`SignedInUser`] if +//! the user has presented a certificate. These two subtypes have more specific +//! information, like the user's username and active certificate. +//! +//! [`SignedInUser`] is particularly signifigant in that this is the struct used to modify +//! the data stored for the current user. This is accomplished through the +//! [`as_mut()`](SignedInUser::as_mut) method. Changes made this way must be persisted +//! using [`save()`](SignedInUser::save()) or by dropping the user. +//! +//! [`PartialUser`] is the main way of modifying data stored for users who aren't +//! currently connecting. These are mainly obtained through the +//! [`UserManager::lookup_user()`] method. Unlinke with [`SignedInUser`], these are not +//! committed on drop, and [`PartialUser::store()`] must be manually called use rustls::Certificate; use serde::{Deserialize, Serialize}; use sled::Transactional; @@ -21,7 +41,7 @@ impl PartialUser { /// A list of certificate hashes registered to this user /// - /// Can be looked up using [`UserManager::lookup_certficate`] to get full information + /// Can be looked up using [`UserManager::lookup_certificate()`] to get full information pub fn certificates(&self) -> &Vec { &self.certificates } @@ -94,7 +114,11 @@ pub struct NotSignedInUser { } impl NotSignedInUser { - /// Register a new user with this certificate + /// Register a new user with this certificate. + /// + /// This creates a new user & user data entry in the database with the given username. + /// From now on, when this user logs in with this certificate, they will be + /// automatically authenticated, and their user data automatically retrieved. /// /// # Errors /// The provided username must be unique, or else an error will be raised. @@ -150,7 +174,13 @@ impl NotSignedInUser { /// Attach this certificate to an existing user /// - /// Try to add this certificate to another user using a username and password + /// Try to add this certificate to another user using a username and password. If + /// successful, the user this certificate is attached to will be able to automatically + /// log in with either this certificate or any of the certificates they already had + /// registered. + /// + /// This method returns the new SignedInUser instance representing the now-attached + /// user. /// /// # Errors /// This will error if the username and password are incorrect, or if the user has yet From 18228bb1c595c67e7f92bebcc94f4892ad4e5884 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 14:25:36 -0500 Subject: [PATCH 004/113] Save SignedInUser on drop, and make changes necessary to that end Which was a surprisingly large number of changes /shrug --- src/user_management/manager.rs | 12 +++--- src/user_management/user.rs | 78 ++++++++++++++++++++++------------ 2 files changed, 58 insertions(+), 32 deletions(-) diff --git a/src/user_management/manager.rs b/src/user_management/manager.rs index 4f93e5e..426096a 100644 --- a/src/user_management/manager.rs +++ b/src/user_management/manager.rs @@ -105,12 +105,12 @@ impl UserManager { if let Some(certificate_data) = self.lookup_certificate(cert_hash)? { let user_inner = self.lookup_user(&certificate_data.owner_username)? .expect("Database corruption: Certificate data refers to non-existant user"); - Ok(User::SignedIn(SignedInUser { - username: certificate_data.owner_username, - active_certificate: certificate_data.certificate, - manager: self.clone(), - inner: user_inner, - })) + Ok(User::SignedIn(SignedInUser::new( + certificate_data.owner_username, + certificate_data.certificate, + self.clone(), + user_inner, + ))) } else { Ok(User::NotSignedIn(NotSignedInUser { certificate: certificate.clone(), diff --git a/src/user_management/user.rs b/src/user_management/user.rs index ca61472..13ee056 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -19,7 +19,7 @@ //! [`UserManager::lookup_user()`] method. Unlinke with [`SignedInUser`], these are not //! committed on drop, and [`PartialUser::store()`] must be manually called use rustls::Certificate; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; use sled::Transactional; use crate::user_management::UserManager; @@ -86,7 +86,7 @@ impl AsMut for PartialUser { /// Any information about the connecting user #[derive(Clone, Debug)] -pub enum User { +pub enum User { /// A user who is connected without using a client certificate. /// /// This could be a user who has an account but just isn't presenting a certificate at @@ -124,28 +124,25 @@ impl NotSignedInUser { /// The provided username must be unique, or else an error will be raised. /// /// Additional errors might occur if there is a problem writing to the database - pub fn register( + pub fn register( self, username: String, - ) -> Result> - where - UserData: Serialize + Default - { + ) -> Result> { if self.manager.users.contains_key(username.as_str())? { Err(super::UserManagerError::UsernameNotUnique) } else { let cert_hash = UserManager::hash_certificate(&self.certificate); - let newser = SignedInUser { - inner: PartialUser { + let newser = SignedInUser::new( + username.clone(), + self.certificate.clone(), + self.manager, + PartialUser { data: UserData::default(), certificates: vec![cert_hash], pass_hash: None, }, - username: username.clone(), - active_certificate: self.certificate.clone(), - manager: self.manager, - }; + ); let cert_info = CertificateData { certificate: self.certificate, @@ -188,13 +185,10 @@ impl NotSignedInUser { /// /// Additional errors might occur if an error occurs during database lookup and /// deserialization - pub fn attach<'de, UserData>( + pub fn attach( username: impl AsRef<[u8]>, password: impl AsRef<[u8]>, - ) -> Result> - where - UserData: Serialize + Deserialize<'de> - { + ) -> Result> { todo!() } } @@ -203,16 +197,35 @@ impl NotSignedInUser { /// Data about a logged in user /// /// For more information about the user lifecycle and sign-in stages, see [`User`] -pub struct SignedInUser { +pub struct SignedInUser { pub (crate) username: String, pub (crate) active_certificate: Certificate, pub (crate) manager: UserManager, pub (crate) inner: PartialUser, + /// Indicates that [`SignedInUser::as_mut()`] has been called, but [`SignedInUser::save()`] has not + has_changed: bool, } -impl SignedInUser { +impl SignedInUser { + + /// Create a new user from parts + pub (crate) fn new( + username: String, + active_certificate: Certificate, + manager: UserManager, + inner: PartialUser + ) -> Self { + Self { + username, + active_certificate, + manager, + inner, + has_changed: false, + } + } + /// Get the [`Certificate`] that the user is currently using to connect. - pub const fn active_certificate(&self) -> &Certificate { + pub fn active_certificate(&self) -> &Certificate { &self.active_certificate } @@ -232,7 +245,7 @@ impl SignedInUser { /// Get the user's current username. /// /// NOTE: This is not guaranteed not to change. - pub const fn username(&self) -> &String { + pub fn username(&self) -> &String { &self.username } @@ -256,24 +269,37 @@ impl SignedInUser { /// /// Updates caused by calling methods directly on the user do not need to be saved. /// This is only for changes made to the UserData. - pub fn save(&self) -> Result<()> + pub fn save(&mut self) -> Result<()> where UserData: Serialize { - self.inner.store(&self.manager.users, &self.username) + self.inner.store(&self.manager.users, &self.username)?; + self.has_changed = false; + Ok(()) } } -impl AsRef for SignedInUser { +impl std::ops::Drop for SignedInUser { + fn drop(&mut self) { + if self.has_changed { + if let Err(e) = self.save() { + error!("Failed to save user data to database: {:?}", e); + } + } + } +} + +impl AsRef for SignedInUser { fn as_ref(&self) -> &UserData { &self.inner.data } } -impl AsMut for SignedInUser { +impl AsMut for SignedInUser { /// NOTE: Changes made to the user data won't be persisted until SignedInUser::save /// is called fn as_mut(&mut self) -> &mut UserData { + self.has_changed = true; &mut self.inner.data } } From abcb2965749f04d673bf0c3d3c5d9c79a1efeaf1 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 14:27:36 -0500 Subject: [PATCH 005/113] Cleaned up some warnings in user_management *sweep sweep sweep sweep sweep* --- src/user_management/manager.rs | 4 ++-- src/user_management/mod.rs | 2 -- src/user_management/user.rs | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/user_management/manager.rs b/src/user_management/manager.rs index 426096a..da25cf9 100644 --- a/src/user_management/manager.rs +++ b/src/user_management/manager.rs @@ -71,7 +71,7 @@ impl UserManager { /// # Errors /// An error is thrown if there is an error reading from the database or if data /// recieved from the database is corrupt - pub fn lookup_user<'de, UserData>( + pub fn lookup_user( &self, username: impl AsRef<[u8]> ) -> Result>> @@ -93,7 +93,7 @@ impl UserManager { /// /// # Panics /// Pancis if the database is corrupt - pub fn get_user<'de, UserData>( + pub fn get_user( &self, cert: Option<&Certificate> ) -> Result> diff --git a/src/user_management/mod.rs b/src/user_management/mod.rs index 4384355..fbc8868 100644 --- a/src/user_management/mod.rs +++ b/src/user_management/mod.rs @@ -23,8 +23,6 @@ mod manager; pub use manager::UserManager; pub use user::User; pub use manager::CertificateData; -use crate::types::Request; -use user::{NotSignedInUser, SignedInUser}; #[derive(Debug)] pub enum UserManagerError { diff --git a/src/user_management/user.rs b/src/user_management/user.rs index 13ee056..7d4be89 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -42,13 +42,13 @@ impl PartialUser { /// A list of certificate hashes registered to this user /// /// Can be looked up using [`UserManager::lookup_certificate()`] to get full information - pub fn certificates(&self) -> &Vec { + pub const fn certificates(&self) -> &Vec { &self.certificates } /// The bcrypt hash of the user's password pub fn pass_hash(&self) -> Option<&str> { - self.pass_hash.as_ref().map(|s| s.as_str()) + self.pass_hash.as_deref() } /// Write user data to the database From ff5f294dae51034007eb12bce6c7a883534a45cc Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 15:21:26 -0500 Subject: [PATCH 006/113] Restricted access to several fields of SignedInUser --- src/user_management/user.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/user_management/user.rs b/src/user_management/user.rs index 7d4be89..445b081 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -198,10 +198,10 @@ impl NotSignedInUser { /// /// For more information about the user lifecycle and sign-in stages, see [`User`] pub struct SignedInUser { - pub (crate) username: String, - pub (crate) active_certificate: Certificate, - pub (crate) manager: UserManager, - pub (crate) inner: PartialUser, + username: String, + active_certificate: Certificate, + manager: UserManager, + inner: PartialUser, /// Indicates that [`SignedInUser::as_mut()`] has been called, but [`SignedInUser::save()`] has not has_changed: bool, } From fb205cd397f1bc3495e6754d83a3cec52caf03d3 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 16:07:52 -0500 Subject: [PATCH 007/113] Switched from bcrypt to argon --- Cargo.toml | 2 +- src/user_management/mod.rs | 14 +++++++------- src/user_management/user.rs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e38e3bf..9027145 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ mime_guess = { version = "2.0.3", optional = true } sled = { version = "0.34.6", optional = true } bincode = { version = "1.3.1", optional = true } serde = { version = "1.0", optional = true } -bcrypt = { version = "0.9", optional = true } +rust-argon2 = { version = "0.8.2", optional = true } crc32fast = { version = "1.2.1", optional = true } [[example]] diff --git a/src/user_management/mod.rs b/src/user_management/mod.rs index fbc8868..2e56a81 100644 --- a/src/user_management/mod.rs +++ b/src/user_management/mod.rs @@ -31,7 +31,7 @@ pub enum UserManagerError { DatabaseError(sled::Error), DatabaseTransactionError(sled::transaction::TransactionError), DeserializeError(bincode::Error), - BcryptError(bcrypt::BcryptError), + Argon2Error(argon2::Error), } impl From for UserManagerError { @@ -52,9 +52,9 @@ impl From for UserManagerError { } } -impl From for UserManagerError { - fn from(error: bcrypt::BcryptError) -> Self { - Self::BcryptError(error) +impl From for UserManagerError { + fn from(error: argon2::Error) -> Self { + Self::Argon2Error(error) } } @@ -64,7 +64,7 @@ impl std::error::Error for UserManagerError { Self::DatabaseError(e) => Some(e), Self::DatabaseTransactionError(e) => Some(e), Self::DeserializeError(e) => Some(e), - Self::BcryptError(e) => Some(e), + Self::Argon2Error(e) => Some(e), _ => None } } @@ -83,8 +83,8 @@ impl std::fmt::Display for UserManagerError { write!(f, "Error accessing the user database: {}", e), Self::DeserializeError(e) => write!(f, "Recieved messy data from database, possible corruption: {}", e), - Self::BcryptError(e) => - write!(f, "Bcrypt Error, likely malformed password hash, possible database corruption: {}", e), + Self::Argon2Error(e) => + write!(f, "Argon2 Error, likely malformed password hash, possible database corruption: {}", e), } } } diff --git a/src/user_management/user.rs b/src/user_management/user.rs index 445b081..3abba2b 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -259,7 +259,7 @@ impl SignedInUser { try_password: impl AsRef<[u8]> ) -> Result { if let Some(hash) = &self.inner.pass_hash { - Ok(bcrypt::verify(try_password, hash.as_str())?) + Ok(argon2::verify_encoded(hash.as_str(), try_password.as_ref())?) } else { Err(super::UserManagerError::PasswordNotSet) } From f0517ef0b725f47f09b639fc1f409b0a4fe254d7 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 16:24:38 -0500 Subject: [PATCH 008/113] Fix user management example --- examples/user_management.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/user_management.rs b/examples/user_management.rs index 55e9e04..a597934 100644 --- a/examples/user_management.rs +++ b/examples/user_management.rs @@ -1,5 +1,6 @@ use anyhow::*; -use futures::{future::BoxFuture, FutureExt}; +use futures_core::future::BoxFuture; +use futures_util::FutureExt; use log::LevelFilter; use northstar::{ GEMINI_MIME, @@ -51,7 +52,7 @@ fn handle_request(request: Request) -> BoxFuture<'static, Result> { if request.path_segments()[0].eq("push") { // User connecting to /push if let Some(push) = request.input() { user.as_mut().push_str(push); - user.save(); + user.save()?; } else { return Ok(Response::input_lossy("Enter a string to push")); } From 1908d0a0d7ff3e7988966f471f4d9be42eed8820 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 16:25:13 -0500 Subject: [PATCH 009/113] Implement the set password method --- Cargo.toml | 3 +- src/user_management/user.rs | 62 ++++++++++++++++++++++++++++++++----- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9027145..af9a29d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ repository = "https://github.com/panicbit/northstar" documentation = "https://docs.rs/northstar" [features] -user_management = ["sled", "bincode", "serde/derive", "bcrypt", "crc32fast"] +user_management = ["sled", "bincode", "serde/derive", "rust-argon2", "crc32fast", "ring"] default = ["serve_dir"] serve_dir = ["mime_guess", "tokio/fs"] @@ -31,6 +31,7 @@ bincode = { version = "1.3.1", optional = true } serde = { version = "1.0", optional = true } rust-argon2 = { version = "0.8.2", optional = true } crc32fast = { version = "1.2.1", optional = true } +ring = { version = "0.16.15", optional = true } [[example]] name = "user_management" diff --git a/src/user_management/user.rs b/src/user_management/user.rs index 3abba2b..a1ad27c 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -26,6 +26,22 @@ use crate::user_management::UserManager; use crate::user_management::Result; use crate::user_management::manager::CertificateData; +const ARGON2_CONFIG: argon2::Config = argon2::Config { + ad: &[], + hash_length: 32, + lanes: 1, + mem_cost: 4096, + secret: &[], + thread_mode: argon2::ThreadMode::Sequential, + time_cost: 3, + variant: argon2::Variant::Argon2id, + version: argon2::Version::Version13, +}; + +lazy_static::lazy_static! { + static ref RANDOM: ring::rand::SystemRandom = ring::rand::SystemRandom::new(); +} + /// An struct corresponding to the data stored in the user tree /// /// In order to generate a full user obj, you need to perform a lookup with a specific @@ -34,7 +50,7 @@ use crate::user_management::manager::CertificateData; pub struct PartialUser { pub data: UserData, pub certificates: Vec, - pub pass_hash: Option, + pub pass_hash: Option<(Vec, [u8; 32])>, } impl PartialUser { @@ -46,11 +62,6 @@ impl PartialUser { &self.certificates } - /// The bcrypt hash of the user's password - pub fn pass_hash(&self) -> Option<&str> { - self.pass_hash.as_deref() - } - /// Write user data to the database /// /// This MUST be called if the user data is modified using the AsMut trait, or else @@ -258,13 +269,48 @@ impl SignedInUser { &self, try_password: impl AsRef<[u8]> ) -> Result { - if let Some(hash) = &self.inner.pass_hash { - Ok(argon2::verify_encoded(hash.as_str(), try_password.as_ref())?) + if let Some((hash, salt)) = &self.inner.pass_hash { + Ok(argon2::verify_raw( + try_password.as_ref(), + salt, + hash.as_ref(), + &ARGON2_CONFIG, + )?) } else { Err(super::UserManagerError::PasswordNotSet) } } + /// Set's the password for this user + /// + /// By default, users have no password, meaning the cannot add any certificates beyond + /// the one they created their account with. However, by setting their password, + /// users are able to add more devices to their account, and recover their account if + /// it's lost. Note that this will completely overwrite the users old password. + /// + /// Use [`SignedInUser::check_password()`] and [`NotSignedInUser::attach()`] to check + /// the password against another one, or to link a new certificate. + /// + /// Because this method uses a key derivation algorithm, this should be considered a + /// very expensive operation. + pub fn set_password( + &mut self, + password: impl AsRef<[u8]>, + ) -> Result<()> { + let salt: [u8; 32] = ring::rand::generate(&*RANDOM) + .expect("Error generating random salt") + .expose(); + self.inner.pass_hash = Some(( + argon2::hash_raw( + password.as_ref(), + salt.as_ref(), + &ARGON2_CONFIG, + )?, + salt, + )); + Ok(()) + } + /// Write any updates to the user to the database. /// /// Updates caused by calling methods directly on the user do not need to be saved. From 2f1196228fdbc7ca4157fefd5b25641725c3f839 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 16:54:29 -0500 Subject: [PATCH 010/113] Return a full SignedInUser from lookup_user(), restrict access to PartialUser Okay this is a much nicer API tbh. Why didn't I do this from the start? 'cause I'm bad at planning that's why. We got there eventually though! --- src/user_management/manager.rs | 16 ++++------ src/user_management/user.rs | 54 +++++++++++----------------------- 2 files changed, 23 insertions(+), 47 deletions(-) diff --git a/src/user_management/manager.rs b/src/user_management/manager.rs index da25cf9..015132a 100644 --- a/src/user_management/manager.rs +++ b/src/user_management/manager.rs @@ -73,13 +73,14 @@ impl UserManager { /// recieved from the database is corrupt pub fn lookup_user( &self, - username: impl AsRef<[u8]> - ) -> Result>> + username: impl AsRef + ) -> Result>> where UserData: Serialize + DeserializeOwned { - if let Some(bytes) = self.users.get(username)? { - Ok(Some(bincode::deserialize_from(bytes.as_ref())?)) + if let Some(bytes) = self.users.get(username.as_ref())? { + let inner: PartialUser = bincode::deserialize_from(bytes.as_ref())?; + Ok(Some(SignedInUser::new(username.as_ref().to_owned(), None, self.clone(), inner))) } else { Ok(None) } @@ -105,12 +106,7 @@ impl UserManager { if let Some(certificate_data) = self.lookup_certificate(cert_hash)? { let user_inner = self.lookup_user(&certificate_data.owner_username)? .expect("Database corruption: Certificate data refers to non-existant user"); - Ok(User::SignedIn(SignedInUser::new( - certificate_data.owner_username, - certificate_data.certificate, - self.clone(), - user_inner, - ))) + Ok(User::SignedIn(user_inner.with_cert(certificate_data.certificate))) } else { Ok(User::NotSignedIn(NotSignedInUser { certificate: certificate.clone(), diff --git a/src/user_management/user.rs b/src/user_management/user.rs index a1ad27c..96cdad2 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -10,14 +10,9 @@ //! information, like the user's username and active certificate. //! //! [`SignedInUser`] is particularly signifigant in that this is the struct used to modify -//! the data stored for the current user. This is accomplished through the +//! the data stored for almost all users. This is accomplished through the //! [`as_mut()`](SignedInUser::as_mut) method. Changes made this way must be persisted //! using [`save()`](SignedInUser::save()) or by dropping the user. -//! -//! [`PartialUser`] is the main way of modifying data stored for users who aren't -//! currently connecting. These are mainly obtained through the -//! [`UserManager::lookup_user()`] method. Unlinke with [`SignedInUser`], these are not -//! committed on drop, and [`PartialUser::store()`] must be manually called use rustls::Certificate; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use sled::Transactional; @@ -47,7 +42,7 @@ lazy_static::lazy_static! { /// In order to generate a full user obj, you need to perform a lookup with a specific /// certificate. #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct PartialUser { +pub (crate) struct PartialUser { pub data: UserData, pub certificates: Vec, pub pass_hash: Option<(Vec, [u8; 32])>, @@ -55,18 +50,11 @@ pub struct PartialUser { impl PartialUser { - /// A list of certificate hashes registered to this user - /// - /// Can be looked up using [`UserManager::lookup_certificate()`] to get full information - pub const fn certificates(&self) -> &Vec { - &self.certificates - } - /// Write user data to the database /// /// This MUST be called if the user data is modified using the AsMut trait, or else /// changes will not be written to the database - pub fn store(&self, tree: &sled::Tree, username: impl AsRef<[u8]>) -> Result<()> + fn store(&self, tree: &sled::Tree, username: impl AsRef<[u8]>) -> Result<()> where UserData: Serialize { @@ -78,23 +66,6 @@ impl PartialUser { } } -impl AsRef for PartialUser { - /// Access any data the application has stored for the user. - fn as_ref(&self) -> &UserData { - &self.data - } -} - -impl AsMut for PartialUser { - /// Modify the data stored for a user - /// - /// IMPORTANT: Changes will not be written to the database until - /// [`PartialUser::store()`] is called - fn as_mut(&mut self) -> &mut UserData { - &mut self.data - } -} - /// Any information about the connecting user #[derive(Clone, Debug)] pub enum User { @@ -146,7 +117,7 @@ impl NotSignedInUser { let newser = SignedInUser::new( username.clone(), - self.certificate.clone(), + Some(self.certificate.clone()), self.manager, PartialUser { data: UserData::default(), @@ -210,7 +181,7 @@ impl NotSignedInUser { /// For more information about the user lifecycle and sign-in stages, see [`User`] pub struct SignedInUser { username: String, - active_certificate: Certificate, + active_certificate: Option, manager: UserManager, inner: PartialUser, /// Indicates that [`SignedInUser::as_mut()`] has been called, but [`SignedInUser::save()`] has not @@ -222,7 +193,7 @@ impl SignedInUser { /// Create a new user from parts pub (crate) fn new( username: String, - active_certificate: Certificate, + active_certificate: Option, manager: UserManager, inner: PartialUser ) -> Self { @@ -235,9 +206,18 @@ impl SignedInUser { } } + /// Update the active certificate + pub (crate) fn with_cert(mut self, cert: Certificate) -> Self { + self.active_certificate = Some(cert); + self + } + /// Get the [`Certificate`] that the user is currently using to connect. - pub fn active_certificate(&self) -> &Certificate { - &self.active_certificate + /// + /// If this user was retrieved by a [`UserManager::lookup_user()`], this will be + /// [`None`]. In all other cases, this will be [`Some`]. + pub fn active_certificate(&self) -> Option<&Certificate> { + self.active_certificate.as_ref() } /// Produce a list of all [`Certificate`]s registered to this account From 79b06b08ba49ec0df53b6c0b829b26d53530b763 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 17:00:32 -0500 Subject: [PATCH 011/113] Refactor SignedInUser -> RegisteredUser --- src/user_management/manager.rs | 6 +++--- src/user_management/mod.rs | 4 ++-- src/user_management/user.rs | 34 +++++++++++++++++----------------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/user_management/manager.rs b/src/user_management/manager.rs index 015132a..d9df8e1 100644 --- a/src/user_management/manager.rs +++ b/src/user_management/manager.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize, de::DeserializeOwned}; use std::path::Path; use crate::user_management::{User, Result}; -use crate::user_management::user::{SignedInUser, NotSignedInUser, PartialUser}; +use crate::user_management::user::{RegisteredUser, NotSignedInUser, PartialUser}; #[derive(Debug, Clone, Deserialize, Serialize)] /// Data stored in the certificate tree about a certain certificate @@ -74,13 +74,13 @@ impl UserManager { pub fn lookup_user( &self, username: impl AsRef - ) -> Result>> + ) -> Result>> where UserData: Serialize + DeserializeOwned { if let Some(bytes) = self.users.get(username.as_ref())? { let inner: PartialUser = bincode::deserialize_from(bytes.as_ref())?; - Ok(Some(SignedInUser::new(username.as_ref().to_owned(), None, self.clone(), inner))) + Ok(Some(RegisteredUser::new(username.as_ref().to_owned(), None, self.clone(), inner))) } else { Ok(None) } diff --git a/src/user_management/mod.rs b/src/user_management/mod.rs index 2e56a81..1864e8a 100644 --- a/src/user_management/mod.rs +++ b/src/user_management/mod.rs @@ -12,9 +12,9 @@ //! * Ask users with a certificate not yet linked to an account to create an account using //! [`NotSignedInUser::register()`] or link their certificate to an existing account //! with a password using [`NotSignedInUser::attach()`]. -//! * You should now have a [`SignedInUser`] either from registering/attaching a +//! * You should now have a [`RegisteredUser`] either from registering/attaching a //! [`NotSignedInUser`] or because the user was already registered -//! * Access and modify user data using [`SignedInUser::as_mut()`], changes are +//! * Access and modify user data using [`RegisteredUser::as_mut()`], changes are //! automatically persisted to the database (on user drop). //! //! Use of this module requires the `user_management` feature to be enabled diff --git a/src/user_management/user.rs b/src/user_management/user.rs index 96cdad2..049c0cc 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -5,14 +5,14 @@ //! //! [`User`] is the most common for of user struct, and typically comes from calling //! [`Request::user()`](crate::types::Request::user()). This is an enum with several -//! variants, and can be specialized into a [`NotSignedInUser`] or a [`SignedInUser`] if +//! variants, and can be specialized into a [`NotSignedInUser`] or a [`RegisteredUser`] if //! the user has presented a certificate. These two subtypes have more specific //! information, like the user's username and active certificate. //! -//! [`SignedInUser`] is particularly signifigant in that this is the struct used to modify +//! [`RegisteredUser`] is particularly signifigant in that this is the struct used to modify //! the data stored for almost all users. This is accomplished through the -//! [`as_mut()`](SignedInUser::as_mut) method. Changes made this way must be persisted -//! using [`save()`](SignedInUser::save()) or by dropping the user. +//! [`as_mut()`](RegisteredUser::as_mut) method. Changes made this way must be persisted +//! using [`save()`](RegisteredUser::save()) or by dropping the user. use rustls::Certificate; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use sled::Transactional; @@ -83,7 +83,7 @@ pub enum User { NotSignedIn(NotSignedInUser), /// A user connecting with an identified account - SignedIn(SignedInUser), + SignedIn(RegisteredUser), } #[derive(Clone, Debug)] @@ -109,13 +109,13 @@ impl NotSignedInUser { pub fn register( self, username: String, - ) -> Result> { + ) -> Result> { if self.manager.users.contains_key(username.as_str())? { Err(super::UserManagerError::UsernameNotUnique) } else { let cert_hash = UserManager::hash_certificate(&self.certificate); - let newser = SignedInUser::new( + let newser = RegisteredUser::new( username.clone(), Some(self.certificate.clone()), self.manager, @@ -158,7 +158,7 @@ impl NotSignedInUser { /// log in with either this certificate or any of the certificates they already had /// registered. /// - /// This method returns the new SignedInUser instance representing the now-attached + /// This method returns the new RegisteredUser instance representing the now-attached /// user. /// /// # Errors @@ -170,7 +170,7 @@ impl NotSignedInUser { pub fn attach( username: impl AsRef<[u8]>, password: impl AsRef<[u8]>, - ) -> Result> { + ) -> Result> { todo!() } } @@ -179,16 +179,16 @@ impl NotSignedInUser { /// Data about a logged in user /// /// For more information about the user lifecycle and sign-in stages, see [`User`] -pub struct SignedInUser { +pub struct RegisteredUser { username: String, active_certificate: Option, manager: UserManager, inner: PartialUser, - /// Indicates that [`SignedInUser::as_mut()`] has been called, but [`SignedInUser::save()`] has not + /// Indicates that [`RegisteredUser::as_mut()`] has been called, but [`RegisteredUser::save()`] has not has_changed: bool, } -impl SignedInUser { +impl RegisteredUser { /// Create a new user from parts pub (crate) fn new( @@ -268,7 +268,7 @@ impl SignedInUser { /// users are able to add more devices to their account, and recover their account if /// it's lost. Note that this will completely overwrite the users old password. /// - /// Use [`SignedInUser::check_password()`] and [`NotSignedInUser::attach()`] to check + /// Use [`RegisteredUser::check_password()`] and [`NotSignedInUser::attach()`] to check /// the password against another one, or to link a new certificate. /// /// Because this method uses a key derivation algorithm, this should be considered a @@ -305,7 +305,7 @@ impl SignedInUser { } } -impl std::ops::Drop for SignedInUser { +impl std::ops::Drop for RegisteredUser { fn drop(&mut self) { if self.has_changed { if let Err(e) = self.save() { @@ -315,14 +315,14 @@ impl std::ops::Drop for SignedInUser AsRef for SignedInUser { +impl AsRef for RegisteredUser { fn as_ref(&self) -> &UserData { &self.inner.data } } -impl AsMut for SignedInUser { - /// NOTE: Changes made to the user data won't be persisted until SignedInUser::save +impl AsMut for RegisteredUser { + /// NOTE: Changes made to the user data won't be persisted until RegisteredUser::save /// is called fn as_mut(&mut self) -> &mut UserData { self.has_changed = true; From cdc3921a836cd05a23922c34a80aa61a19a33ef6 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 17:07:33 -0500 Subject: [PATCH 012/113] Refix docs Oh so /that's/ how you disable a lint --- src/user_management/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/user_management/mod.rs b/src/user_management/mod.rs index 1864e8a..029f43c 100644 --- a/src/user_management/mod.rs +++ b/src/user_management/mod.rs @@ -23,6 +23,11 @@ mod manager; pub use manager::UserManager; pub use user::User; pub use manager::CertificateData; +// Imports for docs +#[allow(unused_imports)] +use user::{NotSignedInUser, RegisteredUser}; +#[allow(unused_imports)] +use crate::types::Request; #[derive(Debug)] pub enum UserManagerError { From 766c472cbf1e461597b9cda0514a415ff2091721 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 17:41:32 -0500 Subject: [PATCH 013/113] Added the `add_certificate` and `attach` methods --- src/user_management/user.rs | 108 ++++++++++++++++++++++++++---------- 1 file changed, 78 insertions(+), 30 deletions(-) diff --git a/src/user_management/user.rs b/src/user_management/user.rs index 049c0cc..06617c1 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -113,39 +113,21 @@ impl NotSignedInUser { if self.manager.users.contains_key(username.as_str())? { Err(super::UserManagerError::UsernameNotUnique) } else { - let cert_hash = UserManager::hash_certificate(&self.certificate); - - let newser = RegisteredUser::new( - username.clone(), + let mut newser = RegisteredUser::new( + username, Some(self.certificate.clone()), self.manager, PartialUser { data: UserData::default(), - certificates: vec![cert_hash], + certificates: Vec::with_capacity(1), pass_hash: None, }, ); - let cert_info = CertificateData { - certificate: self.certificate, - owner_username: username, - }; - - let newser_serialized = bincode::serialize(&newser.inner)?; - let cert_info_serialized = bincode::serialize(&cert_info)?; - - (&newser.manager.users, &newser.manager.certificates) - .transaction(|(tx_usr, tx_crt)| { - tx_usr.insert( - newser.username.as_str(), - newser_serialized.clone(), - )?; - tx_crt.insert( - cert_hash.to_le_bytes().as_ref(), - cert_info_serialized.clone(), - )?; - Ok(()) - })?; //TODO + // As a nice bonus, calling add_certificate with a user not yet in the + // database creates the user and adds the certificate in a single transaction. + // Because of this, we can delegate here ^^ + newser.add_certificate(self.certificate)?; Ok(newser) } @@ -158,8 +140,19 @@ impl NotSignedInUser { /// log in with either this certificate or any of the certificates they already had /// registered. /// + /// This method can check the user's password to ensure that they match before + /// registering. If you want to skip this verification, perhaps because you've + /// already verified that this user owns this account, then you can pass [`None`] as + /// the password to skip the password check. + /// /// This method returns the new RegisteredUser instance representing the now-attached - /// user. + /// user, or [`None`] if the username and password didn't match. + /// + /// Because this method both performs a bcrypt verification and a database access, it + /// should be considered expensive. + /// + /// If you already have a [`RegisteredUser`] that you would like to attach a + /// certificate to, consider using [`RegisteredUser::add_certificate()`] /// /// # Errors /// This will error if the username and password are incorrect, or if the user has yet @@ -168,10 +161,23 @@ impl NotSignedInUser { /// Additional errors might occur if an error occurs during database lookup and /// deserialization pub fn attach( - username: impl AsRef<[u8]>, - password: impl AsRef<[u8]>, - ) -> Result> { - todo!() + self, + username: impl AsRef, + password: Option>, + ) -> Result>> { + if let Some(mut user) = self.manager.lookup_user(username)? { + // Perform password check, if caller wants + if let Some(password) = password { + if !user.check_password(password)? { + return Ok(None); + } + } + + user.add_certificate(self.certificate)?; + Ok(Some(user)) + } else { + Ok(None) + } } } @@ -207,6 +213,10 @@ impl RegisteredUser { } /// Update the active certificate + /// + /// This is not to be confused with [`RegisteredUser::add_certificate`], which + /// performs the database operations needed to register a new certificate to a user. + /// This literally just marks the active certificate. pub (crate) fn with_cert(mut self, cert: Certificate) -> Self { self.active_certificate = Some(cert); self @@ -303,6 +313,44 @@ impl RegisteredUser { self.has_changed = false; Ok(()) } + + /// Register a new certificate to this user + /// + /// This adds a new certificate to this user for use in logins. This requires a + /// couple database accesses, one in order to link the user to the certificate, and + /// one in order to link the certificate to the user. + /// + /// If you have a [`NotSignedInUser`] and are looking for a way to link them to an + /// existing user, consider [`NotSignedInUser::attach()`], which contains facilities for + /// password checking and automatically performs the user lookup. + pub fn add_certificate(&mut self, certificate: Certificate) -> Result<()> { + let cert_hash = UserManager::hash_certificate(&certificate); + + self.inner.certificates.push(cert_hash); + + let cert_info = CertificateData { + certificate, + owner_username: self.username.clone(), + }; + + let inner_serialized = bincode::serialize(&self.inner)?; + let cert_info_serialized = bincode::serialize(&cert_info)?; + + (&self.manager.users, &self.manager.certificates) + .transaction(|(tx_usr, tx_crt)| { + tx_usr.insert( + self.username.as_str(), + inner_serialized.clone(), + )?; + tx_crt.insert( + cert_hash.to_le_bytes().as_ref(), + cert_info_serialized.clone(), + )?; + Ok(()) + })?; + + Ok(()) + } } impl std::ops::Drop for RegisteredUser { From 26e0fd2702a9f023823490fc957e34e0064b363a Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 21:37:02 -0500 Subject: [PATCH 014/113] 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 015/113] 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 016/113] 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 017/113] 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 018/113] 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 019/113] 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 020/113] 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 021/113] 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 29c831649dd48cb7e7dddf17956f5bcd2c561000 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 23:39:07 -0500 Subject: [PATCH 022/113] Changed the add_route API to allow the use of simpler, async handlers --- Cargo.toml | 1 - examples/certificates.rs | 64 +++++++++++++++++++--------------------- examples/document.rs | 59 +++++++++++++++++------------------- examples/routing.rs | 20 ++++--------- examples/serve_dir.rs | 13 +++----- src/lib.rs | 10 ++++--- 6 files changed, 73 insertions(+), 94 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9ad991d..14847b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,5 +28,4 @@ mime_guess = { version = "2.0.3", optional = true } [dev-dependencies] env_logger = "0.8.1" -futures-util = "0.3.7" tokio = { version = "0.3.1", features = ["macros", "rt-multi-thread", "sync"] } diff --git a/examples/certificates.rs b/examples/certificates.rs index 143c71c..6fdcafe 100644 --- a/examples/certificates.rs +++ b/examples/certificates.rs @@ -1,6 +1,4 @@ use anyhow::*; -use futures_core::future::BoxFuture; -use futures_util::FutureExt; use log::LevelFilter; use tokio::sync::RwLock; use northstar::{Certificate, GEMINI_MIME, GEMINI_PORT, Request, Response, Server}; @@ -31,44 +29,42 @@ async fn main() -> Result<()> { /// selecting a username. They'll then get a message confirming their account creation. /// Any time this user visits the site in the future, they'll get a personalized welcome /// message. -fn handle_request(users: Arc>>, request: Request) -> BoxFuture<'static, Result> { - async move { - if let Some(Certificate(cert_bytes)) = request.certificate() { - // The user provided a certificate - let users_read = users.read().await; - if let Some(user) = users_read.get(cert_bytes) { - // The user has already registered +async fn handle_request(users: Arc>>, request: Request) -> Result { + if let Some(Certificate(cert_bytes)) = request.certificate() { + // The user provided a certificate + let users_read = users.read().await; + if let Some(user) = users_read.get(cert_bytes) { + // The user has already registered + Ok( + Response::success_with_body( + &GEMINI_MIME, + format!("Welcome {}!", user) + ) + ) + } else { + // The user still needs to register + drop(users_read); + if let Some(query_part) = request.uri().query() { + // The user provided some input (a username request) + let username = query_part.as_str(); + let mut users_write = users.write().await; + users_write.insert(cert_bytes.clone(), username.to_owned()); Ok( Response::success_with_body( &GEMINI_MIME, - format!("Welcome {}!", user) + format!( + "Your account has been created {}! Welcome!", + username + ) ) ) } else { - // The user still needs to register - drop(users_read); - if let Some(query_part) = request.uri().query() { - // The user provided some input (a username request) - let username = query_part.as_str(); - let mut users_write = users.write().await; - users_write.insert(cert_bytes.clone(), username.to_owned()); - Ok( - Response::success_with_body( - &GEMINI_MIME, - format!( - "Your account has been created {}! Welcome!", - username - ) - ) - ) - } else { - // The user didn't provide input, and should be prompted - Response::input("What username would you like?") - } + // The user didn't provide input, and should be prompted + Response::input("What username would you like?") } - } else { - // The user didn't provide a certificate - Ok(Response::client_certificate_required()) } - }.boxed() + } else { + // The user didn't provide a certificate + Ok(Response::client_certificate_required()) + } } diff --git a/examples/document.rs b/examples/document.rs index cc889c6..bc72f49 100644 --- a/examples/document.rs +++ b/examples/document.rs @@ -1,6 +1,4 @@ use anyhow::*; -use futures_core::future::BoxFuture; -use futures_util::FutureExt; use log::LevelFilter; use northstar::{Server, Request, Response, GEMINI_PORT, Document}; use northstar::document::HeadingLevel::*; @@ -17,36 +15,33 @@ async fn main() -> Result<()> { .await } -fn handle_request(_request: Request) -> BoxFuture<'static, Result> { - async move { - let mut document = Document::new(); +async fn handle_request(_request: Request) -> Result { + let mut document = Document::new(); - document - .add_preformatted(include_str!("northstar_logo.txt")) - .add_blank_line() - .add_link("https://docs.rs/northstar", "Documentation") - .add_link("https://github.com/panicbit/northstar", "GitHub") - .add_blank_line() - .add_heading(H1, "Usage") - .add_blank_line() - .add_text("Add the latest version of northstar to your `Cargo.toml`.") - .add_blank_line() - .add_heading(H2, "Manually") - .add_blank_line() - .add_preformatted_with_alt("toml", r#"northstar = "0.3.0" # check crates.io for the latest version"#) - .add_blank_line() - .add_heading(H2, "Automatically") - .add_blank_line() - .add_preformatted_with_alt("sh", "cargo add northstar") - .add_blank_line() - .add_heading(H1, "Generating a key & certificate") - .add_blank_line() - .add_preformatted_with_alt("sh", concat!( - "mkdir cert && cd cert\n", - "openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365", - )); + document + .add_preformatted(include_str!("northstar_logo.txt")) + .add_blank_line() + .add_link("https://docs.rs/northstar", "Documentation") + .add_link("https://github.com/panicbit/northstar", "GitHub") + .add_blank_line() + .add_heading(H1, "Usage") + .add_blank_line() + .add_text("Add the latest version of northstar to your `Cargo.toml`.") + .add_blank_line() + .add_heading(H2, "Manually") + .add_blank_line() + .add_preformatted_with_alt("toml", r#"northstar = "0.3.0" # check crates.io for the latest version"#) + .add_blank_line() + .add_heading(H2, "Automatically") + .add_blank_line() + .add_preformatted_with_alt("sh", "cargo add northstar") + .add_blank_line() + .add_heading(H1, "Generating a key & certificate") + .add_blank_line() + .add_preformatted_with_alt("sh", concat!( + "mkdir cert && cd cert\n", + "openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365", + )); - Ok(Response::document(document)) - } - .boxed() + Ok(Response::document(document)) } diff --git a/examples/routing.rs b/examples/routing.rs index 742a620..4bbf9c3 100644 --- a/examples/routing.rs +++ b/examples/routing.rs @@ -1,6 +1,4 @@ use anyhow::*; -use futures_core::future::BoxFuture; -use futures_util::FutureExt; use log::LevelFilter; use northstar::{Document, document::HeadingLevel, Request, Response, GEMINI_PORT}; @@ -18,25 +16,19 @@ async fn main() -> Result<()> { .await } -fn handle_base(_: Request) -> BoxFuture<'static, Result> { +async fn handle_base(_: Request) -> Result { let doc = generate_doc("base"); - async move { - Ok(Response::document(doc)) - }.boxed() + Ok(Response::document(doc)) } -fn handle_short(_: Request) -> BoxFuture<'static, Result> { +async fn handle_short(_: Request) -> Result { let doc = generate_doc("short"); - async move { - Ok(Response::document(doc)) - }.boxed() + Ok(Response::document(doc)) } -fn handle_long(_: Request) -> BoxFuture<'static, Result> { +async fn handle_long(_: Request) -> Result { let doc = generate_doc("long"); - async move { - Ok(Response::document(doc)) - }.boxed() + Ok(Response::document(doc)) } fn generate_doc(route_name: &str) -> Document { diff --git a/examples/serve_dir.rs b/examples/serve_dir.rs index de3e0b0..bb81add 100644 --- a/examples/serve_dir.rs +++ b/examples/serve_dir.rs @@ -1,6 +1,4 @@ use anyhow::*; -use futures_core::future::BoxFuture; -use futures_util::FutureExt; use log::LevelFilter; use northstar::{Server, Request, Response, GEMINI_PORT}; @@ -16,12 +14,9 @@ async fn main() -> Result<()> { .await } -fn handle_request(request: Request) -> BoxFuture<'static, Result> { - async move { - let path = request.path_segments(); - let response = northstar::util::serve_dir("public", &path).await?; +async fn handle_request(request: Request) -> Result { + let path = request.path_segments(); + let response = northstar::util::serve_dir("public", &path).await?; - Ok(response) - } - .boxed() + Ok(response) } diff --git a/src/lib.rs b/src/lib.rs index 1812967..7f7f4d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ use std::{ path::PathBuf, time::Duration, }; -use futures_core::future::BoxFuture; +use futures_core::future::{BoxFuture, Future}; use tokio::{ prelude::*, io::{self, BufStream}, @@ -290,11 +290,13 @@ 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: H) -> Self + pub fn add_route(mut self, path: &'static str, handler: H) -> Self where - H: Fn(Request) -> HandlerResponse + Send + Sync + 'static, + H: Send + Sync + 'static + Fn(Request) -> F, + F: Send + Sync + 'static + Future> { - self.routes.add_route(path, Arc::new(handler)); + let wrapped = Arc::new(move|req| Box::pin((handler)(req)) as HandlerResponse); + self.routes.add_route(path, wrapped); self } From 5612ce1085883c72cee603fe09d9f04a95966552 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 23:51:25 -0500 Subject: [PATCH 023/113] Removed unnecessary dependency on futures-rs --- Cargo.toml | 1 - src/lib.rs | 5 +++-- src/util.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 14847b4..0f004c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,6 @@ tokio = { version = "0.3.1", features = ["io-util","net","time", "rt"] } mime = "0.3.16" uriparse = "0.6.3" percent-encoding = "2.1.0" -futures-core = "0.3.7" log = "0.4.11" webpki = "0.21.0" lazy_static = "1.4.0" diff --git a/src/lib.rs b/src/lib.rs index 7f7f4d6..b2f97a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,8 +7,9 @@ use std::{ sync::Arc, path::PathBuf, time::Duration, + pin::Pin, }; -use futures_core::future::{BoxFuture, Future}; +use std::future::Future; use tokio::{ prelude::*, io::{self, BufStream}, @@ -36,7 +37,7 @@ pub const REQUEST_URI_MAX_LEN: usize = 1024; pub const GEMINI_PORT: u16 = 1965; pub (crate) type Handler = Arc HandlerResponse + Send + Sync>; -pub (crate) type HandlerResponse = BoxFuture<'static, Result>; +pub (crate) type HandlerResponse = Pin> + Send>>; #[derive(Clone)] pub struct Server { diff --git a/src/util.rs b/src/util.rs index 5c623aa..33ca6d6 100644 --- a/src/util.rs +++ b/src/util.rs @@ -13,7 +13,7 @@ use crate::types::{Document, document::HeadingLevel::*}; use crate::types::Response; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::task::Poll; -use futures_core::future::Future; +use std::future::Future; use tokio::time; #[cfg(feature="serve_dir")] From f0798b66a338fe6fc2293c44983181fbee1b01f5 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 23:52:34 -0500 Subject: [PATCH 024/113] Update changelog for improved handlers --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 435e8ee..8bbbd46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - build time and size by [@Alch-Emi](https://github.com/Alch-Emi) ### Changed - Added route API [@Alch-Emi](https://github.com/Alch-Emi) +- API for adding handlers now accepts async handlers [@Alch-Emi](https://github.com/Alch-Emi) ## [0.3.0] - 2020-11-14 ### Added From 59e3222ce8513b591f43be0b2fac4b6502c6c27a Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Fri, 20 Nov 2020 13:22:34 -0500 Subject: [PATCH 025/113] 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 026/113] 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(); From 349f6da698066accd481dde1017f74718029b9bf Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Fri, 20 Nov 2020 21:15:37 -0500 Subject: [PATCH 027/113] Add rate limiting feature ugghhhhhh I hate how big the governer crate is but there's no alternative besides DIY --- Cargo.toml | 2 ++ src/lib.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 9ad991d..d53d578 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ documentation = "https://docs.rs/northstar" [features] default = ["serve_dir"] serve_dir = ["mime_guess", "tokio/fs"] +rate-limiting = ["governor"] [dependencies] anyhow = "1.0.33" @@ -25,6 +26,7 @@ log = "0.4.11" webpki = "0.21.0" lazy_static = "1.4.0" mime_guess = { version = "2.0.3", optional = true } +governor = { version = "0.3.1", optional = true } [dev-dependencies] env_logger = "0.8.1" diff --git a/src/lib.rs b/src/lib.rs index e957262..eaafc9a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,8 @@ use tokio_rustls::{rustls, TlsAcceptor}; use rustls::*; use anyhow::*; use lazy_static::lazy_static; +#[cfg(feature="rate-limiting")] +use governor::clock::{Clock, DefaultClock}; use crate::util::opt_timeout; use routing::RoutingNode; @@ -30,13 +32,26 @@ pub mod routing; pub use mime; pub use uriparse as uri; +#[cfg(feature="rate-limiting")] +pub use governor::Quota; pub use types::*; pub const REQUEST_URI_MAX_LEN: usize = 1024; pub const GEMINI_PORT: u16 = 1965; +#[cfg(feature="rate-limiting")] +lazy_static! { + static ref CLOCK: DefaultClock = DefaultClock::default(); +} + type Handler = Arc HandlerResponse + Send + Sync>; pub (crate) type HandlerResponse = BoxFuture<'static, Result>; +#[cfg(feature="rate-limiting")] +type RateLimiter = governor::RateLimiter< + std::net::IpAddr, + governor::state::keyed::DefaultKeyedStateStore, + governor::clock::DefaultClock, +>; #[derive(Clone)] pub struct Server { @@ -45,6 +60,8 @@ pub struct Server { routes: Arc>, timeout: Duration, complex_timeout: Option, + #[cfg(feature="rate-limiting")] + rate_limits: Arc>, } impl Server { @@ -67,6 +84,9 @@ impl Server { } async fn serve_client(self, stream: TcpStream) -> Result<()> { + #[cfg(feature="rate-limiting")] + let peer_addr = stream.peer_addr()?.ip(); + let fut_accept_request = async { let stream = self.tls_acceptor.accept(stream).await .context("Failed to establish TLS session")?; @@ -83,6 +103,13 @@ impl Server { let (mut request, mut stream) = fut_accept_request.await .context("Client timed out while waiting for response")??; + #[cfg(feature="rate-limiting")] + if let Some(resp) = self.check_rate_limits(peer_addr, &request) { + self.send_response(resp, &mut stream).await + .context("Failed to send response")?; + return Ok(()) + } + debug!("Client requested: {}", request.uri()); // Identify the client certificate from the tls stream. This is the first @@ -164,6 +191,21 @@ impl Server { Ok(()) } + + #[cfg(feature="rate-limiting")] + fn check_rate_limits(&self, addr: std::net::IpAddr, req: &Request) -> Option { + if let Some((_, limiter)) = self.rate_limits.match_request(req) { + if let Err(when) = limiter.check_key(&addr) { + return Some(Response::new(ResponseHeader { + status: Status::SLOW_DOWN, + meta: Meta::new( + when.wait_time_from(CLOCK.now()).as_secs().to_string() + ).unwrap() + })) + } + } + None + } } pub struct Builder { @@ -173,6 +215,8 @@ pub struct Builder { timeout: Duration, complex_body_timeout_override: Option, routes: RoutingNode, + #[cfg(feature="rate-limiting")] + rate_limits: RoutingNode, } impl Builder { @@ -184,6 +228,8 @@ impl Builder { cert_path: PathBuf::from("cert/cert.pem"), key_path: PathBuf::from("cert/key.pem"), routes: RoutingNode::default(), + #[cfg(feature="rate-limiting")] + rate_limits: RoutingNode::default(), } } @@ -300,6 +346,19 @@ impl Builder { self } + #[cfg(feature="rate-limiting")] + /// Add a rate limit to 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 rate_limit(mut self, path: &'static str, quota: Quota) -> Self { + let limiter = RateLimiter::dashmap_with_clock(quota, &CLOCK); + self.rate_limits.add_route(path, limiter); + self + } + pub async fn serve(mut self) -> Result<()> { let config = tls_config(&self.cert_path, &self.key_path) .context("Failed to create TLS config")?; @@ -315,6 +374,8 @@ impl Builder { routes: Arc::new(self.routes), timeout: self.timeout, complex_timeout: self.complex_body_timeout_override, + #[cfg(feature="rate-limiting")] + rate_limits: Arc::new(self.rate_limits), }; server.serve().await From cd7af1025a41665440dc27ad44c6d558b16bd000 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Fri, 20 Nov 2020 23:51:22 -0500 Subject: [PATCH 028/113] Cleanup typos in routes docs I should really use spellcheck more often --- src/routing.rs | 2 +- src/types/request.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routing.rs b/src/routing.rs index 20708f7..bd2d413 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -33,7 +33,7 @@ impl RoutingNode { /// should be represented as `&["dir", "image.png"]`. /// /// If a match is found, it is returned, along with the segments of the path trailing - /// the subpath matcing the route. For example, a route `/foo` recieving a request to + /// the subpath matching the route. For example, a route `/foo` receiving a request to /// `/foo/bar` would produce `vec!["bar"]` /// /// See [`RoutingNode`] for details on how routes are matched. diff --git a/src/types/request.rs b/src/types/request.rs index 02d841b..56c6475 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -55,7 +55,7 @@ impl Request { /// /// 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 + /// to handlers registered through [`add_route`](crate::Builder::add_route()) will /// always have trailing segments set. pub fn trailing_segments(&self) -> &Vec { self.trailing_segments.as_ref().unwrap() @@ -64,7 +64,7 @@ impl Request { /// 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 + /// "endpoint"]`, no matter what route the handler that received 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. From d5f213b2707f5ed268a6d46f62ffd51f67206bf4 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sat, 21 Nov 2020 17:20:32 -0500 Subject: [PATCH 029/113] Add ratelimiting example Was really supposed to add this earlier but I forgot about it. Oops --- examples/ratelimits.rs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 examples/ratelimits.rs diff --git a/examples/ratelimits.rs b/examples/ratelimits.rs new file mode 100644 index 0000000..b9cf2ab --- /dev/null +++ b/examples/ratelimits.rs @@ -0,0 +1,39 @@ +use anyhow::*; +use futures_core::future::BoxFuture; +use futures_util::FutureExt; +use log::LevelFilter; +use northstar::{Server, Request, Response, GEMINI_PORT, Document}; + +#[tokio::main] +async fn main() -> Result<()> { + env_logger::builder() + .filter_module("northstar", LevelFilter::Debug) + .init(); + + let two = std::num::NonZeroU32::new(2).unwrap(); + + Server::bind(("localhost", GEMINI_PORT)) + .add_route("/", handle_request) + .rate_limit("/limit", northstar::Quota::per_minute(two)) + .serve() + .await +} + +fn handle_request(request: Request) -> BoxFuture<'static, Result> { + async move { + let mut document = Document::new(); + + if let Some("limit") = request.trailing_segments().get(0).map(String::as_str) { + document.add_text("You're on a rate limited page!") + .add_text("You can only access this page twice per minute"); + } else { + document.add_text("You're on a normal page!") + .add_text("You can access this page as much as you like."); + } + document.add_blank_line() + .add_link("/limit", "Go to rate limited page") + .add_link("/", "Go to a page that's not rate limited"); + Ok(Response::document(document)) + } + .boxed() +} From 6e82ae059dc047dbd2b043aa16bef131fe7e2af5 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sat, 21 Nov 2020 23:03:56 -0500 Subject: [PATCH 030/113] Add user management routes --- examples/user_management.rs | 42 +---- src/user_management/mod.rs | 2 + src/user_management/pages/askcert/exists.gmi | 5 + src/user_management/pages/askcert/success.gmi | 11 ++ src/user_management/pages/login/success.gmi | 7 + src/user_management/pages/login/wrong.gmi | 8 + src/user_management/pages/nsi.gmi | 8 + src/user_management/pages/register/exists.gmi | 6 + .../pages/register/success.gmi | 6 + src/user_management/pages/settings.gmi | 3 + src/user_management/pages/unauth.gmi | 9 ++ src/user_management/routes.rs | 143 ++++++++++++++++++ src/user_management/user.rs | 7 +- 13 files changed, 215 insertions(+), 42 deletions(-) create mode 100644 src/user_management/pages/askcert/exists.gmi create mode 100644 src/user_management/pages/askcert/success.gmi create mode 100644 src/user_management/pages/login/success.gmi create mode 100644 src/user_management/pages/login/wrong.gmi create mode 100644 src/user_management/pages/nsi.gmi create mode 100644 src/user_management/pages/register/exists.gmi create mode 100644 src/user_management/pages/register/success.gmi create mode 100644 src/user_management/pages/settings.gmi create mode 100644 src/user_management/pages/unauth.gmi create mode 100644 src/user_management/routes.rs diff --git a/examples/user_management.rs b/examples/user_management.rs index 8f9d570..0f6c1aa 100644 --- a/examples/user_management.rs +++ b/examples/user_management.rs @@ -1,14 +1,11 @@ use anyhow::*; -use futures_core::future::BoxFuture; -use futures_util::FutureExt; use log::LevelFilter; use northstar::{ - GEMINI_MIME, GEMINI_PORT, Request, Response, Server, - user_management::{User, UserManagerError}, + user_management::UserManagementRoutes, }; #[tokio::main] @@ -19,6 +16,7 @@ async fn main() -> Result<()> { Server::bind(("0.0.0.0", GEMINI_PORT)) .add_route("/", handle_request) + .add_um_routes::("/") .serve() .await } @@ -30,38 +28,6 @@ async fn main() -> Result<()> { /// selecting a username. They'll then get a message confirming their account creation. /// Any time this user visits the site in the future, they'll get a personalized welcome /// message. -fn handle_request(request: Request) -> BoxFuture<'static, Result> { - async move { - Ok(match request.user::()? { - User::Unauthenticated => { - Response::client_certificate_required() - }, - User::NotSignedIn(user) => { - if let Some(username) = request.input() { - match user.register::(username.to_owned()) { - Ok(_user) => Response::success(&GEMINI_MIME) - .with_body("Your account has been created!\n=>/ Begin"), - Err(UserManagerError::UsernameNotUnique) => - Response::input_lossy("That username is taken. Try again"), - Err(e) => panic!("Unexpected error: {}", e), - } - } else { - Response::input_lossy("Please pick a username") - } - }, - User::SignedIn(mut user) => { - if request.path_segments()[0].eq("push") { // User connecting to /push - if let Some(push) = request.input() { - user.as_mut().push_str(push); - user.save()?; - } else { - return Ok(Response::input_lossy("Enter a string to push")); - } - } - - Response::success(&GEMINI_MIME) - .with_body(format!("Your current string: {}\n=> /push Push", user.as_ref())) - } - }) - }.boxed() +async fn handle_request(_request: Request) -> Result { + Ok(Response::success_plain("Base handler")) } diff --git a/src/user_management/mod.rs b/src/user_management/mod.rs index 029f43c..face405 100644 --- a/src/user_management/mod.rs +++ b/src/user_management/mod.rs @@ -20,6 +20,8 @@ //! Use of this module requires the `user_management` feature to be enabled pub mod user; mod manager; +mod routes; +pub use routes::UserManagementRoutes; pub use manager::UserManager; pub use user::User; pub use manager::CertificateData; diff --git a/src/user_management/pages/askcert/exists.gmi b/src/user_management/pages/askcert/exists.gmi new file mode 100644 index 0000000..e516676 --- /dev/null +++ b/src/user_management/pages/askcert/exists.gmi @@ -0,0 +1,5 @@ +Hi {username}! + +It looks like you already have an account all set up, and you're good to go. The link below will take you back to the app. + +=> {redirect} Back diff --git a/src/user_management/pages/askcert/success.gmi b/src/user_management/pages/askcert/success.gmi new file mode 100644 index 0000000..a5f5c85 --- /dev/null +++ b/src/user_management/pages/askcert/success.gmi @@ -0,0 +1,11 @@ +Awesome! + +You're certificate was found, and you're good to go. + +If this is your first time, please create an account to get started! + +=> /account/register Sign Up + +If you already have an account, and this is a new certificate that you'd like to link, you can login using your password (if you've set it) below. + +=> /account/login Log In diff --git a/src/user_management/pages/login/success.gmi b/src/user_management/pages/login/success.gmi new file mode 100644 index 0000000..60b3b9f --- /dev/null +++ b/src/user_management/pages/login/success.gmi @@ -0,0 +1,7 @@ +# Success! + +Welcome {username}! + +Your certificate has been linked. + +=> {redirect} Back to app diff --git a/src/user_management/pages/login/wrong.gmi b/src/user_management/pages/login/wrong.gmi new file mode 100644 index 0000000..72f6425 --- /dev/null +++ b/src/user_management/pages/login/wrong.gmi @@ -0,0 +1,8 @@ +# Wrong username or password + +Sorry {username}, + +It looks like that username and password didn't match. + +=> /account/login/{username} Try another password +=> /account/login That's not me! diff --git a/src/user_management/pages/nsi.gmi b/src/user_management/pages/nsi.gmi new file mode 100644 index 0000000..dadba75 --- /dev/null +++ b/src/user_management/pages/nsi.gmi @@ -0,0 +1,8 @@ +Welcome! + +To continue, please create an account, or log in to link this certificate to an existing account. + +=> /account/login Log In +=> /account/register Sign Up + +Note: You can only link a new certificate if you have set a password using your original certificate. If you haven't already, please log in and set a password. diff --git a/src/user_management/pages/register/exists.gmi b/src/user_management/pages/register/exists.gmi new file mode 100644 index 0000000..bba97b2 --- /dev/null +++ b/src/user_management/pages/register/exists.gmi @@ -0,0 +1,6 @@ +# Username Exists + +Unfortunately, it looks like the username {username} is already taken. If this is your account, and you have a password set, you can link this certificate to your account. Otherwise, please choose another username. + +=> /account/register Choose a different username +=> /account/login/{username} Link this certificate diff --git a/src/user_management/pages/register/success.gmi b/src/user_management/pages/register/success.gmi new file mode 100644 index 0000000..c16ca7c --- /dev/null +++ b/src/user_management/pages/register/success.gmi @@ -0,0 +1,6 @@ +# Account Created! + +Welcome {username}! Your account has been created. The link below will take you back to the app, or you can take a moment to review your account settings, including adding a password. + +=> {redirect} Back +=> /account Settings diff --git a/src/user_management/pages/settings.gmi b/src/user_management/pages/settings.gmi new file mode 100644 index 0000000..df6f4c9 --- /dev/null +++ b/src/user_management/pages/settings.gmi @@ -0,0 +1,3 @@ +Welcome {username}! + +=> {redirect} Back to app diff --git a/src/user_management/pages/unauth.gmi b/src/user_management/pages/unauth.gmi new file mode 100644 index 0000000..8073abf --- /dev/null +++ b/src/user_management/pages/unauth.gmi @@ -0,0 +1,9 @@ +Welcome! + +It seems like you don't have a client certificate enabled. In order to log in, you need to connect using a client certificate. If your client supports it, you can use the link below to activate a certificate. + +=> /account/askcert Choose a Certificate + +If your client can't automatically manage client certificates, check the link below for a list of clients that support client certificates. + +=> /account/clients Clients diff --git a/src/user_management/routes.rs b/src/user_management/routes.rs new file mode 100644 index 0000000..6b46a68 --- /dev/null +++ b/src/user_management/routes.rs @@ -0,0 +1,143 @@ +use anyhow::Result; +use tokio::net::ToSocketAddrs; +use serde::{Serialize, de::DeserializeOwned}; + +use crate::{Request, Response}; +use crate::user_management::{User, RegisteredUser, UserManagerError}; + +const UNAUTH: &str = include_str!("pages/unauth.gmi"); + +pub trait UserManagementRoutes: private::Sealed { + fn add_um_routes(self, redir: &'static str) -> Self; +} + +impl UserManagementRoutes for crate::Builder { + fn add_um_routes(self, redir: &'static str) -> Self { + self.add_route("/account", move|r|handle_base::(r, redir)) + .add_route("/account/askcert", move|r|handle_ask_cert::(r, redir)) + .add_route("/account/register", move|r|handle_register::(r, redir)) + .add_route("/account/login", move|r|handle_login::(r, redir)) + } +} + +async fn handle_base(request: Request, redirect: &'static str) -> Result { + Ok(match request.user::()? { + User::Unauthenticated => { + Response::success_gemini(UNAUTH) + }, + User::NotSignedIn(_) => { + Response::success_gemini(include_str!("pages/nsi.gmi")) + }, + User::SignedIn(user) => { + render_settings_menu(user, redirect) + }, + }) +} + +async fn handle_ask_cert(request: Request, redirect: &'static str) -> Result { + Ok(match request.user::()? { + User::Unauthenticated => { + Response::client_certificate_required() + }, + User::NotSignedIn(_) => { + Response::success_gemini(include_str!("pages/askcert/success.gmi")) + }, + User::SignedIn(user) => { + Response::success_gemini(format!( + include_str!("pages/askcert/exists.gmi"), + username = user.username(), + redirect = redirect, + )) + }, + }) +} + +async fn handle_register(request: Request, redirect: &'static str) -> Result { + Ok(match request.user::()? { + User::Unauthenticated => { + Response::success_gemini(UNAUTH) + }, + User::NotSignedIn(nsi) => { + if let Some(username) = request.input() { + match nsi.register::(username.to_owned()) { + Err(UserManagerError::UsernameNotUnique) => { + Response::success_gemini(format!( + include_str!("pages/register/exists.gmi"), + username = username, + )) + }, + Ok(_) => { + Response::success_gemini(format!( + include_str!("pages/register/success.gmi"), + username = username, + redirect = redirect, + )) + }, + Err(e) => return Err(e.into()) + } + } else { + Response::input_lossy("Please pick a username") + } + }, + User::SignedIn(user) => { + render_settings_menu(user, redirect) + }, + }) +} + +async fn handle_login(request: Request, redirect: &'static str) -> Result { + Ok(match request.user::()? { + User::Unauthenticated => { + Response::success_gemini(UNAUTH) + }, + User::NotSignedIn(nsi) => { + if let Some(username) = request.trailing_segments().get(0) { + if let Some(password) = request.input() { + match nsi.attach::(username, Some(password.as_bytes())) { + Err(UserManagerError::PasswordNotSet) | Ok(None) => { + Response::success_gemini(format!( + include_str!("pages/login/wrong.gmi"), + username = username, + )) + }, + Ok(_) => { + Response::success_gemini(format!( + include_str!("pages/login/success.gmi"), + username = username, + redirect = redirect, + )) + }, + Err(e) => return Err(e.into()), + } + } else { + Response::input_lossy("Please enter your password") + } + } else if let Some(username) = request.input() { + Response::redirect_temporary_lossy( + format!("/account/login/{}", username).as_str() + ) + } else { + Response::input_lossy("Please enter your username") + } + }, + User::SignedIn(user) => { + render_settings_menu(user, redirect) + }, + }) +} + +fn render_settings_menu( + user: RegisteredUser, + redirect: &str +) -> Response { + Response::success_gemini(format!( + include_str!("pages/settings.gmi"), + username = user.username(), + redirect = redirect, + )) +} + +mod private { + pub trait Sealed {} + impl Sealed for crate::Builder {} +} diff --git a/src/user_management/user.rs b/src/user_management/user.rs index 06617c1..702d9ab 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -155,15 +155,14 @@ impl NotSignedInUser { /// certificate to, consider using [`RegisteredUser::add_certificate()`] /// /// # Errors - /// This will error if the username and password are incorrect, or if the user has yet - /// to set a password. + /// This will error if the user has yet to set a password. /// /// Additional errors might occur if an error occurs during database lookup and /// deserialization pub fn attach( self, - username: impl AsRef, - password: Option>, + username: &str, + password: Option<&[u8]>, ) -> Result>> { if let Some(mut user) = self.manager.lookup_user(username)? { // Perform password check, if caller wants From 3c7d3457efdcb3dc60a5b086e1f11de0f26d4455 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sun, 22 Nov 2020 00:33:57 -0500 Subject: [PATCH 031/113] Added has_password to RegisteredUser --- src/user_management/user.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/user_management/user.rs b/src/user_management/user.rs index 702d9ab..217f0f8 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -350,6 +350,17 @@ impl RegisteredUser { Ok(()) } + + /// Check if the user has a password set + /// + /// Since authentication is done using client certificates, users aren't required to + /// set a password up front. In some cases, it may be useful to know if a user has or + /// has not set a password yet. + /// + /// This returns `true` if the user has a password set, or `false` otherwise + pub fn has_password(&self) -> bool { + self.inner.pass_hash.is_some() + } } impl std::ops::Drop for RegisteredUser { From 2b5ee337622fc729820173502ca7f9a991a7c2ea Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sun, 22 Nov 2020 00:42:26 -0500 Subject: [PATCH 032/113] Fixed bug where user password was never saved --- src/user_management/pages/settings.gmi | 3 --- src/user_management/user.rs | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 src/user_management/pages/settings.gmi diff --git a/src/user_management/pages/settings.gmi b/src/user_management/pages/settings.gmi deleted file mode 100644 index df6f4c9..0000000 --- a/src/user_management/pages/settings.gmi +++ /dev/null @@ -1,3 +0,0 @@ -Welcome {username}! - -=> {redirect} Back to app diff --git a/src/user_management/user.rs b/src/user_management/user.rs index 217f0f8..b806b7b 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -297,6 +297,7 @@ impl RegisteredUser { )?, salt, )); + self.has_changed = true; Ok(()) } From b03fe0e5f76e815de6f01aa8b6ef66be318be090 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sun, 22 Nov 2020 00:43:15 -0500 Subject: [PATCH 033/113] Add password management facilities --- .../pages/password/success.gmi | 5 ++ src/user_management/routes.rs | 65 +++++++++++++++++-- 2 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 src/user_management/pages/password/success.gmi diff --git a/src/user_management/pages/password/success.gmi b/src/user_management/pages/password/success.gmi new file mode 100644 index 0000000..8c764f8 --- /dev/null +++ b/src/user_management/pages/password/success.gmi @@ -0,0 +1,5 @@ +# Password Updated + +To add a certificate, log in using the new certificate and provide your username and password. + +=> /account Back to settings diff --git a/src/user_management/routes.rs b/src/user_management/routes.rs index 6b46a68..e56c01f 100644 --- a/src/user_management/routes.rs +++ b/src/user_management/routes.rs @@ -2,10 +2,12 @@ use anyhow::Result; use tokio::net::ToSocketAddrs; use serde::{Serialize, de::DeserializeOwned}; -use crate::{Request, Response}; +use crate::{Document, Request, Response}; +use crate::types::document::HeadingLevel; use crate::user_management::{User, RegisteredUser, UserManagerError}; const UNAUTH: &str = include_str!("pages/unauth.gmi"); +const NSI: &str = include_str!("pages/nsi.gmi"); pub trait UserManagementRoutes: private::Sealed { fn add_um_routes(self, redir: &'static str) -> Self; @@ -17,6 +19,7 @@ impl UserManagementRoutes for crate::Builder { .add_route("/account/askcert", move|r|handle_ask_cert::(r, redir)) .add_route("/account/register", move|r|handle_register::(r, redir)) .add_route("/account/login", move|r|handle_login::(r, redir)) + .add_route("/account/password", handle_password::) } } @@ -26,7 +29,7 @@ async fn handle_base(request: Request, r Response::success_gemini(UNAUTH) }, User::NotSignedIn(_) => { - Response::success_gemini(include_str!("pages/nsi.gmi")) + Response::success_gemini(NSI) }, User::SignedIn(user) => { render_settings_menu(user, redirect) @@ -126,15 +129,63 @@ async fn handle_login(request: }) } +async fn handle_password(request: Request) -> Result { + Ok(match request.user::()? { + User::Unauthenticated => { + Response::success_gemini(UNAUTH) + }, + User::NotSignedIn(_) => { + Response::success_gemini(NSI) + }, + User::SignedIn(mut user) => { + if let Some(password) = request.input() { + user.set_password(password)?; + Response::success_gemini(include_str!("pages/password/success.gmi")) + } else { + Response::input( + format!("Please enter a {}password", + if user.has_password() { + "new " + } else { + "" + } + ) + )? + } + }, + }) +} + + fn render_settings_menu( user: RegisteredUser, redirect: &str ) -> Response { - Response::success_gemini(format!( - include_str!("pages/settings.gmi"), - username = user.username(), - redirect = redirect, - )) + Document::new() + .add_heading(HeadingLevel::H1, "User Settings") + .add_blank_line() + .add_text(&format!("Welcome {}!", user.username())) + .add_blank_line() + .add_link(redirect, "Back to the app") + .add_blank_line() + .add_text( + if user.has_password() { + concat!( + "You currently have a password set. This can be used to link any new", + " certificates or clients to your account. If you don't remember your", + " password, or would like to change it, you may do so here.", + ) + } else { + concat!( + "You don't currently have a password set! Without a password, you cannot", + " link any new certificates to your account, and if you lose your current", + " client or certificate, you won't be able to recover your account.", + ) + } + ) + .add_blank_line() + .add_link("/account/password", if user.has_password() { "Change password" } else { "Set password" }) + .into() } mod private { From 0756dd73948b6778b74a4e470b07949fb133fac8 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sun, 22 Nov 2020 00:45:17 -0500 Subject: [PATCH 034/113] Update a few pages --- src/user_management/pages/askcert/exists.gmi | 2 ++ src/user_management/pages/askcert/success.gmi | 4 ++-- src/user_management/pages/nsi.gmi | 2 +- src/user_management/pages/unauth.gmi | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/user_management/pages/askcert/exists.gmi b/src/user_management/pages/askcert/exists.gmi index e516676..e3e1e53 100644 --- a/src/user_management/pages/askcert/exists.gmi +++ b/src/user_management/pages/askcert/exists.gmi @@ -1,3 +1,5 @@ +# Account Exists + Hi {username}! It looks like you already have an account all set up, and you're good to go. The link below will take you back to the app. diff --git a/src/user_management/pages/askcert/success.gmi b/src/user_management/pages/askcert/success.gmi index a5f5c85..bf3d40d 100644 --- a/src/user_management/pages/askcert/success.gmi +++ b/src/user_management/pages/askcert/success.gmi @@ -1,6 +1,6 @@ -Awesome! +# Certificate Found! -You're certificate was found, and you're good to go. +Your certificate was found, and you're good to go. If this is your first time, please create an account to get started! diff --git a/src/user_management/pages/nsi.gmi b/src/user_management/pages/nsi.gmi index dadba75..e8bc713 100644 --- a/src/user_management/pages/nsi.gmi +++ b/src/user_management/pages/nsi.gmi @@ -1,4 +1,4 @@ -Welcome! +# Welcome! To continue, please create an account, or log in to link this certificate to an existing account. diff --git a/src/user_management/pages/unauth.gmi b/src/user_management/pages/unauth.gmi index 8073abf..609ac5c 100644 --- a/src/user_management/pages/unauth.gmi +++ b/src/user_management/pages/unauth.gmi @@ -1,4 +1,4 @@ -Welcome! +# Welcome! It seems like you don't have a client certificate enabled. In order to log in, you need to connect using a client certificate. If your client supports it, you can use the link below to activate a certificate. From 916ac1009cd8f92b127afd3e15de061f39adf294 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sun, 22 Nov 2020 01:05:34 -0500 Subject: [PATCH 035/113] Add docs for add_um_routes --- src/user_management/routes.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/user_management/routes.rs b/src/user_management/routes.rs index e56c01f..6f66b6b 100644 --- a/src/user_management/routes.rs +++ b/src/user_management/routes.rs @@ -9,11 +9,30 @@ use crate::user_management::{User, RegisteredUser, UserManagerError}; const UNAUTH: &str = include_str!("pages/unauth.gmi"); const NSI: &str = include_str!("pages/nsi.gmi"); +/// Import this trait to use [`add_um_routes()`](Self::add_um_routes()) pub trait UserManagementRoutes: private::Sealed { + /// Add pre-configured routes to the serve to handle authentication + /// + /// Specifically, the following routes are added: + /// * `/account`, the main settings & login page + /// * `/account/askcert`, a page which always prompts for a certificate + /// * `/account/register`, for users to register a new account + /// * `/account/login`, for users to link their certificate to an existing account + /// * `/account/password`, to change the user's password + /// + /// If this method is used, no more routes should be added under `/account`. If you + /// would like to direct a user to login from your application, you should send them + /// to `/account`, which will start the login/registration flow. + /// + /// The `redir` argument allows you to specify the point that users will be directed + /// to return to once their account has been created. fn add_um_routes(self, redir: &'static str) -> Self; } impl UserManagementRoutes for crate::Builder { + /// Add pre-configured routes to the serve to handle authentication + /// + /// See [`UserManagementRoutes`] fn add_um_routes(self, redir: &'static str) -> Self { self.add_route("/account", move|r|handle_base::(r, redir)) .add_route("/account/askcert", move|r|handle_ask_cert::(r, redir)) From 502e68f1aaaef7d6824830fb2e89368db65ba275 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sun, 22 Nov 2020 01:29:24 -0500 Subject: [PATCH 036/113] Updated the user management example to be more accessible --- examples/user_management.rs | 106 ++++++++++++++++++++++++++++++++---- 1 file changed, 96 insertions(+), 10 deletions(-) diff --git a/examples/user_management.rs b/examples/user_management.rs index 0f6c1aa..ab88f34 100644 --- a/examples/user_management.rs +++ b/examples/user_management.rs @@ -2,32 +2,118 @@ use anyhow::*; use log::LevelFilter; use northstar::{ GEMINI_PORT, + Document, Request, Response, Server, - user_management::UserManagementRoutes, + user_management::{ + User, + UserManagementRoutes, + }, }; #[tokio::main] +/// An ultra-simple demonstration of authentication. +/// +/// The user should be able to set a secret string that only they can see. They should be +/// able to change this at any time to any thing. Both the string and the user account +/// will persist across restarts. +/// +/// This method sets up and starts the server async fn main() -> Result<()> { + // Turn on logging env_logger::builder() .filter_module("northstar", LevelFilter::Debug) .init(); Server::bind(("0.0.0.0", GEMINI_PORT)) - .add_route("/", handle_request) + + // Add our main routes + .add_route("/", handle_main) + .add_route("/update", handle_update) + + // Add routes for handling user authentication .add_um_routes::("/") + + // Start the server .serve() .await } -/// An ultra-simple demonstration of simple authentication. +/// The landing page /// -/// If the user attempts to connect, they will be prompted to create a client certificate. -/// Once they've made one, they'll be given the opportunity to create an account by -/// selecting a username. They'll then get a message confirming their account creation. -/// Any time this user visits the site in the future, they'll get a personalized welcome -/// message. -async fn handle_request(_request: Request) -> Result { - Ok(Response::success_plain("Base handler")) +/// Displays the user's current secret string, or prompts the user to sign in if they +/// haven't. Includes links to update your string (`/update`) or your account +/// (`/account`). Even though we haven't added an explicit handler for `/account`, this +/// route is managed by northstar. +async fn handle_main(request: Request) -> Result { + + // Check to see if the user is signed in. If they are, we get a copy of their data + // (just a simple [`String`] for this demo, but this can be any serializable struct. + if let User::SignedIn(user) = request.user::()? { + + // If the user is signed in, render and return their page + let response = Document::new() + .add_text("Your personal secret string:") + .add_text(user.as_ref()) + .add_blank_line() + .add_link("/update", "Change your string") + .add_link("/account", "Update your account") + .into(); + Ok(response) + + } else { + + // If the user is not logged in, prompt them to go to /account + let response = Document::new() + .add_text("Please login to use this app.") + .add_blank_line() + .add_link("/account", "Login or create account") + .into(); + Ok(response) + + } +} + +/// The update endpoint +/// +/// Users can update their secret string here. Users who haven't logged in will be +/// promped to do so. +async fn handle_update(request: Request) -> Result { + + // Check if the user is signed in again + if let User::SignedIn(mut user) = request.user::()? { + + // If the user is logged in, check to see if they provided any input. If they + // have, we can set that input as their new string, otherwise we ask them for it + if let Some(string) = request.input() { + + // Update the users data + *user.as_mut() = string.to_owned(); + + // Render a response + let response = Document::new() + .add_text("String updated!") + .add_blank_line() + .add_link("/", "Back") + .into(); + Ok(response) + + } else { + + // Ask the user for some input + Ok(Response::input_lossy("Enter your new string")) + + } + } else { + + // The user isn't logged in, so we should ask them too + let response = Document::new() + .add_text("Please login to use this app.") + .add_blank_line() + .add_link("/account", "Login or create account") + .into(); + Ok(response) + + } } From 7854a2a4c4520c475b74e62d077345e81138ce18 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sun, 22 Nov 2020 02:56:41 -0500 Subject: [PATCH 037/113] Add the add_authenticated_route method I'm really proud of how small the user_management example has gotten --- examples/user_management.rs | 92 ++++++++++++----------------------- src/user_management/routes.rs | 54 +++++++++++++++++++- 2 files changed, 85 insertions(+), 61 deletions(-) diff --git a/examples/user_management.rs b/examples/user_management.rs index ab88f34..964c660 100644 --- a/examples/user_management.rs +++ b/examples/user_management.rs @@ -7,7 +7,7 @@ use northstar::{ Response, Server, user_management::{ - User, + user::RegisteredUser, UserManagementRoutes, }, }; @@ -29,8 +29,8 @@ async fn main() -> Result<()> { Server::bind(("0.0.0.0", GEMINI_PORT)) // Add our main routes - .add_route("/", handle_main) - .add_route("/update", handle_update) + .add_authenticated_route("/", handle_main) + .add_authenticated_route("/update", handle_update) // Add routes for handling user authentication .add_um_routes::("/") @@ -46,74 +46,46 @@ async fn main() -> Result<()> { /// haven't. Includes links to update your string (`/update`) or your account /// (`/account`). Even though we haven't added an explicit handler for `/account`, this /// route is managed by northstar. -async fn handle_main(request: Request) -> Result { - - // Check to see if the user is signed in. If they are, we get a copy of their data - // (just a simple [`String`] for this demo, but this can be any serializable struct. - if let User::SignedIn(user) = request.user::()? { - - // If the user is signed in, render and return their page - let response = Document::new() - .add_text("Your personal secret string:") - .add_text(user.as_ref()) - .add_blank_line() - .add_link("/update", "Change your string") - .add_link("/account", "Update your account") - .into(); - Ok(response) - - } else { - - // If the user is not logged in, prompt them to go to /account - let response = Document::new() - .add_text("Please login to use this app.") - .add_blank_line() - .add_link("/account", "Login or create account") - .into(); - Ok(response) - - } +/// +/// Because this route is registered as an authenticated route, any connections without a +/// certificate will be prompted to add a certificate and register. +async fn handle_main(_req: Request, user: RegisteredUser) -> Result { + // If the user is signed in, render and return their page + let response = Document::new() + .add_text("Your personal secret string:") + .add_text(user.as_ref()) + .add_blank_line() + .add_link("/update", "Change your string") + .add_link("/account", "Update your account") + .into(); + Ok(response) } /// The update endpoint /// -/// Users can update their secret string here. Users who haven't logged in will be -/// promped to do so. -async fn handle_update(request: Request) -> Result { +/// Users can update their secret string here. +async fn handle_update(request: Request, mut user: RegisteredUser) -> Result { - // Check if the user is signed in again - if let User::SignedIn(mut user) = request.user::()? { + // If the user is logged in, check to see if they provided any input. If they + // have, we can set that input as their new string, otherwise we ask them for it + if let Some(string) = request.input() { - // If the user is logged in, check to see if they provided any input. If they - // have, we can set that input as their new string, otherwise we ask them for it - if let Some(string) = request.input() { + // Update the users data + *user.as_mut() = string.to_owned(); - // Update the users data - *user.as_mut() = string.to_owned(); - - // Render a response - let response = Document::new() - .add_text("String updated!") - .add_blank_line() - .add_link("/", "Back") - .into(); - Ok(response) - - } else { - - // Ask the user for some input - Ok(Response::input_lossy("Enter your new string")) - - } - } else { - - // The user isn't logged in, so we should ask them too + // Render a response let response = Document::new() - .add_text("Please login to use this app.") + .add_text("String updated!") .add_blank_line() - .add_link("/account", "Login or create account") + .add_link("/", "Back") .into(); Ok(response) + } else { + + // Ask the user for some input + Ok(Response::input_lossy("Enter your new string")) + } + } diff --git a/src/user_management/routes.rs b/src/user_management/routes.rs index 6f66b6b..41ca2ed 100644 --- a/src/user_management/routes.rs +++ b/src/user_management/routes.rs @@ -2,6 +2,8 @@ use anyhow::Result; use tokio::net::ToSocketAddrs; use serde::{Serialize, de::DeserializeOwned}; +use std::future::Future; + use crate::{Document, Request, Response}; use crate::types::document::HeadingLevel; use crate::user_management::{User, RegisteredUser, UserManagerError}; @@ -27,12 +29,30 @@ pub trait UserManagementRoutes: private::Sealed { /// The `redir` argument allows you to specify the point that users will be directed /// to return to once their account has been created. fn add_um_routes(self, redir: &'static str) -> Self; + + /// Add a special route that requires users to be logged in + /// + /// In addition to the normal [`Request`], your handler will recieve a copy of the + /// [`RegisteredUser`] for the current user. If a user tries to connect to the page + /// without logging in, they will be prompted to register or link an account. + /// + /// To use this method, ensure that [`add_um_routes()`](Self::add_um_routes()) has + /// also been called. + fn add_authenticated_route( + self, + path: &'static str, + handler: Handler, + ) -> Self + where + UserData: Serialize + DeserializeOwned + 'static + Send + Sync, + Handler: Send + Sync + 'static + Fn(Request, RegisteredUser) -> F, + F: Send + Sync + 'static + Future>; } impl UserManagementRoutes for crate::Builder { /// Add pre-configured routes to the serve to handle authentication /// - /// See [`UserManagementRoutes`] + /// See [`UserManagementRoutes::add_um_routes()`] fn add_um_routes(self, redir: &'static str) -> Self { self.add_route("/account", move|r|handle_base::(r, redir)) .add_route("/account/askcert", move|r|handle_ask_cert::(r, redir)) @@ -40,6 +60,38 @@ impl UserManagementRoutes for crate::Builder { .add_route("/account/login", move|r|handle_login::(r, redir)) .add_route("/account/password", handle_password::) } + + /// Add a special route that requires users to be logged in + /// + /// See [`UserManagementRoutes::add_authenticated_route()`] + fn add_authenticated_route( + self, + path: &'static str, + handler: Handler, + ) -> Self + where + UserData: Serialize + DeserializeOwned + 'static + Send + Sync, + Handler: Send + Sync + 'static + Fn(Request, RegisteredUser) -> F, + F: Send + Sync + 'static + Future> + { + let handler = std::sync::Arc::new(handler); + self.add_route(path, move|request| { + let handler = handler.clone(); + async move { + Ok(match request.user::()? { + User::Unauthenticated => { + Response::success_gemini(UNAUTH) + }, + User::NotSignedIn(_) => { + Response::success_gemini(NSI) + }, + User::SignedIn(user) => { + (handler)(request, user).await? + }, + }) + } + }) + } } async fn handle_base(request: Request, redirect: &'static str) -> Result { From 95a4a8d75d17da9adbcaf3b00cbede3ac3a0ec45 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sun, 22 Nov 2020 11:55:33 -0500 Subject: [PATCH 038/113] Reduce number of required `Arc`s Should improve performance because cloning an `Arc` is expensive --- src/lib.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 86bfab4..95d3425 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,13 +36,12 @@ pub use types::*; pub const REQUEST_URI_MAX_LEN: usize = 1024; pub const GEMINI_PORT: u16 = 1965; -type Handler = Arc HandlerResponse + Send + Sync>; +type Handler = Box HandlerResponse + Send + Sync>; pub (crate) type HandlerResponse = Pin> + Send>>; #[derive(Clone)] pub struct Server { tls_acceptor: TlsAcceptor, - listener: Arc, routes: Arc>, timeout: Duration, complex_timeout: Option, @@ -53,9 +52,9 @@ impl Server { Builder::bind(addr) } - async fn serve(self) -> Result<()> { + async fn serve(self, listener: TcpListener) -> Result<()> { loop { - let (stream, _addr) = self.listener.accept().await + let (stream, _addr) = listener.accept().await .context("Failed to accept client")?; let this = self.clone(); @@ -298,7 +297,7 @@ impl Builder { H: Send + Sync + 'static + Fn(Request) -> F, F: Send + Sync + 'static + Future> { - let wrapped = Arc::new(move|req| Box::pin((handler)(req)) as HandlerResponse); + let wrapped = Box::new(move|req| Box::pin((handler)(req)) as HandlerResponse); self.routes.add_route(path, wrapped); self } @@ -314,13 +313,12 @@ impl Builder { let server = Server { tls_acceptor: TlsAcceptor::from(config), - listener: Arc::new(listener), routes: Arc::new(self.routes), timeout: self.timeout, complex_timeout: self.complex_body_timeout_override, }; - server.serve().await + server.serve(listener).await } } From 9916770bd266e05b3a3ba87527a3d9f18105ce86 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sun, 22 Nov 2020 20:56:15 -0500 Subject: [PATCH 039/113] Added add_authenticated_input_route --- examples/user_management.rs | 34 ++++++++------------ src/user_management/routes.rs | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 21 deletions(-) diff --git a/examples/user_management.rs b/examples/user_management.rs index 964c660..3329b40 100644 --- a/examples/user_management.rs +++ b/examples/user_management.rs @@ -30,7 +30,7 @@ async fn main() -> Result<()> { // Add our main routes .add_authenticated_route("/", handle_main) - .add_authenticated_route("/update", handle_update) + .add_authenticated_input_route("/update", "Enter your new string:", handle_update) // Add routes for handling user authentication .add_um_routes::("/") @@ -64,28 +64,20 @@ async fn handle_main(_req: Request, user: RegisteredUser) -> Result) -> Result { +async fn handle_update(_request: Request, mut user: RegisteredUser, input: String) -> Result { - // If the user is logged in, check to see if they provided any input. If they - // have, we can set that input as their new string, otherwise we ask them for it - if let Some(string) = request.input() { + // The user has already been prompted to log in if they weren't and asked to give an + // input string, so all we need to do is... - // Update the users data - *user.as_mut() = string.to_owned(); + // Update the users data + *user.as_mut() = input; - // Render a response - let response = Document::new() - .add_text("String updated!") - .add_blank_line() - .add_link("/", "Back") - .into(); - Ok(response) - - } else { - - // Ask the user for some input - Ok(Response::input_lossy("Enter your new string")) - - } + // Render a response + let response = Document::new() + .add_text("String updated!") + .add_blank_line() + .add_link("/", "Back") + .into(); + Ok(response) } diff --git a/src/user_management/routes.rs b/src/user_management/routes.rs index 2225cb1..11675a6 100644 --- a/src/user_management/routes.rs +++ b/src/user_management/routes.rs @@ -47,6 +47,38 @@ pub trait UserManagementRoutes: private::Sealed { UserData: Serialize + DeserializeOwned + 'static + Send + Sync, Handler: Clone + Send + Sync + 'static + Fn(Request, RegisteredUser) -> F, F: Send + Sync + 'static + Future>; + + /// Add a special route that requires users to be logged in AND takes input + /// + /// Like with [`add_authenticated_route()`](Self::add_authenticated_route()), this + /// prompts the user to log in if they haven't already, but additionally prompts the + /// user for input before running the handler with both the user object and the input + /// they provided. + /// + /// To a user, this might look something like this: + /// * Click a link to `/your/route` + /// * See a screen asking you to sign in or create an account + /// * Create a new account, and return to the app. + /// * Now, clicking the link shows the prompt provided. + /// * After entering some value, the user receives the response from the handler. + /// + /// For a user whose already logged in, this will just look like a normal input route, + /// where they enter some query and see a page. This method just takes the burden of + /// having to check if the user sent a query string and respond with an INPUT response + /// if not. + /// + /// To use this method, ensure that [`add_um_routes()`](Self::add_um_routes()) has + /// also been called. + fn add_authenticated_input_route( + self, + path: &'static str, + prompt: &'static str, + handler: Handler, + ) -> Self + where + UserData: Serialize + DeserializeOwned + 'static + Send + Sync, + Handler: Clone + Send + Sync + 'static + Fn(Request, RegisteredUser, String) -> F, + F: Send + Sync + 'static + Future>; } impl UserManagementRoutes for crate::Builder { @@ -91,6 +123,32 @@ impl UserManagementRoutes for crate::Builder { } }) } + + /// Add a special route that requires users to be logged in AND takes input + /// + /// See [`UserManagementRoutes::add_authenticated_input_route()`] + fn add_authenticated_input_route( + self, + path: &'static str, + prompt: &'static str, + handler: Handler, + ) -> Self + where + UserData: Serialize + DeserializeOwned + 'static + Send + Sync, + Handler: Clone + Send + Sync + 'static + Fn(Request, RegisteredUser, String) -> F, + F: Send + Sync + 'static + Future> + { + self.add_authenticated_route(path, move|request, user| { + let handler = handler.clone(); + async move { + if let Some(input) = request.input().map(str::to_owned) { + (handler.clone())(request, user, input).await + } else { + Response::input(prompt) + } + } + }) + } } async fn handle_base(request: Request, redirect: &'static str) -> Result { From 28a4d64c5ffd82fd3214ad6f1e9467ed59ea0116 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sun, 22 Nov 2020 22:24:36 -0500 Subject: [PATCH 040/113] Segregate features surrounding multiple certificates & passwords to a seperate feature --- Cargo.toml | 3 +- src/user_management/mod.rs | 4 ++ src/user_management/routes.rs | 75 ++++++++++++++++++++++++++--------- src/user_management/user.rs | 8 ++++ 4 files changed, 71 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3163cfa..e434fab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,8 @@ repository = "https://github.com/panicbit/northstar" documentation = "https://docs.rs/northstar" [features] -user_management = ["sled", "bincode", "serde/derive", "rust-argon2", "crc32fast", "ring"] +user_management = ["sled", "bincode", "serde/derive", "crc32fast"] +user_management_advanced = ["rust-argon2", "ring", "user_management"] default = ["serve_dir"] serve_dir = ["mime_guess", "tokio/fs"] diff --git a/src/user_management/mod.rs b/src/user_management/mod.rs index face405..ce51e6d 100644 --- a/src/user_management/mod.rs +++ b/src/user_management/mod.rs @@ -38,6 +38,7 @@ pub enum UserManagerError { DatabaseError(sled::Error), DatabaseTransactionError(sled::transaction::TransactionError), DeserializeError(bincode::Error), + #[cfg(feature = "user_management_advanced")] Argon2Error(argon2::Error), } @@ -59,6 +60,7 @@ impl From for UserManagerError { } } +#[cfg(feature = "user_management_advanced")] impl From for UserManagerError { fn from(error: argon2::Error) -> Self { Self::Argon2Error(error) @@ -71,6 +73,7 @@ impl std::error::Error for UserManagerError { Self::DatabaseError(e) => Some(e), Self::DatabaseTransactionError(e) => Some(e), Self::DeserializeError(e) => Some(e), + #[cfg(feature = "user_management_advanced")] Self::Argon2Error(e) => Some(e), _ => None } @@ -90,6 +93,7 @@ impl std::fmt::Display for UserManagerError { write!(f, "Error accessing the user database: {}", e), Self::DeserializeError(e) => write!(f, "Recieved messy data from database, possible corruption: {}", e), + #[cfg(feature = "user_management_advanced")] Self::Argon2Error(e) => write!(f, "Argon2 Error, likely malformed password hash, possible database corruption: {}", e), } diff --git a/src/user_management/routes.rs b/src/user_management/routes.rs index 11675a6..31ddbf3 100644 --- a/src/user_management/routes.rs +++ b/src/user_management/routes.rs @@ -9,7 +9,10 @@ use crate::types::document::HeadingLevel; use crate::user_management::{User, RegisteredUser, UserManagerError}; const UNAUTH: &str = include_str!("pages/unauth.gmi"); +#[cfg(feature = "user_management_advanced")] const NSI: &str = include_str!("pages/nsi.gmi"); +#[cfg(not(feature = "user_management_advanced"))] +const NSI: &str = include_str!("pages/nopass/nsi.gmi"); /// Import this trait to use [`add_um_routes()`](Self::add_um_routes()) pub trait UserManagementRoutes: private::Sealed { @@ -86,11 +89,18 @@ impl UserManagementRoutes for crate::Builder { /// /// See [`UserManagementRoutes::add_um_routes()`] fn add_um_routes(self, redir: &'static str) -> Self { - self.add_route("/account", move|r|handle_base::(r, redir)) + #[allow(unused_mut)] + let mut modified_self = self.add_route("/account", move|r|handle_base::(r, redir)) .add_route("/account/askcert", move|r|handle_ask_cert::(r, redir)) - .add_route("/account/register", move|r|handle_register::(r, redir)) - .add_route("/account/login", move|r|handle_login::(r, redir)) - .add_route("/account/password", handle_password::) + .add_route("/account/register", move|r|handle_register::(r, redir)); + + #[cfg(feature = "user_management_advanced")] { + modified_self = modified_self + .add_route("/account/login", move|r|handle_login::(r, redir)) + .add_route("/account/password", handle_password::); + } + + modified_self } /// Add a special route that requires users to be logged in @@ -171,7 +181,12 @@ async fn handle_ask_cert(request: Reques Response::client_certificate_required() }, User::NotSignedIn(_) => { - Response::success_gemini(include_str!("pages/askcert/success.gmi")) + #[cfg(feature = "user_management_advanced")] { + Response::success_gemini(include_str!("pages/askcert/success.gmi")) + } + #[cfg(not(feature = "user_management_advanced"))] { + Response::success_gemini(include_str!("pages/nopass/askcert/success.gmi")) + } }, User::SignedIn(user) => { Response::success_gemini(format!( @@ -192,17 +207,34 @@ async fn handle_register(reque if let Some(username) = request.input() { match nsi.register::(username.to_owned()) { Err(UserManagerError::UsernameNotUnique) => { - Response::success_gemini(format!( - include_str!("pages/register/exists.gmi"), - username = username, - )) + #[cfg(feature = "user_management_advanced")] { + Response::success_gemini(format!( + include_str!("pages/register/exists.gmi"), + username = username, + )) + } + #[cfg(not(feature = "user_management_advanced"))] { + Response::success_gemini(format!( + include_str!("pages/register/exists.gmi"), + username = username, + )) + } }, Ok(_) => { - Response::success_gemini(format!( - include_str!("pages/register/success.gmi"), - username = username, - redirect = redirect, - )) + #[cfg(feature = "user_management_advanced")] { + Response::success_gemini(format!( + include_str!("pages/register/success.gmi"), + username = username, + redirect = redirect, + )) + } + #[cfg(not(feature = "user_management_advanced"))] { + Response::success_gemini(format!( + include_str!("pages/nopass/register/success.gmi"), + username = username, + redirect = redirect, + )) + } }, Err(e) => return Err(e.into()) } @@ -216,6 +248,7 @@ async fn handle_register(reque }) } +#[cfg(feature = "user_management_advanced")] async fn handle_login(request: Request, redirect: &'static str) -> Result { Ok(match request.user::()? { User::Unauthenticated => { @@ -257,6 +290,7 @@ async fn handle_login(request: }) } +#[cfg(feature = "user_management_advanced")] async fn handle_password(request: Request) -> Result { Ok(match request.user::()? { User::Unauthenticated => { @@ -289,13 +323,17 @@ fn render_settings_menu( user: RegisteredUser, redirect: &str ) -> Response { - Document::new() + let mut document = Document::new(); + document .add_heading(HeadingLevel::H1, "User Settings") .add_blank_line() .add_text(&format!("Welcome {}!", user.username())) .add_blank_line() .add_link(redirect, "Back to the app") - .add_blank_line() + .add_blank_line(); + + #[cfg(feature = "user_management_advanced")] + document .add_text( if user.has_password() { concat!( @@ -312,8 +350,9 @@ fn render_settings_menu( } ) .add_blank_line() - .add_link("/account/password", if user.has_password() { "Change password" } else { "Set password" }) - .into() + .add_link("/account/password", if user.has_password() { "Change password" } else { "Set password" }); + + document.into() } mod private { diff --git a/src/user_management/user.rs b/src/user_management/user.rs index b806b7b..8894462 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -21,6 +21,7 @@ use crate::user_management::UserManager; use crate::user_management::Result; use crate::user_management::manager::CertificateData; +#[cfg(feature = "user_management_advanced")] const ARGON2_CONFIG: argon2::Config = argon2::Config { ad: &[], hash_length: 32, @@ -33,6 +34,7 @@ const ARGON2_CONFIG: argon2::Config = argon2::Config { version: argon2::Version::Version13, }; +#[cfg(feature = "user_management_advanced")] lazy_static::lazy_static! { static ref RANDOM: ring::rand::SystemRandom = ring::rand::SystemRandom::new(); } @@ -45,6 +47,7 @@ lazy_static::lazy_static! { pub (crate) struct PartialUser { pub data: UserData, pub certificates: Vec, + #[cfg(feature = "user_management_advanced")] pub pass_hash: Option<(Vec, [u8; 32])>, } @@ -120,6 +123,7 @@ impl NotSignedInUser { PartialUser { data: UserData::default(), certificates: Vec::with_capacity(1), + #[cfg(feature = "user_management_advanced")] pass_hash: None, }, ); @@ -133,6 +137,7 @@ impl NotSignedInUser { } } + #[cfg(feature = "user_management_advanced")] /// Attach this certificate to an existing user /// /// Try to add this certificate to another user using a username and password. If @@ -249,6 +254,7 @@ impl RegisteredUser { &self.username } + #[cfg(feature = "user_management_advanced")] /// Check a password against the user's password hash /// /// # Errors @@ -270,6 +276,7 @@ impl RegisteredUser { } } + #[cfg(feature = "user_management_advanced")] /// Set's the password for this user /// /// By default, users have no password, meaning the cannot add any certificates beyond @@ -352,6 +359,7 @@ impl RegisteredUser { Ok(()) } + #[cfg(feature = "user_management_advanced")] /// Check if the user has a password set /// /// Since authentication is done using client certificates, users aren't required to From 2400ef87960d1b6aeb43376123232201c929d990 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Mon, 23 Nov 2020 10:01:53 -0500 Subject: [PATCH 041/113] Move user management routes to their seperate feature --- Cargo.toml | 3 ++- src/user_management/mod.rs | 2 ++ src/user_management/pages/nopass/askcert/success.gmi | 5 +++++ src/user_management/pages/nopass/nsi.gmi | 5 +++++ src/user_management/pages/nopass/register/exists.gmi | 5 +++++ src/user_management/pages/nopass/register/success.gmi | 5 +++++ 6 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/user_management/pages/nopass/askcert/success.gmi create mode 100644 src/user_management/pages/nopass/nsi.gmi create mode 100644 src/user_management/pages/nopass/register/exists.gmi create mode 100644 src/user_management/pages/nopass/register/success.gmi diff --git a/Cargo.toml b/Cargo.toml index e434fab..371d46d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ documentation = "https://docs.rs/northstar" [features] user_management = ["sled", "bincode", "serde/derive", "crc32fast"] user_management_advanced = ["rust-argon2", "ring", "user_management"] +user_management_routes = ["user_management"] default = ["serve_dir"] serve_dir = ["mime_guess", "tokio/fs"] @@ -35,7 +36,7 @@ ring = { version = "0.16.15", optional = true } [[example]] name = "user_management" -required-features = ["user_management"] +required-features = ["user_management_routes"] [dev-dependencies] env_logger = "0.8.1" diff --git a/src/user_management/mod.rs b/src/user_management/mod.rs index ce51e6d..75b0802 100644 --- a/src/user_management/mod.rs +++ b/src/user_management/mod.rs @@ -20,7 +20,9 @@ //! Use of this module requires the `user_management` feature to be enabled pub mod user; mod manager; +#[cfg(feature = "user_management_routes")] mod routes; +#[cfg(feature = "user_management_routes")] pub use routes::UserManagementRoutes; pub use manager::UserManager; pub use user::User; diff --git a/src/user_management/pages/nopass/askcert/success.gmi b/src/user_management/pages/nopass/askcert/success.gmi new file mode 100644 index 0000000..6045292 --- /dev/null +++ b/src/user_management/pages/nopass/askcert/success.gmi @@ -0,0 +1,5 @@ +# Certificate Found! + +Your certificate was found, all that's left to do is pick a username! + +=> /account/register Sign Up diff --git a/src/user_management/pages/nopass/nsi.gmi b/src/user_management/pages/nopass/nsi.gmi new file mode 100644 index 0000000..4d574cd --- /dev/null +++ b/src/user_management/pages/nopass/nsi.gmi @@ -0,0 +1,5 @@ +# Welcome! + +To continue, please create an account. + +=> /account/register Set up my account diff --git a/src/user_management/pages/nopass/register/exists.gmi b/src/user_management/pages/nopass/register/exists.gmi new file mode 100644 index 0000000..0dab48a --- /dev/null +++ b/src/user_management/pages/nopass/register/exists.gmi @@ -0,0 +1,5 @@ +# Username Exists + +Unfortunately, it looks like the username {username} is already taken. + +=> /account/register Choose a different username diff --git a/src/user_management/pages/nopass/register/success.gmi b/src/user_management/pages/nopass/register/success.gmi new file mode 100644 index 0000000..593a22d --- /dev/null +++ b/src/user_management/pages/nopass/register/success.gmi @@ -0,0 +1,5 @@ +# Account Created! + +Welcome {username}! Your account has been created. + +=> {redirect} Back to app From 79907398847ad8fa067f67bc0916718a6bf012c8 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Mon, 23 Nov 2020 11:55:40 -0500 Subject: [PATCH 042/113] Move the handler type to it's own mod, change to an enum The new enum can be converted to from anything that could previously be passed to add_route, so this is not a breaking change. If fact, from the end user's perspective, nothing changed, but internally, this gives us a lot of potential as far as having multiple types of routes. --- src/handling.rs | 114 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 29 +++--------- src/util.rs | 31 ------------- 3 files changed, 119 insertions(+), 55 deletions(-) create mode 100644 src/handling.rs diff --git a/src/handling.rs b/src/handling.rs new file mode 100644 index 0000000..244efa0 --- /dev/null +++ b/src/handling.rs @@ -0,0 +1,114 @@ +//! Types for handling requests +//! +//! The main type is the [`Handler`], which wraps a more specific type of handler and +//! manages delegating responses to it. +//! +//! For most purposes, you should never have to manually create any of these structs +//! yourself, though it may be useful to look at the implementations of [`From`] on +//! [`Handler`], as these are the things that can be used as handlers for routes. +use anyhow::Result; + +use std::{ + pin::Pin, + future::Future, + task::Poll, + panic::{catch_unwind, AssertUnwindSafe}, +}; + +use crate::types::{Response, Request}; + +/// A struct representing something capable of handling a request. +/// +/// In the future, this may have multiple varieties, but at the minute, it just wraps an +/// [`Fn`](std::ops::Fn). +/// +/// The most useful part of the documentation for this is the implementations of [`From`] +/// on it, as this is what can be passed to +/// [`Builder::add_route`](crate::Builder::add_route) in order to create a new route. +/// Each implementation has bespoke docs that describe how the type is used, and what +/// response is produced. +pub enum Handler { + FnHandler(HandlerInner), +} + +/// Since we can't store train objects, we need to wrap fn handlers in a box +type HandlerInner = Box HandlerResponse + Send + Sync>; +/// Same with dyn Futures +type HandlerResponse = Pin> + Send>>; + +impl Handler { + /// Handle an incoming request + /// + /// This delegates to the request to the appropriate method of handling it, whether + /// that's fetching a file or directory listing, cloning a static response, or handing + /// the request to a wrapped handler function. + /// + /// Any unexpected errors that occur will be printed to the log and potentially + /// reported to the user, depending on the handler type. + pub async fn handle(&self, request: Request) -> Response { + match self { + Self::FnHandler(inner) => { + let fut_handle = (inner)(request); + let fut_handle = AssertUnwindSafe(fut_handle); + + HandlerCatchUnwind::new(fut_handle).await + .unwrap_or_else(|err| { + error!("Handler failed: {:?}", err); + Response::server_error("").unwrap() + }) + }, + } + } +} + +impl From for Handler +where + H: 'static + Fn(Request) -> R + Send + Sync, + R: 'static + Future> + Send, +{ + /// Wrap an [`Fn`] in a [`Handler`] struct + /// + /// This automatically boxes both the [`Fn`] and the [`Fn`]'s response. + /// + /// Any requests passed to the handler will be directly handed down to the handler, + /// with the request as the first argument. The response provided will be sent to the + /// requester. If the handler panics or returns an [`Err`], this will be logged, and + /// the requester will be sent a [`SERVER_ERROR`](Response::server_error()). + fn from(handler: H) -> Self { + Self::FnHandler( + Box::new(move|req| Box::pin((handler)(req)) as HandlerResponse) + ) + } +} + +/// A utility for catching unwinds on Futures. +/// +/// This is adapted from the futures-rs CatchUnwind, in an effort to reduce the large +/// amount of dependencies tied into the feature that provides this simple struct. +#[must_use = "futures do nothing unless polled"] +struct HandlerCatchUnwind { + future: AssertUnwindSafe, +} + +impl HandlerCatchUnwind { + fn new(future: AssertUnwindSafe) -> Self { + Self { future } + } +} + +impl Future for HandlerCatchUnwind { + type Output = Result; + + fn poll( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context + ) -> Poll { + match catch_unwind(AssertUnwindSafe(|| self.future.as_mut().poll(cx))) { + Ok(res) => res, + Err(e) => { + error!("Handler panic! {:?}", e); + Poll::Ready(Response::server_error("")) + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 1926bfe..9ef0aad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,12 @@ #[macro_use] extern crate log; use std::{ - panic::AssertUnwindSafe, convert::TryFrom, io::BufReader, sync::Arc, path::PathBuf, time::Duration, - pin::Pin, }; -use std::future::Future; use tokio::{ prelude::*, io::{self, BufStream}, @@ -29,6 +26,7 @@ use routing::RoutingNode; pub mod types; pub mod util; pub mod routing; +pub mod handling; pub use mime; pub use uriparse as uri; @@ -37,8 +35,7 @@ pub use types::*; pub const REQUEST_URI_MAX_LEN: usize = 1024; pub const GEMINI_PORT: u16 = 1965; -type Handler = Box HandlerResponse + Send + Sync>; -pub (crate) type HandlerResponse = Pin> + Send>>; +use handling::Handler; #[derive(Clone)] pub struct Server { @@ -97,19 +94,8 @@ impl Server { request.set_cert(client_cert); let response = if let Some((trailing, handler)) = self.routes.match_request(&request) { - request.set_trailing(trailing); - - let handler = (handler)(request); - let handler = AssertUnwindSafe(handler); - - 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")? + handler.handle(request).await } else { Response::not_found() }; @@ -293,13 +279,8 @@ 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: H) -> Self - where - H: Send + Sync + 'static + Fn(Request) -> F, - F: Send + Sync + 'static + Future> - { - let wrapped = Box::new(move|req| Box::pin((handler)(req)) as HandlerResponse); - self.routes.add_route(path, wrapped); + pub fn add_route(mut self, path: &'static str, handler: impl Into) -> Self { + self.routes.add_route(path, handler.into()); self } diff --git a/src/util.rs b/src/util.rs index 33ca6d6..945e2f7 100644 --- a/src/util.rs +++ b/src/util.rs @@ -11,8 +11,6 @@ use tokio::{ #[cfg(feature="serve_dir")] use crate::types::{Document, document::HeadingLevel::*}; use crate::types::Response; -use std::panic::{catch_unwind, AssertUnwindSafe}; -use std::task::Poll; use std::future::Future; use tokio::time; @@ -128,35 +126,6 @@ where T: ToOwned + ?Sized, {} -/// A utility for catching unwinds on Futures. -/// -/// This is adapted from the futures-rs CatchUnwind, in an effort to reduce the large -/// amount of dependencies tied into the feature that provides this simple struct. -#[must_use = "futures do nothing unless polled"] -pub (crate) struct HandlerCatchUnwind { - future: AssertUnwindSafe, -} - -impl HandlerCatchUnwind { - pub(super) fn new(future: AssertUnwindSafe) -> Self { - Self { future } - } -} - -impl Future for HandlerCatchUnwind { - type Output = Result, Box>; - - fn poll( - mut self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context - ) -> Poll { - match catch_unwind(AssertUnwindSafe(|| self.future.as_mut().poll(cx))) { - Ok(res) => res.map(Ok), - Err(e) => Poll::Ready(Err(e)) - } - } -} - pub(crate) async fn opt_timeout(duration: Option, future: impl Future) -> Result { match duration { Some(duration) => time::timeout(duration, future).await, From c3d738186009ed7662056c0b85467019fc6c155d Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Mon, 23 Nov 2020 23:30:59 -0500 Subject: [PATCH 043/113] Added a static handler type. Impl AsRef for Response im so tired i havent slept enough in so long but also i havent done any productive work like do you think im using this project as an excuse or like a bad coping mechanism for my mental health or something like that cause thats what its starting to feel like --- examples/document.rs | 17 +++++------- src/handling.rs | 61 ++++++++++++++++++++++++++++++++++++++++++- src/types/response.rs | 12 +++++++++ src/util.rs | 1 + 4 files changed, 80 insertions(+), 11 deletions(-) diff --git a/examples/document.rs b/examples/document.rs index 9d4bdc2..5ea2678 100644 --- a/examples/document.rs +++ b/examples/document.rs @@ -1,6 +1,6 @@ use anyhow::*; use log::LevelFilter; -use northstar::{Server, Request, Response, GEMINI_PORT, Document}; +use northstar::{Server, Response, GEMINI_PORT, Document}; use northstar::document::HeadingLevel::*; #[tokio::main] @@ -9,14 +9,7 @@ async fn main() -> Result<()> { .filter_module("northstar", LevelFilter::Debug) .init(); - Server::bind(("localhost", GEMINI_PORT)) - .add_route("/",handle_request) - .serve() - .await -} - -async fn handle_request(_request: Request) -> Result { - let response = Document::new() + let response: Response = Document::new() .add_preformatted(include_str!("northstar_logo.txt")) .add_blank_line() .add_link("https://docs.rs/northstar", "Documentation") @@ -41,5 +34,9 @@ async fn handle_request(_request: Request) -> Result { "openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365", )) .into(); - Ok(response) + + Server::bind(("localhost", GEMINI_PORT)) + .add_route("/", response) + .serve() + .await } diff --git a/src/handling.rs b/src/handling.rs index 244efa0..b8bb811 100644 --- a/src/handling.rs +++ b/src/handling.rs @@ -15,7 +15,7 @@ use std::{ panic::{catch_unwind, AssertUnwindSafe}, }; -use crate::types::{Response, Request}; +use crate::{Document, types::{Body, Response, Request}}; /// A struct representing something capable of handling a request. /// @@ -29,6 +29,7 @@ use crate::types::{Response, Request}; /// response is produced. pub enum Handler { FnHandler(HandlerInner), + StaticHandler(Response), } /// Since we can't store train objects, we need to wrap fn handlers in a box @@ -57,6 +58,26 @@ impl Handler { Response::server_error("").unwrap() }) }, + Self::StaticHandler(response) => { + let body = response.as_ref(); + match body { + None => Response::new(response.header().clone()), + Some(Body::Bytes(bytes)) => { + Response::new(response.header().clone()) + .with_body(bytes.clone()) + }, + _ => { + error!(concat!( + "Cannot construct a static handler with a reader-based body! ", + " We're sending a response so that the client doesn't crash, but", + " given that this is a release build you should really fix this." + )); + Response::server_error( + "Very bad server error, go tell the sysadmin to look at the logs." + ).unwrap() + } + } + } } } } @@ -81,6 +102,44 @@ where } } +// We tolerate a fallible `impl From` because this is *really* not the kind of thing the +// user should be catching in runtime. +#[allow(clippy::fallible_impl_from)] +impl From for Handler { + /// Serve an unchanging response + /// + /// Any and all requests to this handler will be responded to with the same response, + /// no matter what. This is good for static content that is provided by your app. + /// For serving files & directories, try looking at creating a handler from a path + /// + /// ## Panics + /// This response type **CANNOT** be created using Responses with [`Reader`] bodies. + /// Attempting to do this will cause a panic. Don't. + /// + /// [`Reader`]: Body::Reader + fn from(response: Response) -> Self { + #[cfg(debug_assertions)] { + // We have another check once the handler is actually called that is not + // disabled for release builds + if let Some(Body::Reader(_)) = response.as_ref() { + panic!("Cannot construct a static handler with a reader-based body"); + } + } + Self::StaticHandler(response) + } +} + +impl From<&Document> for Handler { + /// Serve an unchanging response, shorthand for From + /// + /// This document will be sent in response to any requests that arrive at this + /// handler. As with all documents, this will be a successful response with a + /// `text/gemini` MIME. + fn from(doc: &Document) -> Self { + Self::StaticHandler(doc.into()) + } +} + /// A utility for catching unwinds on Futures. /// /// This is adapted from the futures-rs CatchUnwind, in an effort to reduce the large diff --git a/src/types/response.rs b/src/types/response.rs index dceec4e..3964ae1 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -96,6 +96,18 @@ impl Response { } } +impl AsRef> for Response { + fn as_ref(&self) -> &Option { + &self.body + } +} + +impl AsMut> for Response { + fn as_mut(&mut self) -> &mut Option { + &mut self.body + } +} + impl> From for Response { fn from(doc: D) -> Self { Self::document(doc) diff --git a/src/util.rs b/src/util.rs index 945e2f7..1fe5591 100644 --- a/src/util.rs +++ b/src/util.rs @@ -10,6 +10,7 @@ use tokio::{ }; #[cfg(feature="serve_dir")] use crate::types::{Document, document::HeadingLevel::*}; +#[cfg(feature="serve_dir")] use crate::types::Response; use std::future::Future; use tokio::time; From df2350a8bb88c9a769a957ad7b841c857868b347 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Tue, 24 Nov 2020 13:58:18 -0500 Subject: [PATCH 044/113] Switch to a much lighter in-house rate-limiting solution, and use consistant naming of ratelimiting --- Cargo.toml | 4 +- examples/{ratelimits.rs => ratelimiting.rs} | 6 +- src/lib.rs | 55 +++++++--------- src/ratelimiting.rs | 72 +++++++++++++++++++++ 4 files changed, 100 insertions(+), 37 deletions(-) rename examples/{ratelimits.rs => ratelimiting.rs} (91%) create mode 100644 src/ratelimiting.rs diff --git a/Cargo.toml b/Cargo.toml index d53d578..8b5432c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ documentation = "https://docs.rs/northstar" [features] default = ["serve_dir"] serve_dir = ["mime_guess", "tokio/fs"] -rate-limiting = ["governor"] +ratelimiting = ["dashmap"] [dependencies] anyhow = "1.0.33" @@ -26,7 +26,7 @@ log = "0.4.11" webpki = "0.21.0" lazy_static = "1.4.0" mime_guess = { version = "2.0.3", optional = true } -governor = { version = "0.3.1", optional = true } +dashmap = { version = "3.11.10", optional = true } [dev-dependencies] env_logger = "0.8.1" diff --git a/examples/ratelimits.rs b/examples/ratelimiting.rs similarity index 91% rename from examples/ratelimits.rs rename to examples/ratelimiting.rs index b9cf2ab..48b215c 100644 --- a/examples/ratelimits.rs +++ b/examples/ratelimiting.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use anyhow::*; use futures_core::future::BoxFuture; use futures_util::FutureExt; @@ -10,11 +12,9 @@ async fn main() -> Result<()> { .filter_module("northstar", LevelFilter::Debug) .init(); - let two = std::num::NonZeroU32::new(2).unwrap(); - Server::bind(("localhost", GEMINI_PORT)) .add_route("/", handle_request) - .rate_limit("/limit", northstar::Quota::per_minute(two)) + .ratelimit("/limit", 2, Duration::from_secs(60)) .serve() .await } diff --git a/src/lib.rs b/src/lib.rs index eaafc9a..3b99331 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ use std::{ sync::Arc, path::PathBuf, time::Duration, + net::IpAddr, }; use futures_core::future::BoxFuture; use tokio::{ @@ -21,37 +22,25 @@ use tokio_rustls::{rustls, TlsAcceptor}; use rustls::*; use anyhow::*; use lazy_static::lazy_static; -#[cfg(feature="rate-limiting")] -use governor::clock::{Clock, DefaultClock}; use crate::util::opt_timeout; use routing::RoutingNode; +use ratelimiting::RateLimiter; pub mod types; pub mod util; pub mod routing; +#[cfg(feature = "ratelimiting")] +pub mod ratelimiting; pub use mime; pub use uriparse as uri; -#[cfg(feature="rate-limiting")] -pub use governor::Quota; pub use types::*; pub const REQUEST_URI_MAX_LEN: usize = 1024; pub const GEMINI_PORT: u16 = 1965; -#[cfg(feature="rate-limiting")] -lazy_static! { - static ref CLOCK: DefaultClock = DefaultClock::default(); -} - type Handler = Arc HandlerResponse + Send + Sync>; pub (crate) type HandlerResponse = BoxFuture<'static, Result>; -#[cfg(feature="rate-limiting")] -type RateLimiter = governor::RateLimiter< - std::net::IpAddr, - governor::state::keyed::DefaultKeyedStateStore, - governor::clock::DefaultClock, ->; #[derive(Clone)] pub struct Server { @@ -60,8 +49,8 @@ pub struct Server { routes: Arc>, timeout: Duration, complex_timeout: Option, - #[cfg(feature="rate-limiting")] - rate_limits: Arc>, + #[cfg(feature="ratelimiting")] + rate_limits: Arc>>, } impl Server { @@ -84,7 +73,7 @@ impl Server { } async fn serve_client(self, stream: TcpStream) -> Result<()> { - #[cfg(feature="rate-limiting")] + #[cfg(feature="ratelimiting")] let peer_addr = stream.peer_addr()?.ip(); let fut_accept_request = async { @@ -103,7 +92,7 @@ impl Server { let (mut request, mut stream) = fut_accept_request.await .context("Client timed out while waiting for response")??; - #[cfg(feature="rate-limiting")] + #[cfg(feature="ratelimiting")] if let Some(resp) = self.check_rate_limits(peer_addr, &request) { self.send_response(resp, &mut stream).await .context("Failed to send response")?; @@ -192,15 +181,13 @@ impl Server { Ok(()) } - #[cfg(feature="rate-limiting")] - fn check_rate_limits(&self, addr: std::net::IpAddr, req: &Request) -> Option { + #[cfg(feature="ratelimiting")] + fn check_rate_limits(&self, addr: IpAddr, req: &Request) -> Option { if let Some((_, limiter)) = self.rate_limits.match_request(req) { - if let Err(when) = limiter.check_key(&addr) { + if let Err(when) = limiter.check_key(addr) { return Some(Response::new(ResponseHeader { status: Status::SLOW_DOWN, - meta: Meta::new( - when.wait_time_from(CLOCK.now()).as_secs().to_string() - ).unwrap() + meta: Meta::new(when.as_secs().to_string()).unwrap() })) } } @@ -215,8 +202,8 @@ pub struct Builder { timeout: Duration, complex_body_timeout_override: Option, routes: RoutingNode, - #[cfg(feature="rate-limiting")] - rate_limits: RoutingNode, + #[cfg(feature="ratelimiting")] + rate_limits: RoutingNode>, } impl Builder { @@ -228,7 +215,7 @@ impl Builder { cert_path: PathBuf::from("cert/cert.pem"), key_path: PathBuf::from("cert/key.pem"), routes: RoutingNode::default(), - #[cfg(feature="rate-limiting")] + #[cfg(feature="ratelimiting")] rate_limits: RoutingNode::default(), } } @@ -346,15 +333,19 @@ impl Builder { self } - #[cfg(feature="rate-limiting")] + #[cfg(feature="ratelimiting")] /// Add a rate limit to a route /// + /// The server will allow at most `burst` connections to any endpoints under this + /// route in a period of `period`. All extra requests will recieve a `SLOW_DOWN`, and + /// not be sent to the handler. + /// /// 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 rate_limit(mut self, path: &'static str, quota: Quota) -> Self { - let limiter = RateLimiter::dashmap_with_clock(quota, &CLOCK); + pub fn ratelimit(mut self, path: &'static str, burst: usize, period: Duration) -> Self { + let limiter = RateLimiter::new(period, burst); self.rate_limits.add_route(path, limiter); self } @@ -374,7 +365,7 @@ impl Builder { routes: Arc::new(self.routes), timeout: self.timeout, complex_timeout: self.complex_body_timeout_override, - #[cfg(feature="rate-limiting")] + #[cfg(feature="ratelimiting")] rate_limits: Arc::new(self.rate_limits), }; diff --git a/src/ratelimiting.rs b/src/ratelimiting.rs new file mode 100644 index 0000000..df60e8e --- /dev/null +++ b/src/ratelimiting.rs @@ -0,0 +1,72 @@ +use dashmap::DashMap; + +use std::{hash::Hash, collections::VecDeque, time::{Duration, Instant}}; + +/// A simple struct to manage rate limiting. +/// +/// Does not require a leaky bucket thread to empty it out, but may occassionally need to +/// trim old keys using [`trim_keys()`]. +/// +/// [`trim_keys()`][Self::trim_keys()] +pub struct RateLimiter { + log: DashMap>, + burst: usize, + period: Duration, +} + +impl RateLimiter { + /// Create a new ratelimiter that allows at most `burst` connections in `period` + pub fn new(period: Duration, burst: usize) -> Self { + Self { + log: DashMap::with_capacity(8), + period, + burst, + } + } + + /// Check if a key may pass + /// + /// If the key has made less than `self.burst` connections in the last `self.period`, + /// then the key is allowed to connect, which is denoted by an `Ok` result. This will + /// register as a new connection from that key. + /// + /// If the key is not allowed to connect, than a [`Duration`] denoting the amount of + /// time until the key is permitted is returned, wrapped in an `Err` + pub fn check_key(&self, key: K) -> Result<(), Duration> { + let now = Instant::now(); + let count_after = now - self.period; + + let mut connections = self.log.entry(key) + .or_insert_with(||VecDeque::with_capacity(self.burst)); + let connections = connections.value_mut(); + + // Chcek if space can be made available. We don't need to trim all expired + // connections, just the one in question to allow this connection. + if let Some(earliest_conn) = connections.front() { + if earliest_conn < &count_after { + connections.pop_front(); + } + } + + // Check if the connection should be allowed + if connections.len() == self.burst { + Err(connections[0] + self.period - now) + } else { + connections.push_back(now); + Ok(()) + } + } + + /// Remove any expired keys from the ratelimiter + /// + /// This only needs to be called if keys are continuously being added. If keys are + /// being reused, or come from a finite set, then you don't need to worry about this. + /// + /// If you have many keys coming from a large set, you should infrequently call this + /// to prevent a memory leak. + pub fn trim_keys(&self) { + let count_after = Instant::now() - self.period; + + self.log.retain(|_, conns| conns.back().unwrap() > &count_after); + } +} From b2a671993e55659c39a62efc784d2b9bcf185a3b Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Tue, 24 Nov 2020 14:51:21 -0500 Subject: [PATCH 045/113] Add ability to iterate over a routing node --- src/routing.rs | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/routing.rs b/src/routing.rs index bd2d413..d021d95 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -137,6 +137,41 @@ impl RoutingNode { to_shrink.extend(shrink.values_mut().map(|n| &mut n.1)); } } + + /// Iterate over the items in this map + /// + /// This includes not just the direct children of this node, but also all children of + /// those children. No guarantees are made as to the order values are visited in. + /// + /// ## Example + /// ``` + /// # use std::collections::HashSet; + /// # use northstar::routing::RoutingNode; + /// let mut map = RoutingNode::::default(); + /// map.add_route("/", 0); + /// map.add_route("/hello/world", 1312); + /// map.add_route("/example", 621); + /// + /// let values: HashSet<&usize> = map.iter().collect(); + /// assert!(values.contains(&0)); + /// assert!(values.contains(&1312)); + /// assert!(values.contains(&621)); + /// assert!(!values.contains(&1)); + /// ``` + pub fn iter(&self) -> Iter<'_, T> { + Iter { + unexplored: vec![self], + } + } +} + +impl<'a, T> IntoIterator for &'a RoutingNode { + type Item = &'a T; + type IntoIter = Iter<'a, T>; + + fn into_iter(self) -> Iter<'a, T> { + self.iter() + } } impl Default for RoutingNode { @@ -155,3 +190,25 @@ impl std::fmt::Display for ConflictingRouteError { write!(f, "Attempted to create a route with the same matcher as an existing route") } } + +#[derive(Clone)] +/// An iterator over the values in a [`RoutingNode`] map +pub struct Iter<'a, T> { + unexplored: Vec<&'a RoutingNode>, +} + +impl<'a, T> Iterator for Iter<'a, T> { + type Item = &'a T; + + fn next(&mut self) -> Option { + while let Some(node) = self.unexplored.pop() { + self.unexplored.extend(node.1.values()); + if node.0.is_some() { + return node.0.as_ref(); + } + } + None + } +} + +impl std::iter::FusedIterator for Iter<'_, T> { } From 560760c489db611451c5243cbb65a0ed8ef0fefb Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Tue, 24 Nov 2020 15:08:05 -0500 Subject: [PATCH 046/113] Added another test for RoutingNode --- src/routing.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/routing.rs b/src/routing.rs index d021d95..e77faf8 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -23,6 +23,31 @@ use crate::types::Request; /// /// Routing is only performed on normalized paths, so "/endpoint" and "/endpoint/" are /// considered to be the same route. +/// +/// ``` +/// # use northstar::routing::RoutingNode; +/// let mut routes = RoutingNode::<&'static str>::default(); +/// routes.add_route("/", "base"); +/// routes.add_route("/trans/rights/", "short route"); +/// routes.add_route("/trans/rights/r/human", "long route"); +/// +/// assert_eq!( +/// routes.match_path(&["any", "other", "request"]), +/// Some((vec![&"any", &"other", &"request"], &"base")) +/// ); +/// assert_eq!( +/// routes.match_path(&["trans", "rights"]), +/// Some((vec![], &"short route")) +/// ); +/// assert_eq!( +/// routes.match_path(&["trans", "rights", "now"]), +/// Some((vec![&"now"], &"short route")) +/// ); +/// assert_eq!( +/// routes.match_path(&["trans", "rights", "r", "human", "rights"]), +/// Some((vec![&"rights"], &"long route")) +/// ); +/// ``` pub struct RoutingNode(Option, HashMap); impl RoutingNode { From 3e07e24e41b8d01380f5e111dcc98316a4c4e55f Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Tue, 24 Nov 2020 16:45:30 -0500 Subject: [PATCH 047/113] Added automatic clearing of ratelimit keys --- src/lib.rs | 23 ++++++++++++++++++++++- src/ratelimiting.rs | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index db72d7e..192c1f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,8 +7,9 @@ use std::{ sync::Arc, path::PathBuf, time::Duration, - net::IpAddr, }; +#[cfg(feature = "ratelimiting")] +use std::net::IpAddr; use futures_core::future::BoxFuture; use tokio::{ prelude::*, @@ -16,6 +17,8 @@ use tokio::{ net::{TcpStream, ToSocketAddrs}, time::timeout, }; +#[cfg(feature = "ratelimiting")] +use tokio::time::interval; use tokio::net::TcpListener; use rustls::ClientCertVerifier; use rustls::internal::msgs::handshake::DigitallySignedStruct; @@ -25,6 +28,7 @@ use anyhow::*; use lazy_static::lazy_static; use crate::util::opt_timeout; use routing::RoutingNode; +#[cfg(feature = "ratelimiting")] use ratelimiting::RateLimiter; pub mod types; @@ -60,6 +64,9 @@ impl Server { } async fn serve(self) -> Result<()> { + #[cfg(feature = "ratelimiting")] + tokio::spawn(prune_ratelimit_log(self.rate_limits.clone())); + loop { let (stream, _addr) = self.listener.accept().await .context("Failed to accept client")?; @@ -434,6 +441,17 @@ async fn send_response_body(body: Body, stream: &mut (impl AsyncWrite + Unpin)) Ok(()) } +#[cfg(feature="ratelimiting")] +/// Every 5 minutes, remove excess keys from all ratelimiters +async fn prune_ratelimit_log(rate_limits: Arc>>) -> Never { + let mut interval = interval(tokio::time::Duration::from_secs(10)); + let log = rate_limits.as_ref(); + loop { + interval.tick().await; + log.iter().for_each(RateLimiter::trim_keys_verbose); + } +} + fn tls_config(cert_path: &PathBuf, key_path: &PathBuf) -> Result> { let mut config = ServerConfig::new(AllowAnonOrSelfsignedClient::new()); @@ -549,3 +567,6 @@ mod tests { let _: &Mime = &GEMINI_MIME; } } + +#[cfg(feature = "ratelimiting")] +enum Never {} diff --git a/src/ratelimiting.rs b/src/ratelimiting.rs index df60e8e..68c086e 100644 --- a/src/ratelimiting.rs +++ b/src/ratelimiting.rs @@ -1,6 +1,6 @@ use dashmap::DashMap; -use std::{hash::Hash, collections::VecDeque, time::{Duration, Instant}}; +use std::{fmt::Display, collections::VecDeque, hash::Hash, time::{Duration, Instant}}; /// A simple struct to manage rate limiting. /// @@ -64,9 +64,43 @@ impl RateLimiter { /// /// If you have many keys coming from a large set, you should infrequently call this /// to prevent a memory leak. + /// + /// If debug level logging is enabled, this prints an *approximate* number of keys + /// removed to the log. For more precise output, use [`trim_keys_verbose()`] + /// + /// [`trim_keys_verbose()`]: RateLimiter::trim_keys_verbose() pub fn trim_keys(&self) { let count_after = Instant::now() - self.period; + let len: isize = self.log.len() as isize; self.log.retain(|_, conns| conns.back().unwrap() > &count_after); + let removed = len - self.log.len() as isize; + if removed.is_positive() { + debug!("Pruned approximately {} expired ratelimit keys", removed); + } + } +} + +impl RateLimiter { + + /// Remove any expired keys from the ratelimiter + /// + /// This only needs to be called if keys are continuously being added. If keys are + /// being reused, or come from a finite set, then you don't need to worry about this. + /// + /// If you have many keys coming from a large set, you should infrequently call this + /// to prevent a memory leak. + /// + /// If debug level logging is on, this prints out any removed keys. + pub fn trim_keys_verbose(&self) { + let count_after = Instant::now() - self.period; + + self.log.retain(|ip, conns| { + let should_keep = conns.back().unwrap() > &count_after; + if !should_keep { + debug!("Pruned expired ratelimit key: {}", ip); + } + should_keep + }); } } From b4e99c5adf1d35ccc5fd897898ffc1451b0307bb Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Tue, 24 Nov 2020 16:48:28 -0500 Subject: [PATCH 048/113] Added ratelimiting to examples --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 8b5432c..ad9948b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,3 +32,7 @@ dashmap = { version = "3.11.10", optional = true } env_logger = "0.8.1" futures-util = "0.3.7" tokio = { version = "0.3.1", features = ["macros", "rt-multi-thread", "sync"] } + +[[example]] +name = "ratelimiting" +required-features = ["ratelimiting"] From 8a929e17e0abddcc5c351880ec325217b3a57777 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Tue, 24 Nov 2020 16:58:30 -0500 Subject: [PATCH 049/113] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07fc9ee..6a77f3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - customizable TLS cert & key paths by [@Alch-Emi] - `server_dir` default feature for serve_dir utils [@Alch-Emi] - Docments can be converted into responses with std::convert::Into [@Alch-Emi] +- Added ratelimiting API [@Alch-Emi] ### Improved - build time and size by [@Alch-Emi](https://github.com/Alch-Emi) - build time and size by [@Alch-Emi] From 970813f29555525670c5295ac84c1cdb004d91f1 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Tue, 24 Nov 2020 17:22:47 -0500 Subject: [PATCH 050/113] Added FilesHandler --- examples/serve_dir.rs | 13 ++++--------- src/handling.rs | 32 +++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/examples/serve_dir.rs b/examples/serve_dir.rs index bb81add..b79c08d 100644 --- a/examples/serve_dir.rs +++ b/examples/serve_dir.rs @@ -1,6 +1,8 @@ +use std::path::PathBuf; + use anyhow::*; use log::LevelFilter; -use northstar::{Server, Request, Response, GEMINI_PORT}; +use northstar::{Server, GEMINI_PORT}; #[tokio::main] async fn main() -> Result<()> { @@ -9,14 +11,7 @@ async fn main() -> Result<()> { .init(); Server::bind(("localhost", GEMINI_PORT)) - .add_route("/", handle_request) + .add_route("/", PathBuf::from("public")) .serve() .await } - -async fn handle_request(request: Request) -> Result { - let path = request.path_segments(); - let response = northstar::util::serve_dir("public", &path).await?; - - Ok(response) -} diff --git a/src/handling.rs b/src/handling.rs index b8bb811..de2d750 100644 --- a/src/handling.rs +++ b/src/handling.rs @@ -14,6 +14,8 @@ use std::{ task::Poll, panic::{catch_unwind, AssertUnwindSafe}, }; +#[cfg(feature = "serve_dir")] +use std::path::PathBuf; use crate::{Document, types::{Body, Response, Request}}; @@ -30,6 +32,8 @@ use crate::{Document, types::{Body, Response, Request}}; pub enum Handler { FnHandler(HandlerInner), StaticHandler(Response), + #[cfg(feature = "serve_dir")] + FilesHandler(PathBuf), } /// Since we can't store train objects, we need to wrap fn handlers in a box @@ -77,7 +81,15 @@ impl Handler { ).unwrap() } } - } + }, + #[cfg(feature = "serve_dir")] + Self::FilesHandler(path) => { + let resp = crate::util::serve_dir(path, request.trailing_segments()).await; + resp.unwrap_or_else(|e| { + warn!("Unexpected error serving from dir {}: {:?}", path.display(), e); + Response::server_error("").unwrap() + }) + }, } } } @@ -140,6 +152,24 @@ impl From<&Document> for Handler { } } +#[cfg(feature = "serve_dir")] +impl From for Handler { + /// Serve files from a directory + /// + /// Any requests directed to this handler will be served from this path. For example, + /// if a handler serving files from the path `./public/` and bound to `/serve` + /// receives a request for `/serve/file.txt`, it will respond with the contents of the + /// file at `./public/file.txt`. + /// + /// This is equivilent to serving files using [`util::serve_dir()`], and as such will + /// include directory listings. + /// + /// [`util::serve_dir()`]: crate::util::serve_dir() + fn from(path: PathBuf) -> Self { + Self::FilesHandler(path) + } +} + /// A utility for catching unwinds on Futures. /// /// This is adapted from the futures-rs CatchUnwind, in an effort to reduce the large From 13b1eddd00b37fe7b3207481ba7e16a7aa260aa1 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Tue, 24 Nov 2020 17:28:54 -0500 Subject: [PATCH 051/113] Allow serving a single file --- examples/serve_dir.rs | 3 ++- src/handling.rs | 12 ++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/examples/serve_dir.rs b/examples/serve_dir.rs index b79c08d..cfdb345 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)) - .add_route("/", PathBuf::from("public")) + .add_route("/", PathBuf::from("public")) // Serve directory listings & file contents + .add_route("/about", PathBuf::from("README.md")) // Serve a single file .serve() .await } diff --git a/src/handling.rs b/src/handling.rs index de2d750..6f747b9 100644 --- a/src/handling.rs +++ b/src/handling.rs @@ -84,9 +84,14 @@ impl Handler { }, #[cfg(feature = "serve_dir")] Self::FilesHandler(path) => { - let resp = crate::util::serve_dir(path, request.trailing_segments()).await; + let resp = if path.is_dir() { + crate::util::serve_dir(path, request.trailing_segments()).await + } else { + let mime = crate::util::guess_mime_from_path(&path); + crate::util::serve_file(path, &mime).await + }; resp.unwrap_or_else(|e| { - warn!("Unexpected error serving from dir {}: {:?}", path.display(), e); + warn!("Unexpected error serving from {}: {:?}", path.display(), e); Response::server_error("").unwrap() }) }, @@ -164,6 +169,9 @@ impl From for Handler { /// This is equivilent to serving files using [`util::serve_dir()`], and as such will /// include directory listings. /// + /// The path to a single file can be passed in order to serve only a single file for + /// any and all requests. + /// /// [`util::serve_dir()`]: crate::util::serve_dir() fn from(path: PathBuf) -> Self { Self::FilesHandler(path) From b69aba139fd07e372825b42fa4745f43a0a1e428 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Wed, 25 Nov 2020 00:42:09 -0500 Subject: [PATCH 052/113] Rebrand as kochab I spent /so/ long looking for that figlet font. __ __ __ / /______ _____/ /_ ____ _/ /_ / //_/ __ \/ ___/ __ \/ __ `/ __ \ / ,< / /_/ / /__/ / / / /_/ / /_/ / /_/|_|\____/\___/_/ /_/\__,_/_.___/ --- CHANGELOG.md | 41 +------------------------------------ Cargo.toml | 11 +++++----- README.md | 31 +++++++++++++--------------- examples/certificates.rs | 4 ++-- examples/document.rs | 33 +++++++++++++++-------------- examples/kochab_logo.txt | 8 ++++++++ examples/northstar_logo.txt | 5 ----- examples/ratelimiting.rs | 6 ++---- examples/routing.rs | 6 +++--- examples/serve_dir.rs | 4 ++-- examples/user_management.rs | 6 +++--- public/README.gemini | 33 ----------------------------- public/README.gmi | 37 +++++++++++++++++++++++++++++++++ src/lib.rs | 8 ++++---- src/routing.rs | 4 ++-- src/types/document.rs | 30 +++++++++++++-------------- src/types/meta.rs | 2 +- 17 files changed, 117 insertions(+), 152 deletions(-) create mode 100644 examples/kochab_logo.txt delete mode 100644 examples/northstar_logo.txt delete mode 100644 public/README.gemini create mode 100644 public/README.gmi diff --git a/CHANGELOG.md b/CHANGELOG.md index 91dbf43..fd233a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,43 +4,4 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] -### Added -- `document` API for creating Gemini documents -- preliminary timeout API, incl a special case for complex MIMEs by [@Alch-Emi] -- `Response::success_*` variants by [@Alch-Emi] -- `redirect_temporary_lossy` for `Response` and `ResponseHeader` -- `bad_request_lossy` for `Response` and `ResponseHeader` -- support for a lot more mime-types in `guess_mime_from_path`, backed by the `mime_guess` crate -- customizable TLS cert & key paths by [@Alch-Emi] -- `server_dir` default feature for serve_dir utils [@Alch-Emi] -- Docments can be converted into responses with std::convert::Into [@Alch-Emi] -- Added ratelimiting API [@Alch-Emi] -### Improved -- build time and size by [@Alch-Emi] -- Improved error handling in serve_dir [@Alch-Emi] -### Changed -- Added route API [@Alch-Emi](https://github.com/Alch-Emi) -- API for adding handlers now accepts async handlers [@Alch-Emi](https://github.com/Alch-Emi) -- `Response::success` now takes a request body [@Alch-Emi] - -## [0.3.0] - 2020-11-14 -### Added -- `GEMINI_MIME_STR`, the `&str` representation of the Gemini MIME -- `Meta::new_lossy`, constructor that never fails -- `Meta::MAX_LEN`, which is `1024` -- "lossy" constructors for `Response` and `Status` (see `Meta::new_lossy`) - -### Changed -- `Meta::new` now rejects strings exceeding `Meta::MAX_LEN` (`1024`) -- Some `Response` and `Status` constructors are now infallible -- Improve error messages - -### Deprecated -- Instead of `gemini_mime()` use `GEMINI_MIME` - -## [0.2.0] - 2020-11-14 -### Added -- Access to client certificates by [@Alch-Emi] - -[@Alch-Emi]: https://github.com/Alch-Emi +As of yet no versions have been released diff --git a/Cargo.toml b/Cargo.toml index 6362ef0..0d80a93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,11 @@ [package] -name = "northstar" +name = "kochab" version = "0.3.1" -authors = ["panicbit "] +authors = ["Emii Tatsuo ", "panicbit "] edition = "2018" -license = "MIT OR Apache-2.0" -description = "Gemini server implementation" -repository = "https://github.com/panicbit/northstar" -documentation = "https://docs.rs/northstar" +license = "Hippocratic 2.1" +description = "Ergonomic Gemini SDK" +repository = "https://github.com/Alch-Emi/kochab" [features] user_management = ["sled", "bincode", "serde/derive", "crc32fast"] diff --git a/README.md b/README.md index 6a06f9b..b6a4b1f 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,23 @@ ``` - __ __ __ - ____ ____ _____/ /_/ /_ _____/ /_____ ______ - / __ \/ __ \/ ___/ __/ __ \/ ___/ __/ __ `/ ___/ - / / / / /_/ / / / /_/ / / (__ ) /_/ /_/ / / -/_/ /_/\____/_/ \__/_/ /_/____/\__/\__,_/_/ + *. ,.-*,,.. + .` `. .,-'` ````--*,,,.. + .` ;*` ```''-o + * ,' __ __ __ + `. ,' / /______ _____/ /_ ____ _/ /_ + ⭐ / //_/ __ \/ ___/ __ \/ __ `/ __ \ + / ,< / /_/ / /__/ / / / /_/ / /_/ / + /_/|_|\____/\___/_/ /_/\__,_/_.___/ ``` +# kochab -- [Documentation](https://docs.rs/northstar) -- [GitHub](https://github.com/panicbit/northstar) +Kochab is an extension & a fork of the Gemini SDK [northstar]. Where northstar creates an efficient and flexible foundation for Gemini projects, kochab seeks to be as ergonomic and intuitive as possible, making it possible to get straight into getting your ideas into geminispace, with no worrying about needing to build the tools to get there. # Usage -Add the latest version of northstar to your `Cargo.toml`. - -## Manually +It is currently only possible to use kochab through it's git repo, although it may wind up on crates.rs someday. ```toml -northstar = "0.3.0" # check crates.io for the latest version -``` - -## Automatically - -```sh -cargo add northstar +kochab = { git = "https://github.com/Alch-Emi/kochab.git" } ``` # Generating a key & certificate @@ -35,3 +30,5 @@ openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 36 and enter your domain name (e.g. "localhost" for testing) as Common Name (CN). Alternatively, if you want to include multiple domains add something like `-addext "subjectAltName = DNS:localhost, DNS:example.org"`. + +[northstar]: https://github.com/panicbit/northstar "Northstar GitHub" diff --git a/examples/certificates.rs b/examples/certificates.rs index 5cf997c..a5ca78b 100644 --- a/examples/certificates.rs +++ b/examples/certificates.rs @@ -1,7 +1,7 @@ use anyhow::*; use log::LevelFilter; use tokio::sync::RwLock; -use northstar::{Certificate, GEMINI_PORT, Request, Response, Server}; +use kochab::{Certificate, GEMINI_PORT, Request, Response, Server}; use std::collections::HashMap; use std::sync::Arc; @@ -11,7 +11,7 @@ type CertBytes = Vec; #[tokio::main] async fn main() -> Result<()> { env_logger::builder() - .filter_module("northstar", LevelFilter::Debug) + .filter_module("kochab", LevelFilter::Debug) .init(); let users = Arc::>>::default(); diff --git a/examples/document.rs b/examples/document.rs index 5ea2678..a3d3a6c 100644 --- a/examples/document.rs +++ b/examples/document.rs @@ -1,33 +1,36 @@ use anyhow::*; use log::LevelFilter; -use northstar::{Server, Response, GEMINI_PORT, Document}; -use northstar::document::HeadingLevel::*; +use kochab::{Server, Response, GEMINI_PORT, Document}; +use kochab::document::HeadingLevel::*; #[tokio::main] async fn main() -> Result<()> { env_logger::builder() - .filter_module("northstar", LevelFilter::Debug) + .filter_module("kochab", LevelFilter::Debug) .init(); let response: Response = Document::new() - .add_preformatted(include_str!("northstar_logo.txt")) + .add_preformatted_with_alt("kochab", include_str!("kochab_logo.txt")) .add_blank_line() - .add_link("https://docs.rs/northstar", "Documentation") - .add_link("https://github.com/panicbit/northstar", "GitHub") + .add_text( + concat!( + "Kochab is an extension & a fork of the Gemini SDK [northstar]. Where", + " northstar creates an efficient and flexible foundation for Gemini projects,", + " kochab seeks to be as ergonomic and intuitive as possible, making it", + " possible to get straight into getting your ideas into geminispace, with no", + " worrying about needing to build the tools to get there." + ) + ) .add_blank_line() - .add_heading(H1, "Usage") + .add_link("https://github.com/Alch-Emi/kochab", "GitHub") .add_blank_line() - .add_text("Add the latest version of northstar to your `Cargo.toml`.") + .add_heading(H2, "Usage") .add_blank_line() - .add_heading(H2, "Manually") + .add_text("Add the latest version of kochab to your `Cargo.toml`.") .add_blank_line() - .add_preformatted_with_alt("toml", r#"northstar = "0.3.0" # check crates.io for the latest version"#) + .add_preformatted_with_alt("toml", r#"kochab = { git = "https://github.com/Alch-Emi/kochab.git" }"#) .add_blank_line() - .add_heading(H2, "Automatically") - .add_blank_line() - .add_preformatted_with_alt("sh", "cargo add northstar") - .add_blank_line() - .add_heading(H1, "Generating a key & certificate") + .add_heading(H2, "Generating a key & certificate") .add_blank_line() .add_preformatted_with_alt("sh", concat!( "mkdir cert && cd cert\n", diff --git a/examples/kochab_logo.txt b/examples/kochab_logo.txt new file mode 100644 index 0000000..c01ae2e --- /dev/null +++ b/examples/kochab_logo.txt @@ -0,0 +1,8 @@ + *. ,.-*,,.. + .` `. .,-'` ````--*,,,.. + .` ;*` ```''-o + * ,' __ __ __ + `. ,' / /______ _____/ /_ ____ _/ /_ + ⭐ / //_/ __ \/ ___/ __ \/ __ `/ __ \ + / ,< / /_/ / /__/ / / / /_/ / /_/ / + /_/|_|\____/\___/_/ /_/\__,_/_.___/ diff --git a/examples/northstar_logo.txt b/examples/northstar_logo.txt deleted file mode 100644 index 9fe390c..0000000 --- a/examples/northstar_logo.txt +++ /dev/null @@ -1,5 +0,0 @@ - __ __ __ - ____ ____ _____/ /_/ /_ _____/ /_____ ______ - / __ \/ __ \/ ___/ __/ __ \/ ___/ __/ __ `/ ___/ - / / / / /_/ / / / /_/ / / (__ ) /_/ /_/ / / -/_/ /_/\____/_/ \__/_/ /_/____/\__/\__,_/_/ \ No newline at end of file diff --git a/examples/ratelimiting.rs b/examples/ratelimiting.rs index 48b215c..9838d82 100644 --- a/examples/ratelimiting.rs +++ b/examples/ratelimiting.rs @@ -1,15 +1,13 @@ use std::time::Duration; use anyhow::*; -use futures_core::future::BoxFuture; -use futures_util::FutureExt; use log::LevelFilter; -use northstar::{Server, Request, Response, GEMINI_PORT, Document}; +use kochab::{Server, Request, Response, GEMINI_PORT, Document}; #[tokio::main] async fn main() -> Result<()> { env_logger::builder() - .filter_module("northstar", LevelFilter::Debug) + .filter_module("kochab", LevelFilter::Debug) .init(); Server::bind(("localhost", GEMINI_PORT)) diff --git a/examples/routing.rs b/examples/routing.rs index a2fb78d..dfa871b 100644 --- a/examples/routing.rs +++ b/examples/routing.rs @@ -1,14 +1,14 @@ use anyhow::*; use log::LevelFilter; -use northstar::{Document, document::HeadingLevel, Request, Response, GEMINI_PORT}; +use kochab::{Document, document::HeadingLevel, Request, Response, GEMINI_PORT}; #[tokio::main] async fn main() -> Result<()> { env_logger::builder() - .filter_module("northstar", LevelFilter::Debug) + .filter_module("kochab", LevelFilter::Debug) .init(); - northstar::Server::bind(("localhost", GEMINI_PORT)) + kochab::Server::bind(("localhost", GEMINI_PORT)) .add_route("/", handle_base) .add_route("/route", handle_short) .add_route("/route/long", handle_long) diff --git a/examples/serve_dir.rs b/examples/serve_dir.rs index cfdb345..a41ca7e 100644 --- a/examples/serve_dir.rs +++ b/examples/serve_dir.rs @@ -2,12 +2,12 @@ use std::path::PathBuf; use anyhow::*; use log::LevelFilter; -use northstar::{Server, GEMINI_PORT}; +use kochab::{Server, GEMINI_PORT}; #[tokio::main] async fn main() -> Result<()> { env_logger::builder() - .filter_module("northstar", LevelFilter::Debug) + .filter_module("kochab", LevelFilter::Debug) .init(); Server::bind(("localhost", GEMINI_PORT)) diff --git a/examples/user_management.rs b/examples/user_management.rs index 3329b40..740a0f1 100644 --- a/examples/user_management.rs +++ b/examples/user_management.rs @@ -1,6 +1,6 @@ use anyhow::*; use log::LevelFilter; -use northstar::{ +use kochab::{ GEMINI_PORT, Document, Request, @@ -23,7 +23,7 @@ use northstar::{ async fn main() -> Result<()> { // Turn on logging env_logger::builder() - .filter_module("northstar", LevelFilter::Debug) + .filter_module("kochab", LevelFilter::Debug) .init(); Server::bind(("0.0.0.0", GEMINI_PORT)) @@ -45,7 +45,7 @@ async fn main() -> Result<()> { /// Displays the user's current secret string, or prompts the user to sign in if they /// haven't. Includes links to update your string (`/update`) or your account /// (`/account`). Even though we haven't added an explicit handler for `/account`, this -/// route is managed by northstar. +/// route is managed by kochab. /// /// Because this route is registered as an authenticated route, any connections without a /// certificate will be prompted to add a certificate and register. diff --git a/public/README.gemini b/public/README.gemini deleted file mode 100644 index 9de0018..0000000 --- a/public/README.gemini +++ /dev/null @@ -1,33 +0,0 @@ -``` - __ __ __ - ____ ____ _____/ /_/ /_ _____/ /_____ ______ - / __ \/ __ \/ ___/ __/ __ \/ ___/ __/ __ `/ ___/ - / / / / /_/ / / / /_/ / / (__ ) /_/ /_/ / / -/_/ /_/\____/_/ \__/_/ /_/____/\__/\__,_/_/ -``` - -=> https://docs.rs/northstar Documentation -=> https://github.com/panicbit/northstar GitHub - -# Usage - -Add the latest version of northstar to your `Cargo.toml`. - -## Manually - -```toml -northstar = "0.3.0" # check crates.io for the latest version -``` - -## Automatically - -```sh -cargo add northstar -``` - -# Generating a key & certificate - -```sh -mkdir cert && cd cert -openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -``` diff --git a/public/README.gmi b/public/README.gmi new file mode 100644 index 0000000..4522571 --- /dev/null +++ b/public/README.gmi @@ -0,0 +1,37 @@ +```kochab + *. ,.-*,,.. + .` `. .,-'` ````--*,,,.. + .` ;*` ```''-o + * ,' __ __ __ + `. ,' / /______ _____/ /_ ____ _/ /_ + ⭐ / //_/ __ \/ ___/ __ \/ __ `/ __ \ + / ,< / /_/ / /__/ / / / /_/ / /_/ / + /_/|_|\____/\___/_/ /_/\__,_/_.___/ +``` + +Kochab is an extension & a fork of the Gemini SDK northstar. Where northstar creates an efficient and flexible foundation for Gemini projects, kochab seeks to be as ergonomic and intuitive as possible, making it possible to get straight into getting your ideas into geminispace, with no worrying about needing to build the tools to get there. + +=> https://github.com/panicbit/northstar Northstar GitHub + +## Usage + +It is currently only possible to use kochab through it's git repo, although it may wind up on crates.rs someday. + +```toml +kochab = { git = "https://github.com/Alch-Emi/kochab.git" } +``` + +## Generating a key & certificate + +Run +```sh +mkdir cert && cd cert +openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 +``` +and enter your domain name (e.g. "localhost" for testing) as Common Name (CN). + +Alternatively, if you want to include multiple domains add something like: +``` +-addext "subjectAltName = DNS:localhost, DNS:example.org" +``` + diff --git a/src/lib.rs b/src/lib.rs index 338b960..827b21b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -273,7 +273,7 @@ impl Builder { self } - /// Sets the directory that northstar should look for TLS certs and keys into + /// Sets the directory that kochab should look for TLS certs and keys into /// /// Northstar will look for files called `cert.pem` and `key.pem` in the provided /// directory. @@ -288,7 +288,7 @@ impl Builder { .set_key(dir.join("key.pem")) } - /// Set the path to the TLS certificate northstar will use + /// Set the path to the TLS certificate kochab will use /// /// This defaults to `cert/cert.pem`. /// @@ -299,7 +299,7 @@ impl Builder { self } - /// Set the path to the ertificate key northstar will use + /// Set the path to the ertificate key kochab will use /// /// This defaults to `cert/key.pem`. /// @@ -509,7 +509,7 @@ pub const GEMINI_MIME_STR: &str = "text/gemini"; lazy_static! { /// Mime for Gemini documents ("text/gemini") - pub static ref GEMINI_MIME: Mime = GEMINI_MIME_STR.parse().expect("northstar BUG"); + pub static ref GEMINI_MIME: Mime = GEMINI_MIME_STR.parse().unwrap(); } #[deprecated(note = "Use `GEMINI_MIME` instead", since = "0.3.0")] diff --git a/src/routing.rs b/src/routing.rs index e77faf8..69ff34e 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -25,7 +25,7 @@ use crate::types::Request; /// considered to be the same route. /// /// ``` -/// # use northstar::routing::RoutingNode; +/// # use kochab::routing::RoutingNode; /// let mut routes = RoutingNode::<&'static str>::default(); /// routes.add_route("/", "base"); /// routes.add_route("/trans/rights/", "short route"); @@ -171,7 +171,7 @@ impl RoutingNode { /// ## Example /// ``` /// # use std::collections::HashSet; - /// # use northstar::routing::RoutingNode; + /// # use kochab::routing::RoutingNode; /// let mut map = RoutingNode::::default(); /// map.add_route("/", 0); /// map.add_route("/hello/world", 1312); diff --git a/src/types/document.rs b/src/types/document.rs index d71c851..f5b5291 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -7,9 +7,9 @@ //! # Examples //! //! ``` -//! use northstar::document::HeadingLevel::*; +//! use kochab::document::HeadingLevel::*; //! -//! let mut document = northstar::Document::new(); +//! let mut document = kochab::Document::new(); //! //! document.add_heading(H1, "Heading 1"); //! document.add_heading(H2, "Heading 2"); @@ -57,7 +57,7 @@ impl Document { /// # Examples /// /// ``` - /// let document = northstar::Document::new(); + /// let document = kochab::Document::new(); /// /// assert_eq!(document.to_string(), ""); /// ``` @@ -73,7 +73,7 @@ impl Document { /// # Examples /// /// ```compile_fail - /// use northstar::document::{Document, Item, Text}; + /// use kochab::document::{Document, Item, Text}; /// /// let mut document = Document::new(); /// let text = Text::new_lossy("foo"); @@ -95,7 +95,7 @@ impl Document { /// # Examples /// /// ```compile_fail - /// use northstar::document::{Document, Item, Text}; + /// use kochab::document::{Document, Item, Text}; /// /// let mut document = Document::new(); /// let items = vec!["foo", "bar", "baz"] @@ -120,7 +120,7 @@ impl Document { /// # Examples /// /// ``` - /// let mut document = northstar::Document::new(); + /// let mut document = kochab::Document::new(); /// /// document.add_blank_line(); /// @@ -141,7 +141,7 @@ impl Document { /// # Examples /// /// ``` - /// let mut document = northstar::Document::new(); + /// let mut document = kochab::Document::new(); /// /// document.add_text("hello\n* world!"); /// @@ -168,7 +168,7 @@ impl Document { /// # Examples /// /// ``` - /// let mut document = northstar::Document::new(); + /// let mut document = kochab::Document::new(); /// /// document.add_link("https://wikipedia.org", "Wiki\n\nWiki"); /// @@ -198,7 +198,7 @@ impl Document { /// # Examples /// /// ``` - /// let mut document = northstar::Document::new(); + /// let mut document = kochab::Document::new(); /// /// document.add_link_without_label("https://wikipedia.org"); /// @@ -230,7 +230,7 @@ impl Document { /// # Examples /// /// ``` - /// let mut document = northstar::Document::new(); + /// let mut document = kochab::Document::new(); /// /// document.add_preformatted("a\n b\n c"); /// @@ -251,7 +251,7 @@ impl Document { /// # Examples /// /// ``` - /// let mut document = northstar::Document::new(); + /// let mut document = kochab::Document::new(); /// /// document.add_preformatted_with_alt("rust", "fn main() {\n}\n"); /// @@ -282,9 +282,9 @@ impl Document { /// # Examples /// /// ``` - /// use northstar::document::HeadingLevel::H1; + /// use kochab::document::HeadingLevel::H1; /// - /// let mut document = northstar::Document::new(); + /// let mut document = kochab::Document::new(); /// /// document.add_heading(H1, "Welcome!"); /// @@ -311,7 +311,7 @@ impl Document { /// # Examples /// /// ``` - /// let mut document = northstar::Document::new(); + /// let mut document = kochab::Document::new(); /// /// document.add_unordered_list_item("milk"); /// document.add_unordered_list_item("eggs"); @@ -334,7 +334,7 @@ impl Document { /// # Examples /// /// ``` - /// let mut document = northstar::Document::new(); + /// let mut document = kochab::Document::new(); /// /// document.add_quote("I think,\ntherefore I am"); /// diff --git a/src/types/meta.rs b/src/types/meta.rs index bfb36e5..8123d46 100644 --- a/src/types/meta.rs +++ b/src/types/meta.rs @@ -33,7 +33,7 @@ impl Meta { let meta: String = match truncate_pos { None => meta.into(), - Some(truncate_pos) => meta.get(..truncate_pos).expect("northstar BUG").into(), + Some(truncate_pos) => meta.get(..truncate_pos).unwrap().into(), }; Self(meta) From f1302a1dbbd46a2476e9a697b345194076bdd01d Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Wed, 25 Nov 2020 00:52:39 -0500 Subject: [PATCH 053/113] Add licenses --- LICENSE.md | 33 +++++++++++++++++++++++++++++++++ LICENSE_NORTHSTAR.md | 9 +++++++++ 2 files changed, 42 insertions(+) create mode 100644 LICENSE.md create mode 100644 LICENSE_NORTHSTAR.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..1543a57 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,33 @@ +kochab Copyright 2020 Emii Tatsuo & panicbit (“Licensor”) + +Hippocratic License Version Number: 2.1. + +Purpose. The purpose of this License is for the Licensor named above to permit the Licensee (as defined below) broad permission, if consistent with Human Rights Laws and Human Rights Principles (as each is defined below), to use and work with the Software (as defined below) within the full scope of Licensor’s copyright and patent rights, if any, in the Software, while ensuring attribution and protecting the Licensor from liability. + +Permission and Conditions. The Licensor grants permission by this license (“License”), free of charge, to the extent of Licensor’s rights under applicable copyright and patent law, to any person or entity (the “Licensee”) obtaining a copy of this software and associated documentation files (the “Software”), to do everything with the Software that would otherwise infringe (i) the Licensor’s copyright in the Software or (ii) any patent claims to the Software that the Licensor can license or becomes able to license, subject to all of the following terms and conditions: + +* Acceptance. This License is automatically offered to every person and entity subject to its terms and conditions. Licensee accepts this License and agrees to its terms and conditions by taking any action with the Software that, absent this License, would infringe any intellectual property right held by Licensor. + +* Notice. Licensee must ensure that everyone who gets a copy of any part of this Software from Licensee, with or without changes, also receives the License and the above copyright notice (and if included by the Licensor, patent, trademark and attribution notice). Licensee must cause any modified versions of the Software to carry prominent notices stating that Licensee changed the Software. For clarity, although Licensee is free to create modifications of the Software and distribute only the modified portion created by Licensee with additional or different terms, the portion of the Software not modified must be distributed pursuant to this License. If anyone notifies Licensee in writing that Licensee has not complied with this Notice section, Licensee can keep this License by taking all practical steps to comply within 30 days after the notice. If Licensee does not do so, Licensee’s License (and all rights licensed hereunder) shall end immediately. + +* Compliance with Human Rights Principles and Human Rights Laws. + + 1. Human Rights Principles. + + (a) Licensee is advised to consult the articles of the United Nations Universal Declaration of Human Rights and the United Nations Global Compact that define recognized principles of international human rights (the “Human Rights Principles”). Licensee shall use the Software in a manner consistent with Human Rights Principles. + + (b) Unless the Licensor and Licensee agree otherwise, any dispute, controversy, or claim arising out of or relating to (i) Section 1(a) regarding Human Rights Principles, including the breach of Section 1(a), termination of this License for breach of the Human Rights Principles, or invalidity of Section 1(a) or (ii) a determination of whether any Law is consistent or in conflict with Human Rights Principles pursuant to Section 2, below, shall be settled by arbitration in accordance with the Hague Rules on Business and Human Rights Arbitration (the “Rules”); provided, however, that Licensee may elect not to participate in such arbitration, in which event this License (and all rights licensed hereunder) shall end immediately. The number of arbitrators shall be one unless the Rules require otherwise. + + Unless both the Licensor and Licensee agree to the contrary: (1) All documents and information concerning the arbitration shall be public and may be disclosed by any party; (2) The repository referred to under Article 43 of the Rules shall make available to the public in a timely manner all documents concerning the arbitration which are communicated to it, including all submissions of the parties, all evidence admitted into the record of the proceedings, all transcripts or other recordings of hearings and all orders, decisions and awards of the arbitral tribunal, subject only to the arbitral tribunal's powers to take such measures as may be necessary to safeguard the integrity of the arbitral process pursuant to Articles 18, 33, 41 and 42 of the Rules; and (3) Article 26(6) of the Rules shall not apply. + + 2. Human Rights Laws. The Software shall not be used by any person or entity for any systems, activities, or other uses that violate any Human Rights Laws. “Human Rights Laws” means any applicable laws, regulations, or rules (collectively, “Laws”) that protect human, civil, labor, privacy, political, environmental, security, economic, due process, or similar rights; provided, however, that such Laws are consistent and not in conflict with Human Rights Principles (a dispute over the consistency or a conflict between Laws and Human Rights Principles shall be determined by arbitration as stated above). Where the Human Rights Laws of more than one jurisdiction are applicable or in conflict with respect to the use of the Software, the Human Rights Laws that are most protective of the individuals or groups harmed shall apply. + + 3. Indemnity. Licensee shall hold harmless and indemnify Licensor (and any other contributor) against all losses, damages, liabilities, deficiencies, claims, actions, judgments, settlements, interest, awards, penalties, fines, costs, or expenses of whatever kind, including Licensor’s reasonable attorneys’ fees, arising out of or relating to Licensee’s use of the Software in violation of Human Rights Laws or Human Rights Principles. + +* Failure to Comply. Any failure of Licensee to act according to the terms and conditions of this License is both a breach of the License and an infringement of the intellectual property rights of the Licensor (subject to exceptions under Laws, e.g., fair use). In the event of a breach or infringement, the terms and conditions of this License may be enforced by Licensor under the Laws of any jurisdiction to which Licensee is subject. Licensee also agrees that the Licensor may enforce the terms and conditions of this License against Licensee through specific performance (or similar remedy under Laws) to the extent permitted by Laws. For clarity, except in the event of a breach of this License, infringement, or as otherwise stated in this License, Licensor may not terminate this License with Licensee. + +* Enforceability and Interpretation. If any term or provision of this License is determined to be invalid, illegal, or unenforceable by a court of competent jurisdiction, then such invalidity, illegality, or unenforceability shall not affect any other term or provision of this License or invalidate or render unenforceable such term or provision in any other jurisdiction; provided, however, subject to a court modification pursuant to the immediately following sentence, if any term or provision of this License pertaining to Human Rights Laws or Human Rights Principles is deemed invalid, illegal, or unenforceable against Licensee by a court of competent jurisdiction, all rights in the Software granted to Licensee shall be deemed null and void as between Licensor and Licensee. Upon a determination that any term or provision is invalid, illegal, or unenforceable, to the extent permitted by Laws, the court may modify this License to affect the original purpose that the Software be used in compliance with Human Rights Principles and Human Rights Laws as closely as possible. The language in this License shall be interpreted as to its fair meaning and not strictly for or against any party. + +* Disclaimer. TO THE FULL EXTENT ALLOWED BY LAW, THIS SOFTWARE COMES “AS IS,” WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED, AND LICENSOR AND ANY OTHER CONTRIBUTOR SHALL NOT BE LIABLE TO ANYONE FOR ANY DAMAGES OR OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THIS LICENSE, UNDER ANY KIND OF LEGAL CLAIM. + +This Hippocratic License is an Ethical Source license (https://ethicalsource.dev) and is offered for use by licensors and licensees at their own risk, on an “AS IS” basis, and with no warranties express or implied, to the maximum extent permitted by Laws. diff --git a/LICENSE_NORTHSTAR.md b/LICENSE_NORTHSTAR.md new file mode 100644 index 0000000..ad75e57 --- /dev/null +++ b/LICENSE_NORTHSTAR.md @@ -0,0 +1,9 @@ +Several components of this software were written by panicbit, and are released under the MIT License, available below. The specific elements written by panicbit can be found on panicbit's GitHub repo (https://github.com/panicbit/northstar), or by examining the git history. Please note that this project as a whole is licensed under *Hippocratic 2.1* and is **NOT** available under the MIT license. + +``` +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` From 81761b69d1027da67580e6bbecbcec4090a257ed Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Wed, 25 Nov 2020 02:04:37 -0500 Subject: [PATCH 054/113] Update Cargo.toml --- Cargo.toml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0d80a93..25a1338 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,15 @@ [package] name = "kochab" -version = "0.3.1" -authors = ["Emii Tatsuo ", "panicbit "] -edition = "2018" -license = "Hippocratic 2.1" +version = "0.1.0" description = "Ergonomic Gemini SDK" +authors = ["Emii Tatsuo ", "panicbit "] +license = "Hippocratic-2.1" +keywords = ["gemini", "server", "smallnet"] +categories = ["asynchronous", "network-programming"] +edition = "2018" repository = "https://github.com/Alch-Emi/kochab" +readme = "README.md" +include = ["src/**", "Cargo.*", "CHANGELOG.md", "LICENSE*", "README.md"] [features] user_management = ["sled", "bincode", "serde/derive", "crc32fast"] From a4f1017c5ffd1aae9076f22cd158bff618a72124 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Wed, 25 Nov 2020 15:39:00 -0500 Subject: [PATCH 055/113] Added automatic certificate generation (Closes #4) All my roommates left and now my apartment is lonely, but at least I can still pour unhealthy amounts of time into software projects nobody will ever use --- Cargo.toml | 4 +- src/gencert.rs | 125 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 52 ++++++++++++++++---- 3 files changed, 171 insertions(+), 10 deletions(-) create mode 100644 src/gencert.rs diff --git a/Cargo.toml b/Cargo.toml index 25a1338..f6abbf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,13 @@ readme = "README.md" include = ["src/**", "Cargo.*", "CHANGELOG.md", "LICENSE*", "README.md"] [features] +default = ["serve_dir", "certgen"] user_management = ["sled", "bincode", "serde/derive", "crc32fast"] user_management_advanced = ["rust-argon2", "ring", "user_management"] user_management_routes = ["user_management"] -default = ["serve_dir"] serve_dir = ["mime_guess", "tokio/fs"] ratelimiting = ["dashmap"] +certgen = ["rcgen"] [dependencies] anyhow = "1.0.33" @@ -38,6 +39,7 @@ serde = { version = "1.0", optional = true } rust-argon2 = { version = "0.8.2", optional = true } crc32fast = { version = "1.2.1", optional = true } ring = { version = "0.16.15", optional = true } +rcgen = { version = "0.8.5", optional = true } [dev-dependencies] env_logger = "0.8.1" diff --git a/src/gencert.rs b/src/gencert.rs new file mode 100644 index 0000000..224c65b --- /dev/null +++ b/src/gencert.rs @@ -0,0 +1,125 @@ +use anyhow::{bail, Context, Result}; +use rustls::ServerConfig; + +use std::fs; +use std::io::{stdin, stdout, Write}; +use std::path::Path; + +#[derive(Clone, Debug)] +/// The mode to use for determining the domains to use for a new certificate. Only +/// applies to [`CertGenMode::gencert()`]. +pub enum CertGenMode { + + /// Do not generate any certificates. Error if not available. + None, + + /// Use a provided set of domains + Preset(Vec), + + /// Prompt the user using stdin/stdout to enter domains. + Interactive, +} + +impl CertGenMode { + /// Generate a new self-signed certificate + /// + /// Assumes that certificates do not already exist, and will overwrite anything at the + /// provided paths. The paths provided should be paths to non-existant files which + /// the program has access to write to. + /// + /// ## Errors + /// + /// Returns an error if [`CertGenMode::None`], or if there is an error generating the + /// certificate, or writing to either of the provided files. + pub fn gencert(self, cert: impl AsRef, key: impl AsRef) -> Result { + let (domains, interactive) = match self { + Self::None => bail!("Automatic certificate generation disabled"), + Self::Preset(domains) => (domains, false), + Self::Interactive => (prompt_domains(), true), + }; + + let certificate = rcgen::generate_simple_self_signed(domains) + .context("Could not generate a certificate with the given domains")?; + + fs::create_dir_all( + cert.as_ref() + .parent() + .expect("Received directory as certificate path, should be a file.") + )?; + + fs::create_dir_all( + key.as_ref() + .parent() + .expect("Received directory as key path, should be a file.") + )?; + + fs::write(cert.as_ref(), &certificate.serialize_pem()?.as_bytes()) + .context("Failed to write newly generated certificate to file")?; + fs::write(key.as_ref(), &certificate.serialize_private_key_pem().as_bytes()) + .context("Failed to write newly generated private key to file")?; + + if interactive { + println!("Certificate generated successfully!"); + } + + Ok(certificate) + } + + /// Attempts to load a certificate/key from a file, or generate it if not found + /// + /// The produced certificate & key is immediately fed to a [`rustls::ServerConfig`] + /// + /// See [`CertGenMode::gencert()`] for more info. + /// + /// ## Errors + /// + /// Returns an error if a certificate is not found **and** cannot be generated. + pub fn load_or_generate(self, to: &mut ServerConfig, cert: impl AsRef, key: impl AsRef) -> Result<()> { + match (crate::load_cert_chain(&cert.as_ref().into()), crate::load_key(&key.as_ref().into())) { + (Ok(cert_chain), Ok(key)) => { + to.set_single_cert(cert_chain, key) + .context("Failed to use loaded TLS certificate")?; + }, + (Err(e), _) | (_, Err(e)) => { + warn!("Failed to load certificate from file: {}, now trying automatic generation", e); + let cert = self.gencert(cert, key).context("Could not generate certificate")?; + to.set_single_cert( + vec![rustls::Certificate(cert.serialize_der()?)], + rustls::PrivateKey(cert.serialize_private_key_der()) + )?; + } + } + Ok(()) + } +} + +/// Attempt to get domains by prompting the user +/// +/// Guaranteed to return at least one domain. The user is provided `localhost` as a +/// default. +/// +/// ## Panics +/// Panics if reading from stdin or writing to stdout returns an error. +pub fn prompt_domains() -> Vec { + let mut domains = Vec::with_capacity(1); + let mut input = String::with_capacity(8); + println!("Now generating self-signed certificate..."); + print!("Please enter a domain (CN) for your certificate [localhost]: "); + stdout().flush().unwrap(); + loop { + stdin().read_line(&mut input).unwrap(); + let domain = input.trim(); + if domain.is_empty() { + if domains.is_empty() { + println!("Using `localhost` as domain."); + domains.push("localhost".to_string()); + } + return domains; + } else { + domains.push(domain.to_owned()); + input.clear(); + print!("Add another domain, or finish with a blank input: "); + stdout().flush().unwrap(); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 827b21b..1c8c7d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,9 +37,13 @@ pub mod handling; pub mod ratelimiting; #[cfg(feature = "user_management")] pub mod user_management; +#[cfg(feature = "certgen")] +pub mod gencert; #[cfg(feature="user_management")] use user_management::UserManager; +#[cfg(feature = "certgen")] +use gencert::CertGenMode; pub use mime; pub use uriparse as uri; @@ -246,6 +250,8 @@ pub struct Builder { rate_limits: RoutingNode>, #[cfg(feature="user_management")] data_dir: PathBuf, + #[cfg(feature="certgen")] + certgen_mode: CertGenMode, } impl Builder { @@ -261,6 +267,8 @@ impl Builder { rate_limits: RoutingNode::default(), #[cfg(feature="user_management")] data_dir: "data".into(), + #[cfg(feature="certgen")] + certgen_mode: CertGenMode::Interactive, } } @@ -273,6 +281,18 @@ impl Builder { self } + #[cfg(feature="certgen")] + /// Determine where certificate config comes from, if generation is required + /// + /// If a certificate & keyfile are not available on the provided path, they can be + /// generated. If this happens, several modes can be used to generate them, including + /// not generating them and just erroring, interactively prompting the user for + /// information, and using pre-provided information. + pub fn set_certificate_generation_mode(mut self, mode: CertGenMode) -> Self { + self.certgen_mode = mode; + self + } + /// Sets the directory that kochab should look for TLS certs and keys into /// /// Northstar will look for files called `cert.pem` and `key.pem` in the provided @@ -401,8 +421,12 @@ impl Builder { } pub async fn serve(mut self) -> Result<()> { - let config = tls_config(&self.cert_path, &self.key_path) - .context("Failed to create TLS config")?; + let config = tls_config( + &self.cert_path, + &self.key_path, + #[cfg(feature="certgen")] + self.certgen_mode + ).context("Failed to create TLS config")?; let listener = TcpListener::bind(self.addr).await .context("Failed to create socket")?; @@ -467,15 +491,25 @@ async fn prune_ratelimit_log(rate_limits: Arc>>) } } -fn tls_config(cert_path: &PathBuf, key_path: &PathBuf) -> Result> { +fn tls_config( + cert_path: &PathBuf, + key_path: &PathBuf, + #[cfg(feature = "certgen")] + mode: CertGenMode, +) -> Result> { let mut config = ServerConfig::new(AllowAnonOrSelfsignedClient::new()); - let cert_chain = load_cert_chain(cert_path) - .context("Failed to load TLS certificate")?; - let key = load_key(key_path) - .context("Failed to load TLS key")?; - config.set_single_cert(cert_chain, key) - .context("Failed to use loaded TLS certificate")?; + #[cfg(feature = "certgen")] + mode.load_or_generate(&mut config, cert_path, key_path)?; + + #[cfg(not(feature = "certgen"))] { + let cert_chain = load_cert_chain(cert_path) + .context("Failed to load TLS certificate")?; + let key = load_key(key_path) + .context("Failed to load TLS key")?; + config.set_single_cert(cert_chain, key) + .context("Failed to use loaded TLS certificate")?; + } Ok(config.into()) } From 0b0d92597951d5a5f7e485a828d9740d490a7794 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Wed, 25 Nov 2020 15:44:20 -0500 Subject: [PATCH 056/113] Updated README to reflect automatic certificate generation --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b6a4b1f..a8cfcc6 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,13 @@ kochab = { git = "https://github.com/Alch-Emi/kochab.git" } # Generating a key & certificate -Run +By default, kochab enables the `certgen` feature, which will automatically generate a certificate for you. All you need to do is run the program once and follow the prompts printed to stdout. You can override this behavior by disabling the feature, or by using the methods in the `Builder`. + +If you want to generate a certificate manually, you can use the command: + ```sh mkdir cert && cd cert openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 ``` -and enter your domain name (e.g. "localhost" for testing) as Common Name (CN). - -Alternatively, if you want to include multiple domains add something like `-addext "subjectAltName = DNS:localhost, DNS:example.org"`. [northstar]: https://github.com/panicbit/northstar "Northstar GitHub" From 1766ab4c9ce37f81859aafa85ee64a5add36c2bb Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Wed, 25 Nov 2020 15:52:44 -0500 Subject: [PATCH 057/113] Merge `Builder` and `Server` in the public API into one concept --- src/handling.rs | 2 +- src/lib.rs | 16 ++++++---------- src/types/request.rs | 2 +- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/handling.rs b/src/handling.rs index 6f747b9..f869605 100644 --- a/src/handling.rs +++ b/src/handling.rs @@ -26,7 +26,7 @@ use crate::{Document, types::{Body, Response, Request}}; /// /// The most useful part of the documentation for this is the implementations of [`From`] /// on it, as this is what can be passed to -/// [`Builder::add_route`](crate::Builder::add_route) in order to create a new route. +/// [`Server::add_route()`](crate::Server::add_route()) in order to create a new route. /// Each implementation has bespoke docs that describe how the type is used, and what /// response is produced. pub enum Handler { diff --git a/src/lib.rs b/src/lib.rs index 1c8c7d5..caa0073 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,7 +55,7 @@ pub const GEMINI_PORT: u16 = 1965; use handling::Handler; #[derive(Clone)] -pub struct Server { +struct ServerInner { tls_acceptor: TlsAcceptor, routes: Arc>, timeout: Duration, @@ -66,11 +66,7 @@ pub struct Server { manager: UserManager, } -impl Server { - pub fn bind(addr: A) -> Builder { - Builder::bind(addr) - } - +impl ServerInner { async fn serve(self, listener: TcpListener) -> Result<()> { #[cfg(feature = "ratelimiting")] tokio::spawn(prune_ratelimit_log(self.rate_limits.clone())); @@ -239,7 +235,7 @@ impl Server { } } -pub struct Builder { +pub struct Server { addr: A, cert_path: PathBuf, key_path: PathBuf, @@ -254,8 +250,8 @@ pub struct Builder { certgen_mode: CertGenMode, } -impl Builder { - fn bind(addr: A) -> Self { +impl Server { + pub fn bind(addr: A) -> Self { Self { addr, timeout: Duration::from_secs(1), @@ -433,7 +429,7 @@ impl Builder { self.routes.shrink(); - let server = Server { + let server = ServerInner { tls_acceptor: TlsAcceptor::from(config), routes: Arc::new(self.routes), timeout: self.timeout, diff --git a/src/types/request.rs b/src/types/request.rs index 2c29f43..96363e1 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -75,7 +75,7 @@ impl Request { /// /// 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`](crate::Builder::add_route()) will + /// to handlers registered through [`add_route()`](crate::Server::add_route()) will /// always have trailing segments set. pub fn trailing_segments(&self) -> &Vec { self.trailing_segments.as_ref().unwrap() From 0633512f86eaf17e9852e26a5671e70a3bc56b8a Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Wed, 25 Nov 2020 16:04:04 -0500 Subject: [PATCH 058/113] Remove deprecated gemini_mime function --- src/gencert.rs | 8 ++++++++ src/lib.rs | 5 ----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/gencert.rs b/src/gencert.rs index 224c65b..33ff027 100644 --- a/src/gencert.rs +++ b/src/gencert.rs @@ -1,3 +1,11 @@ +//! Tools for automatically generating certificates +//! +//! Really, the only thing you will probably ever need from this module if you aren't +//! developing the project is the [`CertGenMode`] enum, which can be passed to +//! [`Server::set_certificate_generation_mode()`]. You won't even need to call any +//! methods on it or anything. +//! +//! [`Server::set_certificate_generation_mode()`]: crate::Server::set_certificate_generation_mode() use anyhow::{bail, Context, Result}; use rustls::ServerConfig; diff --git a/src/lib.rs b/src/lib.rs index caa0073..bbb3153 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -542,11 +542,6 @@ lazy_static! { pub static ref GEMINI_MIME: Mime = GEMINI_MIME_STR.parse().unwrap(); } -#[deprecated(note = "Use `GEMINI_MIME` instead", since = "0.3.0")] -pub fn gemini_mime() -> Result { - Ok(GEMINI_MIME.clone()) -} - /// A client cert verifier that accepts all connections /// /// Unfortunately, rustls doesn't provide a ClientCertVerifier that accepts self-signed From 82c990359803f56f9c8d7dec9df7536923893889 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Wed, 25 Nov 2020 20:28:55 -0500 Subject: [PATCH 059/113] serve_dir is no longer a default feature --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f6abbf2..6d5e0a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ readme = "README.md" include = ["src/**", "Cargo.*", "CHANGELOG.md", "LICENSE*", "README.md"] [features] -default = ["serve_dir", "certgen"] +default = ["certgen"] user_management = ["sled", "bincode", "serde/derive", "crc32fast"] user_management_advanced = ["rust-argon2", "ring", "user_management"] user_management_routes = ["user_management"] From 475b256a5a45f5a0e15816961270ec7e358d4476 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Wed, 25 Nov 2020 20:29:31 -0500 Subject: [PATCH 060/113] Fix example in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a8cfcc6..35a60f1 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Kochab is an extension & a fork of the Gemini SDK [northstar]. Where northstar It is currently only possible to use kochab through it's git repo, although it may wind up on crates.rs someday. ```toml -kochab = { git = "https://github.com/Alch-Emi/kochab.git" } +kochab = { git = "https://github.com/Alch-Emi/kochab.git", branch = "kochab" } ``` # Generating a key & certificate From 0ea4c6273121590c22a40f243a3454b708355e8d Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Wed, 25 Nov 2020 20:37:43 -0500 Subject: [PATCH 061/113] Fix a few bugs from combining features --- src/ratelimiting.rs | 2 +- src/user_management/routes.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ratelimiting.rs b/src/ratelimiting.rs index 68c086e..a3af264 100644 --- a/src/ratelimiting.rs +++ b/src/ratelimiting.rs @@ -7,7 +7,7 @@ use std::{fmt::Display, collections::VecDeque, hash::Hash, time::{Duration, Inst /// Does not require a leaky bucket thread to empty it out, but may occassionally need to /// trim old keys using [`trim_keys()`]. /// -/// [`trim_keys()`][Self::trim_keys()] +/// [`trim_keys()`]: Self::trim_keys() pub struct RateLimiter { log: DashMap>, burst: usize, diff --git a/src/user_management/routes.rs b/src/user_management/routes.rs index 31ddbf3..20168d4 100644 --- a/src/user_management/routes.rs +++ b/src/user_management/routes.rs @@ -84,7 +84,7 @@ pub trait UserManagementRoutes: private::Sealed { F: Send + Sync + 'static + Future>; } -impl UserManagementRoutes for crate::Builder { +impl UserManagementRoutes for crate::Server { /// Add pre-configured routes to the serve to handle authentication /// /// See [`UserManagementRoutes::add_um_routes()`] @@ -357,5 +357,5 @@ fn render_settings_menu( mod private { pub trait Sealed {} - impl Sealed for crate::Builder {} + impl Sealed for crate::Server {} } From 98583e737fa71baa6df8a9a72a005e1926c1cc29 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Wed, 25 Nov 2020 21:25:35 -0500 Subject: [PATCH 062/113] Fix user management --- src/user_management/routes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/user_management/routes.rs b/src/user_management/routes.rs index 20168d4..0aeb28b 100644 --- a/src/user_management/routes.rs +++ b/src/user_management/routes.rs @@ -116,7 +116,7 @@ impl UserManagementRoutes for crate::Server { Handler: Clone + Send + Sync + 'static + Fn(Request, RegisteredUser) -> F, F: Send + Sync + 'static + Future> { - self.add_route(path, move|request| { + self.add_route(path, move|request: Request| { let handler = handler.clone(); async move { Ok(match request.user::()? { From e83f2ca109b2e89e4107f67211b06ac110226aea Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Thu, 26 Nov 2020 13:50:46 -0500 Subject: [PATCH 063/113] Login flow now redirects back to where it started Closes #8 Still in testing, but seems good I think --- examples/user_management.rs | 2 +- src/user_management/pages/unauth.gmi | 4 +- src/user_management/routes.rs | 149 ++++++++++++++++++++------- 3 files changed, 113 insertions(+), 42 deletions(-) diff --git a/examples/user_management.rs b/examples/user_management.rs index 740a0f1..bbb14f1 100644 --- a/examples/user_management.rs +++ b/examples/user_management.rs @@ -33,7 +33,7 @@ async fn main() -> Result<()> { .add_authenticated_input_route("/update", "Enter your new string:", handle_update) // Add routes for handling user authentication - .add_um_routes::("/") + .add_um_routes::() // Start the server .serve() diff --git a/src/user_management/pages/unauth.gmi b/src/user_management/pages/unauth.gmi index 609ac5c..a234a4c 100644 --- a/src/user_management/pages/unauth.gmi +++ b/src/user_management/pages/unauth.gmi @@ -2,8 +2,8 @@ It seems like you don't have a client certificate enabled. In order to log in, you need to connect using a client certificate. If your client supports it, you can use the link below to activate a certificate. -=> /account/askcert Choose a Certificate +=> /account/askcert/{redirect} Choose a Certificate If your client can't automatically manage client certificates, check the link below for a list of clients that support client certificates. -=> /account/clients Clients +=> /account/clients/{redirect} Clients diff --git a/src/user_management/routes.rs b/src/user_management/routes.rs index 0aeb28b..6c46383 100644 --- a/src/user_management/routes.rs +++ b/src/user_management/routes.rs @@ -2,17 +2,24 @@ use anyhow::Result; use tokio::net::ToSocketAddrs; use serde::{Serialize, de::DeserializeOwned}; +#[cfg(feature = "dashmap")] +use dashmap::DashMap; +#[cfg(not(feature = "dashmap"))] +use std::collections::HashMap; +#[cfg(not(feature = "dashmap"))] +use std::sync::RwLock; + use std::future::Future; use crate::{Document, Request, Response}; use crate::types::document::HeadingLevel; -use crate::user_management::{User, RegisteredUser, UserManagerError}; - -const UNAUTH: &str = include_str!("pages/unauth.gmi"); -#[cfg(feature = "user_management_advanced")] -const NSI: &str = include_str!("pages/nsi.gmi"); -#[cfg(not(feature = "user_management_advanced"))] -const NSI: &str = include_str!("pages/nopass/nsi.gmi"); +use crate::user_management::{ + User, + RegisteredUser, + UserManager, + UserManagerError, + user::NotSignedInUser, +}; /// Import this trait to use [`add_um_routes()`](Self::add_um_routes()) pub trait UserManagementRoutes: private::Sealed { @@ -31,7 +38,7 @@ pub trait UserManagementRoutes: private::Sealed { /// /// The `redir` argument allows you to specify the point that users will be directed /// to return to once their account has been created. - fn add_um_routes(self, redir: &'static str) -> Self; + fn add_um_routes(self) -> Self; /// Add a special route that requires users to be logged in /// @@ -88,15 +95,15 @@ impl UserManagementRoutes for crate::Server { /// Add pre-configured routes to the serve to handle authentication /// /// See [`UserManagementRoutes::add_um_routes()`] - fn add_um_routes(self, redir: &'static str) -> Self { + fn add_um_routes(self) -> Self { #[allow(unused_mut)] - let mut modified_self = self.add_route("/account", move|r|handle_base::(r, redir)) - .add_route("/account/askcert", move|r|handle_ask_cert::(r, redir)) - .add_route("/account/register", move|r|handle_register::(r, redir)); + let mut modified_self = self.add_route("/account", handle_base::) + .add_route("/account/askcert", handle_ask_cert::) + .add_route("/account/register", handle_register::); #[cfg(feature = "user_management_advanced")] { modified_self = modified_self - .add_route("/account/login", move|r|handle_login::(r, redir)) + .add_route("/account/login", handle_login::) .add_route("/account/password", handle_password::); } @@ -119,11 +126,14 @@ impl UserManagementRoutes for crate::Server { self.add_route(path, move|request: Request| { let handler = handler.clone(); async move { + let segments = request.path_segments(); + let segments = segments.iter().map(String::as_ref).collect::>(); Ok(match request.user::()? { User::Unauthenticated => { - Response::success_gemini(UNAUTH) + render_unauth_page(segments) }, - User::NotSignedIn(_) => { + User::NotSignedIn(user) => { + save_redirect(&user, segments); Response::success_gemini(NSI) }, User::SignedIn(user) => { @@ -161,26 +171,46 @@ impl UserManagementRoutes for crate::Server { } } -async fn handle_base(request: Request, redirect: &'static str) -> Result { +#[cfg(feature = "user_management_advanced")] +const NSI: &str = include_str!("pages/nsi.gmi"); +#[cfg(not(feature = "user_management_advanced"))] +const NSI: &str = include_str!("pages/nopass/nsi.gmi"); + +// TODO periodically clean these +#[cfg(feature = "dashmap")] +lazy_static::lazy_static! { + static ref PENDING_REDIRECTS: DashMap = Default::default(); +} + +#[cfg(not(feature = "dashmap"))] +lazy_static::lazy_static! { + static ref PENDING_REDIRECTS: RwLock> = Default::default(); +} + +async fn handle_base(request: Request) -> Result { + let segments = request.trailing_segments().iter().map(String::as_str).collect::>(); Ok(match request.user::()? { User::Unauthenticated => { - Response::success_gemini(UNAUTH) + render_unauth_page(segments) }, - User::NotSignedIn(_) => { + User::NotSignedIn(usr) => { + save_redirect(&usr, segments); Response::success_gemini(NSI) }, User::SignedIn(user) => { - render_settings_menu(user, redirect) + render_settings_menu(user) }, }) } -async fn handle_ask_cert(request: Request, redirect: &'static str) -> Result { +async fn handle_ask_cert(request: Request) -> Result { Ok(match request.user::()? { User::Unauthenticated => { Response::client_certificate_required() }, - User::NotSignedIn(_) => { + User::NotSignedIn(nsi) => { + let segments = request.trailing_segments().iter().map(String::as_str).collect::>(); + save_redirect(&nsi, segments); #[cfg(feature = "user_management_advanced")] { Response::success_gemini(include_str!("pages/askcert/success.gmi")) } @@ -192,16 +222,16 @@ async fn handle_ask_cert(request: Reques Response::success_gemini(format!( include_str!("pages/askcert/exists.gmi"), username = user.username(), - redirect = redirect, + redirect = get_redirect(&user), )) }, }) } -async fn handle_register(request: Request, redirect: &'static str) -> Result { +async fn handle_register(request: Request) -> Result { Ok(match request.user::()? { User::Unauthenticated => { - Response::success_gemini(UNAUTH) + render_unauth_page(&[""]) }, User::NotSignedIn(nsi) => { if let Some(username) = request.input() { @@ -220,19 +250,19 @@ async fn handle_register(reque )) } }, - Ok(_) => { + Ok(user) => { #[cfg(feature = "user_management_advanced")] { Response::success_gemini(format!( include_str!("pages/register/success.gmi"), username = username, - redirect = redirect, + redirect = get_redirect(&user), )) } #[cfg(not(feature = "user_management_advanced"))] { Response::success_gemini(format!( include_str!("pages/nopass/register/success.gmi"), username = username, - redirect = redirect, + redirect = get_redirect(&user), )) } }, @@ -243,16 +273,16 @@ async fn handle_register(reque } }, User::SignedIn(user) => { - render_settings_menu(user, redirect) + render_settings_menu(user) }, }) } #[cfg(feature = "user_management_advanced")] -async fn handle_login(request: Request, redirect: &'static str) -> Result { +async fn handle_login(request: Request) -> Result { Ok(match request.user::()? { User::Unauthenticated => { - Response::success_gemini(UNAUTH) + render_unauth_page(&[""]) }, User::NotSignedIn(nsi) => { if let Some(username) = request.trailing_segments().get(0) { @@ -264,11 +294,11 @@ async fn handle_login(request: username = username, )) }, - Ok(_) => { + Ok(Some(user)) => { Response::success_gemini(format!( include_str!("pages/login/success.gmi"), username = username, - redirect = redirect, + redirect = get_redirect(&user), )) }, Err(e) => return Err(e.into()), @@ -285,7 +315,7 @@ async fn handle_login(request: } }, User::SignedIn(user) => { - render_settings_menu(user, redirect) + render_settings_menu(user) }, }) } @@ -294,9 +324,10 @@ async fn handle_login(request: async fn handle_password(request: Request) -> Result { Ok(match request.user::()? { User::Unauthenticated => { - Response::success_gemini(UNAUTH) + render_unauth_page(&[""]) }, - User::NotSignedIn(_) => { + User::NotSignedIn(nsi) => { + save_redirect(&nsi, &[""]); Response::success_gemini(NSI) }, User::SignedIn(mut user) => { @@ -318,10 +349,8 @@ async fn handle_password(reque }) } - fn render_settings_menu( - user: RegisteredUser, - redirect: &str + user: RegisteredUser ) -> Response { let mut document = Document::new(); document @@ -329,7 +358,7 @@ fn render_settings_menu( .add_blank_line() .add_text(&format!("Welcome {}!", user.username())) .add_blank_line() - .add_link(redirect, "Back to the app") + .add_link(get_redirect(&user).as_str(), "Back to the app") .add_blank_line(); #[cfg(feature = "user_management_advanced")] @@ -355,6 +384,48 @@ fn render_settings_menu( document.into() } +fn render_unauth_page<'a>( + redirect: impl AsRef<[&'a str]>, +) -> Response { + Response::success_gemini(format!( + include_str!("pages/unauth.gmi"), + redirect = redirect.as_ref().join("/"), + )) +} + +fn save_redirect<'a>( + user: &NotSignedInUser, + redirect: impl AsRef<[&'a str]>, +) { + let mut redirect = redirect.as_ref().join("/"); + redirect.insert(0, '/'); + if redirect.len() > 1 { + #[cfg(feature = "dashmap")] + let ref_to_map = &*PENDING_REDIRECTS; + #[cfg(not(feature = "dashmap"))] + let mut ref_to_map = PENDING_REDIRECTS.write().unwrap(); + + let cert_hash = UserManager::hash_certificate(&user.certificate); + debug!("Added \"{}\" as redirect for cert {:x}", redirect, cert_hash); + ref_to_map.insert(cert_hash, redirect); + } +} + +fn get_redirect(user: &RegisteredUser) -> String { + let cert_hash = UserManager::hash_certificate(user.active_certificate().unwrap()); + + #[cfg(feature = "dashmap")] + let maybe_redir = PENDING_REDIRECTS.get(&cert_hash).map(|r| r.clone()); + #[cfg(not(feature = "dashmap"))] + let ref_to_map = PENDING_REDIRECTS.read().unwrap(); + #[cfg(not(feature = "dashmap"))] + let maybe_redir = ref_to_map.get(&cert_hash).cloned(); + + let redirect = maybe_redir.unwrap_or_else(||"/".to_string()); + debug!("Accessed redirect to \"{}\" for cert {:x}", redirect, cert_hash); + redirect +} + mod private { pub trait Sealed {} impl Sealed for crate::Server {} From 47c6fae79f59d6d35c23435e6b8675e3e3428645 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Thu, 26 Nov 2020 21:04:02 -0500 Subject: [PATCH 064/113] Expose user manager in request --- src/types/request.rs | 8 ++++++++ src/user_management/manager.rs | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/types/request.rs b/src/types/request.rs index 96363e1..a914a76 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -126,6 +126,14 @@ impl Request { { Ok(self.manager.get_user(self.certificate())?) } + + #[cfg(feature="user_management")] + /// Expose the server's UserManager + /// + /// Can be used to query users, or directly access the database + pub fn user_manager(&self) -> &UserManager { + &self.manager + } } impl ops::Deref for Request { diff --git a/src/user_management/manager.rs b/src/user_management/manager.rs index d9df8e1..61035d9 100644 --- a/src/user_management/manager.rs +++ b/src/user_management/manager.rs @@ -26,7 +26,7 @@ struct CertificateDef(Vec); /// /// Wraps a [`sled::Db`] pub struct UserManager { - db: sled::Db, + pub db: sled::Db, pub (crate) users: sled::Tree, // user_id:String maps to data:UserData pub (crate) certificates: sled::Tree, // certificate:u64 maps to data:CertificateData } @@ -40,8 +40,8 @@ impl UserManager { pub fn new(dir: impl AsRef) -> Result { let db = sled::open(dir)?; Ok(Self { - users: db.open_tree("users")?, - certificates: db.open_tree("certificates")?, + users: db.open_tree("gay.emii.kochab.users")?, + certificates: db.open_tree("gay.emii.kochab.certificates")?, db, }) } From 1e5e4c8731db7511bd98691e231129f0bd9cfb9b Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Fri, 27 Nov 2020 17:24:16 -0500 Subject: [PATCH 065/113] Allow directly setting a database when building --- src/lib.rs | 29 ++++++++++++++++++++++++++++- src/user_management/manager.rs | 5 +---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bbb3153..9cdf1cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -246,6 +246,8 @@ pub struct Server { rate_limits: RoutingNode>, #[cfg(feature="user_management")] data_dir: PathBuf, + #[cfg(feature="user_management")] + database: Option, #[cfg(feature="certgen")] certgen_mode: CertGenMode, } @@ -263,6 +265,8 @@ impl Server { rate_limits: RoutingNode::default(), #[cfg(feature="user_management")] data_dir: "data".into(), + #[cfg(feature="user_management")] + database: None, #[cfg(feature="certgen")] certgen_mode: CertGenMode::Interactive, } @@ -271,12 +275,30 @@ impl Server { #[cfg(feature="user_management")] /// Sets the directory to store user data in /// + /// This will only be used if a database is not provided with [`set_database()`]. + /// /// Defaults to `./data` if not specified + /// + /// [`set_database()`]: Self::set_database() pub fn set_database_dir(mut self, path: impl Into) -> Self { self.data_dir = path.into(); self } + #[cfg(feature="user_management")] + /// Sets a specific database to use + /// + /// This opens to trees within the database, both namespaced to avoid collisions. + /// + /// If this is not provided, a database will be opened at the directory provided by + /// [`set_database_dir()`] + /// + /// [`set_database_dir()`]: Self::set_database_dir() + pub fn set_database(mut self, db: sled::Db) -> Self { + self.database = Some(db); + self + } + #[cfg(feature="certgen")] /// Determine where certificate config comes from, if generation is required /// @@ -429,6 +451,9 @@ impl Server { self.routes.shrink(); + #[cfg(feature="user_management")] + let data_dir = self.data_dir; + let server = ServerInner { tls_acceptor: TlsAcceptor::from(config), routes: Arc::new(self.routes), @@ -437,7 +462,9 @@ impl Server { #[cfg(feature="ratelimiting")] rate_limits: Arc::new(self.rate_limits), #[cfg(feature="user_management")] - manager: UserManager::new(self.data_dir)?, + manager: UserManager::new( + self.database.unwrap_or_else(move|| sled::open(data_dir).unwrap()) + )?, }; server.serve(listener).await diff --git a/src/user_management/manager.rs b/src/user_management/manager.rs index 61035d9..4dd639a 100644 --- a/src/user_management/manager.rs +++ b/src/user_management/manager.rs @@ -1,8 +1,6 @@ use rustls::Certificate; use serde::{Deserialize, Serialize, de::DeserializeOwned}; -use std::path::Path; - use crate::user_management::{User, Result}; use crate::user_management::user::{RegisteredUser, NotSignedInUser, PartialUser}; @@ -37,8 +35,7 @@ impl UserManager { /// /// The `dir` argument is the path to a data directory, to be populated using sled. /// This will be created if it does not exist. - pub fn new(dir: impl AsRef) -> Result { - let db = sled::open(dir)?; + pub fn new(db: sled::Db) -> Result { Ok(Self { users: db.open_tree("gay.emii.kochab.users")?, certificates: db.open_tree("gay.emii.kochab.certificates")?, From 6c3ae626e900055d30b278f65af2e3a6726bc782 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Fri, 27 Nov 2020 18:01:49 -0500 Subject: [PATCH 066/113] Added ability to iterate through users in the database --- src/user_management/manager.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/user_management/manager.rs b/src/user_management/manager.rs index 4dd639a..4bdabd9 100644 --- a/src/user_management/manager.rs +++ b/src/user_management/manager.rs @@ -83,6 +83,29 @@ impl UserManager { } } + /// Produce a list of all users in the database + /// + /// # Panics + /// An panics if there is an error reading from the database or if data recieved from + /// the database is corrupt + pub fn all_users( + &self, + ) -> Vec> + where + UserData: Serialize + DeserializeOwned + { + self.users.iter() + .map(|result| { + let (username, bytes) = result.expect("Failed to connect to database"); + let inner: PartialUser = bincode::deserialize_from(bytes.as_ref()) + .expect("Received malformed data from database"); + let username = String::from_utf8(username.to_vec()) + .expect("Malformed username in database"); + RegisteredUser::new(username, None, self.clone(), inner) + }) + .collect() + } + /// Attempt to determine the user who sent a request based on the certificate. /// /// # Errors From fa0d031ee865e012227744f6eab55b6950d409ae Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Fri, 27 Nov 2020 18:36:24 -0500 Subject: [PATCH 067/113] Fix crash on successful log in --- src/user_management/user.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/user_management/user.rs b/src/user_management/user.rs index 8894462..d3c333e 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -177,7 +177,8 @@ impl NotSignedInUser { } } - user.add_certificate(self.certificate)?; + user.add_certificate(self.certificate.clone())?; + user.active_certificate = Some(self.certificate); Ok(Some(user)) } else { Ok(None) From 49e62a2300200025422f81788f53a6afbe5b7253 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Fri, 27 Nov 2020 19:50:01 -0500 Subject: [PATCH 068/113] Add clients page --- src/user_management/pages/clients.gmi | 32 +++++++++++++++++++++++++++ src/user_management/routes.rs | 5 ++++- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 src/user_management/pages/clients.gmi diff --git a/src/user_management/pages/clients.gmi b/src/user_management/pages/clients.gmi new file mode 100644 index 0000000..53714f3 --- /dev/null +++ b/src/user_management/pages/clients.gmi @@ -0,0 +1,32 @@ +# Client-cert Compatible Gemini Clients + +The following is an (incomplete) list of clients known to support client certicates. You will need to use one of these (or another client-cert compatible client) in order to log in. + +## Amfora (TUI) + +Amfora supports client certs, although they currently need to be manually linked. Instructions for how to do this can be found on Amfora's GitHub + +=> https://github.com/makeworld-the-better-one/amfora#client-certificates Instructions +=> https://github.com/makeworld-the-better-one/amfora.git Website + +## Kristal (GUI) + +Kristal supports certificates out of the box! All you need to do is click on a link requesting a client certificate, and Kristall will prompt you to create one. Remember to set an account password if you're using ephemeral certificates though, otherwise you won't be able to log back in when they expire! + +=> https://github.com/MasterQ32/kristall Website + +## AV-98 (TUI) + +AV-98 (The OG gemini client) will automatically generate a client certificate for you when you click on a link that requires one. + +=> https://tildegit.org/solderpunk/AV-98 Website + +## Gemini for iOS (Mobile) + +=> https://github.com/pitr/gemini-ios Website + +## Others? + +If you know of more clients, please feel free to add them to this list by opening a PR or an issue on the kochab (Gemini Server SDK) GitHub! + +=> https://github.com/Alch-Emi/kochab/issues/new New Issue diff --git a/src/user_management/routes.rs b/src/user_management/routes.rs index 6c46383..55b613b 100644 --- a/src/user_management/routes.rs +++ b/src/user_management/routes.rs @@ -96,10 +96,13 @@ impl UserManagementRoutes for crate::Server { /// /// See [`UserManagementRoutes::add_um_routes()`] fn add_um_routes(self) -> Self { + let clients_page = Response::success_gemini(include_str!("pages/clients.gmi")); + #[allow(unused_mut)] let mut modified_self = self.add_route("/account", handle_base::) .add_route("/account/askcert", handle_ask_cert::) - .add_route("/account/register", handle_register::); + .add_route("/account/register", handle_register::) + .add_route("/account/clients", clients_page); #[cfg(feature = "user_management_advanced")] { modified_self = modified_self From eca5c2fd447acebd518ab6f8c0f2124fe79e09f4 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Sat, 28 Nov 2020 14:23:39 -0500 Subject: [PATCH 069/113] Add a couple logging messages --- src/user_management/user.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/user_management/user.rs b/src/user_management/user.rs index d3c333e..0b96f77 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -116,6 +116,8 @@ impl NotSignedInUser { if self.manager.users.contains_key(username.as_str())? { Err(super::UserManagerError::UsernameNotUnique) } else { + info!("User {} registered!", username); + let mut newser = RegisteredUser::new( username, Some(self.certificate.clone()), @@ -177,6 +179,8 @@ impl NotSignedInUser { } } + let certhash = UserManager::hash_certificate(&self.certificate); + info!("User {} attached certificate with hash {:x}", username, certhash); user.add_certificate(self.certificate.clone())?; user.active_certificate = Some(self.certificate); Ok(Some(user)) From 4a2293ddc66b89f4c60df6ef0fc20f76b5def3de Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Sat, 28 Nov 2020 15:01:39 -0500 Subject: [PATCH 070/113] Add specific methods for getting at UserData --- src/user_management/user.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/user_management/user.rs b/src/user_management/user.rs index 0b96f77..baa139a 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -375,6 +375,23 @@ impl RegisteredUser { pub fn has_password(&self) -> bool { self.inner.pass_hash.is_some() } + + /// Get an immutable reference to the data associated with this user + pub fn data(&self) -> &UserData { + &self.inner.data + } + + /// Get a mutable reference to the data associated with this user + /// + /// This automatically flags the user data as needing to be saved to the database, + /// which automatically performs the action when this user falls out of scope. If + /// need be, you can push these changes to the database sooner by calling [`save()`] + /// + /// [`save()`]: Self::save() + pub fn mut_data(&mut self) -> &mut UserData { + self.has_changed = true; + &mut self.inner.data + } } impl std::ops::Drop for RegisteredUser { @@ -389,15 +406,12 @@ impl std::ops::Drop for RegisteredUser AsRef for RegisteredUser { fn as_ref(&self) -> &UserData { - &self.inner.data + self.data() } } impl AsMut for RegisteredUser { - /// NOTE: Changes made to the user data won't be persisted until RegisteredUser::save - /// is called fn as_mut(&mut self) -> &mut UserData { - self.has_changed = true; - &mut self.inner.data + self.mut_data() } } From 2c09831d226f66eef92a7747cbfa71b3714dff0c Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Mon, 30 Nov 2020 17:46:28 -0500 Subject: [PATCH 071/113] Move to GitLab --- Cargo.toml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6d5e0a7..ab91deb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ license = "Hippocratic-2.1" keywords = ["gemini", "server", "smallnet"] categories = ["asynchronous", "network-programming"] edition = "2018" -repository = "https://github.com/Alch-Emi/kochab" +repository = "https://gitlab.com/Alch_Emi/kochab" readme = "README.md" include = ["src/**", "Cargo.*", "CHANGELOG.md", "LICENSE*", "README.md"] diff --git a/README.md b/README.md index 35a60f1..78536fe 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Kochab is an extension & a fork of the Gemini SDK [northstar]. Where northstar It is currently only possible to use kochab through it's git repo, although it may wind up on crates.rs someday. ```toml -kochab = { git = "https://github.com/Alch-Emi/kochab.git", branch = "kochab" } +kochab = { git = "https://gitlab.com/Alch_Emi/kochab.git", branch = "kochab" } ``` # Generating a key & certificate From bc6d0b89bcc959e20b6a5387d9d6e96a55210cff Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Mon, 30 Nov 2020 17:48:19 -0500 Subject: [PATCH 072/113] Make it clearer that kochab handles certificate generation --- README.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 78536fe..f923469 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,8 @@ kochab = { git = "https://gitlab.com/Alch_Emi/kochab.git", branch = "kochab" } # Generating a key & certificate -By default, kochab enables the `certgen` feature, which will automatically generate a certificate for you. All you need to do is run the program once and follow the prompts printed to stdout. You can override this behavior by disabling the feature, or by using the methods in the `Builder`. +By default, kochab enables the `certgen` feature, which will **automatically generate a certificate** for you. All you need to do is run the program once and follow the prompts printed to stdout. You can override this behavior by disabling the feature, or by using the methods in the `Builder`. -If you want to generate a certificate manually, you can use the command: - -```sh -mkdir cert && cd cert -openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -``` +If you want to generate a certificate manually, it's recommended that you temporarily enable the `certgen` feature to do it, and then disable it once you're done, although you can also use the `openssl` client tool if you wish [northstar]: https://github.com/panicbit/northstar "Northstar GitHub" From 86ed2407610627d3df511d32d71fc56e9e1609b7 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Mon, 30 Nov 2020 21:18:37 -0500 Subject: [PATCH 073/113] Remove dependancy on `mime` when `serve_dir` is off --- Cargo.toml | 5 ++--- src/lib.rs | 20 -------------------- src/types.rs | 1 - src/types/meta.rs | 7 ------- src/types/response.rs | 9 ++++----- src/types/response_header.rs | 3 +-- src/util.rs | 14 ++++++-------- 7 files changed, 13 insertions(+), 46 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ab91deb..6a7127b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ include = ["src/**", "Cargo.*", "CHANGELOG.md", "LICENSE*", "README.md"] [features] default = ["certgen"] -user_management = ["sled", "bincode", "serde/derive", "crc32fast"] +user_management = ["sled", "bincode", "serde/derive", "crc32fast", "lazy_static"] user_management_advanced = ["rust-argon2", "ring", "user_management"] user_management_routes = ["user_management"] serve_dir = ["mime_guess", "tokio/fs"] @@ -25,12 +25,11 @@ anyhow = "1.0.33" rustls = { version = "0.18.1", features = ["dangerous_configuration"] } tokio-rustls = "0.20.0" tokio = { version = "0.3.1", features = ["io-util","net","time", "rt"] } -mime = "0.3.16" uriparse = "0.6.3" percent-encoding = "2.1.0" log = "0.4.11" webpki = "0.21.0" -lazy_static = "1.4.0" +lazy_static = { version = "1.4.0", optional = true } mime_guess = { version = "2.0.3", optional = true } dashmap = { version = "3.11.10", optional = true } sled = { version = "0.34.6", optional = true } diff --git a/src/lib.rs b/src/lib.rs index 9cdf1cf..83ae391 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,6 @@ use rustls::internal::msgs::handshake::DigitallySignedStruct; use tokio_rustls::{rustls, TlsAcceptor}; use rustls::*; use anyhow::*; -use lazy_static::lazy_static; use crate::util::opt_timeout; use routing::RoutingNode; #[cfg(feature = "ratelimiting")] @@ -45,7 +44,6 @@ use user_management::UserManager; #[cfg(feature = "certgen")] use gencert::CertGenMode; -pub use mime; pub use uriparse as uri; pub use types::*; @@ -561,14 +559,6 @@ fn load_key(key_path: &PathBuf) -> Result { Ok(key) } -/// Mime for Gemini documents -pub const GEMINI_MIME_STR: &str = "text/gemini"; - -lazy_static! { - /// Mime for Gemini documents ("text/gemini") - pub static ref GEMINI_MIME: Mime = GEMINI_MIME_STR.parse().unwrap(); -} - /// A client cert verifier that accepts all connections /// /// Unfortunately, rustls doesn't provide a ClientCertVerifier that accepts self-signed @@ -625,15 +615,5 @@ impl ClientCertVerifier for AllowAnonOrSelfsignedClient { } } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn gemini_mime_parses() { - let _: &Mime = &GEMINI_MIME; - } -} - #[cfg(feature = "ratelimiting")] enum Never {} diff --git a/src/types.rs b/src/types.rs index b376427..0ad442d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,4 +1,3 @@ -pub use ::mime::Mime; pub use rustls::Certificate; pub use uriparse::URIReference; diff --git a/src/types/meta.rs b/src/types/meta.rs index 8123d46..80477fc 100644 --- a/src/types/meta.rs +++ b/src/types/meta.rs @@ -1,5 +1,4 @@ use anyhow::*; -use crate::Mime; use crate::util::Cowy; @@ -46,12 +45,6 @@ impl Meta { pub fn as_str(&self) -> &str { &self.0 } - - pub fn to_mime(&self) -> Result { - let mime = self.as_str().parse::() - .context("Meta is not a valid MIME")?; - Ok(mime) - } } #[cfg(test)] diff --git a/src/types/response.rs b/src/types/response.rs index 39c7cdb..1baf59e 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -3,9 +3,8 @@ use std::borrow::Borrow; use anyhow::*; use uriparse::URIReference; -use crate::types::{ResponseHeader, Body, Mime, Document}; +use crate::types::{ResponseHeader, Body, Document}; use crate::util::Cowy; -use crate::GEMINI_MIME; pub struct Response { header: ResponseHeader, @@ -44,7 +43,7 @@ impl Response { } /// Create a successful response with a given body and MIME - pub fn success(mime: &Mime, body: impl Into) -> Self { + pub fn success(mime: impl ToString, body: impl Into) -> Self { Self { header: ResponseHeader::success(mime), body: Some(body.into()), @@ -53,12 +52,12 @@ impl Response { /// Create a successful response with a `text/gemini` MIME pub fn success_gemini(body: impl Into) -> Self { - Self::success(&GEMINI_MIME, body) + Self::success("text/gemini", body) } /// Create a successful response with a `text/plain` MIME pub fn success_plain(body: impl Into) -> Self { - Self::success(&mime::TEXT_PLAIN, body) + Self::success("text/plain", body) } pub fn server_error(reason: impl Cowy) -> Result { diff --git a/src/types/response_header.rs b/src/types/response_header.rs index b2b5e20..3e214d1 100644 --- a/src/types/response_header.rs +++ b/src/types/response_header.rs @@ -2,7 +2,6 @@ use std::convert::TryInto; use anyhow::*; use uriparse::URIReference; -use crate::Mime; use crate::util::Cowy; use crate::types::{Status, Meta}; @@ -27,7 +26,7 @@ impl ResponseHeader { } } - pub fn success(mime: &Mime) -> Self { + pub fn success(mime: impl ToString) -> Self { Self { status: Status::SUCCESS, meta: Meta::new_lossy(mime.to_string()), diff --git a/src/util.rs b/src/util.rs index 97df128..32d521d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,7 +1,5 @@ #[cfg(feature="serve_dir")] use std::path::{Path, PathBuf}; -#[cfg(feature="serve_dir")] -use mime::Mime; use anyhow::*; #[cfg(feature="serve_dir")] use tokio::{ @@ -16,7 +14,7 @@ use std::future::Future; use tokio::time; #[cfg(feature="serve_dir")] -pub async fn serve_file>(path: P, mime: &Mime) -> Result { +pub async fn serve_file>(path: P, mime: &str) -> Result { let path = path.as_ref(); let file = match File::open(path).await { @@ -81,7 +79,7 @@ pub async fn serve_dir, P: AsRef>(dir: D, virtual_path: &[P if !path.is_dir() { let mime = guess_mime_from_path(&path); - return serve_file(path, &mime).await; + return serve_file(path, mime).await; } serve_dir_listing(path, virtual_path).await @@ -131,19 +129,19 @@ async fn serve_dir_listing, B: AsRef>(path: P, virtual_path } #[cfg(feature="serve_dir")] -pub fn guess_mime_from_path>(path: P) -> Mime { +pub fn guess_mime_from_path>(path: P) -> &'static str { let path = path.as_ref(); let extension = path.extension().and_then(|s| s.to_str()); let extension = match extension { Some(extension) => extension, - None => return mime::APPLICATION_OCTET_STREAM, + None => return "application/octet-stream" }; if let "gemini" | "gmi" = extension { - return crate::GEMINI_MIME.clone(); + return "text/gemini"; } - mime_guess::from_ext(extension).first_or_octet_stream() + mime_guess::from_ext(extension).first_raw().unwrap_or("application/octet-stream") } #[cfg(feature="serve_dir")] From 244fd25112880d2e204007d35a10317ff2437c5d Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Tue, 1 Dec 2020 02:31:08 -0500 Subject: [PATCH 074/113] Completely reworked request handling to be able to serve SCGI Multi ~~track~~ protocol ~~drifting~~ abstraction!! --- Cargo.toml | 13 +- examples/certificates.rs | 6 +- examples/document.rs | 6 +- examples/ratelimiting.rs | 6 +- examples/routing.rs | 6 +- examples/serve_dir.rs | 6 +- examples/user_management.rs | 5 +- molly-brown.conf | 20 +++ src/lib.rs | 325 ++++++++++++++++++++++++++++++------ src/types.rs | 3 + src/types/request.rs | 69 ++++++-- 11 files changed, 371 insertions(+), 94 deletions(-) create mode 100644 molly-brown.conf diff --git a/Cargo.toml b/Cargo.toml index 6a7127b..cd33c61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,24 +12,26 @@ readme = "README.md" include = ["src/**", "Cargo.*", "CHANGELOG.md", "LICENSE*", "README.md"] [features] -default = ["certgen"] +default = ["scgi_srv"] user_management = ["sled", "bincode", "serde/derive", "crc32fast", "lazy_static"] user_management_advanced = ["rust-argon2", "ring", "user_management"] user_management_routes = ["user_management"] serve_dir = ["mime_guess", "tokio/fs"] ratelimiting = ["dashmap"] -certgen = ["rcgen"] +certgen = ["rcgen", "gemini_srv"] +gemini_srv = ["tokio-rustls", "webpki", "rustls"] +scgi_srv = [] [dependencies] anyhow = "1.0.33" -rustls = { version = "0.18.1", features = ["dangerous_configuration"] } -tokio-rustls = "0.20.0" tokio = { version = "0.3.1", features = ["io-util","net","time", "rt"] } uriparse = "0.6.3" percent-encoding = "2.1.0" log = "0.4.11" -webpki = "0.21.0" lazy_static = { version = "1.4.0", optional = true } +rustls = { version = "0.18.1", features = ["dangerous_configuration"], optional = true} +webpki = { version = "0.21.0", optional = true} +tokio-rustls = { version = "0.20.0", optional = true} mime_guess = { version = "2.0.3", optional = true } dashmap = { version = "3.11.10", optional = true } sled = { version = "0.34.6", optional = true } @@ -39,6 +41,7 @@ rust-argon2 = { version = "0.8.2", optional = true } crc32fast = { version = "1.2.1", optional = true } ring = { version = "0.16.15", optional = true } rcgen = { version = "0.8.5", optional = true } +squeegee = { git = "https://gitlab.com/Alch_Emi/squeegee.git", branch = "main", optional = true } [dev-dependencies] env_logger = "0.8.1" diff --git a/examples/certificates.rs b/examples/certificates.rs index a5ca78b..28e7e1d 100644 --- a/examples/certificates.rs +++ b/examples/certificates.rs @@ -1,7 +1,7 @@ use anyhow::*; use log::LevelFilter; use tokio::sync::RwLock; -use kochab::{Certificate, GEMINI_PORT, Request, Response, Server}; +use kochab::{Certificate, Request, Response, Server}; use std::collections::HashMap; use std::sync::Arc; @@ -16,9 +16,9 @@ async fn main() -> Result<()> { let users = Arc::>>::default(); - Server::bind(("0.0.0.0", GEMINI_PORT)) + Server::new() .add_route("/", move|req| handle_request(users.clone(), req)) - .serve() + .serve_unix("kochab.sock") .await } diff --git a/examples/document.rs b/examples/document.rs index a3d3a6c..032a03b 100644 --- a/examples/document.rs +++ b/examples/document.rs @@ -1,6 +1,6 @@ use anyhow::*; use log::LevelFilter; -use kochab::{Server, Response, GEMINI_PORT, Document}; +use kochab::{Server, Response, Document}; use kochab::document::HeadingLevel::*; #[tokio::main] @@ -38,8 +38,8 @@ async fn main() -> Result<()> { )) .into(); - Server::bind(("localhost", GEMINI_PORT)) + Server::new() .add_route("/", response) - .serve() + .serve_unix("kochab.sock") .await } diff --git a/examples/ratelimiting.rs b/examples/ratelimiting.rs index 9838d82..455fc22 100644 --- a/examples/ratelimiting.rs +++ b/examples/ratelimiting.rs @@ -2,7 +2,7 @@ use std::time::Duration; use anyhow::*; use log::LevelFilter; -use kochab::{Server, Request, Response, GEMINI_PORT, Document}; +use kochab::{Server, Request, Response, Document}; #[tokio::main] async fn main() -> Result<()> { @@ -10,10 +10,10 @@ async fn main() -> Result<()> { .filter_module("kochab", LevelFilter::Debug) .init(); - Server::bind(("localhost", GEMINI_PORT)) + Server::new() .add_route("/", handle_request) .ratelimit("/limit", 2, Duration::from_secs(60)) - .serve() + .serve_unix("kochab.sock") .await } diff --git a/examples/routing.rs b/examples/routing.rs index dfa871b..2586c58 100644 --- a/examples/routing.rs +++ b/examples/routing.rs @@ -1,6 +1,6 @@ use anyhow::*; use log::LevelFilter; -use kochab::{Document, document::HeadingLevel, Request, Response, GEMINI_PORT}; +use kochab::{Document, document::HeadingLevel, Request, Response}; #[tokio::main] async fn main() -> Result<()> { @@ -8,11 +8,11 @@ async fn main() -> Result<()> { .filter_module("kochab", LevelFilter::Debug) .init(); - kochab::Server::bind(("localhost", GEMINI_PORT)) + kochab::Server::new() .add_route("/", handle_base) .add_route("/route", handle_short) .add_route("/route/long", handle_long) - .serve() + .serve_unix("kochab.sock") .await } diff --git a/examples/serve_dir.rs b/examples/serve_dir.rs index a41ca7e..fdbc846 100644 --- a/examples/serve_dir.rs +++ b/examples/serve_dir.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use anyhow::*; use log::LevelFilter; -use kochab::{Server, GEMINI_PORT}; +use kochab::Server; #[tokio::main] async fn main() -> Result<()> { @@ -10,9 +10,9 @@ async fn main() -> Result<()> { .filter_module("kochab", LevelFilter::Debug) .init(); - Server::bind(("localhost", GEMINI_PORT)) + Server::new() .add_route("/", PathBuf::from("public")) // Serve directory listings & file contents .add_route("/about", PathBuf::from("README.md")) // Serve a single file - .serve() + .serve_unix("kochab.sock") .await } diff --git a/examples/user_management.rs b/examples/user_management.rs index bbb14f1..d0e945d 100644 --- a/examples/user_management.rs +++ b/examples/user_management.rs @@ -1,7 +1,6 @@ use anyhow::*; use log::LevelFilter; use kochab::{ - GEMINI_PORT, Document, Request, Response, @@ -26,7 +25,7 @@ async fn main() -> Result<()> { .filter_module("kochab", LevelFilter::Debug) .init(); - Server::bind(("0.0.0.0", GEMINI_PORT)) + Server::new() // Add our main routes .add_authenticated_route("/", handle_main) @@ -36,7 +35,7 @@ async fn main() -> Result<()> { .add_um_routes::() // Start the server - .serve() + .serve_unix("kochab.sock") .await } diff --git a/molly-brown.conf b/molly-brown.conf new file mode 100644 index 0000000..c27d70c --- /dev/null +++ b/molly-brown.conf @@ -0,0 +1,20 @@ +# This is a super simple molly brown config file for the purpose of testing SCGI +# applications. Although you are welcome to use this as a base for an actual webserver, +# please find somewhere better for your production sockets. +# +# You can get a copy of molly brown and more information about configuring it from: +# https://tildegit.org/solderpunk/molly-brown +# +# Once installed, run the test server using the command +# molly-brown -c molly-brown.conf + +Port = 1965 +Hostname = "localhost" +CertPath = "cert/cert.pem" +KeyPath = "cert/key.pem" + +AccessLog = "/dev/stdout" +ErrorLog = "/dev/stderr" + +[SCGIPaths] +"/" = "kochab.sock" diff --git a/src/lib.rs b/src/lib.rs index 83ae391..c820eb5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,26 +1,43 @@ #[macro_use] extern crate log; use std::{ - convert::TryFrom, - io::BufReader, sync::Arc, - path::PathBuf, time::Duration, }; -#[cfg(feature = "ratelimiting")] -use std::net::IpAddr; +#[cfg(feature = "gemini_srv")] +use std::{ + convert::TryFrom, + path::PathBuf, +}; +#[cfg(feature = "scgi_srv")] +use std::{ + collections::HashMap, + net::SocketAddr, + str::FromStr, +}; use tokio::{ + io, + io::BufReader, + net::TcpListener, + net::ToSocketAddrs, prelude::*, - io::{self, BufStream}, - net::{TcpStream, ToSocketAddrs}, +}; +#[cfg(feature = "scgi_srv")] +use tokio::net::UnixListener; +#[cfg(feature = "gemini_srv")] +use tokio::{ time::timeout, + net::TcpStream, }; #[cfg(feature = "ratelimiting")] use tokio::time::interval; -use tokio::net::TcpListener; +#[cfg(feature = "gemini_srv")] use rustls::ClientCertVerifier; +#[cfg(feature = "gemini_srv")] use rustls::internal::msgs::handshake::DigitallySignedStruct; +#[cfg(feature = "gemini_srv")] use tokio_rustls::{rustls, TlsAcceptor}; +#[cfg(feature = "gemini_srv")] use rustls::*; use anyhow::*; use crate::util::opt_timeout; @@ -54,6 +71,7 @@ use handling::Handler; #[derive(Clone)] struct ServerInner { + #[cfg(feature = "gemini_srv")] tls_acceptor: TlsAcceptor, routes: Arc>, timeout: Duration, @@ -65,7 +83,7 @@ struct ServerInner { } impl ServerInner { - async fn serve(self, listener: TcpListener) -> Result<()> { + async fn serve_ip(self, listener: TcpListener) -> Result<()> { #[cfg(feature = "ratelimiting")] tokio::spawn(prune_ratelimit_log(self.rate_limits.clone())); @@ -82,48 +100,110 @@ impl ServerInner { } } - async fn serve_client(self, stream: TcpStream) -> Result<()> { - #[cfg(feature="ratelimiting")] - let peer_addr = stream.peer_addr()?.ip(); + #[cfg(feature = "scgi_srv")] + // Yeah it's code duplication, but I can't find a way around it, so this is what we're + // getting for now + async fn serve_unix(self, listener: UnixListener) -> Result<()> { + #[cfg(feature = "ratelimiting")] + tokio::spawn(prune_ratelimit_log(self.rate_limits.clone())); + loop { + let (stream, _addr) = listener.accept().await + .context("Failed to accept client")?; + let this = self.clone(); + + tokio::spawn(async move { + if let Err(err) = this.serve_client(stream).await { + error!("{:?}", err); + } + }); + } + } + + async fn serve_client( + &self, + #[cfg(feature = "gemini_srv")] + stream: TcpStream, + #[cfg(feature = "scgi_srv")] + stream: impl AsyncWrite + AsyncRead + Unpin, + ) -> Result<()> { let fut_accept_request = async { + #[cfg(feature = "gemini_srv")] let stream = self.tls_acceptor.accept(stream).await .context("Failed to establish TLS session")?; - let mut stream = BufStream::new(stream); + let mut stream = BufReader::new(stream); - #[cfg(feature="user_management")] let request = self.receive_request(&mut stream).await .context("Failed to receive request")?; - #[cfg(not(feature="user_management"))] - let request = Self::receive_request(&mut stream).await - .context("Failed to receive request")?; Result::<_, anyhow::Error>::Ok((request, stream)) }; - // Use a timeout for interacting with the client - let fut_accept_request = timeout(self.timeout, fut_accept_request); - let (mut request, mut stream) = fut_accept_request.await - .context("Client timed out while waiting for response")??; - #[cfg(feature="ratelimiting")] + // Wait for the request to be parsed + let (mut request, mut stream) = { + #[cfg(feature = "gemini_srv")] { + // Use a timeout for interacting with the client + let fut_accept_request = timeout(self.timeout, fut_accept_request); + fut_accept_request.await + .context("Client timed out while waiting for response")?? + } + #[cfg(feature = "scgi_srv")] + fut_accept_request.await? + }; + + // Determine the remote client's IP address for logging and ratelimiting + let peer_addr = { + #[cfg(feature = "gemini_srv")] { + stream.get_ref() + .get_ref() + .0 + .peer_addr()? + .ip() + } + #[cfg(feature = "scgi_srv")] { + SocketAddr::from_str( + request.headers() + .get("REMOTE_ADDR") + .ok_or(ParseError::Malformed("REMOTE_ADDR header not received"))? + .as_str() + ).context("Received malformed IP address from upstream")? + .ip() + } + }; + + #[cfg(feature = "ratelimiting")] + // Perform ratelimiting checks if let Some(resp) = self.check_rate_limits(peer_addr, &request) { + + // Log warning + warn!( + "Client from {} requesting {} was turned away by ratelimiting", + peer_addr, + request.uri() + ); + + // Send error response self.send_response(resp, &mut stream).await .context("Failed to send response")?; + + // Exit return Ok(()) } - debug!("Client requested: {}", request.uri()); + info!("{} requested: {}", peer_addr, request.uri()); // Identify the client certificate from the tls stream. This is the first // certificate in the certificate chain. - let client_cert = stream.get_ref() - .get_ref() - .1 - .get_peer_certificates() - .and_then(|mut v| if v.is_empty() {None} else {Some(v.remove(0))}); + #[cfg(feature = "gemini_srv")] { // This is done earlier for `scgi_srv` + let client_cert = stream.get_ref() + .get_ref() + .1 + .get_peer_certificates() + .and_then(|mut v| if v.is_empty() {None} else {Some(v.remove(0))}); - request.set_cert(client_cert); + request.set_cert(client_cert); + } let response = if let Some((trailing, handler)) = self.routes.match_request(&request) { request.set_trailing(trailing); @@ -197,13 +277,13 @@ impl ServerInner { None } + #[cfg(feature = "gemini_srv")] async fn receive_request( - #[cfg(feature="user_management")] &self, - stream: &mut (impl AsyncBufRead + Unpin) + stream: &mut (impl AsyncBufRead + Unpin), ) -> Result { - let limit = REQUEST_URI_MAX_LEN + "\r\n".len(); - let mut stream = stream.take(limit as u64); + const HEADER_LIMIT: usize = REQUEST_URI_MAX_LEN + "\r\n".len(); + let mut stream = stream.take(HEADER_LIMIT as u64); let mut uri = Vec::new(); stream.read_until(b'\n', &mut uri).await?; @@ -223,23 +303,123 @@ impl ServerInner { let uri = URIReference::try_from(&*uri) .context("Request URI is invalid")? .into_owned(); - let request = Request::from_uri( + + Request::new( uri, #[cfg(feature="user_management")] self.manager.clone(), - ) .context("Failed to create request from URI")?; + ).context("Failed to create request from URI") + } - Ok(request) + #[cfg(feature = "scgi_srv")] + async fn receive_request( + &self, + stream: &mut (impl AsyncBufRead + Unpin), + ) -> Result { + let mut buff = Vec::with_capacity(4); + + #[allow(clippy::char_lit_as_u8)] + // Read the length of the header netstring (e.g. "120:") + stream.read_until(':' as u8, &mut buff).await?; + + buff.pop(); // Remove the trailing ':' + let len = std::str::from_utf8(&*buff) + .ok() + .and_then(|s| usize::from_str(s).ok()) + .ok_or(ParseError::Malformed("netstring length"))?; + + // Read in the headers + buff.clear(); + buff.resize(len + 1, 0); + stream.read_exact(buff.as_mut()).await?; + buff.truncate(len - 1); // Remove the final \x00, + + // Parse the headers + let (maybe_trailing, headers) = buff.split(|b| *b == 0) // Headers are null delimiited + .map(|bytes| // Convert to an &str + std::str::from_utf8(bytes) + .map_err(|_| ParseError::Malformed("scgi headers")) + .map(str::trim) + ) + .try_fold( // Turn the array of [header, value, header, ...] into a map + (Option::<&str>::None, HashMap::::with_capacity(16)), + |(last_header, mut headers), s| { + s.map(|text| { + match last_header { + None => (Some(text), headers), + Some(header) => { + headers.insert(header.to_string(), text.to_string()); + (None, headers) + } + } + }) + } + )?; + + // If there's not the same number of headers as values, that's a problem + if maybe_trailing.is_some() { + bail!(ParseError::Malformed("trailing header")); + } + + // Check the content length info + let cont_len_val = headers.get("CONTENT_LENGTH") + .ok_or(ParseError::Malformed("No content length header!"))?; + let cont_len = usize::from_str(cont_len_val) + .map_err(|_| ParseError::Malformed("Malformed content length"))?; + if cont_len > 0 { + bail!(ParseError::Malformed("Gemini SCGI requests should not have a body")); + } + + // Spec requires setting an SCGI header to one + if *headers.get("SCGI").ok_or(ParseError::Malformed("No SCGI header"))? != "1" { + bail!(ParseError::Malformed("SCGI header not set to \"1\"")); + } + + trace!("Headers received: {:?}", headers); + + Ok(Request::new(headers)?) } } -pub struct Server { - addr: A, - cert_path: PathBuf, - key_path: PathBuf, +#[derive(Debug)] +#[cfg(feature = "scgi_srv")] +enum ParseError { + IO(io::Error), + Malformed(&'static str), +} + +#[cfg(feature = "scgi_srv")] +impl From for ParseError { + fn from(e: io::Error) -> Self { + Self::IO(e) + } +} + +#[cfg(feature = "scgi_srv")] +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::IO(e) => write!(f, "IO Error while parsing and responding SCGI: {}", e), + Self::Malformed(e) => write!(f, "SCGI request malformed at {}", e), + } + } +} + +#[cfg(feature = "scgi_srv")] +impl std::error::Error for ParseError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + if let Self::IO(e) = self { Some(e) } else { None } + } +} + +pub struct Server { timeout: Duration, complex_body_timeout_override: Option, routes: RoutingNode, + #[cfg(feature = "gemini_srv")] + cert_path: PathBuf, + #[cfg(feature = "gemini_srv")] + key_path: PathBuf, #[cfg(feature="ratelimiting")] rate_limits: RoutingNode>, #[cfg(feature="user_management")] @@ -250,15 +430,16 @@ pub struct Server { certgen_mode: CertGenMode, } -impl Server { - pub fn bind(addr: A) -> Self { +impl Server { + pub fn new() -> Self { Self { - addr, timeout: Duration::from_secs(1), 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(), + #[cfg(feature = "gemini_srv")] + cert_path: PathBuf::from("cert/cert.pem"), + #[cfg(feature = "gemini_srv")] + key_path: PathBuf::from("cert/key.pem"), #[cfg(feature="ratelimiting")] rate_limits: RoutingNode::default(), #[cfg(feature="user_management")] @@ -309,6 +490,7 @@ impl Server { self } + #[cfg(feature = "gemini_srv")] /// Sets the directory that kochab should look for TLS certs and keys into /// /// Northstar will look for files called `cert.pem` and `key.pem` in the provided @@ -324,6 +506,7 @@ impl Server { .set_key(dir.join("key.pem")) } + #[cfg(feature = "gemini_srv")] /// Set the path to the TLS certificate kochab will use /// /// This defaults to `cert/cert.pem`. @@ -335,6 +518,7 @@ impl Server { self } + #[cfg(feature = "gemini_srv")] /// Set the path to the ertificate key kochab will use /// /// This defaults to `cert/key.pem`. @@ -436,7 +620,8 @@ impl Server { self } - pub async fn serve(mut self) -> Result<()> { + fn build(mut self) -> Result { + #[cfg(feature = "gemini_srv")] let config = tls_config( &self.cert_path, &self.key_path, @@ -444,28 +629,51 @@ impl Server { self.certgen_mode ).context("Failed to create TLS config")?; - let listener = TcpListener::bind(self.addr).await - .context("Failed to create socket")?; - self.routes.shrink(); #[cfg(feature="user_management")] let data_dir = self.data_dir; - let server = ServerInner { - tls_acceptor: TlsAcceptor::from(config), + Ok(ServerInner { routes: Arc::new(self.routes), timeout: self.timeout, complex_timeout: self.complex_body_timeout_override, + #[cfg(feature = "gemini_srv")] + tls_acceptor: TlsAcceptor::from(config), #[cfg(feature="ratelimiting")] rate_limits: Arc::new(self.rate_limits), #[cfg(feature="user_management")] manager: UserManager::new( self.database.unwrap_or_else(move|| sled::open(data_dir).unwrap()) )?, - }; + }) + } - server.serve(listener).await + /// Start serving requests on a given bound address & port + /// + /// `addr` can be anything `tokio` can parse, including just a string like + /// "localhost:1965" + pub async fn serve_ip(self, addr: impl ToSocketAddrs) -> Result<()> { + let server = self.build()?; + let socket = TcpListener::bind(addr).await?; + server.serve_ip(socket).await + } + + #[cfg(feature = "scgi_srv")] + /// Start serving requests on a given unix socket + /// + /// Requires an address in the form of a path to bind to. This is only available when + /// in `scgi_srv` mode. + pub async fn serve_unix(self, addr: impl AsRef) -> Result<()> { + let server = self.build()?; + let socket = UnixListener::bind(addr)?; + server.serve_unix(socket).await + } +} + +impl Default for Server { + fn default() -> Self { + Self::new() } } @@ -512,6 +720,7 @@ async fn prune_ratelimit_log(rate_limits: Arc>>) } } +#[cfg(feature = "gemini_srv")] fn tls_config( cert_path: &PathBuf, key_path: &PathBuf, @@ -535,20 +744,22 @@ fn tls_config( Ok(config.into()) } +#[cfg(feature = "gemini_srv")] fn load_cert_chain(cert_path: &PathBuf) -> Result> { let certs = std::fs::File::open(cert_path) .with_context(|| format!("Failed to open `{:?}`", cert_path))?; - let mut certs = BufReader::new(certs); + let mut certs = std::io::BufReader::new(certs); let certs = rustls::internal::pemfile::certs(&mut certs) .map_err(|_| anyhow!("failed to load certs `{:?}`", cert_path))?; Ok(certs) } +#[cfg(feature = "gemini_srv")] fn load_key(key_path: &PathBuf) -> Result { let keys = std::fs::File::open(key_path) .with_context(|| format!("Failed to open `{:?}`", key_path))?; - let mut keys = BufReader::new(keys); + let mut keys = std::io::BufReader::new(keys); let mut keys = rustls::internal::pemfile::pkcs8_private_keys(&mut keys) .map_err(|_| anyhow!("failed to load key `{:?}`", key_path))?; @@ -559,11 +770,14 @@ fn load_key(key_path: &PathBuf) -> Result { Ok(key) } +#[cfg(feature = "gemini_srv")] /// A client cert verifier that accepts all connections /// /// Unfortunately, rustls doesn't provide a ClientCertVerifier that accepts self-signed /// certificates, so we need to implement this ourselves. struct AllowAnonOrSelfsignedClient { } + +#[cfg(feature = "gemini_srv")] impl AllowAnonOrSelfsignedClient { /// Create a new verifier @@ -573,6 +787,7 @@ impl AllowAnonOrSelfsignedClient { } +#[cfg(feature = "gemini_srv")] impl ClientCertVerifier for AllowAnonOrSelfsignedClient { fn client_auth_root_subjects( diff --git a/src/types.rs b/src/types.rs index 0ad442d..ccbf1b2 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,4 +1,7 @@ +#[cfg(feature = "gemini_srv")] pub use rustls::Certificate; +#[cfg(feature = "scgi_srv")] +pub type Certificate = String; pub use uriparse::URIReference; mod meta; diff --git a/src/types/request.rs b/src/types/request.rs index a4889d3..db812c4 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -1,8 +1,13 @@ use std::ops; +#[cfg(feature = "scgi_srv")] +use std::{ + collections::HashMap, + convert::TryFrom, +}; use anyhow::*; use percent_encoding::percent_decode_str; use uriparse::URIReference; -use rustls::Certificate; +use crate::types::Certificate; #[cfg(feature="user_management")] use serde::{Serialize, de::DeserializeOwned}; @@ -16,28 +21,37 @@ pub struct Request { trailing_segments: Option>, #[cfg(feature="user_management")] manager: UserManager, + #[cfg(feature = "scgi_srv")] + headers: HashMap, } impl Request { - pub fn from_uri( - uri: URIReference<'static>, - #[cfg(feature="user_management")] - manager: UserManager, - ) -> Result { - Self::with_certificate( - uri, - None, - #[cfg(feature="user_management")] - manager - ) - } - - pub fn with_certificate( + pub fn new( + #[cfg(feature = "gemini_srv")] mut uri: URIReference<'static>, - certificate: Option, + #[cfg(feature = "scgi_srv")] + headers: HashMap, #[cfg(feature="user_management")] manager: UserManager, ) -> Result { + #[cfg(feature = "scgi_srv")] + let (mut uri, certificate) = ( + URIReference::try_from( + format!( + "{}{}", + headers.get("PATH_INFO") + .context("PATH_INFO header not received from SCGI client")? + .as_str(), + headers.get("QUERY_STRING") + .map(|q| format!("?{}", q)) + .unwrap_or_else(String::new), + ).as_str() + ) + .context("Request URI is invalid")? + .into_owned(), + headers.get("TLS_CLIENT_HASH").cloned(), + ); + uri.normalize(); let input = match uri.query() { @@ -54,8 +68,13 @@ impl Request { Ok(Self { uri, input, + #[cfg(feature = "scgi_srv")] certificate, + #[cfg(feature = "gemini_srv")] + certificate: None, trailing_segments: None, + #[cfg(feature = "scgi_srv")] + headers, #[cfg(feature="user_management")] manager, }) @@ -103,6 +122,24 @@ impl Request { self.input.as_deref() } + #[cfg(feature="scgi_srv")] + /// View any headers sent by the SCGI client + /// + /// When an SCGI client delivers a request (e.g. when your gemini server sends a + /// request to this app), it includes many headers which aren't always included in + /// the request otherwise. Bear in mind that **not all SCGI clients send the same + /// headers**, and these are *never* available when operating in `gemini_srv` mode. + /// + /// Some examples of headers mollybrown sets are: + /// - `REMOTE_ADDR` (The user's IP address and port) + /// - `TLS_CLIENT_SUBJECT_CN` (The CommonName on the user's certificate, when present) + /// - `SERVER_NAME` (The host name of the server the request was received on) + /// - `SERVER_SOFTWARE` (= "MOLLY_BROWN") + /// - `SCRIPT_PATH` (The prefix the script is being served on) + pub const fn headers(&self) -> &HashMap { + &self.headers + } + pub fn set_cert(&mut self, cert: Option) { self.certificate = cert; } From 8b9fbce489f707a0f603aa7e419d38d825569ae8 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Tue, 1 Dec 2020 14:43:15 -0500 Subject: [PATCH 075/113] Fix user management module, rework certificates to use hashes --- Cargo.toml | 4 +-- src/lib.rs | 29 +++++++++++-------- src/types.rs | 4 --- src/types/request.rs | 27 +++++++++++++---- src/user_management/manager.rs | 44 +++++++--------------------- src/user_management/mod.rs | 19 ++++++++---- src/user_management/routes.rs | 21 ++++++-------- src/user_management/user.rs | 53 +++++++++++----------------------- 8 files changed, 90 insertions(+), 111 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cd33c61..257f7d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ include = ["src/**", "Cargo.*", "CHANGELOG.md", "LICENSE*", "README.md"] [features] default = ["scgi_srv"] user_management = ["sled", "bincode", "serde/derive", "crc32fast", "lazy_static"] -user_management_advanced = ["rust-argon2", "ring", "user_management"] +user_management_advanced = ["rust-argon2", "user_management"] user_management_routes = ["user_management"] serve_dir = ["mime_guess", "tokio/fs"] ratelimiting = ["dashmap"] @@ -28,6 +28,7 @@ tokio = { version = "0.3.1", features = ["io-util","net","time", "rt"] } uriparse = "0.6.3" percent-encoding = "2.1.0" log = "0.4.11" +ring = "0.16.15" lazy_static = { version = "1.4.0", optional = true } rustls = { version = "0.18.1", features = ["dangerous_configuration"], optional = true} webpki = { version = "0.21.0", optional = true} @@ -39,7 +40,6 @@ bincode = { version = "1.3.1", optional = true } serde = { version = "1.0", optional = true } rust-argon2 = { version = "0.8.2", optional = true } crc32fast = { version = "1.2.1", optional = true } -ring = { version = "0.16.15", optional = true } rcgen = { version = "0.8.5", optional = true } squeegee = { git = "https://gitlab.com/Alch_Emi/squeegee.git", branch = "main", optional = true } diff --git a/src/lib.rs b/src/lib.rs index c820eb5..4a4756b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,16 +5,15 @@ use std::{ time::Duration, }; #[cfg(feature = "gemini_srv")] -use std::{ - convert::TryFrom, - path::PathBuf, -}; +use std::convert::TryFrom; #[cfg(feature = "scgi_srv")] use std::{ collections::HashMap, net::SocketAddr, str::FromStr, }; +#[cfg(any(feature = "gemini_srv", feature = "user_management"))] +use std::path::PathBuf; use tokio::{ io, io::BufReader, @@ -125,7 +124,7 @@ impl ServerInner { #[cfg(feature = "gemini_srv")] stream: TcpStream, #[cfg(feature = "scgi_srv")] - stream: impl AsyncWrite + AsyncRead + Unpin, + stream: impl AsyncWrite + AsyncRead + Unpin + Send, ) -> Result<()> { let fut_accept_request = async { #[cfg(feature = "gemini_srv")] @@ -218,7 +217,7 @@ impl ServerInner { Ok(()) } - async fn send_response(&self, mut response: Response, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> { + async fn send_response(&self, mut response: Response, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> { let maybe_body = response.take_body(); let header = response.header(); @@ -280,7 +279,7 @@ impl ServerInner { #[cfg(feature = "gemini_srv")] async fn receive_request( &self, - stream: &mut (impl AsyncBufRead + Unpin), + stream: &mut (impl AsyncBufRead + Unpin + Send), ) -> Result { const HEADER_LIMIT: usize = REQUEST_URI_MAX_LEN + "\r\n".len(); let mut stream = stream.take(HEADER_LIMIT as u64); @@ -377,7 +376,13 @@ impl ServerInner { trace!("Headers received: {:?}", headers); - Ok(Request::new(headers)?) + Ok( + Request::new( + headers, + #[cfg(feature = "user_management")] + self.manager.clone(), + )? + ) } } @@ -653,7 +658,7 @@ impl Server { /// /// `addr` can be anything `tokio` can parse, including just a string like /// "localhost:1965" - pub async fn serve_ip(self, addr: impl ToSocketAddrs) -> Result<()> { + pub async fn serve_ip(self, addr: impl ToSocketAddrs + Send) -> Result<()> { let server = self.build()?; let socket = TcpListener::bind(addr).await?; server.serve_ip(socket).await @@ -677,7 +682,7 @@ impl Default for Server { } } -async fn send_response_header(header: &ResponseHeader, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> { +async fn send_response_header(header: &ResponseHeader, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> { let header = format!( "{status} {meta}\r\n", status = header.status.code(), @@ -690,7 +695,7 @@ async fn send_response_header(header: &ResponseHeader, stream: &mut (impl AsyncW Ok(()) } -async fn maybe_send_response_body(maybe_body: Option, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> { +async fn maybe_send_response_body(maybe_body: Option, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> { if let Some(body) = maybe_body { send_response_body(body, stream).await?; } @@ -698,7 +703,7 @@ async fn maybe_send_response_body(maybe_body: Option, stream: &mut (impl A Ok(()) } -async fn send_response_body(body: Body, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> { +async fn send_response_body(body: Body, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> { match body { Body::Bytes(bytes) => stream.write_all(&bytes).await?, Body::Reader(mut reader) => { io::copy(&mut reader, stream).await?; }, diff --git a/src/types.rs b/src/types.rs index ccbf1b2..385fa8c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,7 +1,3 @@ -#[cfg(feature = "gemini_srv")] -pub use rustls::Certificate; -#[cfg(feature = "scgi_srv")] -pub type Certificate = String; pub use uriparse::URIReference; mod meta; diff --git a/src/types/request.rs b/src/types/request.rs index db812c4..ae8b844 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -1,4 +1,5 @@ use std::ops; +use std::convert::TryInto; #[cfg(feature = "scgi_srv")] use std::{ collections::HashMap, @@ -7,9 +8,10 @@ use std::{ use anyhow::*; use percent_encoding::percent_decode_str; use uriparse::URIReference; -use crate::types::Certificate; #[cfg(feature="user_management")] use serde::{Serialize, de::DeserializeOwned}; +#[cfg(feature = "gemini_srv")] +use ring::digest; #[cfg(feature="user_management")] use crate::user_management::{UserManager, User}; @@ -17,7 +19,7 @@ use crate::user_management::{UserManager, User}; pub struct Request { uri: URIReference<'static>, input: Option, - certificate: Option, + certificate: Option<[u8; 32]>, trailing_segments: Option>, #[cfg(feature="user_management")] manager: UserManager, @@ -49,7 +51,13 @@ impl Request { ) .context("Request URI is invalid")? .into_owned(), - headers.get("TLS_CLIENT_HASH").cloned(), + headers.get("TLS_CLIENT_HASH") + .map(|hsh| { + ring::test::from_hex(hsh.as_str()) + .expect("Received invalid certificate fingerprint from upstream") + .try_into() + .expect("Received certificate fingerprint of invalid lenght from upstream") + }), ); uri.normalize(); @@ -140,8 +148,14 @@ impl Request { &self.headers } - pub fn set_cert(&mut self, cert: Option) { - self.certificate = cert; + #[cfg(feature = "gemini_srv")] + pub (crate) fn set_cert(&mut self, cert: Option) { + self.certificate = cert.map(|cert| { + digest::digest(&digest::SHA256, cert.0.as_ref()) + .as_ref() + .try_into() + .expect("SHA256 didn't return 256 bits") + }); } pub fn set_trailing(&mut self, segments: Vec) { @@ -149,7 +163,8 @@ impl Request { } #[allow(clippy::missing_const_for_fn)] - pub fn certificate(&self) -> Option<&Certificate> { + /// Get the fingerprint of the certificate the user is connecting with + pub fn certificate(&self) -> Option<&[u8; 32]> { self.certificate.as_ref() } diff --git a/src/user_management/manager.rs b/src/user_management/manager.rs index 4bdabd9..009f802 100644 --- a/src/user_management/manager.rs +++ b/src/user_management/manager.rs @@ -1,24 +1,8 @@ -use rustls::Certificate; -use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use serde::{Serialize, de::DeserializeOwned}; use crate::user_management::{User, Result}; use crate::user_management::user::{RegisteredUser, NotSignedInUser, PartialUser}; -#[derive(Debug, Clone, Deserialize, Serialize)] -/// Data stored in the certificate tree about a certain certificate -pub struct CertificateData { - #[serde(with = "CertificateDef")] - /// The certificate in question - pub certificate: Certificate, - - /// The username of the user to which this certificate is registered - pub owner_username: String, -} - -#[derive(Serialize, Deserialize)] -#[serde(remote = "Certificate")] -struct CertificateDef(Vec); - #[derive(Debug, Clone)] /// A struct containing information for managing users. /// @@ -43,21 +27,14 @@ impl UserManager { }) } - /// Produce a u32 hash from a certificate, used for [`lookup_certificate()`](Self::lookup_certificate()) - pub fn hash_certificate(cert: &Certificate) -> u32 { - let mut hasher = crc32fast::Hasher::new(); - hasher.update(cert.0.as_ref()); - hasher.finalize() - } - - /// Lookup information about a certificate based on it's u32 hash + /// Lookup the owner of a certificate based on it's fingerprint /// /// # Errors /// An error is thrown if there is an error reading from the database or if data /// recieved from the database is corrupt - pub fn lookup_certificate(&self, cert: u32) -> Result> { - if let Some(bytes) = self.certificates.get(cert.to_le_bytes())? { - Ok(Some(bincode::deserialize(&bytes)?)) + pub fn lookup_certificate(&self, cert: [u8; 32]) -> Result> { + if let Some(bytes) = self.certificates.get(cert)? { + Ok(Some(std::str::from_utf8(bytes.as_ref())?.to_string())) } else { Ok(None) } @@ -116,20 +93,19 @@ impl UserManager { /// Pancis if the database is corrupt pub fn get_user( &self, - cert: Option<&Certificate> + cert: Option<&[u8; 32]> ) -> Result> where UserData: Serialize + DeserializeOwned { if let Some(certificate) = cert { - let cert_hash = Self::hash_certificate(certificate); - if let Some(certificate_data) = self.lookup_certificate(cert_hash)? { - let user_inner = self.lookup_user(&certificate_data.owner_username)? + if let Some(username) = self.lookup_certificate(*certificate)? { + let user_inner = self.lookup_user(&username)? .expect("Database corruption: Certificate data refers to non-existant user"); - Ok(User::SignedIn(user_inner.with_cert(certificate_data.certificate))) + Ok(User::SignedIn(user_inner.with_cert(*certificate))) } else { Ok(User::NotSignedIn(NotSignedInUser { - certificate: certificate.clone(), + certificate: *certificate, manager: self.clone(), })) } diff --git a/src/user_management/mod.rs b/src/user_management/mod.rs index 75b0802..288a65d 100644 --- a/src/user_management/mod.rs +++ b/src/user_management/mod.rs @@ -26,7 +26,6 @@ mod routes; pub use routes::UserManagementRoutes; pub use manager::UserManager; pub use user::User; -pub use manager::CertificateData; // Imports for docs #[allow(unused_imports)] use user::{NotSignedInUser, RegisteredUser}; @@ -39,7 +38,8 @@ pub enum UserManagerError { PasswordNotSet, DatabaseError(sled::Error), DatabaseTransactionError(sled::transaction::TransactionError), - DeserializeError(bincode::Error), + DeserializeBincodeError(bincode::Error), + DeserializeUtf8Error(std::str::Utf8Error), #[cfg(feature = "user_management_advanced")] Argon2Error(argon2::Error), } @@ -58,7 +58,13 @@ impl From for UserManagerError { impl From for UserManagerError { fn from(error: bincode::Error) -> Self { - Self::DeserializeError(error) + Self::DeserializeBincodeError(error) + } +} + +impl From for UserManagerError { + fn from(error: std::str::Utf8Error) -> Self { + Self::DeserializeUtf8Error(error) } } @@ -74,7 +80,8 @@ impl std::error::Error for UserManagerError { match self { Self::DatabaseError(e) => Some(e), Self::DatabaseTransactionError(e) => Some(e), - Self::DeserializeError(e) => Some(e), + Self::DeserializeBincodeError(e) => Some(e), + Self::DeserializeUtf8Error(e) => Some(e), #[cfg(feature = "user_management_advanced")] Self::Argon2Error(e) => Some(e), _ => None @@ -93,8 +100,10 @@ impl std::fmt::Display for UserManagerError { write!(f, "Error accessing the user database: {}", e), Self::DatabaseTransactionError(e) => write!(f, "Error accessing the user database: {}", e), - Self::DeserializeError(e) => + Self::DeserializeBincodeError(e) => write!(f, "Recieved messy data from database, possible corruption: {}", e), + Self::DeserializeUtf8Error(e) => + write!(f, "Recieved invalid UTF-8 from database, possible corruption: {}", e), #[cfg(feature = "user_management_advanced")] Self::Argon2Error(e) => write!(f, "Argon2 Error, likely malformed password hash, possible database corruption: {}", e), diff --git a/src/user_management/routes.rs b/src/user_management/routes.rs index 55b613b..e7f17f3 100644 --- a/src/user_management/routes.rs +++ b/src/user_management/routes.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use tokio::net::ToSocketAddrs; use serde::{Serialize, de::DeserializeOwned}; #[cfg(feature = "dashmap")] @@ -16,7 +15,6 @@ use crate::types::document::HeadingLevel; use crate::user_management::{ User, RegisteredUser, - UserManager, UserManagerError, user::NotSignedInUser, }; @@ -91,7 +89,7 @@ pub trait UserManagementRoutes: private::Sealed { F: Send + Sync + 'static + Future>; } -impl UserManagementRoutes for crate::Server { +impl UserManagementRoutes for crate::Server { /// Add pre-configured routes to the serve to handle authentication /// /// See [`UserManagementRoutes::add_um_routes()`] @@ -187,7 +185,7 @@ lazy_static::lazy_static! { #[cfg(not(feature = "dashmap"))] lazy_static::lazy_static! { - static ref PENDING_REDIRECTS: RwLock> = Default::default(); + static ref PENDING_REDIRECTS: RwLock> = Default::default(); } async fn handle_base(request: Request) -> Result { @@ -408,28 +406,27 @@ fn save_redirect<'a>( #[cfg(not(feature = "dashmap"))] let mut ref_to_map = PENDING_REDIRECTS.write().unwrap(); - let cert_hash = UserManager::hash_certificate(&user.certificate); - debug!("Added \"{}\" as redirect for cert {:x}", redirect, cert_hash); - ref_to_map.insert(cert_hash, redirect); + debug!("Added \"{}\" as redirect for cert {:x?}", redirect, &user.certificate); + ref_to_map.insert(user.certificate, redirect); } } fn get_redirect(user: &RegisteredUser) -> String { - let cert_hash = UserManager::hash_certificate(user.active_certificate().unwrap()); + let cert = user.active_certificate().unwrap(); #[cfg(feature = "dashmap")] - let maybe_redir = PENDING_REDIRECTS.get(&cert_hash).map(|r| r.clone()); + let maybe_redir = PENDING_REDIRECTS.get(cert).map(|r| r.clone()); #[cfg(not(feature = "dashmap"))] let ref_to_map = PENDING_REDIRECTS.read().unwrap(); #[cfg(not(feature = "dashmap"))] - let maybe_redir = ref_to_map.get(&cert_hash).cloned(); + let maybe_redir = ref_to_map.get(cert).cloned(); let redirect = maybe_redir.unwrap_or_else(||"/".to_string()); - debug!("Accessed redirect to \"{}\" for cert {:x}", redirect, cert_hash); + debug!("Accessed redirect to \"{}\" for cert {:x?}", redirect, cert); redirect } mod private { pub trait Sealed {} - impl Sealed for crate::Server {} + impl Sealed for crate::Server {} } diff --git a/src/user_management/user.rs b/src/user_management/user.rs index baa139a..043c1ff 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -13,13 +13,11 @@ //! the data stored for almost all users. This is accomplished through the //! [`as_mut()`](RegisteredUser::as_mut) method. Changes made this way must be persisted //! using [`save()`](RegisteredUser::save()) or by dropping the user. -use rustls::Certificate; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use sled::Transactional; use crate::user_management::UserManager; use crate::user_management::Result; -use crate::user_management::manager::CertificateData; #[cfg(feature = "user_management_advanced")] const ARGON2_CONFIG: argon2::Config = argon2::Config { @@ -46,7 +44,7 @@ lazy_static::lazy_static! { #[derive(Clone, Debug, Deserialize, Serialize)] pub (crate) struct PartialUser { pub data: UserData, - pub certificates: Vec, + pub certificates: Vec<[u8; 32]>, #[cfg(feature = "user_management_advanced")] pub pass_hash: Option<(Vec, [u8; 32])>, } @@ -94,7 +92,7 @@ pub enum User { /// /// For more information about the user lifecycle and sign-in stages, see [`User`] pub struct NotSignedInUser { - pub (crate) certificate: Certificate, + pub (crate) certificate: [u8; 32], pub (crate) manager: UserManager, } @@ -120,7 +118,7 @@ impl NotSignedInUser { let mut newser = RegisteredUser::new( username, - Some(self.certificate.clone()), + Some(self.certificate), self.manager, PartialUser { data: UserData::default(), @@ -179,9 +177,8 @@ impl NotSignedInUser { } } - let certhash = UserManager::hash_certificate(&self.certificate); - info!("User {} attached certificate with hash {:x}", username, certhash); - user.add_certificate(self.certificate.clone())?; + info!("User {} attached certificate with fingerprint {:x?}", username, &self.certificate[..]); + user.add_certificate(self.certificate)?; user.active_certificate = Some(self.certificate); Ok(Some(user)) } else { @@ -196,7 +193,7 @@ impl NotSignedInUser { /// For more information about the user lifecycle and sign-in stages, see [`User`] pub struct RegisteredUser { username: String, - active_certificate: Option, + active_certificate: Option<[u8; 32]>, manager: UserManager, inner: PartialUser, /// Indicates that [`RegisteredUser::as_mut()`] has been called, but [`RegisteredUser::save()`] has not @@ -208,7 +205,7 @@ impl RegisteredUser { /// Create a new user from parts pub (crate) fn new( username: String, - active_certificate: Option, + active_certificate: Option<[u8; 32]>, manager: UserManager, inner: PartialUser ) -> Self { @@ -226,30 +223,22 @@ impl RegisteredUser { /// This is not to be confused with [`RegisteredUser::add_certificate`], which /// performs the database operations needed to register a new certificate to a user. /// This literally just marks the active certificate. - pub (crate) fn with_cert(mut self, cert: Certificate) -> Self { + pub (crate) fn with_cert(mut self, cert: [u8; 32]) -> Self { self.active_certificate = Some(cert); self } - /// Get the [`Certificate`] that the user is currently using to connect. + /// Get the fingerprint of the certificate that the user is currently using. /// /// If this user was retrieved by a [`UserManager::lookup_user()`], this will be /// [`None`]. In all other cases, this will be [`Some`]. - pub fn active_certificate(&self) -> Option<&Certificate> { + pub fn active_certificate(&self) -> Option<&[u8; 32]> { self.active_certificate.as_ref() } - /// Produce a list of all [`Certificate`]s registered to this account - pub fn all_certificates(&self) -> Vec { - self.inner.certificates - .iter() - .map( - |cid| self.manager.lookup_certificate(*cid) - .expect("Database corruption: User refers to non-existant certificate") - .expect("Error accessing database") - .certificate - ) - .collect() + /// Produce a list of all certificate fingerprints registered to this account + pub fn all_certificates(&self) -> &Vec<[u8; 32]> { + &self.inner.certificates } /// Get the user's current username. @@ -335,18 +324,10 @@ impl RegisteredUser { /// If you have a [`NotSignedInUser`] and are looking for a way to link them to an /// existing user, consider [`NotSignedInUser::attach()`], which contains facilities for /// password checking and automatically performs the user lookup. - pub fn add_certificate(&mut self, certificate: Certificate) -> Result<()> { - let cert_hash = UserManager::hash_certificate(&certificate); - - self.inner.certificates.push(cert_hash); - - let cert_info = CertificateData { - certificate, - owner_username: self.username.clone(), - }; + pub fn add_certificate(&mut self, certificate: [u8; 32]) -> Result<()> { + self.inner.certificates.push(certificate); let inner_serialized = bincode::serialize(&self.inner)?; - let cert_info_serialized = bincode::serialize(&cert_info)?; (&self.manager.users, &self.manager.certificates) .transaction(|(tx_usr, tx_crt)| { @@ -355,8 +336,8 @@ impl RegisteredUser { inner_serialized.clone(), )?; tx_crt.insert( - cert_hash.to_le_bytes().as_ref(), - cert_info_serialized.clone(), + &certificate, + self.username.as_bytes(), )?; Ok(()) })?; From a92b3788e2a6f1d2ea33579e17dc78305322fbe0 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Tue, 1 Dec 2020 15:15:30 -0500 Subject: [PATCH 076/113] Added ./data to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9f7250a..6ec7fee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target Cargo.lock /cert/ +/data/ /public/ From f922f8c70dab28404f5dd0768550de95a5e97559 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Tue, 1 Dec 2020 16:36:29 -0500 Subject: [PATCH 077/113] Merged Meta, Status, and ResponseHeader into Response --- src/handling.rs | 26 ++---- src/lib.rs | 57 +++++++------ src/types.rs | 9 -- src/types/meta.rs | 130 ----------------------------- src/types/response.rs | 153 ++++++++++++++++++++++------------ src/types/response_header.rs | 97 --------------------- src/types/status.rs | 82 ------------------ src/user_management/routes.rs | 16 ++-- src/util.rs | 36 ++++---- 9 files changed, 162 insertions(+), 444 deletions(-) delete mode 100644 src/types/meta.rs delete mode 100644 src/types/response_header.rs delete mode 100644 src/types/status.rs diff --git a/src/handling.rs b/src/handling.rs index f869605..0ab3979 100644 --- a/src/handling.rs +++ b/src/handling.rs @@ -59,41 +59,33 @@ impl Handler { HandlerCatchUnwind::new(fut_handle).await .unwrap_or_else(|err| { error!("Handler failed: {:?}", err); - Response::server_error("").unwrap() + Response::temporary_failure("") }) }, Self::StaticHandler(response) => { - let body = response.as_ref(); - match body { - None => Response::new(response.header().clone()), - Some(Body::Bytes(bytes)) => { - Response::new(response.header().clone()) - .with_body(bytes.clone()) - }, + match &response.body { + None => Response::new(response.status, &response.meta), + Some(Body::Bytes(bytes)) => Response::success(&response.meta, bytes.clone()), _ => { error!(concat!( "Cannot construct a static handler with a reader-based body! ", " We're sending a response so that the client doesn't crash, but", " given that this is a release build you should really fix this." )); - Response::server_error( + Response::permanent_failure( "Very bad server error, go tell the sysadmin to look at the logs." - ).unwrap() + ) } } }, #[cfg(feature = "serve_dir")] Self::FilesHandler(path) => { - let resp = if path.is_dir() { + if path.is_dir() { crate::util::serve_dir(path, request.trailing_segments()).await } else { let mime = crate::util::guess_mime_from_path(&path); crate::util::serve_file(path, &mime).await - }; - resp.unwrap_or_else(|e| { - warn!("Unexpected error serving from {}: {:?}", path.display(), e); - Response::server_error("").unwrap() - }) + } }, } } @@ -204,7 +196,7 @@ impl Future for HandlerCatchUnwind { Ok(res) => res, Err(e) => { error!("Handler panic! {:?}", e); - Poll::Ready(Response::server_error("")) + Poll::Ready(Ok(Response::temporary_failure(""))) } } } diff --git a/src/lib.rs b/src/lib.rs index 4a4756b..face42d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,8 @@ use std::{ }; #[cfg(any(feature = "gemini_srv", feature = "user_management"))] use std::path::PathBuf; +#[cfg(feature="ratelimiting")] +use std::net::IpAddr; use tokio::{ io, io::BufReader, @@ -217,15 +219,12 @@ impl ServerInner { Ok(()) } - async fn send_response(&self, mut response: Response, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> { - let maybe_body = response.take_body(); - let header = response.header(); - + async fn send_response(&self, response: Response, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> { let use_complex_timeout = - header.status.is_success() && - maybe_body.is_some() && - header.meta.as_str() != "text/plain" && - header.meta.as_str() != "text/gemini" && + response.is_success() && + response.body.is_some() && + response.meta != "text/plain" && + response.meta != "text/gemini" && self.complex_timeout.is_some(); let send_general_timeout; @@ -244,13 +243,13 @@ impl ServerInner { opt_timeout(send_general_timeout, async { // Send the header - opt_timeout(send_header_timeout, send_response_header(response.header(), stream)) + opt_timeout(send_header_timeout, send_response_header(&response, stream)) .await .context("Timed out while sending response header")? .context("Failed to write response header")?; // Send the body - opt_timeout(send_body_timeout, maybe_send_response_body(maybe_body, stream)) + opt_timeout(send_body_timeout, send_response_body(response.body, stream)) .await .context("Timed out while sending response body")? .context("Failed to write response body")?; @@ -267,10 +266,7 @@ impl ServerInner { fn check_rate_limits(&self, addr: IpAddr, req: &Request) -> Option { if let Some((_, limiter)) = self.rate_limits.match_request(req) { if let Err(when) = limiter.check_key(addr) { - return Some(Response::new(ResponseHeader { - status: Status::SLOW_DOWN, - meta: Meta::new(when.as_secs().to_string()).unwrap() - })) + return Some(Response::slow_down(when.as_secs())) } } None @@ -682,11 +678,19 @@ impl Default for Server { } } -async fn send_response_header(header: &ResponseHeader, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> { +async fn send_response_header(response: &Response, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> { + + let meta = if response.meta.len() > 1024 { + warn!("Attempted to send response with META exceeding maximum length, truncating"); + &response.meta[..1024] + } else { + &response.meta[..] + }; + let header = format!( "{status} {meta}\r\n", - status = header.status.code(), - meta = header.meta.as_str(), + status = response.status, + meta = meta, ); stream.write_all(header.as_bytes()).await?; @@ -695,22 +699,17 @@ async fn send_response_header(header: &ResponseHeader, stream: &mut (impl AsyncW Ok(()) } -async fn maybe_send_response_body(maybe_body: Option, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> { - if let Some(body) = maybe_body { - send_response_body(body, stream).await?; +async fn send_response_body(mut body: Option, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> { + match &mut body { + Some(Body::Bytes(ref bytes)) => stream.write_all(&bytes).await?, + Some(Body::Reader(ref mut reader)) => { io::copy(reader, stream).await?; }, + None => {}, } - Ok(()) -} - -async fn send_response_body(body: Body, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> { - match body { - Body::Bytes(bytes) => stream.write_all(&bytes).await?, - Body::Reader(mut reader) => { io::copy(&mut reader, stream).await?; }, + if body.is_some() { + stream.flush().await?; } - stream.flush().await?; - Ok(()) } diff --git a/src/types.rs b/src/types.rs index 385fa8c..6fb59a0 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,17 +1,8 @@ pub use uriparse::URIReference; -mod meta; -pub use self::meta::Meta; - mod request; pub use request::Request; -mod response_header; -pub use response_header::ResponseHeader; - -mod status; -pub use status::{Status, StatusCategory}; - mod response; pub use response::Response; diff --git a/src/types/meta.rs b/src/types/meta.rs deleted file mode 100644 index 80477fc..0000000 --- a/src/types/meta.rs +++ /dev/null @@ -1,130 +0,0 @@ -use anyhow::*; -use crate::util::Cowy; - - -#[derive(Debug,Clone,PartialEq,Eq,Default)] -pub struct Meta(String); - -impl Meta { - pub const MAX_LEN: usize = 1024; - - /// 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().len() <= Self::MAX_LEN, "Meta must not exceed {} bytes", Self::MAX_LEN); - - Ok(Self(meta.into())) - } - - /// Creates a new "Meta" string. - /// Truncates `meta` to before: - /// - the first occurrence of `\n` - /// - the character that makes `meta` exceed `Meta::MAX_LEN` - pub fn new_lossy(meta: impl Cowy) -> Self { - let meta = meta.as_ref(); - let truncate_pos = meta.char_indices().position(|(i, ch)| { - let is_newline = ch == '\n'; - let exceeds_limit = (i + ch.len_utf8()) > Self::MAX_LEN; - - is_newline || exceeds_limit - }); - - let meta: String = match truncate_pos { - None => meta.into(), - Some(truncate_pos) => meta.get(..truncate_pos).unwrap().into(), - }; - - Self(meta) - } - - pub fn empty() -> Self { - Self::default() - } - - pub fn as_str(&self) -> &str { - &self.0 - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::iter::repeat; - - #[test] - fn new_rejects_newlines() { - let meta = "foo\nbar"; - let meta = Meta::new(meta); - - assert!(meta.is_err()); - } - - #[test] - fn new_accepts_max_len() { - let meta: String = repeat('x').take(Meta::MAX_LEN).collect(); - let meta = Meta::new(meta); - - assert!(meta.is_ok()); - } - - #[test] - fn new_rejects_exceeding_max_len() { - let meta: String = repeat('x').take(Meta::MAX_LEN + 1).collect(); - let meta = Meta::new(meta); - - assert!(meta.is_err()); - } - - #[test] - fn new_lossy_truncates() { - let meta = "foo\r\nbar\nquux"; - let meta = Meta::new_lossy(meta); - - assert_eq!(meta.as_str(), "foo\r"); - } - - #[test] - fn new_lossy_no_truncate() { - let meta = "foo bar\r"; - let meta = Meta::new_lossy(meta); - - assert_eq!(meta.as_str(), "foo bar\r"); - } - - #[test] - fn new_lossy_empty() { - let meta = ""; - let meta = Meta::new_lossy(meta); - - assert_eq!(meta.as_str(), ""); - } - - #[test] - fn new_lossy_truncates_to_empty() { - let meta = "\n\n\n"; - let meta = Meta::new_lossy(meta); - - assert_eq!(meta.as_str(), ""); - } - - #[test] - fn new_lossy_truncates_to_max_len() { - let meta: String = repeat('x').take(Meta::MAX_LEN + 1).collect(); - let meta = Meta::new_lossy(meta); - - assert_eq!(meta.as_str().len(), Meta::MAX_LEN); - } - - #[test] - fn new_lossy_truncates_multi_byte_sequences() { - let mut meta: String = repeat('x').take(Meta::MAX_LEN - 1).collect(); - meta.push('🦀'); - - assert_eq!(meta.len(), Meta::MAX_LEN + 3); - - let meta = Meta::new_lossy(meta); - - assert_eq!(meta.as_str().len(), Meta::MAX_LEN - 1); - } -} diff --git a/src/types/response.rs b/src/types/response.rs index 1baf59e..4fb331d 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -1,101 +1,148 @@ -use std::convert::TryInto; use std::borrow::Borrow; -use anyhow::*; -use uriparse::URIReference; -use crate::types::{ResponseHeader, Body, Document}; -use crate::util::Cowy; +use crate::types::{Body, Document}; pub struct Response { - header: ResponseHeader, - body: Option, + pub status: u8, + pub meta: String, + pub body: Option, } impl Response { - pub const fn new(header: ResponseHeader) -> Self { + + /// Create a response with a given status and meta + pub fn new(status: u8, meta: impl ToString) -> Self { Self { - header, + status, + meta: meta.to_string(), body: None, } } - #[deprecated( - since = "0.4.0", - note = "Deprecated in favor of Response::success_gemini() or Document::into()" - )] - pub fn document(document: impl Borrow) -> Self { - Self::success_gemini(document) + /// Create a INPUT (10) response with a given prompt + /// + /// Use [`Response::sensitive_input()`] for collecting any sensitive input, as input + /// collected by this request may be logged. + pub fn input(prompt: impl ToString) -> Self { + Self::new(10, prompt) } - pub fn input(prompt: impl Cowy) -> Result { - let header = ResponseHeader::input(prompt)?; - Ok(Self::new(header)) + /// Create a SENSITIVE INPUT (11) response with a given prompt + /// + /// See also [`Response::input()`] for unsensitive inputs + pub fn sensitive_input(prompt: impl ToString) -> Self { + Self::new(11, prompt) } - pub fn input_lossy(prompt: impl Cowy) -> Self { - let header = ResponseHeader::input_lossy(prompt); - Self::new(header) - } - - pub fn redirect_temporary_lossy<'a>(location: impl TryInto>) -> Self { - let header = ResponseHeader::redirect_temporary_lossy(location); - Self::new(header) - } - - /// Create a successful response with a given body and MIME + /// Create a SUCCESS (20) response with a given body and MIME pub fn success(mime: impl ToString, body: impl Into) -> Self { Self { - header: ResponseHeader::success(mime), + status: 20, + meta: mime.to_string(), body: Some(body.into()), } } - /// Create a successful response with a `text/gemini` MIME + /// Create a SUCCESS (20) response with a `text/gemini` MIME pub fn success_gemini(body: impl Into) -> Self { Self::success("text/gemini", body) } - /// Create a successful response with a `text/plain` MIME + /// Create a SUCCESS (20) response with a `text/plain` MIME pub fn success_plain(body: impl Into) -> Self { Self::success("text/plain", body) } - pub fn server_error(reason: impl Cowy) -> Result { - let header = ResponseHeader::server_error(reason)?; - Ok(Self::new(header)) + /// Create a REDIRECT - TEMPORARY (30) response with a destination + pub fn redirect_temporary(dest: impl ToString) -> Self { + Self::new(30, dest) } + /// Create a REDIRECT - PERMANENT (31) response with a destination + pub fn redirect_permanent(dest: impl ToString) -> Self { + Self::new(31, dest) + } + + /// Create a TEMPORARY FAILURE (40) response with a human readable error + pub fn temporary_failure(reason: impl ToString) -> Self { + Self::new(40, reason) + } + + /// Create a SERVER UNAVAILABLE (41) response with a human readable error + /// + /// Used to denote that the server is temporarily unavailable, for example due to + /// heavy load, or maintenance + pub fn server_unavailable(reason: impl ToString) -> Self { + Self::new(41, reason) + } + + /// Create a CGI ERROR (42) response with a human readable error + pub fn cgi_error(reason: impl ToString) -> Self { + Self::new(42, reason) + } + + /// Create a PROXY ERROR (43) response with a human readable error + pub fn proxy_error(reason: impl ToString) -> Self { + Self::new(43, reason) + } + + /// Create a SLOW DOWN (44) response with a wait time in seconds + /// + /// Used to denote that the user should wait a certain number of seconds before + /// sending another request, often for ratelimiting purposes + pub fn slow_down(wait: u64) -> Self { + Self::new(44, wait) + } + + /// Create a PERMANENT FAILURE (50) response with a human readable error + pub fn permanent_failure(reason: impl ToString) -> Self { + Self::new(50, reason) + } + + /// Create a NOT FOUND (51) response with no further information + /// + /// Essentially a 404 pub fn not_found() -> Self { - let header = ResponseHeader::not_found(); - Self::new(header) + Self::new(51, String::new()) } - pub fn bad_request_lossy(reason: impl Cowy) -> Self { - let header = ResponseHeader::bad_request_lossy(reason); - Self::new(header) + /// Create a GONE (52) response with a human readable error + /// + /// For when a resource used to be here, but never will be again + pub fn gone(reason: impl ToString) -> Self { + Self::new(52, reason) } - pub fn client_certificate_required() -> Self { - let header = ResponseHeader::client_certificate_required(); - Self::new(header) + /// Create a PROXY REQUEST REFUSED (53) response with a human readable error + /// + /// The server does not serve content on this domain + pub fn proxy_request_refused(reason: impl ToString) -> Self { + Self::new(53, reason) } - pub fn certificate_not_authorized() -> Self { - let header = ResponseHeader::certificate_not_authorized(); - Self::new(header) + /// Create a BAD REQUEST (59) response with a human readable error + pub fn bad_request(reason: impl ToString) -> Self { + Self::new(59, reason) } - pub fn with_body(mut self, body: impl Into) -> Self { - self.body = Some(body.into()); - self + /// Create a CLIENT CERTIFICATE REQUIRED (60) response with a human readable error + pub fn client_certificate_required(reason: impl ToString) -> Self { + Self::new(60, reason) } - pub const fn header(&self) -> &ResponseHeader { - &self.header + /// Create a CERTIFICATE NOT AUTHORIZED (61) response with a human readable error + pub fn certificate_not_authorized(reason: impl ToString) -> Self { + Self::new(61, reason) } - pub fn take_body(&mut self) -> Option { - self.body.take() + /// Create a CERTIFICATE NOT VALID (62) response with a human readable error + pub fn certificate_not_valid(reason: impl ToString) -> Self { + Self::new(62, reason) + } + + /// True if the response is a SUCCESS (10) response + pub const fn is_success(&self) -> bool { + self.status == 10 } } diff --git a/src/types/response_header.rs b/src/types/response_header.rs deleted file mode 100644 index 3e214d1..0000000 --- a/src/types/response_header.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::convert::TryInto; - -use anyhow::*; -use uriparse::URIReference; -use crate::util::Cowy; -use crate::types::{Status, Meta}; - -#[derive(Debug,Clone)] -pub struct ResponseHeader { - pub status: Status, - pub meta: Meta, -} - -impl ResponseHeader { - pub fn input(prompt: impl Cowy) -> Result { - Ok(Self { - status: Status::INPUT, - meta: Meta::new(prompt).context("Invalid input prompt")?, - }) - } - - pub fn input_lossy(prompt: impl Cowy) -> Self { - Self { - status: Status::INPUT, - meta: Meta::new_lossy(prompt), - } - } - - pub fn success(mime: impl ToString) -> Self { - Self { - status: Status::SUCCESS, - meta: Meta::new_lossy(mime.to_string()), - } - } - - pub fn redirect_temporary_lossy<'a>(location: impl TryInto>) -> Self { - let location = match location.try_into() { - Ok(location) => location, - Err(_) => return Self::bad_request_lossy("Invalid redirect location"), - }; - - Self { - status: Status::REDIRECT_TEMPORARY, - meta: Meta::new_lossy(location.to_string()), - } - } - - pub fn server_error(reason: impl Cowy) -> Result { - Ok(Self { - status: Status::PERMANENT_FAILURE, - meta: Meta::new(reason).context("Invalid server error reason")?, - }) - } - - pub fn server_error_lossy(reason: impl Cowy) -> Self { - Self { - status: Status::PERMANENT_FAILURE, - meta: Meta::new_lossy(reason), - } - } - - pub fn not_found() -> Self { - Self { - status: Status::NOT_FOUND, - meta: Meta::new_lossy("Not found"), - } - } - - pub fn bad_request_lossy(reason: impl Cowy) -> Self { - Self { - status: Status::BAD_REQUEST, - meta: Meta::new_lossy(reason), - } - } - - pub fn client_certificate_required() -> Self { - Self { - status: Status::CLIENT_CERTIFICATE_REQUIRED, - meta: Meta::new_lossy("No certificate provided"), - } - } - - pub fn certificate_not_authorized() -> Self { - Self { - status: Status::CERTIFICATE_NOT_AUTHORIZED, - meta: Meta::new_lossy("Your certificate is not authorized to view this content"), - } - } - - pub const fn status(&self) -> &Status { - &self.status - } - - pub const fn meta(&self) -> &Meta { - &self.meta - } -} diff --git a/src/types/status.rs b/src/types/status.rs deleted file mode 100644 index ba9ee71..0000000 --- a/src/types/status.rs +++ /dev/null @@ -1,82 +0,0 @@ -#[derive(Debug,Copy,Clone,PartialEq,Eq)] -pub struct Status(u8); - -impl Status { - pub const INPUT: Self = Self(10); - pub const SENSITIVE_INPUT: Self = Self(11); - pub const SUCCESS: Self = Self(20); - pub const REDIRECT_TEMPORARY: Self = Self(30); - pub const REDIRECT_PERMANENT: Self = Self(31); - pub const TEMPORARY_FAILURE: Self = Self(40); - pub const SERVER_UNAVAILABLE: Self = Self(41); - pub const CGI_ERROR: Self = Self(42); - pub const PROXY_ERROR: Self = Self(43); - pub const SLOW_DOWN: Self = Self(44); - pub const PERMANENT_FAILURE: Self = Self(50); - pub const NOT_FOUND: Self = Self(51); - pub const GONE: Self = Self(52); - pub const PROXY_REQUEST_REFUSED: Self = Self(53); - pub const BAD_REQUEST: Self = Self(59); - pub const CLIENT_CERTIFICATE_REQUIRED: Self = Self(60); - pub const CERTIFICATE_NOT_AUTHORIZED: Self = Self(61); - pub const CERTIFICATE_NOT_VALID: Self = Self(62); - - pub const fn code(&self) -> u8 { - self.0 - } - - pub fn is_success(&self) -> bool { - self.category().is_success() - } - - #[allow(clippy::missing_const_for_fn)] - pub fn category(&self) -> StatusCategory { - let class = self.0 / 10; - - match class { - 1 => StatusCategory::Input, - 2 => StatusCategory::Success, - 3 => StatusCategory::Redirect, - 4 => StatusCategory::TemporaryFailure, - 5 => StatusCategory::PermanentFailure, - 6 => StatusCategory::ClientCertificateRequired, - _ => StatusCategory::PermanentFailure, - } - } -} - -#[derive(Copy,Clone,PartialEq,Eq)] -pub enum StatusCategory { - Input, - Success, - Redirect, - TemporaryFailure, - PermanentFailure, - ClientCertificateRequired, -} - -impl StatusCategory { - pub fn is_input(&self) -> bool { - *self == Self::Input - } - - pub fn is_success(&self) -> bool { - *self == Self::Success - } - - pub fn redirect(&self) -> bool { - *self == Self::Redirect - } - - pub fn is_temporary_failure(&self) -> bool { - *self == Self::TemporaryFailure - } - - pub fn is_permanent_failure(&self) -> bool { - *self == Self::PermanentFailure - } - - pub fn is_client_certificate_required(&self) -> bool { - *self == Self::ClientCertificateRequired - } -} diff --git a/src/user_management/routes.rs b/src/user_management/routes.rs index e7f17f3..36ad435 100644 --- a/src/user_management/routes.rs +++ b/src/user_management/routes.rs @@ -165,7 +165,7 @@ impl UserManagementRoutes for crate::Server { if let Some(input) = request.input().map(str::to_owned) { (handler.clone())(request, user, input).await } else { - Response::input(prompt) + Ok(Response::input(prompt)) } } }) @@ -207,7 +207,7 @@ async fn handle_base(request: Request) - async fn handle_ask_cert(request: Request) -> Result { Ok(match request.user::()? { User::Unauthenticated => { - Response::client_certificate_required() + Response::client_certificate_required("Please select a client certificate to proceed.") }, User::NotSignedIn(nsi) => { let segments = request.trailing_segments().iter().map(String::as_str).collect::>(); @@ -270,7 +270,7 @@ async fn handle_register(reque Err(e) => return Err(e.into()) } } else { - Response::input_lossy("Please pick a username") + Response::input("Please pick a username") } }, User::SignedIn(user) => { @@ -305,14 +305,14 @@ async fn handle_login(request: Err(e) => return Err(e.into()), } } else { - Response::input_lossy("Please enter your password") + Response::sensitive_input("Please enter your password") } } else if let Some(username) = request.input() { - Response::redirect_temporary_lossy( + Response::redirect_temporary( format!("/account/login/{}", username).as_str() ) } else { - Response::input_lossy("Please enter your username") + Response::input("Please enter your username") } }, User::SignedIn(user) => { @@ -336,7 +336,7 @@ async fn handle_password(reque user.set_password(password)?; Response::success_gemini(include_str!("pages/password/success.gmi")) } else { - Response::input( + Response::sensitive_input( format!("Please enter a {}password", if user.has_password() { "new " @@ -344,7 +344,7 @@ async fn handle_password(reque "" } ) - )? + ) } }, }) diff --git a/src/util.rs b/src/util.rs index 32d521d..4ac9062 100644 --- a/src/util.rs +++ b/src/util.rs @@ -14,7 +14,7 @@ use std::future::Future; use tokio::time; #[cfg(feature="serve_dir")] -pub async fn serve_file>(path: P, mime: &str) -> Result { +pub async fn serve_file>(path: P, mime: &str) -> Response { let path = path.as_ref(); let file = match File::open(path).await { @@ -22,17 +22,17 @@ pub async fn serve_file>(path: P, mime: &str) -> Result Err(err) => match err.kind() { std::io::ErrorKind::PermissionDenied => { warn!("Asked to serve {}, but permission denied by OS", path.display()); - return Ok(Response::not_found()); + return Response::not_found(); }, _ => return warn_unexpected(err, path, line!()), } }; - Ok(Response::success(mime, file)) + Response::success(mime, file) } #[cfg(feature="serve_dir")] -pub async fn serve_dir, P: AsRef>(dir: D, virtual_path: &[P]) -> Result { +pub async fn serve_dir, P: AsRef>(dir: D, virtual_path: &[P]) -> Response { debug!("Dir: {}", dir.as_ref().display()); let dir = dir.as_ref(); let dir = match dir.canonicalize() { @@ -41,11 +41,11 @@ pub async fn serve_dir, P: AsRef>(dir: D, virtual_path: &[P match e.kind() { std::io::ErrorKind::NotFound => { warn!("Path {} not found. Check your configuration.", dir.display()); - return Response::server_error("Server incorrectly configured") + return Response::temporary_failure("Server incorrectly configured") }, std::io::ErrorKind::PermissionDenied => { warn!("Permission denied for {}. Check that the server has access.", dir.display()); - return Response::server_error("Server incorrectly configured") + return Response::temporary_failure("Server incorrectly configured") }, _ => return warn_unexpected(e, dir, line!()), } @@ -61,12 +61,12 @@ pub async fn serve_dir, P: AsRef>(dir: D, virtual_path: &[P Ok(dir) => dir, Err(e) => { match e.kind() { - std::io::ErrorKind::NotFound => return Ok(Response::not_found()), + std::io::ErrorKind::NotFound => return Response::not_found(), std::io::ErrorKind::PermissionDenied => { // Runs when asked to serve a file in a restricted dir // i.e. not /noaccess, but /noaccess/file warn!("Asked to serve {}, but permission denied by OS", path.display()); - return Ok(Response::not_found()); + return Response::not_found(); }, _ => return warn_unexpected(e, path.as_ref(), line!()), } @@ -74,7 +74,7 @@ pub async fn serve_dir, P: AsRef>(dir: D, virtual_path: &[P }; if !path.starts_with(&dir) { - return Ok(Response::not_found()); + return Response::not_found(); } if !path.is_dir() { @@ -86,14 +86,14 @@ pub async fn serve_dir, P: AsRef>(dir: D, virtual_path: &[P } #[cfg(feature="serve_dir")] -async fn serve_dir_listing, B: AsRef>(path: P, virtual_path: &[B]) -> Result { +async fn serve_dir_listing, B: AsRef>(path: P, virtual_path: &[B]) -> Response { let mut dir = match fs::read_dir(path.as_ref()).await { Ok(dir) => dir, Err(err) => match err.kind() { - io::ErrorKind::NotFound => return Ok(Response::not_found()), + io::ErrorKind::NotFound => return Response::not_found(), std::io::ErrorKind::PermissionDenied => { warn!("Asked to serve {}, but permission denied by OS", path.as_ref().display()); - return Ok(Response::not_found()); + return Response::not_found(); }, _ => return warn_unexpected(err, path.as_ref(), line!()), } @@ -109,12 +109,10 @@ async fn serve_dir_listing, B: AsRef>(path: P, virtual_path document.add_link("..", "📁 ../"); } - while let Some(entry) = dir.next_entry().await.context("Failed to list directory")? { + while let Some(entry) = dir.next_entry().await.expect("Failed to list directory") { let file_name = entry.file_name(); let file_name = file_name.to_string_lossy(); - let is_dir = entry.file_type().await - .with_context(|| format!("Failed to get file type of `{}`", entry.path().display()))? - .is_dir(); + let is_dir = entry.file_type().await.unwrap().is_dir(); let trailing_slash = if is_dir { "/" } else { "" }; let uri = format!("./{}{}", file_name, trailing_slash); @@ -125,7 +123,7 @@ async fn serve_dir_listing, B: AsRef>(path: P, virtual_path )); } - Ok(document.into()) + document.into() } #[cfg(feature="serve_dir")] @@ -146,7 +144,7 @@ pub fn guess_mime_from_path>(path: P) -> &'static str { #[cfg(feature="serve_dir")] /// Print a warning to the log asking to file an issue and respond with "Unexpected Error" -pub (crate) fn warn_unexpected(err: impl std::fmt::Debug, path: &Path, line: u32) -> Result { +pub (crate) fn warn_unexpected(err: impl std::fmt::Debug, path: &Path, line: u32) -> Response { warn!( concat!( "Unexpected error serving path {} at util.rs:{}, please report to ", @@ -157,7 +155,7 @@ pub (crate) fn warn_unexpected(err: impl std::fmt::Debug, path: &Path, line: u32 line, err ); - Response::server_error("Unexpected error") + Response::temporary_failure("Unexpected error") } /// A convenience trait alias for `AsRef + Into`, From b265ae985c77162de436e02ad608399ce71f0f3d Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Tue, 1 Dec 2020 17:11:28 -0500 Subject: [PATCH 078/113] Dramatically simplify the certificate example --- examples/certificates.rs | 61 +++++++++------------------------------- 1 file changed, 14 insertions(+), 47 deletions(-) diff --git a/examples/certificates.rs b/examples/certificates.rs index 28e7e1d..cce548b 100644 --- a/examples/certificates.rs +++ b/examples/certificates.rs @@ -1,12 +1,9 @@ -use anyhow::*; +use anyhow::Result; use log::LevelFilter; -use tokio::sync::RwLock; -use kochab::{Certificate, Request, Response, Server}; -use std::collections::HashMap; -use std::sync::Arc; -// Workaround for Certificates not being hashable -type CertBytes = Vec; +use std::fmt::Write; + +use kochab::{Request, Response, Server}; #[tokio::main] async fn main() -> Result<()> { @@ -14,53 +11,23 @@ async fn main() -> Result<()> { .filter_module("kochab", LevelFilter::Debug) .init(); - let users = Arc::>>::default(); - Server::new() - .add_route("/", move|req| handle_request(users.clone(), req)) + .add_route("/", handle_request) .serve_unix("kochab.sock") .await } -/// An ultra-simple demonstration of simple authentication. -/// -/// If the user attempts to connect, they will be prompted to create a client certificate. -/// Once they've made one, they'll be given the opportunity to create an account by -/// selecting a username. They'll then get a message confirming their account creation. -/// Any time this user visits the site in the future, they'll get a personalized welcome -/// message. -async fn handle_request(users: Arc>>, request: Request) -> Result { - if let Some(Certificate(cert_bytes)) = request.certificate() { - // The user provided a certificate - let users_read = users.read().await; - if let Some(user) = users_read.get(cert_bytes) { - // The user has already registered - Ok( - Response::success_gemini(format!("Welcome {}!", user)) - ) - } else { - // The user still needs to register - drop(users_read); - if let Some(query_part) = request.uri().query() { - // The user provided some input (a username request) - let username = query_part.as_str(); - let mut users_write = users.write().await; - users_write.insert(cert_bytes.clone(), username.to_owned()); - Ok( - Response::success_gemini( - format!( - "Your account has been created {}! Welcome!", - username - ) - ) - ) - } else { - // The user didn't provide input, and should be prompted - Response::input("What username would you like?") - } +async fn handle_request(request: Request) -> Result { + if let Some(fingerprint) = request.certificate() { + let mut message = String::from("You connected with a certificate with a fingerprint of:\n"); + + for byte in fingerprint { + write!(&mut message, "{:x}", byte).unwrap(); } + + Ok(Response::success_plain(message)) } else { // The user didn't provide a certificate - Ok(Response::client_certificate_required()) + Ok(Response::client_certificate_required("You didn't provide a client certificate")) } } From 3e2865de1208c74ea1782d80b4a7a766e720e6e2 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Tue, 1 Dec 2020 17:13:54 -0500 Subject: [PATCH 079/113] Fix ratelimiting example Geeze, that code's ancient! How long was that broken? --- examples/ratelimiting.rs | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/examples/ratelimiting.rs b/examples/ratelimiting.rs index 455fc22..f8021cb 100644 --- a/examples/ratelimiting.rs +++ b/examples/ratelimiting.rs @@ -17,21 +17,18 @@ async fn main() -> Result<()> { .await } -fn handle_request(request: Request) -> BoxFuture<'static, Result> { - async move { - let mut document = Document::new(); +async fn handle_request(request: Request) -> Result { + let mut document = Document::new(); - if let Some("limit") = request.trailing_segments().get(0).map(String::as_str) { - document.add_text("You're on a rate limited page!") - .add_text("You can only access this page twice per minute"); - } else { - document.add_text("You're on a normal page!") - .add_text("You can access this page as much as you like."); - } - document.add_blank_line() - .add_link("/limit", "Go to rate limited page") - .add_link("/", "Go to a page that's not rate limited"); - Ok(Response::document(document)) + if let Some("limit") = request.trailing_segments().get(0).map(String::as_str) { + document.add_text("You're on a rate limited page!") + .add_text("You can only access this page twice per minute"); + } else { + document.add_text("You're on a normal page!") + .add_text("You can access this page as much as you like."); } - .boxed() + document.add_blank_line() + .add_link("/limit", "Go to rate limited page") + .add_link("/", "Go to a page that's not rate limited"); + Ok(document.into()) } From de060363f184cc3dd70713e7d3dd4c6abdb31ab6 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Tue, 1 Dec 2020 17:38:26 -0500 Subject: [PATCH 080/113] Fix the dashmap + user_management_routes build --- src/user_management/routes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/user_management/routes.rs b/src/user_management/routes.rs index 36ad435..195f707 100644 --- a/src/user_management/routes.rs +++ b/src/user_management/routes.rs @@ -180,7 +180,7 @@ const NSI: &str = include_str!("pages/nopass/nsi.gmi"); // TODO periodically clean these #[cfg(feature = "dashmap")] lazy_static::lazy_static! { - static ref PENDING_REDIRECTS: DashMap = Default::default(); + static ref PENDING_REDIRECTS: DashMap<[u8; 32], String> = Default::default(); } #[cfg(not(feature = "dashmap"))] From 64001975145ed85b6f911d53a62363b5cc40394a Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Tue, 1 Dec 2020 17:38:52 -0500 Subject: [PATCH 081/113] Treat zero-length input as no input at all Fixes running with molly brown --- src/types/request.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/types/request.rs b/src/types/request.rs index ae8b844..85fabd4 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -62,7 +62,7 @@ impl Request { uri.normalize(); - let input = match uri.query() { + let input = match uri.query().filter(|q| !q.is_empty()) { None => None, Some(query) => { let input = percent_decode_str(query.as_str()) @@ -126,6 +126,11 @@ impl Request { .collect::>() } + /// View any input sent by the user in the query string + /// + /// Any zero-length input is treated as no input at all, and will be reported as + /// [`None`]. This is done in order to provide compatibility with the SCGI header + /// common practice of reporting no query string as a blank input. pub fn input(&self) -> Option<&str> { self.input.as_deref() } From 723986c011f003e8bea47d7263cabdfa415633f6 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Tue, 1 Dec 2020 19:02:49 -0500 Subject: [PATCH 082/113] Add `rewrite_path()` --- src/types/request.rs | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/types/request.rs b/src/types/request.rs index 85fabd4..9f81145 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -4,6 +4,7 @@ use std::convert::TryInto; use std::{ collections::HashMap, convert::TryFrom, + path::Path, }; use anyhow::*; use percent_encoding::percent_decode_str; @@ -25,6 +26,8 @@ pub struct Request { manager: UserManager, #[cfg(feature = "scgi_srv")] headers: HashMap, + #[cfg(feature = "scgi_srv")] + script_path: Option, } impl Request { @@ -37,7 +40,7 @@ impl Request { manager: UserManager, ) -> Result { #[cfg(feature = "scgi_srv")] - let (mut uri, certificate) = ( + let (mut uri, certificate, script_path) = ( URIReference::try_from( format!( "{}{}", @@ -58,6 +61,9 @@ impl Request { .try_into() .expect("Received certificate fingerprint of invalid lenght from upstream") }), + headers.get("SCRIPT_PATH") + .or_else(|| headers.get("SCRIPT_NAME")) + .cloned() ); uri.normalize(); @@ -83,6 +89,8 @@ impl Request { trailing_segments: None, #[cfg(feature = "scgi_srv")] headers, + #[cfg(feature = "scgi_srv")] + script_path, #[cfg(feature="user_management")] manager, }) @@ -192,6 +200,40 @@ impl Request { pub fn user_manager(&self) -> &UserManager { &self.manager } + + /// Attempt to rewrite an absolute URL against the base path of the SCGI script + /// + /// When writing an SCGI script, you cannot assume that your script is mounted on the + /// base path of "/". For example, a request to the gemini server for "/app/path" + /// might be received by your script as "/path" if your script is mounted on "/app/". + /// In this situation, if you linked to "/", you would be sending users to "/", which + /// is not handled by your app, instead of "/app/", where you probably intended to + /// send the user. + /// + /// This method attempts to infer where the script is mounted, and rewrite an absolute + /// url relative to that. For example, if the application was mounted on "/app/", and + /// you passed "/path", the result would be "/app/path". + /// + /// When running in `gemini_srv` mode, the application is always mounted at the base + /// path, so this will always return the path unchanged. + /// + /// Not all SCGI clients will correctly report the application's path, so this may + /// fail if unable to infer the correct path. If this is the case, None will be + /// returned. Currently, the SCGI headers checked are: + /// + /// * `SCRIPT_PATH` (Used by mollybrown) + /// * `SCRIPT_NAME` (Used by GLV-1.12556) + pub fn rewrite_path(&self, path: impl AsRef) -> Option { + #[cfg(feature = "scgi_srv")] { + self.script_path.as_ref().map(|base| { + let base: &Path = base.as_ref(); + base.join(path.as_ref()).display().to_string() + }) + } + #[cfg(feature = "gemini_srv")] { + Some(path.to_string()) + } + } } impl ops::Deref for Request { From aa6c8d8a57fb9960dc2891bbf477d30ff7e6bb40 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Tue, 1 Dec 2020 19:10:50 -0500 Subject: [PATCH 083/113] Remove needless borrow --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index face42d..02ba151 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -701,7 +701,7 @@ async fn send_response_header(response: &Response, stream: &mut (impl AsyncWrite async fn send_response_body(mut body: Option, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> { match &mut body { - Some(Body::Bytes(ref bytes)) => stream.write_all(&bytes).await?, + Some(Body::Bytes(ref bytes)) => stream.write_all(bytes).await?, Some(Body::Reader(ref mut reader)) => { io::copy(reader, stream).await?; }, None => {}, } From 64f54e8e586f21ce53df05915a2261c2812d8d20 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Tue, 1 Dec 2020 20:09:35 -0500 Subject: [PATCH 084/113] Fix rewrite_path --- src/types/request.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/types/request.rs b/src/types/request.rs index 9f81145..f12367b 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -227,7 +227,14 @@ impl Request { #[cfg(feature = "scgi_srv")] { self.script_path.as_ref().map(|base| { let base: &Path = base.as_ref(); - base.join(path.as_ref()).display().to_string() + + // Make path relative + let mut path_as_path: &Path = path.as_ref().as_ref(); + if path_as_path.is_absolute() { + path_as_path = (&path.as_ref()[1..]).as_ref(); + } + + base.join(path_as_path).display().to_string() }) } #[cfg(feature = "gemini_srv")] { From 8f86bac60843cfeb6f43c56a0a89e30f041197b4 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Tue, 1 Dec 2020 21:41:00 -0500 Subject: [PATCH 085/113] Added rewrite_all for body --- src/types/body.rs | 47 ++++++++++++++++++++++++++++++++++++++++++- src/types/response.rs | 45 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/src/types/body.rs b/src/types/body.rs index a7481c3..9a94769 100644 --- a/src/types/body.rs +++ b/src/types/body.rs @@ -1,4 +1,4 @@ -use tokio::io::AsyncRead; +use tokio::io::{AsyncRead, AsyncReadExt}; #[cfg(feature="serve_dir")] use tokio::fs::File; @@ -11,6 +11,51 @@ pub enum Body { Reader(Box), } +impl Body { + /// Called by [`Response::rewrite_all`] + pub (crate) async fn rewrite_all(&mut self, based_on: &crate::Request) -> std::io::Result { + let bytes = match self { + Self::Bytes(bytes) => { + let mut newbytes = Vec::new(); + std::mem::swap(bytes, &mut newbytes); + newbytes + } + Self::Reader(reader) => { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + bytes + } + }; + let mut body = String::from_utf8(bytes).expect("text/gemini wasn't UTF8"); + + let mut maybe_indx = if body.starts_with("=> ") { + Some(3) + } else { + body.find("\n=> ").map(|offset| offset + 4) + }; + while let Some(indx) = maybe_indx { + + // Find the end of the link part + let end = (&body[indx..]).find(&[' ', '\n', '\r'][..]) + .map(|offset| indx + offset ) + .unwrap_or(body.len()); + + // Perform replacement + if let Some(replacement) = based_on.rewrite_path(&body[indx..end]) { + body.replace_range(indx..end, replacement.as_str()); + } else { + return Ok(false) + }; + + // Find next match + maybe_indx = (&body[indx..]).find("\n=> ").map(|offset| offset + 4 + indx); + } + + *self = Self::Bytes(body.into_bytes()); + Ok(true) + } +} + impl> From for Body { fn from(document: D) -> Self { Self::from(document.borrow().to_string()) diff --git a/src/types/response.rs b/src/types/response.rs index 4fb331d..759ef39 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -144,6 +144,51 @@ impl Response { pub const fn is_success(&self) -> bool { self.status == 10 } + + /// Rewrite any links in this response based on the path identified by a request + /// + /// For more information about what rewriting a link means, see + /// [`Request::rewrite_path`]. + /// + /// Currently, this rewrites any links in: + /// * SUCCESS (10) requests with a `text/gemini` MIME + /// * REDIRECT (3X) requests + /// + /// For all other responses, and for any responses without links, this method has no + /// effect. + /// + /// If this response contains a reader-based body, this **MAY** load the reader's + /// contents into memory if the mime is "text/gemini". If an IO error occurs during + /// this process, this error will be raised + /// + /// If the request does not contain enough information to rewrite a link (in other + /// words, if [`Requet::rewrite_path`] returns [`None`]), then false is returned. In + /// all other cases, this method returns true. + /// + /// Panics if a "text/gemini" response is not UTF-8 formatted + pub async fn rewrite_all(&mut self, based_on: &crate::Request) -> std::io::Result { + #[cfg(feature = "scgi_srv")] + match self.status { + 20 if self.meta == "text/gemini" => { + if let Some(body) = self.body.as_mut() { + body.rewrite_all(based_on).await + } else { + Ok(false) + } + }, + 30 | 31 => { + if let Some(path) = based_on.rewrite_path(&self.meta) { + self.meta = path; + Ok(true) + } else { + Ok(false) + } + }, + _ => Ok(true), + } + #[cfg(feature = "gemini_srv")] + Ok(true) + } } impl AsRef> for Response { From 05e4d25a536e5f67f6f11e86a5d5818717e9620c Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Tue, 1 Dec 2020 21:59:00 -0500 Subject: [PATCH 086/113] Added in autorewrite as an option to `Server` --- src/lib.rs | 35 +++++++++++++++++++++++++++++++++-- src/types/request.rs | 1 + 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 02ba151..1280bba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,6 +77,7 @@ struct ServerInner { routes: Arc>, timeout: Duration, complex_timeout: Option, + autorewrite: bool, #[cfg(feature="ratelimiting")] rate_limits: Arc>>, #[cfg(feature="user_management")] @@ -206,13 +207,22 @@ impl ServerInner { request.set_cert(client_cert); } - let response = if let Some((trailing, handler)) = self.routes.match_request(&request) { + let mut response = if let Some((trailing, handler)) = self.routes.match_request(&request) { request.set_trailing(trailing); - handler.handle(request).await + handler.handle(request.clone()).await } else { Response::not_found() }; + match response.rewrite_all(&request).await { + Ok(true) => { /* all is well */ } + Ok(false) => panic!("Upstream did not include SCRIPT_PATH or SCRIPT_NAME"), + Err(e) => { + error!("Error reading text/gemini file from Response reader: {}", e); + response = Response::not_found(); + } + } + self.send_response(response, &mut stream).await .context("Failed to send response")?; @@ -417,6 +427,7 @@ pub struct Server { timeout: Duration, complex_body_timeout_override: Option, routes: RoutingNode, + autorewrite: bool, #[cfg(feature = "gemini_srv")] cert_path: PathBuf, #[cfg(feature = "gemini_srv")] @@ -437,6 +448,7 @@ impl Server { timeout: Duration::from_secs(1), complex_body_timeout_override: Some(Duration::from_secs(30)), routes: RoutingNode::default(), + autorewrite: false, #[cfg(feature = "gemini_srv")] cert_path: PathBuf::from("cert/cert.pem"), #[cfg(feature = "gemini_srv")] @@ -621,6 +633,24 @@ impl Server { self } + /// Enable or disable autorewrite + /// + /// Autorewrite automatically detects links in responses being sent out through kochab + /// and rewrites them to match the base of the script. For example, if the script is + /// mounted on "/app", any links to "/page" would be rewritten as "/app/page". This + /// does nothing when in `gemini_srv` mode. + /// + /// **Note:** If you are serving *very long* `text/gemini` files using `serve_dir`, + /// then you should avoid setting this to true. Additionally, if using this option, + /// do not try to rewrite your links using [`Request::rewrite_path`] or + /// [`Response::rewrite_all`], as this will apply the rewrite twice. + /// + /// For more information about rewriting, see [`Request::rewrite_path`]. + pub fn set_autorewrite(mut self, autorewrite: bool) -> Self { + self.autorewrite = autorewrite; + self + } + fn build(mut self) -> Result { #[cfg(feature = "gemini_srv")] let config = tls_config( @@ -639,6 +669,7 @@ impl Server { routes: Arc::new(self.routes), timeout: self.timeout, complex_timeout: self.complex_body_timeout_override, + autorewrite: self.autorewrite, #[cfg(feature = "gemini_srv")] tls_acceptor: TlsAcceptor::from(config), #[cfg(feature="ratelimiting")] diff --git a/src/types/request.rs b/src/types/request.rs index f12367b..ba7ec49 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -17,6 +17,7 @@ use ring::digest; #[cfg(feature="user_management")] use crate::user_management::{UserManager, User}; +#[derive(Clone)] pub struct Request { uri: URIReference<'static>, input: Option, From 85cbb1d6d8a0b0c8968bcfdaaf16ce1e7276d01e Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Tue, 1 Dec 2020 23:24:10 -0500 Subject: [PATCH 087/113] Remove unused squeegee dep --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 257f7d4..4bd86f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,6 @@ serde = { version = "1.0", optional = true } rust-argon2 = { version = "0.8.2", optional = true } crc32fast = { version = "1.2.1", optional = true } rcgen = { version = "0.8.5", optional = true } -squeegee = { git = "https://gitlab.com/Alch_Emi/squeegee.git", branch = "main", optional = true } [dev-dependencies] env_logger = "0.8.1" From 89e7719939db568101c2992f8ea8cfc2046c0bb5 Mon Sep 17 00:00:00 2001 From: Emii Tatsuo Date: Tue, 1 Dec 2020 23:32:03 -0500 Subject: [PATCH 088/113] Fix stable build --- src/types/body.rs | 5 ++++- src/types/request.rs | 3 ++- src/types/response.rs | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/types/body.rs b/src/types/body.rs index 9a94769..d9aeca3 100644 --- a/src/types/body.rs +++ b/src/types/body.rs @@ -1,4 +1,6 @@ -use tokio::io::{AsyncRead, AsyncReadExt}; +use tokio::io::AsyncRead; +#[cfg(feature="scgi_srv")] +use tokio::io::AsyncReadExt; #[cfg(feature="serve_dir")] use tokio::fs::File; @@ -13,6 +15,7 @@ pub enum Body { impl Body { /// Called by [`Response::rewrite_all`] + #[cfg(feature="scgi_srv")] pub (crate) async fn rewrite_all(&mut self, based_on: &crate::Request) -> std::io::Result { let bytes = match self { Self::Bytes(bytes) => { diff --git a/src/types/request.rs b/src/types/request.rs index ba7ec49..34fddf9 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -59,6 +59,7 @@ impl Request { .map(|hsh| { ring::test::from_hex(hsh.as_str()) .expect("Received invalid certificate fingerprint from upstream") + .as_slice() .try_into() .expect("Received certificate fingerprint of invalid lenght from upstream") }), @@ -239,7 +240,7 @@ impl Request { }) } #[cfg(feature = "gemini_srv")] { - Some(path.to_string()) + Some(path.as_ref().to_string()) } } } diff --git a/src/types/response.rs b/src/types/response.rs index 759ef39..f2f6ec2 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -145,6 +145,7 @@ impl Response { self.status == 10 } + #[cfg_attr(feature="gemini_srv",allow(unused_variables))] /// Rewrite any links in this response based on the path identified by a request /// /// For more information about what rewriting a link means, see From 9feb555b201aa490b6f3e0f11017fc3be35a3f3d Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Wed, 2 Dec 2020 19:18:22 -0500 Subject: [PATCH 089/113] Fix branch name in README example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f923469..69cfe48 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Kochab is an extension & a fork of the Gemini SDK [northstar]. Where northstar It is currently only possible to use kochab through it's git repo, although it may wind up on crates.rs someday. ```toml -kochab = { git = "https://gitlab.com/Alch_Emi/kochab.git", branch = "kochab" } +kochab = { git = "https://gitlab.com/Alch_Emi/kochab.git", branch = "stable" } ``` # Generating a key & certificate From 23c5141dd0ee42ed2a508ec93bc95beaaefead67 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 3 Dec 2020 00:06:48 -0500 Subject: [PATCH 090/113] Add a compiler error giving an actual reason if invalid features are active --- src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 1280bba..ebc25c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,11 @@ #[macro_use] extern crate log; +#[cfg(all(feature = "gemini_srv", feature = "scgi_srv"))] +compile_error!("Please enable only one of either the `gemini_srv` or `scgi_srv` features on the kochab crate"); + +#[cfg(not(any(feature = "gemini_srv", feature = "scgi_srv")))] +compile_error!("Please enable at least one of either the `gemini_srv` or `scgi_srv` features on the kochab crate"); + use std::{ sync::Arc, time::Duration, From 477d31dae39e315943e89788bebce46324237dd5 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 3 Dec 2020 00:09:51 -0500 Subject: [PATCH 091/113] Make it clear in the version string that this is unreleased --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4bd86f4..19554d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kochab" -version = "0.1.0" +version = "0.1.0-unreleased" description = "Ergonomic Gemini SDK" authors = ["Emii Tatsuo ", "panicbit "] license = "Hippocratic-2.1" From 73da764a8f4d0c71ac274b6cfe42d46ab87e836e Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 3 Dec 2020 00:52:25 -0500 Subject: [PATCH 092/113] Switched back to using raw gemini as the default mode --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 19554d0..94452ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ readme = "README.md" include = ["src/**", "Cargo.*", "CHANGELOG.md", "LICENSE*", "README.md"] [features] -default = ["scgi_srv"] +default = ["certgen"] user_management = ["sled", "bincode", "serde/derive", "crc32fast", "lazy_static"] user_management_advanced = ["rust-argon2", "user_management"] user_management_routes = ["user_management"] From fa8adbd2652b4696dfe631a079c8bfb60d4de649 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 3 Dec 2020 00:53:05 -0500 Subject: [PATCH 093/113] Started writing feature docs [UNFINISHED] --- src/lib.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index ebc25c3..829f354 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,67 @@ +//! Kochab is an ergonomic and intuitive library for quickly building highly functional +//! and advanced Gemini applications on either SCGI or raw Gemini. +//! +//! Originally based on [northstar](https://crates.io/crates/northstar), though now far +//! divereged, kochab offers many features to help you build your application without any +//! of the boilerplate or counterintuitive shenanigans. Kochab centers around it's many +//! feature flags, which let you pick out exactly the features you need to build your +//! library, while leaving out features you don't need to keep your application +//! bloat-free. +//! +//! Another central feature of kochab is its multi-protocol abstraction. An application +//! built using kochab can easily compile either as a gemini application or as a SCGI +//! script using only a single feature flag. +//! +//! ## Features +//! +//! Kochab offers a wide array of features, so don't get overwhelmed. By default, you +//! start off with only the `gemini_srv` feature, and you're able to add on more features +//! as you need them. All of kochab's features are documented below. +//! +//! * `ratelimiting` - The ratelimiting feature adds in the ability to limit how often +//! users can access certain areas of an application. This is primarily configured using +//! the [`Server::ratelimit()`] method. +//! +//! * `servedir` - Adds in utilities for serving files & directories from the disk at +//! runtime. The easiest way to use this is to pass a [`PathBuf`] to the +//! [`Server::add_route()`] method, which will either serve a directory or a single file. +//! Files and directories can also be served using the methods in the [`util`] module. +//! +//! * `user_management` - Adds in tools to manage users using a certificate authentication +//! system. The user management suite is one of kocab's biggest features. When active, +//! kochab will maintain a database of registered users, linking each to a certificate. +//! Users also have custom data associated with them, which can be retrieved and modified +//! by the application. +//! +//! * `user_management_advanced` - Allows users to set a password and add additional +//! certificates. Without this feature, one certificate can only have one linked account, +//! unless you manually implement an authentication system. With this feature, kochab +//! will use argon2 to hash and check user passwords, and store user passwords alongside +//! the other user data +//! +//! * `user_management_routes` - The user management routes feature automates much of the +//! hard work of connecting the tools provided with the other two features with endpoints +//! that the user connects to. Kochab will manage all requests to the `/account` route, +//! and create pages to allow users to create an account, link new certificates, or +//! change/set their password. This also adds the ability to set an authenticated route, +//! which will automatically prompt the user to sign in, and give your handler access to +//! the user's data with no added work. This can be used with either the +//! `user_management_advanced` feature, or just the basic `user_management` feature +//! +//! * `certgen` - Enables automatically generating TLS certificates. Since only servers +//! directly using the gemini protocol need TLS certificates, this implies `gemini_srv`, +//! and should not be used with `scgi_srv`. By default, kochab will try to generate a +//! certificate by prompting the user in stdin/stdout, but this behavior can be customized +//! using [`Server::set_certificate_generation_mode()`]. +//! +//! * `dashmap` - Enables some minor optimizations within the `user_management_routes` +//! feature. Automatically enabled by `ratelimiting`. +//! +//! * `gemini_srv`/`scgi_srv` - Switches between serving content using SCGI and serving +//! content as a raw gemini server. One and only one of these features must be enabled, +//! and compilation will fail if both are enabled. See below for more information. + + #[macro_use] extern crate log; #[cfg(all(feature = "gemini_srv", feature = "scgi_srv"))] From dc71a2f2cf540c963db62c615bf29d1efe1da9b0 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 3 Dec 2020 04:35:20 -0500 Subject: [PATCH 094/113] Add .gemgit --- .gemgit | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .gemgit diff --git a/.gemgit b/.gemgit new file mode 100644 index 0000000..2500d09 --- /dev/null +++ b/.gemgit @@ -0,0 +1,4 @@ +name = "Kochab" +url = "https://gitlab.com/Alch_Emi/kochab.git" +desc = "Kochab is an ergonomic and intuitive library for quickly building highly functional and advanced Gemini applications on either SCGI or raw Gemini." +readme = "README.md" From e55f5c675b8669b23dc7b1856148990d9b261c72 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 3 Dec 2020 05:14:09 -0500 Subject: [PATCH 095/113] Rename gencert.rs -> cert.rs, move in a bunch of certificate functions. Make the cert.rs module private. Refactor refactor refactor!!! It's 5:14am and I HAVENT SLEPT THAT'S RIGHT IT'S INSOMNIA HOURS THEYDIES AND GENDERFUCKS!!! --- src/{gencert.rs => cert.rs} | 135 +++++++++++++++++++++++++++++++++--- src/lib.rs | 130 +++------------------------------- 2 files changed, 135 insertions(+), 130 deletions(-) rename src/{gencert.rs => cert.rs} (56%) diff --git a/src/gencert.rs b/src/cert.rs similarity index 56% rename from src/gencert.rs rename to src/cert.rs index 33ff027..8aac2ee 100644 --- a/src/gencert.rs +++ b/src/cert.rs @@ -6,16 +6,29 @@ //! methods on it or anything. //! //! [`Server::set_certificate_generation_mode()`]: crate::Server::set_certificate_generation_mode() -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, Context, Result, ensure}; +#[cfg(feature = "certgen")] +use anyhow::bail; + use rustls::ServerConfig; -use std::fs; -use std::io::{stdin, stdout, Write}; -use std::path::Path; +use std::{path::PathBuf, sync::Arc}; +#[cfg(feature = "certgen")] +use std::{ + fs, + io::{stdin, stdout, Write}, + path::Path, +}; + +use rustls::internal::msgs::handshake::DigitallySignedStruct; +use tokio_rustls::rustls; +use rustls::*; #[derive(Clone, Debug)] -/// The mode to use for determining the domains to use for a new certificate. Only -/// applies to [`CertGenMode::gencert()`]. +#[cfg(feature = "certgen")] +/// The mode to use for determining the domains to use for a new certificate. +/// +/// Used to configure a [`Server`] using [`Server::set_certificate_generation_mode()`] pub enum CertGenMode { /// Do not generate any certificates. Error if not available. @@ -28,6 +41,7 @@ pub enum CertGenMode { Interactive, } +#[cfg(feature = "certgen")] impl CertGenMode { /// Generate a new self-signed certificate /// @@ -83,7 +97,7 @@ impl CertGenMode { /// /// Returns an error if a certificate is not found **and** cannot be generated. pub fn load_or_generate(self, to: &mut ServerConfig, cert: impl AsRef, key: impl AsRef) -> Result<()> { - match (crate::load_cert_chain(&cert.as_ref().into()), crate::load_key(&key.as_ref().into())) { + match (load_cert_chain(&cert.as_ref().into()), load_key(&key.as_ref().into())) { (Ok(cert_chain), Ok(key)) => { to.set_single_cert(cert_chain, key) .context("Failed to use loaded TLS certificate")?; @@ -101,6 +115,7 @@ impl CertGenMode { } } +#[cfg(feature = "certgen")] /// Attempt to get domains by prompting the user /// /// Guaranteed to return at least one domain. The user is provided `localhost` as a @@ -108,7 +123,7 @@ impl CertGenMode { /// /// ## Panics /// Panics if reading from stdin or writing to stdout returns an error. -pub fn prompt_domains() -> Vec { +fn prompt_domains() -> Vec { let mut domains = Vec::with_capacity(1); let mut input = String::with_capacity(8); println!("Now generating self-signed certificate..."); @@ -131,3 +146,107 @@ pub fn prompt_domains() -> Vec { } } } + +pub fn tls_config( + cert_path: &PathBuf, + key_path: &PathBuf, + #[cfg(feature = "certgen")] + mode: CertGenMode, +) -> Result> { + let mut config = ServerConfig::new(AllowAnonOrSelfsignedClient::new()); + + #[cfg(feature = "certgen")] + mode.load_or_generate(&mut config, cert_path, key_path)?; + + #[cfg(not(feature = "certgen"))] { + let cert_chain = load_cert_chain(cert_path) + .context("Failed to load TLS certificate")?; + let key = load_key(key_path) + .context("Failed to load TLS key")?; + config.set_single_cert(cert_chain, key) + .context("Failed to use loaded TLS certificate")?; + } + + Ok(config.into()) +} + +fn load_cert_chain(cert_path: &PathBuf) -> Result> { + let certs = std::fs::File::open(cert_path) + .with_context(|| format!("Failed to open `{:?}`", cert_path))?; + let mut certs = std::io::BufReader::new(certs); + let certs = rustls::internal::pemfile::certs(&mut certs) + .map_err(|_| anyhow!("failed to load certs `{:?}`", cert_path))?; + + Ok(certs) +} + +fn load_key(key_path: &PathBuf) -> Result { + let keys = std::fs::File::open(key_path) + .with_context(|| format!("Failed to open `{:?}`", key_path))?; + let mut keys = std::io::BufReader::new(keys); + let mut keys = rustls::internal::pemfile::pkcs8_private_keys(&mut keys) + .map_err(|_| anyhow!("failed to load key `{:?}`", key_path))?; + + ensure!(!keys.is_empty(), "no key found"); + + let key = keys.swap_remove(0); + + Ok(key) +} + +/// A client cert verifier that accepts all connections +/// +/// Unfortunately, rustls doesn't provide a ClientCertVerifier that accepts self-signed +/// certificates, so we need to implement this ourselves. +struct AllowAnonOrSelfsignedClient { } + +impl AllowAnonOrSelfsignedClient { + + /// Create a new verifier + fn new() -> Arc { + Arc::new(Self {}) + } + +} + +impl ClientCertVerifier for AllowAnonOrSelfsignedClient { + + fn client_auth_root_subjects( + &self, + _: Option<&webpki::DNSName> + ) -> Option { + Some(Vec::new()) + } + + fn client_auth_mandatory(&self, _sni: Option<&webpki::DNSName>) -> Option { + Some(false) + } + + // the below methods are a hack until webpki doesn't break with certain certs + + fn verify_client_cert( + &self, + _: &[Certificate], + _: Option<&webpki::DNSName> + ) -> Result { + Ok(ClientCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &Certificate, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &Certificate, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 829f354..31d020d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,19 +102,15 @@ use tokio::{ }; #[cfg(feature = "ratelimiting")] use tokio::time::interval; -#[cfg(feature = "gemini_srv")] -use rustls::ClientCertVerifier; -#[cfg(feature = "gemini_srv")] -use rustls::internal::msgs::handshake::DigitallySignedStruct; -#[cfg(feature = "gemini_srv")] -use tokio_rustls::{rustls, TlsAcceptor}; -#[cfg(feature = "gemini_srv")] -use rustls::*; use anyhow::*; use crate::util::opt_timeout; use routing::RoutingNode; #[cfg(feature = "ratelimiting")] use ratelimiting::RateLimiter; +#[cfg(feature = "gemini_srv")] +use tokio_rustls::TlsAcceptor; +#[cfg(feature = "gemini_srv")] +use rustls::Session; pub mod types; pub mod util; @@ -124,13 +120,13 @@ pub mod handling; pub mod ratelimiting; #[cfg(feature = "user_management")] pub mod user_management; -#[cfg(feature = "certgen")] -pub mod gencert; +#[cfg(feature = "gemini_srv")] +mod cert; #[cfg(feature="user_management")] use user_management::UserManager; #[cfg(feature = "certgen")] -use gencert::CertGenMode; +pub use cert::CertGenMode; pub use uriparse as uri; pub use types::*; @@ -723,7 +719,7 @@ impl Server { fn build(mut self) -> Result { #[cfg(feature = "gemini_srv")] - let config = tls_config( + let config = cert::tls_config( &self.cert_path, &self.key_path, #[cfg(feature="certgen")] @@ -825,115 +821,5 @@ async fn prune_ratelimit_log(rate_limits: Arc>>) } } -#[cfg(feature = "gemini_srv")] -fn tls_config( - cert_path: &PathBuf, - key_path: &PathBuf, - #[cfg(feature = "certgen")] - mode: CertGenMode, -) -> Result> { - let mut config = ServerConfig::new(AllowAnonOrSelfsignedClient::new()); - - #[cfg(feature = "certgen")] - mode.load_or_generate(&mut config, cert_path, key_path)?; - - #[cfg(not(feature = "certgen"))] { - let cert_chain = load_cert_chain(cert_path) - .context("Failed to load TLS certificate")?; - let key = load_key(key_path) - .context("Failed to load TLS key")?; - config.set_single_cert(cert_chain, key) - .context("Failed to use loaded TLS certificate")?; - } - - Ok(config.into()) -} - -#[cfg(feature = "gemini_srv")] -fn load_cert_chain(cert_path: &PathBuf) -> Result> { - let certs = std::fs::File::open(cert_path) - .with_context(|| format!("Failed to open `{:?}`", cert_path))?; - let mut certs = std::io::BufReader::new(certs); - let certs = rustls::internal::pemfile::certs(&mut certs) - .map_err(|_| anyhow!("failed to load certs `{:?}`", cert_path))?; - - Ok(certs) -} - -#[cfg(feature = "gemini_srv")] -fn load_key(key_path: &PathBuf) -> Result { - let keys = std::fs::File::open(key_path) - .with_context(|| format!("Failed to open `{:?}`", key_path))?; - let mut keys = std::io::BufReader::new(keys); - let mut keys = rustls::internal::pemfile::pkcs8_private_keys(&mut keys) - .map_err(|_| anyhow!("failed to load key `{:?}`", key_path))?; - - ensure!(!keys.is_empty(), "no key found"); - - let key = keys.swap_remove(0); - - Ok(key) -} - -#[cfg(feature = "gemini_srv")] -/// A client cert verifier that accepts all connections -/// -/// Unfortunately, rustls doesn't provide a ClientCertVerifier that accepts self-signed -/// certificates, so we need to implement this ourselves. -struct AllowAnonOrSelfsignedClient { } - -#[cfg(feature = "gemini_srv")] -impl AllowAnonOrSelfsignedClient { - - /// Create a new verifier - fn new() -> Arc { - Arc::new(Self {}) - } - -} - -#[cfg(feature = "gemini_srv")] -impl ClientCertVerifier for AllowAnonOrSelfsignedClient { - - fn client_auth_root_subjects( - &self, - _: Option<&webpki::DNSName> - ) -> Option { - Some(Vec::new()) - } - - fn client_auth_mandatory(&self, _sni: Option<&webpki::DNSName>) -> Option { - Some(false) - } - - // the below methods are a hack until webpki doesn't break with certain certs - - fn verify_client_cert( - &self, - _: &[Certificate], - _: Option<&webpki::DNSName> - ) -> Result { - Ok(ClientCertVerified::assertion()) - } - - fn verify_tls12_signature( - &self, - _message: &[u8], - _cert: &Certificate, - _dss: &DigitallySignedStruct, - ) -> Result { - Ok(HandshakeSignatureValid::assertion()) - } - - fn verify_tls13_signature( - &self, - _message: &[u8], - _cert: &Certificate, - _dss: &DigitallySignedStruct, - ) -> Result { - Ok(HandshakeSignatureValid::assertion()) - } -} - #[cfg(feature = "ratelimiting")] enum Never {} From d71c3f952d3f3f6da3809244afc645b378a8871e Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 3 Dec 2020 05:20:16 -0500 Subject: [PATCH 096/113] Reduce visibility of some methods in CertGenMode, fix docs --- src/cert.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/cert.rs b/src/cert.rs index 8aac2ee..b027e92 100644 --- a/src/cert.rs +++ b/src/cert.rs @@ -28,7 +28,10 @@ use rustls::*; #[cfg(feature = "certgen")] /// The mode to use for determining the domains to use for a new certificate. /// -/// Used to configure a [`Server`] using [`Server::set_certificate_generation_mode()`] +/// Used to configure a [`Server`] using [`set_certificate_generation_mode()`] +/// +/// [`Server`]: crate::Server +/// [`set_certificate_generation_mode()`]: crate::Server::set_certificate_generation_mode pub enum CertGenMode { /// Do not generate any certificates. Error if not available. @@ -49,10 +52,15 @@ impl CertGenMode { /// provided paths. The paths provided should be paths to non-existant files which /// the program has access to write to. /// + /// With very rare exceptions, end users should not ever need to call this method. + /// Instead, just pass the [`CertGenMode`] to [`set_certificate_generation_mode()`] + /// /// ## Errors /// /// Returns an error if [`CertGenMode::None`], or if there is an error generating the /// certificate, or writing to either of the provided files. + /// + /// [`set_certificate_generation_mode()`]: crate::Server::set_certificate_generation_mode pub fn gencert(self, cert: impl AsRef, key: impl AsRef) -> Result { let (domains, interactive) = match self { Self::None => bail!("Automatic certificate generation disabled"), @@ -96,7 +104,7 @@ impl CertGenMode { /// ## Errors /// /// Returns an error if a certificate is not found **and** cannot be generated. - pub fn load_or_generate(self, to: &mut ServerConfig, cert: impl AsRef, key: impl AsRef) -> Result<()> { + fn load_or_generate(self, to: &mut ServerConfig, cert: impl AsRef, key: impl AsRef) -> Result<()> { match (load_cert_chain(&cert.as_ref().into()), load_key(&key.as_ref().into())) { (Ok(cert_chain), Ok(key)) => { to.set_single_cert(cert_chain, key) From fb357b59ebb00f113a2c43db67aad6f999bdc51d Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 3 Dec 2020 08:10:50 -0500 Subject: [PATCH 097/113] Support base64 encoded certificate hashes, remove ring as mandatory dep for scgi_srv Removing ring also means switching out some of the code around user password hashes so that's what that is, if you're wondering. Also, the sunrise today was absolutely beautiful. I haven't been awake for a sunrise in a long time, so I guess that's one upside to not sleeping. Like, melencholy sunsets get a lot of love, but man, nothing fills you with hope and positivity like a sunrise. --- Cargo.toml | 7 +++--- src/lib.rs | 8 +++++++ src/types/request.rs | 47 ++++++++++++++++++++++++++++++------- src/user_management/user.rs | 38 ++++++++++++++++++++++++++---- 4 files changed, 85 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 94452ce..142d3d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,8 +19,8 @@ user_management_routes = ["user_management"] serve_dir = ["mime_guess", "tokio/fs"] ratelimiting = ["dashmap"] certgen = ["rcgen", "gemini_srv"] -gemini_srv = ["tokio-rustls", "webpki", "rustls"] -scgi_srv = [] +gemini_srv = ["tokio-rustls", "webpki", "rustls", "ring"] +scgi_srv = ["base64"] [dependencies] anyhow = "1.0.33" @@ -28,7 +28,8 @@ tokio = { version = "0.3.1", features = ["io-util","net","time", "rt"] } uriparse = "0.6.3" percent-encoding = "2.1.0" log = "0.4.11" -ring = "0.16.15" +ring = { version = "0.16.15", optional = true } +base64 = { version = "0.13.0", optional = true } lazy_static = { version = "1.4.0", optional = true } rustls = { version = "0.18.1", features = ["dangerous_configuration"], optional = true} webpki = { version = "0.21.0", optional = true} diff --git a/src/lib.rs b/src/lib.rs index 31d020d..2ce7630 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,6 +57,14 @@ //! * `dashmap` - Enables some minor optimizations within the `user_management_routes` //! feature. Automatically enabled by `ratelimiting`. //! +//! * `ring` - When using `user_management_advanced` with `scgi_srv`, salts are calculated +//! based off of a simple PRNG and the system time. This should be plenty secure enough, +//! especially since we're using a good number argon2 rounds, but for bonus paranoia +//! points, you can add ring as a dependency to source secure random. This is enabled +//! automatically on `gemini_srv`, since `ring` is added as a dependency for certificate +//! processing. When not using the `user_management_advanced` feature, this does nothing +//! but increase your build time & size. +//! //! * `gemini_srv`/`scgi_srv` - Switches between serving content using SCGI and serving //! content as a raw gemini server. One and only one of these features must be enabled, //! and compilation will fail if both are enabled. See below for more information. diff --git a/src/types/request.rs b/src/types/request.rs index 34fddf9..f3e3801 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -1,4 +1,5 @@ use std::ops; +#[cfg(feature = "gemini_srv")] use std::convert::TryInto; #[cfg(feature = "scgi_srv")] use std::{ @@ -41,6 +42,7 @@ impl Request { manager: UserManager, ) -> Result { #[cfg(feature = "scgi_srv")] + #[allow(clippy::or_fun_call)] // Lay off it's a macro let (mut uri, certificate, script_path) = ( URIReference::try_from( format!( @@ -55,14 +57,8 @@ impl Request { ) .context("Request URI is invalid")? .into_owned(), - headers.get("TLS_CLIENT_HASH") - .map(|hsh| { - ring::test::from_hex(hsh.as_str()) - .expect("Received invalid certificate fingerprint from upstream") - .as_slice() - .try_into() - .expect("Received certificate fingerprint of invalid lenght from upstream") - }), + headers.get("TLS_CLIENT_HASH").map(hash_decode) + .ok_or(anyhow!("Received malformed TLS client hash from upstream. Expected 256 bit hex or b64 encoded"))?, headers.get("SCRIPT_PATH") .or_else(|| headers.get("SCRIPT_NAME")) .cloned() @@ -245,6 +241,41 @@ impl Request { } } +#[allow(clippy::ptr_arg)] // This is a single use function that expects a &String +/// Attempt to decode a 256 bit hash +/// +/// Will attempt to decode first as hexadecimal, and then as base64. If both fail, return +/// [`None`] +fn hash_decode(hash: &String) -> Option<[u8; 32]> { + let mut buffer = [0u8; 32]; + if hash.len() == 64 { // Looks like a hex + // Lifted (lightly modified) from ring::test::from_hex + for (i, digits) in hash.as_bytes().chunks(2).enumerate() { + let hi = from_hex_digit(digits[0])?; + let lo = from_hex_digit(digits[1])?; + buffer[i] = (hi * 0x10) | lo; + } + Some(buffer) + } else if hash.len() == 44 { // Look like base64 + base64::decode_config_slice(hash, base64::STANDARD, &mut buffer).ok()?; + Some(buffer) + } else { + None + } +} + +/// Attempt to decode a hex encoded nibble to u8 +/// +/// Returns [`None`] if not a valid hex character +fn from_hex_digit(d: u8) -> Option { + match d { + b'0'..=b'9' => Some(d - b'0'), + b'a'..=b'f' => Some(d - b'a' + 10u8), + b'A'..=b'F' => Some(d - b'A' + 10u8), + _ => None, + } +} + impl ops::Deref for Request { type Target = URIReference<'static>; diff --git a/src/user_management/user.rs b/src/user_management/user.rs index 043c1ff..bdfa85a 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -16,6 +16,9 @@ use serde::{Deserialize, Serialize, de::DeserializeOwned}; use sled::Transactional; +#[cfg(not(feature = "ring"))] +use std::time::{SystemTime, UNIX_EPOCH}; + use crate::user_management::UserManager; use crate::user_management::Result; @@ -32,7 +35,7 @@ const ARGON2_CONFIG: argon2::Config = argon2::Config { version: argon2::Version::Version13, }; -#[cfg(feature = "user_management_advanced")] +#[cfg(all(feature = "user_management_advanced", feature = "ring"))] lazy_static::lazy_static! { static ref RANDOM: ring::rand::SystemRandom = ring::rand::SystemRandom::new(); } @@ -287,9 +290,24 @@ impl RegisteredUser { &mut self, password: impl AsRef<[u8]>, ) -> Result<()> { - let salt: [u8; 32] = ring::rand::generate(&*RANDOM) - .expect("Error generating random salt") - .expose(); + #[cfg_attr(feature = "ring", allow(unused_mut))] + let mut salt: [u8; 32]; + + // For a simple salt, system time nanos and a bit of PCG is plenty secure enough, + // but if we have ring anyway, may as well use it + #[cfg(feature = "ring")] { + salt = ring::rand::generate(&*RANDOM) + .expect("Error generating random salt") + .expose(); + } + + #[cfg(not(feature = "ring"))] { + let mut random = (SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos() | 0xffff) as u16; + random = random.wrapping_mul(0xd09d); + salt = [0; 32]; + for byte in salt.as_mut() { *byte = pcg8(&mut random) } + } + self.inner.pass_hash = Some(( argon2::hash_raw( password.as_ref(), @@ -396,3 +414,15 @@ impl AsMut for RegisteredUser< self.mut_data() } } + +#[cfg(all(feature = "user_management_advanced", not(feature = "ring")))] +/// Inexpensive but low quality random +fn pcg8(state: &mut u16) -> u8 { + const MUL: u16 = 0xfb85; + const ADD: u16 = 0xfabb; + let mut x = *state; + *state = state.wrapping_mul(MUL).wrapping_add(ADD); + let count = x >> 13; + x ^= x >> 5; + ((x >> 5) as u8).rotate_right(count as u32) +} From d8d15d8f72fe8420073a8576f5683e34b0d382ac Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 3 Dec 2020 08:32:06 -0500 Subject: [PATCH 098/113] Add tolerance for REMOTE_ADDR without a port --- src/lib.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2ce7630..882aa37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -238,13 +238,14 @@ impl ServerInner { .ip() } #[cfg(feature = "scgi_srv")] { - SocketAddr::from_str( - request.headers() - .get("REMOTE_ADDR") - .ok_or(ParseError::Malformed("REMOTE_ADDR header not received"))? - .as_str() - ).context("Received malformed IP address from upstream")? - .ip() + let remote = request.headers() + .get("REMOTE_ADDR") + .ok_or(ParseError::Malformed("REMOTE_ADDR header not received"))? + .as_str(); + SocketAddr::from_str(remote) + .map(|a| a.ip()) + .or_else(|_| std::net::IpAddr::from_str(remote)) + .context("Received malformed IP address from upstream")? } }; From cb9b3ea167f2b8a19369434eddd229009289c4e5 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 3 Dec 2020 10:47:33 -0500 Subject: [PATCH 099/113] Fix crash on no certificate provided --- src/types/request.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/types/request.rs b/src/types/request.rs index f3e3801..d14d052 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -57,8 +57,11 @@ impl Request { ) .context("Request URI is invalid")? .into_owned(), - headers.get("TLS_CLIENT_HASH").map(hash_decode) - .ok_or(anyhow!("Received malformed TLS client hash from upstream. Expected 256 bit hex or b64 encoded"))?, + match headers.get("TLS_CLIENT_HASH").map(hash_decode) { + Some(maybe_hash @ Some(_)) => maybe_hash, + Some(None) => bail!("Received malformed TLS client hash from upstream. Expected 256 bit hex or b64 encoded"), + None => None, + }, headers.get("SCRIPT_PATH") .or_else(|| headers.get("SCRIPT_NAME")) .cloned() From f592ecf73b81fcae68590e4d9e07e77a77513492 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 3 Dec 2020 15:04:12 -0500 Subject: [PATCH 100/113] Move opt_timeout to lib.rs --- src/lib.rs | 10 +++++++++- src/util.rs | 7 ------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 882aa37..afa3698 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,6 +81,7 @@ compile_error!("Please enable at least one of either the `gemini_srv` or `scgi_s use std::{ sync::Arc, time::Duration, + future::Future, }; #[cfg(feature = "gemini_srv")] use std::convert::TryFrom; @@ -99,6 +100,7 @@ use tokio::{ io::BufReader, net::TcpListener, net::ToSocketAddrs, + time, prelude::*, }; #[cfg(feature = "scgi_srv")] @@ -111,7 +113,6 @@ use tokio::{ #[cfg(feature = "ratelimiting")] use tokio::time::interval; use anyhow::*; -use crate::util::opt_timeout; use routing::RoutingNode; #[cfg(feature = "ratelimiting")] use ratelimiting::RateLimiter; @@ -832,3 +833,10 @@ async fn prune_ratelimit_log(rate_limits: Arc>>) #[cfg(feature = "ratelimiting")] enum Never {} + +async fn opt_timeout(duration: Option, future: impl Future) -> Result { + match duration { + Some(duration) => time::timeout(duration, future).await, + None => Ok(future.await), + } +} diff --git a/src/util.rs b/src/util.rs index 4ac9062..d3166dd 100644 --- a/src/util.rs +++ b/src/util.rs @@ -173,10 +173,3 @@ where C: AsRef + Into, T: ToOwned + ?Sized, {} - -pub(crate) async fn opt_timeout(duration: Option, future: impl Future) -> Result { - match duration { - Some(duration) => time::timeout(duration, future).await, - None => Ok(future.await), - } -} From ae247312f777b7558c8b16a79e207efd53fd5b86 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sat, 5 Dec 2020 09:54:05 -0500 Subject: [PATCH 101/113] Fix `gemini_srv` with more conditional comp --- src/types/request.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types/request.rs b/src/types/request.rs index d14d052..2a98553 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -245,6 +245,7 @@ impl Request { } #[allow(clippy::ptr_arg)] // This is a single use function that expects a &String +#[cfg(feature = "scgi_srv")] /// Attempt to decode a 256 bit hash /// /// Will attempt to decode first as hexadecimal, and then as base64. If both fail, return @@ -267,6 +268,7 @@ fn hash_decode(hash: &String) -> Option<[u8; 32]> { } } +#[cfg(feature = "scgi_srv")] /// Attempt to decode a hex encoded nibble to u8 /// /// Returns [`None`] if not a valid hex character From 680c04abe4950600091bbd7affbaafe3c64cb560 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sat, 5 Dec 2020 10:30:13 -0500 Subject: [PATCH 102/113] Add documentation about SCGI --- src/lib.rs | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index afa3698..7f2dd10 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ //! and advanced Gemini applications on either SCGI or raw Gemini. //! //! Originally based on [northstar](https://crates.io/crates/northstar), though now far -//! divereged, kochab offers many features to help you build your application without any +//! diverged, kochab offers many features to help you build your application without any //! of the boilerplate or counterintuitive shenanigans. Kochab centers around it's many //! feature flags, which let you pick out exactly the features you need to build your //! library, while leaving out features you don't need to keep your application @@ -22,7 +22,7 @@ //! users can access certain areas of an application. This is primarily configured using //! the [`Server::ratelimit()`] method. //! -//! * `servedir` - Adds in utilities for serving files & directories from the disk at +//! * `serve_dir` - Adds in utilities for serving files & directories from the disk at //! runtime. The easiest way to use this is to pass a [`PathBuf`] to the //! [`Server::add_route()`] method, which will either serve a directory or a single file. //! Files and directories can also be served using the methods in the [`util`] module. @@ -68,7 +68,114 @@ //! * `gemini_srv`/`scgi_srv` - Switches between serving content using SCGI and serving //! content as a raw gemini server. One and only one of these features must be enabled, //! and compilation will fail if both are enabled. See below for more information. - +//! +//! ## Gemini & SCGI Modes +//! +//! **It is highly recommended that you read this section, *especially* if you don't know +//! what SCGI is.** +//! +//! Central to kochab's repertoire is the ability to serve either content either through +//! raw Gemini or through a reverse proxy with SCGI. This can be accomplished easily +//! using a single feature flag. But first: +//! +//! ### What's SCGI and why should I be using it? +//! +//! You're probably familiar with the Gemini protocol, and how servers serve things on it. +//! Gemini is easy to serve and simple to work with. You probably went into this project +//! expecting to serve content directly through the Gemini protocol. So what's this about +//! SCGI, and why should you bother using it? +//! +//! The problem that SCGI solves is that it is very difficult to have multiple servers on +//! one domain with Gemini. For example, if you wanted to serve your blog on `/blog.gmi`, +//! but wanted to have an app running on `/app`, you would most likely have to use SCGI. +//! +//! SCGI has two parts: A main gemini server (called the SCGI client) that handles TLS +//! for incoming connections and either serves some static content, runs some other +//! handler, or passes it off to the SCGI server, which is what you'd be writing. The +//! SCGI server doesn't directly interact with the client, but gets plenty of information +//! about the connection from the server, like what path the server is being served on +//! (like "/app"), the end user's IP, and the certificate fingerprint being used, if +//! applicable. +//! +//! Because SCGI servers don't need to bother with TLS, compiling your app as SCGI will +//! also be fairly faster, since kochab doesn't need to bring in `rustls` or any +//! TLS certificate generation libraries. +//! +//! This doesn't necessarily mean that SCGI is for every app, but if you suspect that a +//! user might ever need to run your app on a path other than the main one, or may want to +//! serve static files alongside your site, I encourage you to seriously consider it. +//! +//! ### But I don't want to bother learning a new protocol ![blobcat-pout] +//! +//! You don't have to! Kochab handles everything about it, thanks to the magic of +//! abstraction! The only thing you have to change are the feature flags in your +//! `Config.toml`. In fact, you could even expose the feature flag so that users can +//! compile your crate as either a Gemini or SCGI server without needing to write any +//! conditional compilation! +//! +//! ### Getting started +//! +//! **Updating your feature flags** +//! +//! By default, Kochab serves content over raw gemini, to make it easier for new users to +//! jump right into using the library. This is done using the on-by-default `gemini_srv` +//! feature flag. To switch to SCGI, we want to switch off `gemini_srv` and switch on +//! `scgi_srv`. +//! +//! ```toml +//! [dependencies.kochab] +//! git = "https://gitlab.com/Alch_Emi/kochab.git" +//! branch = "stable" +//! default-features = false +//! features = ["scgi_srv"] # and any other features you might need +//! ``` +//! +//! **Testing with a minimal SCGI client** +//! +//! To give your code a run, you'll need a server to handle Gemini requests and pass them +//! off to your SCGI server. There's a few Gemini servers out there with SCGI support, +//! but if you're just interested in giving your code a quick run, I'd recommend +//! mollybrown, which has very good SCGI support and is super easy to set up +//! +//! You can grab a copy of molly brown from [tildegit.org/solderpunk/molly-brown][1]. +//! +//! Once you have it, you can find a super simple configuration file [here][2], and then +//! just run +//! +//! ```sh +//! molly-brown -c molly-brown.conf +//! ``` +//! +//! Now, when you run your code, you can connect to `localhost`, and molly brown will +//! connect to your SCGI server and forward the response on to your Gemini client. +//! +//! **Rewriting Paths** +//! +//! One important difference about writing code for an SCGI server is that your app might +//! be served on a path that's not the base path. If this happens, you suddenly have a +//! distinction between an absolute link for your app, and an absolute link for the gemini +//! server. +//! +//! For example, if an app that's being served on `/app` has a link to `/path`, this could +//! be either: +//! +//! * Meant to be handled by the app by the route at `/path`, which would be `/app/path` +//! for the parent Gemini server, or +//! * Meant to link to some content on the parent gemini server, at `/path`, which would +//! mean linking to a url your app doesn't control +//! +//! Most of the time, you want to do the first one. Thankfully, Kochab makes rewriting +//! links relative to the base of your app super easy. In most cases, all you need is to +//! add a single line to your Server builder pattern. +//! +//! For more information, see [`Server::set_autorewrite()`]. +//! +//! [1]: https://tildegit.org/solderpunk/molly-brown +//! [2]: https://gitlab.com/Alch_Emi/kochab/-/raw/244fd251/molly-brown.conf +//! [blobcat-pout]: https://the-apothecary.club/_matrix/media/r0/thumbnail/the-apothecary.club/10a406405a5bcd699a5328259133bfd9260320a6?height=99&width=20 ":blobcat-pout:" +//! #[macro_use] extern crate log; From 9bc1f317c5ec8aa64ff4481110a4e55a8c5fccca Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sat, 5 Dec 2020 11:05:04 -0500 Subject: [PATCH 103/113] Fix bug where autorewrite was always on, respond 50 and log instead of panic on rewrite error --- src/lib.rs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7f2dd10..b61acbf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -390,6 +390,7 @@ impl ServerInner { request.set_cert(client_cert); } + #[cfg_attr(feature = "gemini_srv", allow(unused_mut))] // Used for scgi_srv only let mut response = if let Some((trailing, handler)) = self.routes.match_request(&request) { request.set_trailing(trailing); handler.handle(request.clone()).await @@ -397,12 +398,25 @@ impl ServerInner { Response::not_found() }; - match response.rewrite_all(&request).await { - Ok(true) => { /* all is well */ } - Ok(false) => panic!("Upstream did not include SCRIPT_PATH or SCRIPT_NAME"), - Err(e) => { - error!("Error reading text/gemini file from Response reader: {}", e); - response = Response::not_found(); + #[cfg(feature = "scgi_srv")] // No point running noop code + if self.autorewrite { + match response.rewrite_all(&request).await { + Ok(true) => { /* all is well */ } + Ok(false) => { + error!( + concat!( + "Upstream did not include SCRIPT_PATH or SCRIPT_NAME, refusing to", + " serve any text/gemini content with absolute links. It's most", + " likely that the proxy server you're using doesn't correctly", + " or completely implement the SCGI specification.", + ) + ); + response = Response::temporary_failure("Server misconfigured"); + }, + Err(e) => { + error!("Error reading text/gemini file from Response reader: {}", e); + response = Response::not_found(); + } } } From 7add331e0bee8587717fe57f65beb514ca2e03e3 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sat, 5 Dec 2020 11:27:30 -0500 Subject: [PATCH 104/113] Made the link rewriting docs *way* better --- src/lib.rs | 66 +++++++++++++++++++++++++++++++++++++------ src/types/request.rs | 12 ++++++-- src/types/response.rs | 15 ++++++---- 3 files changed, 77 insertions(+), 16 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b61acbf..cbe2f4c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -832,17 +832,65 @@ impl Server { /// Enable or disable autorewrite /// - /// Autorewrite automatically detects links in responses being sent out through kochab - /// and rewrites them to match the base of the script. For example, if the script is - /// mounted on "/app", any links to "/page" would be rewritten as "/app/page". This - /// does nothing when in `gemini_srv` mode. + /// Many times, an app will served alongside other apps all on one domain. For + /// example: + /// * `gemini://example.com/gemlog/` might be some static content from a gemlog + /// handled by the gemini server + /// * `gemini://example.com/app/` might be where an SCGI app is hosted + /// * `gemini://example.com/` might be some static landing page linking to both the + /// gemlog and the app /// - /// **Note:** If you are serving *very long* `text/gemini` files using `serve_dir`, - /// then you should avoid setting this to true. Additionally, if using this option, - /// do not try to rewrite your links using [`Request::rewrite_path`] or - /// [`Response::rewrite_all`], as this will apply the rewrite twice. + /// If the user sent a request to `/app/path` in this case, the app would see it as + /// `/path` automatically, so the app doesn't need to care if it's mounted on `/` or + /// `/app`, because it can handle any request to `/path` the same. /// - /// For more information about rewriting, see [`Request::rewrite_path`]. + /// The problem comes when the app needs to write a link. If an app were to send a + /// link to `/path`, expecting the user to come back with a request to `/path`, it + /// might be surprised to see that the user never arrives, and the user might be + /// surprised to find a `51 Not Found` error page. + /// + /// This happens because when the user clicks the link to `/path`, their client takes + /// them to `gemini://example.com/path`. The gemini server doesn't see any apps or + /// files being served on `/path`, so it sends a `NotFound`. + /// + /// The app *should* have linked to `/app/path`, but in order to do that, it would + /// need to know that it was mounted at `/app`, and include a bunch of logic to figure + /// out the write path. Thankfully, kochab can take care of this for you. + /// + /// There are three main tools at your disposal for link rewriting: + /// + /// * [`Server::set_autorewrite()`] is the easiest tool to use, and the one that will + /// work the best for most people. By setting this option on your server, it will + /// automatcially check for any gemini links in it's response before it's sent, and + /// rewrite them to be appropriate relative to the app. In this case, our example + /// app would simply send the link as `/path`, and kochab would catch and rewrite it + /// before it's sent out. For more information about this method, keep reading this + /// method's docs. + /// * [`Response::rewrite_all()`] will attempt to rewrite any links it finds in a + /// single response. This is the method that underlies [`Server::set_autorewrite()`], + /// but by calling it on your responses manually, you can choose exactly what + /// responses are rewritten. + /// * [`Request::rewrite_path()`] will rewrite a single link. This method works best + /// for when you need a lot of precision, like including links that need to be + /// rewritten alongside links that don't, or when you're rewriting links in + /// responses that aren't `text/gemini`. + /// + /// All of these methods work on both `scgi_srv` and `gemini_srv` modes, so you can + /// use them regardless of what you plan on compiling your server to, which is + /// recommended if you're planning to offer compilation to either, or if you would + /// like to be able to change later. It's worth noting that while it will *work*, on + /// `gemini_srv` mode, `gemini_srv` servers are *always* mounted at the base path, so + /// this method really won't do anything other than sign off that the link is good. + /// + /// If there's a problem with rewriting the URLs, typically because the proxy + /// server/SCGI client being used doesn't correctly implement the SCGI spec, then any + /// `text/gemini` responses bearing an absolute link will be `40 TEMPORARY FAILURE`, + /// and an error will be logged explaining what went wrong. + /// + /// For more information about how rewritten paths are calculated, see + /// [`Request::rewrite_path()`].\ + /// For more information about what responses are rewritten, + /// see [`Response::rewrite_all()`]. pub fn set_autorewrite(mut self, autorewrite: bool) -> Self { self.autorewrite = autorewrite; self diff --git a/src/types/request.rs b/src/types/request.rs index 2a98553..9fd5586 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -222,8 +222,16 @@ impl Request { /// fail if unable to infer the correct path. If this is the case, None will be /// returned. Currently, the SCGI headers checked are: /// - /// * `SCRIPT_PATH` (Used by mollybrown) - /// * `SCRIPT_NAME` (Used by GLV-1.12556) + /// * `SCRIPT_PATH` (Used by [mollybrown] and [stargazer]) + /// * `SCRIPT_NAME` (Used by [GLV-1.12556]) + /// + /// [mollybrown]: https://tildegit.org/solderpunk/molly-brown + /// [stargazer]: https://git.sr.ht/~zethra/stargazer/ + /// [GLV-1.12556]: https://github.com/spc476/GLV-1.12556 + /// + /// For an overview of methods for rewriting links, see [`Server::set_autorewrite()`]. + /// + /// [`Server::set_autorewrite()`]: crate::Server::set_autorewrite() pub fn rewrite_path(&self, path: impl AsRef) -> Option { #[cfg(feature = "scgi_srv")] { self.script_path.as_ref().map(|base| { diff --git a/src/types/response.rs b/src/types/response.rs index f2f6ec2..ca0f372 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -148,9 +148,6 @@ impl Response { #[cfg_attr(feature="gemini_srv",allow(unused_variables))] /// Rewrite any links in this response based on the path identified by a request /// - /// For more information about what rewriting a link means, see - /// [`Request::rewrite_path`]. - /// /// Currently, this rewrites any links in: /// * SUCCESS (10) requests with a `text/gemini` MIME /// * REDIRECT (3X) requests @@ -163,10 +160,18 @@ impl Response { /// this process, this error will be raised /// /// If the request does not contain enough information to rewrite a link (in other - /// words, if [`Requet::rewrite_path`] returns [`None`]), then false is returned. In - /// all other cases, this method returns true. + /// words, if [`Request::rewrite_path()`] returns [`None`]), then false is returned. + /// In all other cases, this method returns true. /// /// Panics if a "text/gemini" response is not UTF-8 formatted + /// + /// For an overview of methods for rewriting links, see + /// [`Server::set_autorewrite()`].\ + /// For more information about how rewritten paths are calculated, see + /// [`Request::rewrite_path()`]. + /// + /// [`Server::set_autorewrite()`]: crate::Server::set_autorewrite() + /// [`Request::rewrite_path()`]: crate::Server::set_autorewrite() pub async fn rewrite_all(&mut self, based_on: &crate::Request) -> std::io::Result { #[cfg(feature = "scgi_srv")] match self.status { From 1068c00e552c0e5b6344ce7a4125637b7a223e83 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sat, 5 Dec 2020 11:37:14 -0500 Subject: [PATCH 105/113] Fix broken links in docs --- src/handling.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handling.rs b/src/handling.rs index 0ab3979..13128ac 100644 --- a/src/handling.rs +++ b/src/handling.rs @@ -103,7 +103,7 @@ where /// Any requests passed to the handler will be directly handed down to the handler, /// with the request as the first argument. The response provided will be sent to the /// requester. If the handler panics or returns an [`Err`], this will be logged, and - /// the requester will be sent a [`SERVER_ERROR`](Response::server_error()). + /// the requester will be sent a [`TEMPORARY FAILURE`](Response::temporary_failure()). fn from(handler: H) -> Self { Self::FnHandler( Box::new(move|req| Box::pin((handler)(req)) as HandlerResponse) From 868dfb6f9f729266125c74f54dc9cc6dec99d5ba Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sat, 5 Dec 2020 11:49:17 -0500 Subject: [PATCH 106/113] Update rustls --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 142d3d9..ab08f34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,9 +31,9 @@ log = "0.4.11" ring = { version = "0.16.15", optional = true } base64 = { version = "0.13.0", optional = true } lazy_static = { version = "1.4.0", optional = true } -rustls = { version = "0.18.1", features = ["dangerous_configuration"], optional = true} +rustls = { version = "0.19", features = ["dangerous_configuration"], optional = true} webpki = { version = "0.21.0", optional = true} -tokio-rustls = { version = "0.20.0", optional = true} +tokio-rustls = { version = "0.21.0", optional = true} mime_guess = { version = "2.0.3", optional = true } dashmap = { version = "3.11.10", optional = true } sled = { version = "0.34.6", optional = true } From b1d1fb7c0dff6df20beb3751536c59ada1150d1d Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sat, 5 Dec 2020 13:01:45 -0500 Subject: [PATCH 107/113] Warn the first time a request is missing SCRIPT_PATH --- src/types/request.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/types/request.rs b/src/types/request.rs index 9fd5586..7739b1a 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -67,6 +67,21 @@ impl Request { .cloned() ); + // Send out a warning if the server did not specify a SCRIPT_PATH. + // This should only be done once to avoid spaming log files + #[cfg(feature = "scgi_srv")] + if script_path.is_none() { + static WARN: std::sync::Once = std::sync::Once::new(); + WARN.call_once(|| + warn!(concat!( + "The SCGI server did not send a SCRIPT_PATH, indicating that it", + " doesn't comply with Gemini's SCGI spec. This will cause a problem", + " if the app needs to rewrite a URL. Please consult the proxy server", + " to identify why this is." + )) + ) + } + uri.normalize(); let input = match uri.query().filter(|q| !q.is_empty()) { From 3ef7b2751e40e75b0d631d447bf3d6bd3b01ecc0 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sat, 5 Dec 2020 13:39:29 -0500 Subject: [PATCH 108/113] Warn if a response is sent with a non-success code & a body --- src/lib.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index cbe2f4c..e12ab6c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -428,7 +428,6 @@ impl ServerInner { async fn send_response(&self, response: Response, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> { let use_complex_timeout = - response.is_success() && response.body.is_some() && response.meta != "text/plain" && response.meta != "text/gemini" && @@ -448,6 +447,15 @@ impl ServerInner { send_body_timeout = None; } + if !response.is_success() && response.body.is_some() { + warn!(concat!( + "Received a response with a body, but a status code of {} (!= 20). ", + " Responses should only have a body if their status code is 20. The body", + " will be sent, but this is likely to cause unexpected behavior."), + response.status, + ); + } + opt_timeout(send_general_timeout, async { // Send the header opt_timeout(send_header_timeout, send_response_header(&response, stream)) From f98b94235a5f90d5d9a4534234d800c08ace46f7 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sat, 5 Dec 2020 14:09:16 -0500 Subject: [PATCH 109/113] Cranking out some docs Not finished yet we've got so many docs to write. It's doc writing time! --- src/handling.rs | 6 ++++ src/lib.rs | 6 +++- src/routing.rs | 5 ++++ src/types/body.rs | 11 +++++++ src/types/request.rs | 68 ++++++++++++++++++++++++++++++++++++++----- src/types/response.rs | 39 +++++++++++++++++++++++++ src/util.rs | 5 ++++ 7 files changed, 132 insertions(+), 8 deletions(-) diff --git a/src/handling.rs b/src/handling.rs index 13128ac..f1e2639 100644 --- a/src/handling.rs +++ b/src/handling.rs @@ -30,9 +30,15 @@ use crate::{Document, types::{Body, Response, Request}}; /// Each implementation has bespoke docs that describe how the type is used, and what /// response is produced. pub enum Handler { + + /// A handler that responds to a request by delegating to an [`Fn`] FnHandler(HandlerInner), + + /// A handler that always serves an identical response, for any and all request StaticHandler(Response), + #[cfg(feature = "serve_dir")] + /// A handler that serves a directory, including a directory listing FilesHandler(PathBuf), } diff --git a/src/lib.rs b/src/lib.rs index e12ab6c..c347670 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +#![warn(missing_docs)] //! Kochab is an ergonomic and intuitive library for quickly building highly functional //! and advanced Gemini applications on either SCGI or raw Gemini. //! @@ -228,7 +229,7 @@ use tokio_rustls::TlsAcceptor; #[cfg(feature = "gemini_srv")] use rustls::Session; -pub mod types; +mod types; pub mod util; pub mod routing; pub mod handling; @@ -247,7 +248,10 @@ pub use cert::CertGenMode; pub use uriparse as uri; pub use types::*; +/// The maximun length of a Request URI pub const REQUEST_URI_MAX_LEN: usize = 1024; + +/// The default port for the gemini protocol pub const GEMINI_PORT: u16 = 1965; use handling::Handler; diff --git a/src/routing.rs b/src/routing.rs index 69ff34e..7b12870 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -206,6 +206,11 @@ impl Default for RoutingNode { } #[derive(Debug, Clone, Copy)] +/// An error returned when attempting to register a route that already exists +/// +/// Routes will not be overridden if this error is returned. Routes are never overwritten +/// +/// See [`RoutingNode::add_route_by_path()`] pub struct ConflictingRouteError(); impl std::error::Error for ConflictingRouteError { } diff --git a/src/types/body.rs b/src/types/body.rs index d9aeca3..ef418a0 100644 --- a/src/types/body.rs +++ b/src/types/body.rs @@ -8,8 +8,19 @@ use std::borrow::Borrow; use crate::types::Document; +/// The body of a response +/// +/// The content of a successful response to be sent back to the user. This can be either +/// some bytes which will be sent directly to the user, or a reader which will be read at +/// some point before sending to the user. pub enum Body { + /// In-memory bytes that may be sent back to the user Bytes(Vec), + + /// A reader which will be streamed to the user + /// + /// If a reader blocks for too long, it MAY be killed before finishing, which results + /// in the user receiving a malformed response or timing out. Reader(Box), } diff --git a/src/types/request.rs b/src/types/request.rs index 7739b1a..fb9367a 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -19,6 +19,20 @@ use ring::digest; use crate::user_management::{UserManager, User}; #[derive(Clone)] +/// A request from a Gemini client to the app +/// +/// When originally sent out by a client, a request is literally just a URL, and honestly, +/// if you want to use it as just a URL, that'll work fine! +/// +/// That said, kochab and any proxies the request might hit add a little bit more +/// information that you can use, like +/// * [What TLS certificate (if any) did the client use](Self::certificate) +/// * [What part of the path is relevant (ie, everything after the route)](Self::trailing_segments) +/// * [Is the user registered with the user database?](Self::user) +/// +/// The only way to get your hands on one of these bad boys is when you register an [`Fn`] +/// based handler to a [`Server`](crate::Server), and a user makes a request to the +/// endpoint. pub struct Request { uri: URIReference<'static>, input: Option, @@ -33,7 +47,34 @@ pub struct Request { } impl Request { - pub fn new( + /// Construct a new request + /// + /// When in `gemini_srv` mode, this is done using a URL. If you do construct a + /// request this way, by default it will not have a certificate attached, so make + /// sure you add in a certificate with [`Request::set_cert()`]. + /// + /// By contrast, in `scgi_srv` mode, the certificate fingerprint is grabbed out of the + /// request parameters, so you don't need to do anything. The headers passed should + /// be the header sent by the SCGI client. + /// + /// When in SCGI mode, the following headers are expected: + /// + /// * `PATH_INFO`: The part of the path following the route the app is bound to + /// * `QUERY_STRING`: The part of the request following ?, url encoded. Will produce + /// an error if it contains invalid UTF-8. No error if missing + /// * `TLS_CLIENT_HASH`: Optional. The base64 or hex encoded SHA256 sum of the DER + /// certificate of the requester. + /// * `SCRIPT_PATH` or `SCRIPT_NAME`: The base path the app is mounted on + /// + /// # Errors + /// + /// Produces an error if: + /// * The SCGI server didn't include the mandatory `PATH_INFO` header + /// * The provided URI reference is invalid, including if the SCGI server sent an + /// invalid `PATH_INFO` + /// * The `TLS_CLIENT_HASH` sent by the SCGI server isn't sha256, or is encoded with + /// something other than base64 or hexadecimal + pub (crate) fn new( #[cfg(feature = "gemini_srv")] mut uri: URIReference<'static>, #[cfg(feature = "scgi_srv")] @@ -112,6 +153,19 @@ impl Request { }) } + /// The URI reference requested by the user + /// + /// Although they are not exactly the same thing, it is generally preferred to use the + /// [`Request::trailing_segments()`] method if possible. + /// + /// Returns the URIReference requested by the user. **If running in SCGI mode, this + /// will contain only the parts of the URIReference that were relevant to the app.** + /// This means you will get `/path`, not `/app/path`. + /// + /// When running in `scgi_srv` mode, this is guaranteed to be a relative reference. + /// When running in `gemini_srv` mode, clients are obliged by the spec to send a full + /// URI, but if a client fails to respect this, kochab will still accept and pass on + /// the relative reference. pub const fn uri(&self) -> &URIReference { &self.uri } @@ -123,11 +177,6 @@ impl Request { /// 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()`](crate::Server::add_route()) will - /// always have trailing segments set. pub fn trailing_segments(&self) -> &Vec { self.trailing_segments.as_ref().unwrap() } @@ -167,6 +216,10 @@ impl Request { /// the request otherwise. Bear in mind that **not all SCGI clients send the same /// headers**, and these are *never* available when operating in `gemini_srv` mode. /// + /// By using this method, you are almost certainly reducing the number of proxy + /// servers your app supports, and you are strongly encouraged to find a different + /// method. + /// /// Some examples of headers mollybrown sets are: /// - `REMOTE_ADDR` (The user's IP address and port) /// - `TLS_CLIENT_SUBJECT_CN` (The CommonName on the user's certificate, when present) @@ -187,7 +240,8 @@ impl Request { }); } - pub fn set_trailing(&mut self, segments: Vec) { + /// Sets the segments returned by [`Request::trailing_segments()`] + pub (crate) fn set_trailing(&mut self, segments: Vec) { self.trailing_segments = Some(segments); } diff --git a/src/types/response.rs b/src/types/response.rs index ca0f372..dadfa79 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -2,9 +2,48 @@ use std::borrow::Borrow; use crate::types::{Body, Document}; +/// A response to a client's [`Request`] +/// +/// Requests in Gemini are pretty simple. They consist of three parts: +/// +/// * A two status code, similar to the status codes in HTML. You don't need to know +/// anything about these, since this part of the response will be filled in for you +/// depending on the associated function you use to create the Response +/// * A meta, a <1024 byte string whose meaning depends on the status +/// * A body, but only for successful requests +/// +/// Responses will be identical in both `scgi_srv` mode and `gemini_srv` mode. +/// +/// [`Request`]: crate::Request pub struct Response { + /// The status code of the request. A value between 10 and 62 + /// + /// Each block of 10 status codes (e.g. 10-19) has a specific meaning or category, + /// defined in depth in the gemini documentation. Generally: + /// + /// * 1X is input + /// * 20 is success + /// * 3X is redirect + /// * >= 40 is an error pub status: u8, + + /// The meta associated with this request + /// + /// Because the meaning of the meta field depends on the status, please consult the + /// status code before interpreting this value. The function signature of the method + /// used to create the response should also provide more detail about what the field + /// is. In general, the meaning of the meta for a status code is + /// + /// * If the status code is 20, the meta is the mime type of the body + /// * If the status code is 3X, the meta is a URL to redirect to + /// * If the status code is 44, the meta is a time in seconds until ratelimiting ends + /// * If the status code is anything els, the meta is a message or prompt for the user pub meta: String, + + /// The body of this request + /// + /// This never needs to be present, and **cannot** be present if the status code != + /// 20. pub body: Option, } diff --git a/src/util.rs b/src/util.rs index d3166dd..79d6cfa 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,3 +1,8 @@ +//! Utilities for serving a file or directory +//! +//! ⚠️ Docs still under construction & API not yet stable ⚠️ +#![allow(missing_docs)] + #[cfg(feature="serve_dir")] use std::path::{Path, PathBuf}; use anyhow::*; From 05089bfea6cc4aafd3e4d1d3977ae605e5cf0a8b Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sun, 6 Dec 2020 10:24:54 -0500 Subject: [PATCH 110/113] Redo Handler docs --- src/handling.rs | 103 +++++++++++++++++++++++++++++++++++------------- src/lib.rs | 7 ++-- src/routing.rs | 6 +++ 3 files changed, 84 insertions(+), 32 deletions(-) diff --git a/src/handling.rs b/src/handling.rs index f1e2639..ba72283 100644 --- a/src/handling.rs +++ b/src/handling.rs @@ -2,10 +2,6 @@ //! //! The main type is the [`Handler`], which wraps a more specific type of handler and //! manages delegating responses to it. -//! -//! For most purposes, you should never have to manually create any of these structs -//! yourself, though it may be useful to look at the implementations of [`From`] on -//! [`Handler`], as these are the things that can be used as handlers for routes. use anyhow::Result; use std::{ @@ -21,24 +17,63 @@ use crate::{Document, types::{Body, Response, Request}}; /// A struct representing something capable of handling a request. /// -/// In the future, this may have multiple varieties, but at the minute, it just wraps an -/// [`Fn`](std::ops::Fn). +/// A crucial part of the documentation for this is the implementations of [`From`], as +/// this is what can be passed to [`Server::add_route()`](crate::Server::add_route()) in +/// order to create a new route. /// -/// The most useful part of the documentation for this is the implementations of [`From`] -/// on it, as this is what can be passed to -/// [`Server::add_route()`](crate::Server::add_route()) in order to create a new route. -/// Each implementation has bespoke docs that describe how the type is used, and what -/// response is produced. +/// Detailed descriptions on each variant also describe how each kind of handler works, +/// and how they can be created pub enum Handler { /// A handler that responds to a request by delegating to an [`Fn`] + /// + /// Most often created by using the implementation by using the implementation of + /// [`From`] + /// + /// If you're feeling overwhelmed by the function signature, don't panic. Please see + /// the [example](#example). + /// + /// Any requests passed to the handler will be directly handed down to the handler, + /// with the request as the first argument. The response provided will be sent to the + /// requester. If the handler panics or returns an [`Err`], this will be logged, and + /// the requester will be sent a [`TEMPORARY FAILURE`](Response::temporary_failure()). + /// + /// [`From`]: #impl-From FnHandler(HandlerInner), /// A handler that always serves an identical response, for any and all request + /// + /// Any and all requests to this handler will be responded to with the same response, + /// no matter what. This is good for static content that is provided by your app. + /// For serving files & directories, try looking at creating a [`FilesHandler`] by + /// [passing a directory](#impl-From). + /// + /// Most often created by using [`From`] or [`From`] + /// + /// [`FilesHandler`]: Self::FilesHandler + /// [`From`]: #impl-From + /// [`From`]: #impl-From<%26'_%20Document> StaticHandler(Response), #[cfg(feature = "serve_dir")] /// A handler that serves a directory, including a directory listing + /// + /// Most often created with [`From`] + /// + /// Any requests directed to this handler will be served from this path. For example, + /// if a handler serving files from the path `./public/` and bound to `/serve` + /// receives a request for `/serve/file.txt`, it will respond with the contents of the + /// file at `./public/file.txt`, and automatically infer the MIME type. + /// + /// This is equivilent to serving files using [`util::serve_dir()`], and as such will + /// include directory listings. + /// + /// Additionally, if the path is only a single file, that file will be served in + /// response to *every request*. That is, adding a handler for `/path/to/file.txt` + /// to the route `/hello` will mean that `/hello`, `/hello/file.txt`, and + /// `/hello/irrele/vant` will all be responded to with the contents of `file.txt`. + /// + /// [`From`]: #impl-From FilesHandler(PathBuf), } @@ -102,14 +137,28 @@ where H: 'static + Fn(Request) -> R + Send + Sync, R: 'static + Future> + Send, { - /// Wrap an [`Fn`] in a [`Handler`] struct + /// Wrap an [`Fn`] in a [`Handler`] struct, creating an [`FnHandler`] /// /// This automatically boxes both the [`Fn`] and the [`Fn`]'s response. /// - /// Any requests passed to the handler will be directly handed down to the handler, - /// with the request as the first argument. The response provided will be sent to the - /// requester. If the handler panics or returns an [`Err`], this will be logged, and - /// the requester will be sent a [`TEMPORARY FAILURE`](Response::temporary_failure()). + /// Don't be overwhelmed by the function signature here. It's honestly way simpler + /// than it looks. + /// + /// # Example + /// + /// ``` + /// # use kochab::*; + /// use anyhow::Result; + /// + /// let handler: Handler = handle_request.into(); + /// + /// async fn handle_request(request: Request) -> Result { + /// // This could be done with a StaticHandler, but for demonstration: + /// Ok(Response::success_gemini("Hello world!")) + /// } + /// ``` + /// + /// [`FnHandler`]: Self::FnHandler fn from(handler: H) -> Self { Self::FnHandler( Box::new(move|req| Box::pin((handler)(req)) as HandlerResponse) @@ -123,15 +172,14 @@ where impl From for Handler { /// Serve an unchanging response /// - /// Any and all requests to this handler will be responded to with the same response, - /// no matter what. This is good for static content that is provided by your app. - /// For serving files & directories, try looking at creating a handler from a path - /// /// ## Panics /// This response type **CANNOT** be created using Responses with [`Reader`] bodies. /// Attempting to do this will cause a panic. Don't. /// + /// This will create a [`StaticHandler`] + /// /// [`Reader`]: Body::Reader + /// [`StaticHandler`]: Self::StaticHandler fn from(response: Response) -> Self { #[cfg(debug_assertions)] { // We have another check once the handler is actually called that is not @@ -150,6 +198,10 @@ impl From<&Document> for Handler { /// This document will be sent in response to any requests that arrive at this /// handler. As with all documents, this will be a successful response with a /// `text/gemini` MIME. + /// + /// This will create a [`StaticHandler`] + /// + /// [`StaticHandler`]: Self::StaticHandler fn from(doc: &Document) -> Self { Self::StaticHandler(doc.into()) } @@ -159,18 +211,13 @@ impl From<&Document> for Handler { impl From for Handler { /// Serve files from a directory /// - /// Any requests directed to this handler will be served from this path. For example, - /// if a handler serving files from the path `./public/` and bound to `/serve` - /// receives a request for `/serve/file.txt`, it will respond with the contents of the - /// file at `./public/file.txt`. - /// - /// This is equivilent to serving files using [`util::serve_dir()`], and as such will - /// include directory listings. - /// /// The path to a single file can be passed in order to serve only a single file for /// any and all requests. /// + /// This will create a [`FilesHandler`]. + /// /// [`util::serve_dir()`]: crate::util::serve_dir() + /// [`FilesHandler`]: Handler::FilesHandler fn from(path: PathBuf) -> Self { Self::FilesHandler(path) } diff --git a/src/lib.rs b/src/lib.rs index c347670..8a87cd2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -230,11 +230,11 @@ use tokio_rustls::TlsAcceptor; use rustls::Session; mod types; +mod handling; pub mod util; pub mod routing; -pub mod handling; #[cfg(feature = "ratelimiting")] -pub mod ratelimiting; +mod ratelimiting; #[cfg(feature = "user_management")] pub mod user_management; #[cfg(feature = "gemini_srv")] @@ -247,6 +247,7 @@ pub use cert::CertGenMode; pub use uriparse as uri; pub use types::*; +pub use handling::Handler; /// The maximun length of a Request URI pub const REQUEST_URI_MAX_LEN: usize = 1024; @@ -254,8 +255,6 @@ pub const REQUEST_URI_MAX_LEN: usize = 1024; /// The default port for the gemini protocol pub const GEMINI_PORT: u16 = 1965; -use handling::Handler; - #[derive(Clone)] struct ServerInner { #[cfg(feature = "gemini_srv")] diff --git a/src/routing.rs b/src/routing.rs index 7b12870..07f63a7 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -1,6 +1,12 @@ //! Utilities for routing requests //! +//! For most users, this is more advanced than you need to get. If you're interested in +//! adding routes, please see [`Server::add_route()`], which is how most people will +//! interact with the routing API +//! //! See [`RoutingNode`] for details on how routes are matched. +//! +//! [`Server::add_route()`]: crate::Server::add_route use uriparse::path::{Path, Segment}; From 27f3f2aee0c8acfeb2e3a62bdd03b05061036534 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Mon, 7 Dec 2020 15:58:49 -0500 Subject: [PATCH 111/113] Move URI reference to the crate base --- src/lib.rs | 2 +- src/types.rs | 2 -- src/types/document.rs | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8a87cd2..d12e448 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -245,7 +245,7 @@ use user_management::UserManager; #[cfg(feature = "certgen")] pub use cert::CertGenMode; -pub use uriparse as uri; +pub use uriparse::URIReference; pub use types::*; pub use handling::Handler; diff --git a/src/types.rs b/src/types.rs index 6fb59a0..5eb6a50 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,5 +1,3 @@ -pub use uriparse::URIReference; - mod request; pub use request::Request; diff --git a/src/types/document.rs b/src/types/document.rs index 5430c42..cf13955 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -39,7 +39,7 @@ use std::convert::TryInto; use std::fmt; -use crate::types::URIReference; +use crate::URIReference; use crate::util::Cowy; #[derive(Default)] From 7dff9674b60778f7aa064ed7c4c92ce907ee1f74 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Mon, 7 Dec 2020 15:59:43 -0500 Subject: [PATCH 112/113] Removed some unused imports --- src/util.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/util.rs b/src/util.rs index 79d6cfa..876b7b3 100644 --- a/src/util.rs +++ b/src/util.rs @@ -5,7 +5,6 @@ #[cfg(feature="serve_dir")] use std::path::{Path, PathBuf}; -use anyhow::*; #[cfg(feature="serve_dir")] use tokio::{ fs::{self, File}, @@ -15,8 +14,6 @@ use tokio::{ use crate::types::{Document, document::HeadingLevel::*}; #[cfg(feature="serve_dir")] use crate::types::Response; -use std::future::Future; -use tokio::time; #[cfg(feature="serve_dir")] pub async fn serve_file>(path: P, mime: &str) -> Response { From 6b30521c7706365c478ae1faae90abc2c8f87b34 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Mon, 7 Dec 2020 16:46:13 -0500 Subject: [PATCH 113/113] Add the last two docs for Server --- src/lib.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index d12e448..5039e39 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -631,6 +631,39 @@ impl std::error::Error for ParseError { } } +/// A builder for configuring a kochab server +/// +/// Once created with [`Server::new()`], different configuration methods can be +/// called to set up the server, before finally making a call to [`serve_ip()`] or +/// [`serve_unix()`]. +/// +/// Technically, no methods need to be called in order to create the server, but unless +/// you add at least one route with [`add_route()`], the server will respond with `51 NOT +/// FOUND` to all requests. +/// +/// # Example +/// ```no_run +/// use anyhow::Result; +/// # use kochab::Response; +/// # use kochab::Request; +/// # use kochab::Server; +/// +/// #[tokio::main] +/// async fn main() { +/// Server::new() +/// .add_route("/", hello_world) +/// .serve_ip("localhost:1965") +/// .await; +/// } +/// +/// async fn hello_world(_: Request) -> Result { +/// Ok(Response::success_gemini("Hello world!")) +/// } +/// ``` +/// +/// [`serve_ip()`]: Self::serve_ip() +/// [`serve_unix()`]: Self::serve_unix() +/// [`add_route()`]: Self::add_route() pub struct Server { timeout: Duration, complex_body_timeout_override: Option, @@ -651,6 +684,7 @@ pub struct Server { } impl Server { + /// Instantiate a new [`Server`] with all the default settings pub fn new() -> Self { Self { timeout: Duration::from_secs(1),