use async_trait::async_trait; use log::{error,info}; use url::Url; use tide::prelude::*; use tide::{Request, Response, Next, Result}; use std::sync::Arc; use crate::database; use crate::ApplicationState; #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub struct User { pub me: Url, pub client_id: Url, scope: String } impl User { pub fn check_scope(&self, scope: &str) -> bool { self.scopes().any(|i| i == scope) } pub fn scopes(&self) -> std::str::SplitAsciiWhitespace<'_> { self.scope.split_ascii_whitespace() } #[cfg(test)] pub fn new(me: &str, client_id: &str, scope: &str) -> Self { Self { me: Url::parse(me).unwrap(), client_id: Url::parse(client_id).unwrap(), scope: scope.to_string() } } } async fn get_token_data(token: String, token_endpoint: &http_types::Url, http_client: &surf::Client) -> (http_types::StatusCode, Option) { match http_client.get(token_endpoint).header("Authorization", token).header("Accept", "application/json").send().await { Ok(mut resp) => { if resp.status() == 200 { match resp.body_json::().await { Ok(user) => { info!("Token endpoint request successful. Validated user: {}", user.me); (resp.status(), Some(user)) }, Err(err) => { error!("Token endpoint parsing error (HTTP status {}): {}", resp.status(), err); (http_types::StatusCode::InternalServerError, None) } } } else { error!("Token endpoint returned non-200: {}", resp.status()); (resp.status(), None) } } Err(err) => { error!("Token endpoint connection error: {}", err); (http_types::StatusCode::InternalServerError, None) } } } pub struct IndieAuthMiddleware { cache: Arc>, monitor_task: Option> } impl IndieAuthMiddleware { /// Create a new instance of IndieAuthMiddleware. /// /// Note that creating a new instance automatically launches a task /// to garbage-collect stale cache entries. Please do not create /// instances willy-nilly because of that. pub fn new() -> Self { let cache: Arc> = Arc::new(retainer::Cache::new()); let cache_clone = cache.clone(); let task = async_std::task::spawn(async move { cache_clone.monitor(4, 0.1, std::time::Duration::from_secs(30)).await }); Self { cache, monitor_task: Some(task) } } } impl Drop for IndieAuthMiddleware { fn drop(&mut self) { // Cancel the task, or a VERY FUNNY thing might occur. // If I understand this correctly, keeping a task active // WILL keep an active reference to a value, so I'm pretty sure // that something VERY FUNNY might occur whenever `cache` is dropped // and its related task is not cancelled. So let's cancel it so // [`cache`] can be dropped once and for all. // First, get the ownership of a task, sneakily switching it out with None // (wow, this is sneaky, didn't know Safe Rust could even do that!!!) // (it is safe tho cuz None is no nullptr and dereferencing it doesn't cause unsafety) // (could cause a VERY FUNNY race condition to occur though // if you tried to refer to the value in another thread!) let task = std::mem::take(&mut self.monitor_task).expect("Dropped IndieAuthMiddleware TWICE? Impossible!"); // Then cancel the task, using another task to request cancellation. // Because apparently you can't run async code from Drop... // This should drop the last reference for the [`cache`], // allowing it to be dropped. async_std::task::spawn(async move { task.cancel() }); } } #[async_trait] impl tide::Middleware> for IndieAuthMiddleware where B: database::Storage + Send + Sync + Clone { async fn handle(&self, mut req: Request>, next: Next<'_, ApplicationState>) -> Result { let header = req.header("Authorization"); match header { None => { // TODO: move that to the request handling functions // or make a middleware that refuses to accept unauthenticated requests Ok(Response::builder(401).body(json!({ "error": "unauthorized", "error_description": "Please provide an access token." })).build()) }, Some(value) => { let endpoint = &req.state().token_endpoint; let http_client = &req.state().http_client; let token = value.last().to_string(); match self.cache.get(&token).await { Some(user) => { req.set_ext::(user.clone()); Ok(next.run(req).await) }, None => match get_token_data(value.last().to_string(), endpoint, http_client).await { (http_types::StatusCode::Ok, Some(user)) => { // Note that this can run multiple requests before the value appears in the cache. // This seems to be in line with some other implementations of a function cache // (e.g. the [`cached`](https://lib.rs/crates/cached) crate and Python's `functools.lru_cache`) // // TODO: ensure the duration is no more than the token's remaining time until expiration // (in case the expiration time is defined on the token - AFAIK currently non-standard in IndieAuth) self.cache.insert(token, user.clone(), std::time::Duration::from_secs(600)).await; req.set_ext(user); Ok(next.run(req).await) }, // TODO: Refactor to return Err(IndieAuthError) so downstream middleware could catch it // and present a prettier interface to the error (maybe even hiding data from the user) (http_types::StatusCode::InternalServerError, None) => { Ok(Response::builder(500).body(json!({ "error": "token_endpoint_fail", "error_description": "Token endpoint made a boo-boo and refused to answer." })).build()) }, (_, None) => { Ok(Response::builder(401).body(json!({ "error": "unauthorized", "error_description": "The token endpoint refused to accept your token." })).build()) }, (_, Some(_)) => { // This shouldn't happen. panic!("The token validation function has caught rabies and returns malformed responses. Aborting."); } } } } } } } #[cfg(test)] mod tests { use super::*; #[test] fn user_scopes_are_checkable() { let user = User::new("https://fireburn.ru/", "https://quill.p3k.io/", "create update media"); assert!(user.check_scope("create")); assert!(!user.check_scope("delete")); } }