Merge several pending changes into a virtual master

Octopus merges are just a cheap trick to make weak branches stronger
This commit is contained in:
Emi Tatsuo 2020-11-24 23:04:26 -05:00
Signed by: Emi
GPG key ID: 68FAB2E2E6DFC98B
29 changed files with 1658 additions and 225 deletions

View file

@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- `document` API for creating Gemini documents
- preliminary timeout API, incl a special case for complex MIMEs by [@Alch-Emi]
- `Response::success_with_body` 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
@ -17,10 +17,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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]
- 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

View file

@ -9,6 +9,9 @@ repository = "https://github.com/panicbit/northstar"
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"]
ratelimiting = ["dashmap"]
@ -21,18 +24,30 @@ 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"
mime_guess = { version = "2.0.3", optional = true }
dashmap = { version = "3.11.10", optional = true }
sled = { version = "0.34.6", optional = true }
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 }
[dev-dependencies]
env_logger = "0.8.1"
futures-util = "0.3.7"
tokio = { version = "0.3.1", features = ["macros", "rt-multi-thread", "sync"] }
[[example]]
name = "user_management"
required-features = ["user_management_routes"]
[[example]]
name = "serve_dir"
required-features = ["serve_dir"]
[[example]]
name = "ratelimiting"
required-features = ["ratelimiting"]

View file

@ -1,9 +1,7 @@
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};
use northstar::{Certificate, GEMINI_PORT, Request, Response, Server};
use std::collections::HashMap;
use std::sync::Arc;
@ -31,44 +29,38 @@ 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<RwLock<HashMap<CertBytes, String>>>, request: Request) -> BoxFuture<'static, Result<Response>> {
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<RwLock<HashMap<CertBytes, String>>>, request: Request) -> Result<Response> {
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_with_body(
&GEMINI_MIME,
format!("Welcome {}!", user)
Response::success_gemini(
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())
}
}

View file

@ -1,8 +1,6 @@
use anyhow::*;
use futures_core::future::BoxFuture;
use futures_util::FutureExt;
use log::LevelFilter;
use northstar::{Server, Request, Response, GEMINI_PORT, Document};
use northstar::{Server, Response, GEMINI_PORT, Document};
use northstar::document::HeadingLevel::*;
#[tokio::main]
@ -11,40 +9,34 @@ async fn main() -> Result<()> {
.filter_module("northstar", LevelFilter::Debug)
.init();
let response: Response = Document::new()
.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",
))
.into();
Server::bind(("localhost", GEMINI_PORT))
.add_route("/",handle_request)
.add_route("/", response)
.serve()
.await
}
fn handle_request(_request: Request) -> BoxFuture<'static, Result<Response>> {
async move {
let response = Document::new()
.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",
))
.into();
Ok(response)
}
.boxed()
}

View file

@ -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(req: Request) -> BoxFuture<'static, Result<Response>> {
async fn handle_base(req: Request) -> Result<Response> {
let doc = generate_doc("base", &req);
async move {
Ok(Response::document(doc))
}.boxed()
Ok(doc.into())
}
fn handle_short(req: Request) -> BoxFuture<'static, Result<Response>> {
async fn handle_short(req: Request) -> Result<Response> {
let doc = generate_doc("short", &req);
async move {
Ok(Response::document(doc))
}.boxed()
Ok(doc.into())
}
fn handle_long(req: Request) -> BoxFuture<'static, Result<Response>> {
async fn handle_long(req: Request) -> Result<Response> {
let doc = generate_doc("long", &req);
async move {
Ok(Response::document(doc))
}.boxed()
Ok(doc.into())
}
fn generate_doc(route_name: &str, req: &Request) -> Document {

View file

@ -1,8 +1,8 @@
use std::path::PathBuf;
use anyhow::*;
use futures_core::future::BoxFuture;
use futures_util::FutureExt;
use log::LevelFilter;
use northstar::{Server, Request, Response, GEMINI_PORT};
use northstar::{Server, GEMINI_PORT};
#[tokio::main]
async fn main() -> Result<()> {
@ -11,17 +11,8 @@ async fn main() -> Result<()> {
.init();
Server::bind(("localhost", GEMINI_PORT))
.add_route("/", handle_request)
.add_route("/", PathBuf::from("public")) // Serve directory listings & file contents
.add_route("/about", PathBuf::from("README.md")) // Serve a single file
.serve()
.await
}
fn handle_request(request: Request) -> BoxFuture<'static, Result<Response>> {
async move {
let path = request.path_segments();
let response = northstar::util::serve_dir("public", &path).await?;
Ok(response)
}
.boxed()
}

View file

@ -0,0 +1,83 @@
use anyhow::*;
use log::LevelFilter;
use northstar::{
GEMINI_PORT,
Document,
Request,
Response,
Server,
user_management::{
user::RegisteredUser,
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 our main routes
.add_authenticated_route("/", handle_main)
.add_authenticated_input_route("/update", "Enter your new string:", handle_update)
// Add routes for handling user authentication
.add_um_routes::<String>("/")
// Start the server
.serve()
.await
}
/// The landing page
///
/// 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.
///
/// 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<String>) -> Result<Response> {
// 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.
async fn handle_update(_request: Request, mut user: RegisteredUser<String>, input: String) -> Result<Response> {
// 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() = input;
// Render a response
let response = Document::new()
.add_text("String updated!")
.add_blank_line()
.add_link("/", "Back")
.into();
Ok(response)
}

211
src/handling.rs Normal file
View file

@ -0,0 +1,211 @@
//! 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},
};
#[cfg(feature = "serve_dir")]
use std::path::PathBuf;
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).
///
/// 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),
StaticHandler(Response),
#[cfg(feature = "serve_dir")]
FilesHandler(PathBuf),
}
/// Since we can't store train objects, we need to wrap fn handlers in a box
type HandlerInner = Box<dyn Fn(Request) -> HandlerResponse + Send + Sync>;
/// Same with dyn Futures
type HandlerResponse = Pin<Box<dyn Future<Output = Result<Response>> + 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()
})
},
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()
}
}
},
#[cfg(feature = "serve_dir")]
Self::FilesHandler(path) => {
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 {}: {:?}", path.display(), e);
Response::server_error("").unwrap()
})
},
}
}
}
impl<H, R> From<H> for Handler
where
H: 'static + Fn(Request) -> R + Send + Sync,
R: 'static + Future<Output = Result<Response>> + 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)
)
}
}
// 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<Response> 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<Response>
///
/// 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())
}
}
#[cfg(feature = "serve_dir")]
impl From<PathBuf> 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.
///
/// [`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
/// amount of dependencies tied into the feature that provides this simple struct.
#[must_use = "futures do nothing unless polled"]
struct HandlerCatchUnwind {
future: AssertUnwindSafe<HandlerResponse>,
}
impl HandlerCatchUnwind {
fn new(future: AssertUnwindSafe<HandlerResponse>) -> Self {
Self { future }
}
}
impl Future for HandlerCatchUnwind {
type Output = Result<Response>;
fn poll(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context
) -> Poll<Self::Output> {
match catch_unwind(AssertUnwindSafe(|| self.future.as_mut().poll(cx))) {
Ok(res) => res,
Err(e) => {
error!("Handler panic! {:?}", e);
Poll::Ready(Response::server_error(""))
}
}
}
}

View file

@ -1,7 +1,6 @@
#[macro_use] extern crate log;
use std::{
panic::AssertUnwindSafe,
convert::TryFrom,
io::BufReader,
sync::Arc,
@ -10,7 +9,6 @@ use std::{
};
#[cfg(feature = "ratelimiting")]
use std::net::IpAddr;
use futures_core::future::BoxFuture;
use tokio::{
prelude::*,
io::{self, BufStream},
@ -34,8 +32,14 @@ use ratelimiting::RateLimiter;
pub mod types;
pub mod util;
pub mod routing;
pub mod handling;
#[cfg(feature = "ratelimiting")]
pub mod ratelimiting;
#[cfg(feature = "user_management")]
pub mod user_management;
#[cfg(feature="user_management")]
use user_management::UserManager;
pub use mime;
pub use uriparse as uri;
@ -44,18 +48,18 @@ pub use types::*;
pub const REQUEST_URI_MAX_LEN: usize = 1024;
pub const GEMINI_PORT: u16 = 1965;
type Handler = Arc<dyn Fn(Request) -> HandlerResponse + Send + Sync>;
pub (crate) type HandlerResponse = BoxFuture<'static, Result<Response>>;
use handling::Handler;
#[derive(Clone)]
pub struct Server {
tls_acceptor: TlsAcceptor,
listener: Arc<TcpListener>,
routes: Arc<RoutingNode<Handler>>,
timeout: Duration,
complex_timeout: Option<Duration>,
#[cfg(feature="ratelimiting")]
rate_limits: Arc<RoutingNode<RateLimiter<IpAddr>>>,
#[cfg(feature="user_management")]
manager: UserManager,
}
impl Server {
@ -63,12 +67,12 @@ impl Server {
Builder::bind(addr)
}
async fn serve(self) -> Result<()> {
async fn serve(self, listener: TcpListener) -> Result<()> {
#[cfg(feature = "ratelimiting")]
tokio::spawn(prune_ratelimit_log(self.rate_limits.clone()));
loop {
let (stream, _addr) = self.listener.accept().await
let (stream, _addr) = listener.accept().await
.context("Failed to accept client")?;
let this = self.clone();
@ -89,7 +93,11 @@ impl Server {
.context("Failed to establish TLS session")?;
let mut stream = BufStream::new(stream);
let request = receive_request(&mut stream).await
#[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))
@ -120,19 +128,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()
};
@ -201,6 +198,41 @@ impl Server {
}
None
}
async fn receive_request(
#[cfg(feature="user_management")]
&self,
stream: &mut (impl AsyncBufRead + Unpin)
) -> Result<Request> {
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<A> {
@ -212,6 +244,8 @@ pub struct Builder<A> {
routes: RoutingNode<Handler>,
#[cfg(feature="ratelimiting")]
rate_limits: RoutingNode<RateLimiter<IpAddr>>,
#[cfg(feature="user_management")]
data_dir: PathBuf,
}
impl<A: ToSocketAddrs> Builder<A> {
@ -225,9 +259,20 @@ impl<A: ToSocketAddrs> Builder<A> {
routes: RoutingNode::default(),
#[cfg(feature="ratelimiting")]
rate_limits: RoutingNode::default(),
#[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<PathBuf>) -> Self {
self.data_dir = path.into();
self
}
/// Sets the directory that northstar should look for TLS certs and keys into
///
/// Northstar will look for files called `cert.pem` and `key.pem` in the provided
@ -333,11 +378,8 @@ impl<A: ToSocketAddrs> Builder<A> {
/// "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<H>(mut self, path: &'static str, handler: H) -> Self
where
H: Fn(Request) -> HandlerResponse + Send + Sync + 'static,
{
self.routes.add_route(path, Arc::new(handler));
pub fn add_route(mut self, path: &'static str, handler: impl Into<Handler>) -> Self {
self.routes.add_route(path, handler.into());
self
}
@ -369,46 +411,19 @@ impl<A: ToSocketAddrs> Builder<A> {
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,
#[cfg(feature="ratelimiting")]
rate_limits: Arc::new(self.rate_limits),
#[cfg(feature="user_management")]
manager: UserManager::new(self.data_dir)?,
};
server.serve().await
server.serve(listener).await
}
}
async fn receive_request(stream: &mut (impl AsyncBufRead + Unpin)) -> Result<Request> {
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_header(header: &ResponseHeader, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> {
let header = format!(
"{status} {meta}\r\n",

View file

@ -3,22 +3,40 @@ 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<String>,
certificate: Option<Certificate>,
trailing_segments: Option<Vec<String>>,
#[cfg(feature="user_management")]
manager: UserManager,
}
impl Request {
pub fn from_uri(uri: URIReference<'static>) -> Result<Self> {
Self::with_certificate(uri, None)
pub fn from_uri(
uri: URIReference<'static>,
#[cfg(feature="user_management")]
manager: UserManager,
) -> Result<Self> {
Self::with_certificate(
uri,
None,
#[cfg(feature="user_management")]
manager
)
}
pub fn with_certificate(
mut uri: URIReference<'static>,
certificate: Option<Certificate>
certificate: Option<Certificate>,
#[cfg(feature="user_management")]
manager: UserManager,
) -> Result<Self> {
uri.normalize();
@ -38,6 +56,8 @@ impl Request {
input,
certificate,
trailing_segments: None,
#[cfg(feature="user_management")]
manager,
})
}
@ -94,6 +114,18 @@ impl Request {
pub const 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<UserData>(&self) -> Result<User<UserData>>
where
UserData: Serialize + DeserializeOwned
{
Ok(self.manager.get_user(self.certificate())?)
}
}
impl ops::Deref for Request {

View file

@ -20,8 +20,12 @@ impl Response {
}
}
#[deprecated(
since = "0.4.0",
note = "Deprecated in favor of Response::success_gemini() or Document::into()"
)]
pub fn document(document: impl Borrow<Document>) -> Self {
Self::success_with_body(&GEMINI_MIME, document)
Self::success_gemini(document)
}
pub fn input(prompt: impl Cowy<str>) -> Result<Self> {
@ -34,27 +38,27 @@ impl Response {
Self::new(header)
}
pub fn success(mime: &Mime) -> Self {
let header = ResponseHeader::success(mime);
Self::new(header)
}
pub fn redirect_temporary_lossy<'a>(location: impl TryInto<URIReference<'a>>) -> Self {
let header = ResponseHeader::redirect_temporary_lossy(location);
Self::new(header)
}
/// Create a successful response with a preconfigured body
///
/// This is equivilent to:
///
/// ```ignore
/// Response::success(mime)
/// .with_body(body)
/// ```
pub fn success_with_body(mime: &Mime, body: impl Into<Body>) -> Self {
Self::success(mime)
.with_body(body)
/// Create a successful response with a given body and MIME
pub fn success(mime: &Mime, body: impl Into<Body>) -> Self {
Self {
header: ResponseHeader::success(mime),
body: Some(body.into()),
}
}
/// Create a successful response with a `text/gemini` MIME
pub fn success_gemini(body: impl Into<Body>) -> Self {
Self::success(&GEMINI_MIME, body)
}
/// Create a successful response with a `text/plain` MIME
pub fn success_plain(body: impl Into<Body>) -> Self {
Self::success(&mime::TEXT_PLAIN, body)
}
pub fn server_error(reason: impl Cowy<str>) -> Result<Self> {
@ -96,8 +100,20 @@ impl Response {
}
}
impl<D: Borrow<Document>> From<D> for Response {
fn from(doc: D) -> Self {
Self::document(doc)
impl AsRef<Option<Body>> for Response {
fn as_ref(&self) -> &Option<Body> {
&self.body
}
}
impl AsMut<Option<Body>> for Response {
fn as_mut(&mut self) -> &mut Option<Body> {
&mut self.body
}
}
impl<D: Borrow<Document>> From<D> for Response {
fn from(doc: D) -> Self {
Self::success_gemini(doc)
}
}

View file

@ -0,0 +1,120 @@
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};
#[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<u8>);
#[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<Path>) -> Result<Self> {
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_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
///
/// # 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<Option<CertificateData>> {
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<UserData>(
&self,
username: impl AsRef<str>
) -> Result<Option<RegisteredUser<UserData>>>
where
UserData: Serialize + DeserializeOwned
{
if let Some(bytes) = self.users.get(username.as_ref())? {
let inner: PartialUser<UserData> = bincode::deserialize_from(bytes.as_ref())?;
Ok(Some(RegisteredUser::new(username.as_ref().to_owned(), None, self.clone(), inner)))
} 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<UserData>(
&self,
cert: Option<&Certificate>
) -> Result<User<UserData>>
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(user_inner.with_cert(certificate_data.certificate)))
} else {
Ok(User::NotSignedIn(NotSignedInUser {
certificate: certificate.clone(),
manager: self.clone(),
}))
}
} else {
Ok(User::Unauthenticated)
}
}
}

105
src/user_management/mod.rs Normal file
View file

@ -0,0 +1,105 @@
//! 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 [`RegisteredUser`] either from registering/attaching a
//! [`NotSignedInUser`] or because the user was already registered
//! * 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
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;
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 {
UsernameNotUnique,
PasswordNotSet,
DatabaseError(sled::Error),
DatabaseTransactionError(sled::transaction::TransactionError),
DeserializeError(bincode::Error),
#[cfg(feature = "user_management_advanced")]
Argon2Error(argon2::Error),
}
impl From<sled::Error> for UserManagerError {
fn from(error: sled::Error) -> Self {
Self::DatabaseError(error)
}
}
impl From<sled::transaction::TransactionError> for UserManagerError {
fn from(error: sled::transaction::TransactionError) -> Self {
Self::DatabaseTransactionError(error)
}
}
impl From<bincode::Error> for UserManagerError {
fn from(error: bincode::Error) -> Self {
Self::DeserializeError(error)
}
}
#[cfg(feature = "user_management_advanced")]
impl From<argon2::Error> for UserManagerError {
fn from(error: argon2::Error) -> Self {
Self::Argon2Error(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),
#[cfg(feature = "user_management_advanced")]
Self::Argon2Error(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),
#[cfg(feature = "user_management_advanced")]
Self::Argon2Error(e) =>
write!(f, "Argon2 Error, likely malformed password hash, possible database corruption: {}", e),
}
}
}
pub type Result<T> = std::result::Result<T, UserManagerError>;

View file

@ -0,0 +1,7 @@
# 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.
=> {redirect} Back

View file

@ -0,0 +1,11 @@
# Certificate Found!
Your 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

View file

@ -0,0 +1,7 @@
# Success!
Welcome {username}!
Your certificate has been linked.
=> {redirect} Back to app

View file

@ -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!

View file

@ -0,0 +1,5 @@
# Certificate Found!
Your certificate was found, all that's left to do is pick a username!
=> /account/register Sign Up

View file

@ -0,0 +1,5 @@
# Welcome!
To continue, please create an account.
=> /account/register Set up my account

View file

@ -0,0 +1,5 @@
# Username Exists
Unfortunately, it looks like the username {username} is already taken.
=> /account/register Choose a different username

View file

@ -0,0 +1,5 @@
# Account Created!
Welcome {username}! Your account has been created.
=> {redirect} Back to app

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,361 @@
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};
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 {
/// 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<UserData: Serialize + DeserializeOwned + Default + 'static>(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<UserData, Handler, F>(
self,
path: &'static str,
handler: Handler,
) -> Self
where
UserData: Serialize + DeserializeOwned + 'static + Send + Sync,
Handler: Clone + Send + Sync + 'static + Fn(Request, RegisteredUser<UserData>) -> F,
F: Send + Sync + 'static + Future<Output = Result<Response>>;
/// 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<UserData, Handler, F>(
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<UserData>, String) -> F,
F: Send + Sync + 'static + Future<Output = Result<Response>>;
}
impl<A: ToSocketAddrs> UserManagementRoutes for crate::Builder<A> {
/// Add pre-configured routes to the serve to handle authentication
///
/// See [`UserManagementRoutes::add_um_routes()`]
fn add_um_routes<UserData: Serialize + DeserializeOwned + Default + 'static>(self, redir: &'static str) -> Self {
#[allow(unused_mut)]
let mut modified_self = self.add_route("/account", move|r|handle_base::<UserData>(r, redir))
.add_route("/account/askcert", move|r|handle_ask_cert::<UserData>(r, redir))
.add_route("/account/register", move|r|handle_register::<UserData>(r, redir));
#[cfg(feature = "user_management_advanced")] {
modified_self = modified_self
.add_route("/account/login", move|r|handle_login::<UserData>(r, redir))
.add_route("/account/password", handle_password::<UserData>);
}
modified_self
}
/// Add a special route that requires users to be logged in
///
/// See [`UserManagementRoutes::add_authenticated_route()`]
fn add_authenticated_route<UserData, Handler, F>(
self,
path: &'static str,
handler: Handler,
) -> Self
where
UserData: Serialize + DeserializeOwned + 'static + Send + Sync,
Handler: Clone + Send + Sync + 'static + Fn(Request, RegisteredUser<UserData>) -> F,
F: Send + Sync + 'static + Future<Output = Result<Response>>
{
self.add_route(path, move|request| {
let handler = handler.clone();
async move {
Ok(match request.user::<UserData>()? {
User::Unauthenticated => {
Response::success_gemini(UNAUTH)
},
User::NotSignedIn(_) => {
Response::success_gemini(NSI)
},
User::SignedIn(user) => {
(handler)(request, user).await?
},
})
}
})
}
/// 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<UserData, Handler, F>(
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<UserData>, String) -> F,
F: Send + Sync + 'static + Future<Output = Result<Response>>
{
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<UserData: Serialize + DeserializeOwned>(request: Request, redirect: &'static str) -> Result<Response> {
Ok(match request.user::<UserData>()? {
User::Unauthenticated => {
Response::success_gemini(UNAUTH)
},
User::NotSignedIn(_) => {
Response::success_gemini(NSI)
},
User::SignedIn(user) => {
render_settings_menu(user, redirect)
},
})
}
async fn handle_ask_cert<UserData: Serialize + DeserializeOwned>(request: Request, redirect: &'static str) -> Result<Response> {
Ok(match request.user::<UserData>()? {
User::Unauthenticated => {
Response::client_certificate_required()
},
User::NotSignedIn(_) => {
#[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!(
include_str!("pages/askcert/exists.gmi"),
username = user.username(),
redirect = redirect,
))
},
})
}
async fn handle_register<UserData: Serialize + DeserializeOwned + Default>(request: Request, redirect: &'static str) -> Result<Response> {
Ok(match request.user::<UserData>()? {
User::Unauthenticated => {
Response::success_gemini(UNAUTH)
},
User::NotSignedIn(nsi) => {
if let Some(username) = request.input() {
match nsi.register::<UserData>(username.to_owned()) {
Err(UserManagerError::UsernameNotUnique) => {
#[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(_) => {
#[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())
}
} else {
Response::input_lossy("Please pick a username")
}
},
User::SignedIn(user) => {
render_settings_menu(user, redirect)
},
})
}
#[cfg(feature = "user_management_advanced")]
async fn handle_login<UserData: Serialize + DeserializeOwned + Default>(request: Request, redirect: &'static str) -> Result<Response> {
Ok(match request.user::<UserData>()? {
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::<UserData>(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)
},
})
}
#[cfg(feature = "user_management_advanced")]
async fn handle_password<UserData: Serialize + DeserializeOwned + Default>(request: Request) -> Result<Response> {
Ok(match request.user::<UserData>()? {
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<UserData: Serialize + DeserializeOwned>(
user: RegisteredUser<UserData>,
redirect: &str
) -> Response {
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();
#[cfg(feature = "user_management_advanced")]
document
.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" });
document.into()
}
mod private {
pub trait Sealed {}
impl<A> Sealed for crate::Builder<A> {}
}

398
src/user_management/user.rs Normal file
View file

@ -0,0 +1,398 @@
//! 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 [`RegisteredUser`] if
//! the user has presented a certificate. These two subtypes have more specific
//! information, like the user's username and active certificate.
//!
//! [`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()`](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 {
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,
};
#[cfg(feature = "user_management_advanced")]
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
/// certificate.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub (crate) struct PartialUser<UserData> {
pub data: UserData,
pub certificates: Vec<u32>,
#[cfg(feature = "user_management_advanced")]
pub pass_hash: Option<(Vec<u8>, [u8; 32])>,
}
impl<UserData> PartialUser<UserData> {
/// 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
fn store(&self, tree: &sled::Tree, username: impl AsRef<[u8]>) -> Result<()>
where
UserData: Serialize
{
tree.insert(
&username,
bincode::serialize(&self)?,
)?;
Ok(())
}
}
/// Any information about the connecting user
#[derive(Clone, Debug)]
pub enum User<UserData: Serialize + DeserializeOwned> {
/// 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(RegisteredUser<UserData>),
}
#[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.
///
/// 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.
///
/// Additional errors might occur if there is a problem writing to the database
pub fn register<UserData: Serialize + DeserializeOwned + Default>(
self,
username: String,
) -> Result<RegisteredUser<UserData>> {
if self.manager.users.contains_key(username.as_str())? {
Err(super::UserManagerError::UsernameNotUnique)
} else {
let mut newser = RegisteredUser::new(
username,
Some(self.certificate.clone()),
self.manager,
PartialUser {
data: UserData::default(),
certificates: Vec::with_capacity(1),
#[cfg(feature = "user_management_advanced")]
pass_hash: None,
},
);
// 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)
}
}
#[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
/// 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 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, 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 user has yet to set a password.
///
/// Additional errors might occur if an error occurs during database lookup and
/// deserialization
pub fn attach<UserData: Serialize + DeserializeOwned>(
self,
username: &str,
password: Option<&[u8]>,
) -> Result<Option<RegisteredUser<UserData>>> {
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)
}
}
}
#[derive(Clone, Debug)]
/// Data about a logged in user
///
/// For more information about the user lifecycle and sign-in stages, see [`User`]
pub struct RegisteredUser<UserData: Serialize + DeserializeOwned> {
username: String,
active_certificate: Option<Certificate>,
manager: UserManager,
inner: PartialUser<UserData>,
/// Indicates that [`RegisteredUser::as_mut()`] has been called, but [`RegisteredUser::save()`] has not
has_changed: bool,
}
impl<UserData: Serialize + DeserializeOwned> RegisteredUser<UserData> {
/// Create a new user from parts
pub (crate) fn new(
username: String,
active_certificate: Option<Certificate>,
manager: UserManager,
inner: PartialUser<UserData>
) -> Self {
Self {
username,
active_certificate,
manager,
inner,
has_changed: false,
}
}
/// 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
}
/// Get the [`Certificate`] that the user is currently using to connect.
///
/// 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
pub fn all_certificates(&self) -> Vec<Certificate> {
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 fn username(&self) -> &String {
&self.username
}
#[cfg(feature = "user_management_advanced")]
/// 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<bool> {
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)
}
}
#[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
/// 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 [`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
/// 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,
));
self.has_changed = true;
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.
/// This is only for changes made to the UserData.
pub fn save(&mut self) -> Result<()>
where
UserData: Serialize
{
self.inner.store(&self.manager.users, &self.username)?;
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(())
}
#[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
/// 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<UserData: Serialize + DeserializeOwned> std::ops::Drop for RegisteredUser<UserData> {
fn drop(&mut self) {
if self.has_changed {
if let Err(e) = self.save() {
error!("Failed to save user data to database: {:?}", e);
}
}
}
}
impl<UserData: Serialize + DeserializeOwned> AsRef<UserData> for RegisteredUser<UserData> {
fn as_ref(&self) -> &UserData {
&self.inner.data
}
}
impl<UserData: Serialize + DeserializeOwned> AsMut<UserData> for RegisteredUser<UserData> {
/// 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
}
}

View file

@ -10,10 +10,9 @@ use tokio::{
};
#[cfg(feature="serve_dir")]
use crate::types::{Document, document::HeadingLevel::*};
#[cfg(feature="serve_dir")]
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")]
@ -23,27 +22,58 @@ pub async fn serve_file<P: AsRef<Path>>(path: P, mime: &Mime) -> Result<Response
let file = match File::open(path).await {
Ok(file) => file,
Err(err) => match err.kind() {
io::ErrorKind::NotFound => return Ok(Response::not_found()),
_ => return Err(err.into()),
std::io::ErrorKind::PermissionDenied => {
warn!("Asked to serve {}, but permission denied by OS", path.display());
return Ok(Response::not_found());
},
_ => return warn_unexpected(err, path, line!()),
}
};
Ok(Response::success_with_body(mime, file))
Ok(Response::success(mime, file))
}
#[cfg(feature="serve_dir")]
pub async fn serve_dir<D: AsRef<Path>, P: AsRef<Path>>(dir: D, virtual_path: &[P]) -> Result<Response> {
debug!("Dir: {}", dir.as_ref().display());
let dir = dir.as_ref().canonicalize()
.context("Failed to canonicalize directory")?;
let dir = dir.as_ref();
let dir = match dir.canonicalize() {
Ok(dir) => dir,
Err(e) => {
match e.kind() {
std::io::ErrorKind::NotFound => {
warn!("Path {} not found. Check your configuration.", dir.display());
return Response::server_error("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 warn_unexpected(e, dir, line!()),
}
},
};
let mut path = dir.to_path_buf();
for segment in virtual_path {
path.push(segment);
}
let path = path.canonicalize()
.context("Failed to canonicalize path")?;
let path = match path.canonicalize() {
Ok(dir) => dir,
Err(e) => {
match e.kind() {
std::io::ErrorKind::NotFound => return Ok(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 warn_unexpected(e, path.as_ref(), line!()),
}
},
};
if !path.starts_with(&dir) {
return Ok(Response::not_found());
@ -59,11 +89,15 @@ pub async fn serve_dir<D: AsRef<Path>, P: AsRef<Path>>(dir: D, virtual_path: &[P
#[cfg(feature="serve_dir")]
async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path: &[B]) -> Result<Response> {
let mut dir = match fs::read_dir(path).await {
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()),
_ => return Err(err.into()),
std::io::ErrorKind::PermissionDenied => {
warn!("Asked to serve {}, but permission denied by OS", path.as_ref().display());
return Ok(Response::not_found());
},
_ => return warn_unexpected(err, path.as_ref(), line!()),
}
};
@ -93,7 +127,7 @@ async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path
));
}
Ok(Response::document(document))
Ok(document.into())
}
#[cfg(feature="serve_dir")]
@ -112,6 +146,22 @@ pub fn guess_mime_from_path<P: AsRef<Path>>(path: P) -> Mime {
mime_guess::from_ext(extension).first_or_octet_stream()
}
#[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<Response> {
warn!(
concat!(
"Unexpected error serving path {} at util.rs:{}, please report to ",
env!("CARGO_PKG_REPOSITORY"),
"/issues: {:?}",
),
path.display(),
line,
err
);
Response::server_error("Unexpected error")
}
/// A convenience trait alias for `AsRef<T> + Into<T::Owned>`,
/// most commonly used to accept `&str` or `String`:
///
@ -128,35 +178,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<crate::HandlerResponse>,
}
impl HandlerCatchUnwind {
pub(super) fn new(future: AssertUnwindSafe<crate::HandlerResponse>) -> Self {
Self { future }
}
}
impl Future for HandlerCatchUnwind {
type Output = Result<Result<Response>, Box<dyn std::any::Any + Send>>;
fn poll(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context
) -> Poll<Self::Output> {
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<T>(duration: Option<time::Duration>, future: impl Future<Output = T>) -> Result<T, time::error::Elapsed> {
match duration {
Some(duration) => time::timeout(duration, future).await,