diff options
author | Vika <vika@fireburn.ru> | 2022-02-21 22:10:30 +0300 |
---|---|---|
committer | Vika <vika@fireburn.ru> | 2022-02-21 22:17:31 +0300 |
commit | 093780f094b56745ff3ef2c70ae64b2fc12b8c7a (patch) | |
tree | afaa32a7ec5f0ee0a0446abd9ab1390d6cb7b254 /src/micropub | |
parent | 2882f7d7295979549ea14040db68994ee6bc1589 (diff) | |
download | kittybox-093780f094b56745ff3ef2c70ae64b2fc12b8c7a.tar.zst |
micropub: flesh out query
Diffstat (limited to 'src/micropub')
-rw-r--r-- | src/micropub/mod.rs | 227 |
1 files changed, 180 insertions, 47 deletions
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<String> } -#[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<MicropubError> 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<D: Storage>(db: D) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone { +async fn _query<D: Storage>(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<dyn warp::Reply> + } + match query.q { + QueryType::Config => { + let channels: Vec<MicropubChannel> = 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<D: Storage, T>(db: D, token_endpoint: String, http: hyper::Client<T, hyper::Body>) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + 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::<MicropubQuery>()) - .then(|db: D, host: warp::host::Authority, query: MicropubQuery| async move { - match query.q { - QueryType::Config => { - let channels: Vec<MicropubChannel> = 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::<InvalidQuery>() { - 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<impl warp::Reply, Infallible> { + let error = if err.find::<InvalidQuery>().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); + } } + |