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 + } +}