From 093780f094b56745ff3ef2c70ae64b2fc12b8c7a Mon Sep 17 00:00:00 2001 From: Vika Date: Mon, 21 Feb 2022 22:10:30 +0300 Subject: micropub: flesh out query --- src/micropub/mod.rs | 227 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 180 insertions(+), 47 deletions(-) (limited to 'src/micropub') diff --git a/src/micropub/mod.rs b/src/micropub/mod.rs index 95595cf..dc74d6e 100644 --- a/src/micropub/mod.rs +++ b/src/micropub/mod.rs @@ -1,10 +1,12 @@ +use std::convert::Infallible; + use warp::http::StatusCode; use warp::{Filter, Rejection, reject::InvalidQuery}; use serde_json::{json, Value}; use serde::{Serialize, Deserialize}; use crate::database::{MicropubChannel, Storage}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug, PartialEq)] #[serde(rename_all = "kebab-case")] enum QueryType { Source, @@ -13,17 +15,19 @@ enum QueryType { SyndicateTo } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] struct MicropubQuery { q: QueryType, url: Option } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, PartialEq, Debug)] #[serde(rename_all = "snake_case")] enum ErrorType { InvalidRequest, - InternalServerError + InternalServerError, + NotFound, + NotAuthorized, } #[derive(Serialize, Deserialize)] @@ -34,9 +38,12 @@ struct MicropubError { impl From for StatusCode { fn from(err: MicropubError) -> Self { + use ErrorType::*; match err.error { - ErrorType::InvalidRequest => StatusCode::BAD_REQUEST, - ErrorType::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR + InvalidRequest => StatusCode::BAD_REQUEST, + InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, + NotFound => StatusCode::NOT_FOUND, + NotAuthorized => StatusCode::UNAUTHORIZED } } } @@ -50,52 +57,178 @@ impl MicropubError { } } -pub fn query(db: D) -> impl Filter + Clone { +async fn _query(db: D, host: warp::host::Authority, query: MicropubQuery, user: crate::indieauth::User) -> impl warp::Reply { + let user_authority = warp::http::Uri::try_from(user.me.as_str()).unwrap().authority().unwrap().clone(); + // TODO compare with potential list of allowed websites + // to allow one user to edit several websites with one token + if host != user_authority { + return Box::new(warp::reply::with_status( + warp::reply::json(&MicropubError::new( + ErrorType::NotAuthorized, + "This user is not authorized to use Micropub on this website." + )), + StatusCode::UNAUTHORIZED + )) as Box + } + match query.q { + QueryType::Config => { + let channels: Vec = match db.get_channels(host.as_str()).await { + Ok(chans) => chans, + Err(err) => return Box::new(warp::reply::with_status( + warp::reply::json(&MicropubError::new( + ErrorType::InternalServerError, + &format!("Error fetching channels: {}", err) + )), + StatusCode::INTERNAL_SERVER_ERROR + )) + }; + + Box::new(warp::reply::json(json!({ + "q": [ + QueryType::Source, + QueryType::Config, + QueryType::Channel, + QueryType::SyndicateTo + ], + "channels": channels, + "_kittybox_authority": host.as_str() + }).as_object().unwrap())) + }, + QueryType::Source => { + match query.url { + Some(url) => { + if warp::http::Uri::try_from(&url).unwrap().authority().unwrap() != &user_authority { + return Box::new(warp::reply::with_status( + warp::reply::json(&MicropubError::new( + ErrorType::NotAuthorized, + "You are requesting a post from a website that doesn't belong to you." + )), + StatusCode::UNAUTHORIZED + )) + } + match db.get_post(&url).await { + Ok(some) => match some { + Some(post) => Box::new(warp::reply::json(&post)), + None => Box::new(warp::reply::with_status( + warp::reply::json(&MicropubError::new( + ErrorType::NotFound, + "The specified MF2 object was not found in database." + )), + StatusCode::NOT_FOUND + )) + }, + Err(err) => { + return Box::new(warp::reply::json(&MicropubError::new( + ErrorType::InternalServerError, + &format!("Backend error: {}", err) + ))) + } + } + }, + None => todo!() + } + }, + _ => { + todo!() + } + } +} + +pub fn query(db: D, token_endpoint: String, http: hyper::Client) -> impl Filter + Clone +where T: hyper::client::connect::Connect + Clone + Send + Sync + 'static { warp::get() .map(move || db.clone()) .and(crate::util::require_host()) .and(warp::query::()) - .then(|db: D, host: warp::host::Authority, query: MicropubQuery| async move { - match query.q { - QueryType::Config => { - let channels: Vec = match db.get_channels(host.as_str()).await { - Ok(chans) => chans, - Err(err) => return warp::reply::json(&MicropubError::new( - ErrorType::InternalServerError, - &format!("Error fetching channels: {}", err) - )) - }; - - warp::reply::json(json!({ - "q": [ - QueryType::Source, - QueryType::Config, - QueryType::Channel, - QueryType::SyndicateTo - ], - "channels": channels, - "_kittybox_authority": host.as_str() - }).as_object().unwrap()) - }, - _ => { - todo!() - } - } - }) - .recover(|err: Rejection| async move { - let error = if let Some(_) = err.find::() { - MicropubError::new( - ErrorType::InvalidRequest, - "Invalid query parameters sent. Try ?q=config to see what you can do." + .and(crate::indieauth::require_token(token_endpoint, http)) + .then(_query) +} + +pub async fn recover(err: Rejection) -> Result { + let error = if err.find::().is_some() { + MicropubError::new( + ErrorType::InvalidRequest, + "Invalid query parameters sent. Try ?q=config to see what you can do." + ) + } else { + log::error!("Unhandled rejection: {:?}", err); + MicropubError::new( + ErrorType::InternalServerError, + &format!("Unknown error: {:?}", err) + ) + }; + + Ok(warp::reply::with_status(warp::reply::json(&error), error.into())) +} + +#[cfg(test)] +impl MicropubQuery { + fn config() -> Self { + Self { + q: QueryType::Config, + url: None + } + } + + fn source(url: &str) -> Self { + Self { + q: QueryType::Source, + url: Some(url.to_owned()) + } + } +} + + +#[cfg(test)] +mod tests { + use hyper::body::HttpBody; + use crate::micropub::MicropubError; + use warp::{Filter, Reply}; + + #[tokio::test] + async fn test_query_wrong_auth() { + let mut res = warp::test::request() + .filter(&warp::any().then(|| super::_query( + crate::database::MemoryStorage::new(), + warp::host::Authority::from_static("aaronparecki.com"), + super::MicropubQuery::config(), + crate::indieauth::User::new( + "https://fireburn.ru/", + "https://quill.p3k.io/", + "create update media" ) - } else { - log::error!("Unhandled rejection: {:?}", err); - MicropubError::new( - ErrorType::InternalServerError, - &format!("Unknown error: {:?}", err) + ))) + .await + .unwrap() + .into_response(); + + assert_eq!(res.status(), 401); + let body = res.body_mut().data().await.unwrap().unwrap(); + let json: MicropubError = serde_json::from_slice(&body as &[u8]).unwrap(); + assert_eq!(json.error, super::ErrorType::NotAuthorized); + } + + #[tokio::test] + async fn test_query_foreign_url() { + let mut res = warp::test::request() + .filter(&warp::any().then(|| super::_query( + crate::database::MemoryStorage::new(), + warp::host::Authority::from_static("aaronparecki.com"), + super::MicropubQuery::source("https://aaronparecki.com/feeds/main"), + crate::indieauth::User::new( + "https://fireburn.ru/", + "https://quill.p3k.io/", + "create update media" ) - }; + ))) + .await + .unwrap() + .into_response(); - Ok(warp::reply::with_status(warp::reply::json(&error), error.into())) - }) + assert_eq!(res.status(), 401); + let body = res.body_mut().data().await.unwrap().unwrap(); + let json: MicropubError = serde_json::from_slice(&body as &[u8]).unwrap(); + assert_eq!(json.error, super::ErrorType::NotAuthorized); + } } + -- cgit 1.4.1