about summary refs log tree commit diff
path: root/src/micropub
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2022-02-21 22:10:30 +0300
committerVika <vika@fireburn.ru>2022-02-21 22:17:31 +0300
commit093780f094b56745ff3ef2c70ae64b2fc12b8c7a (patch)
treeafaa32a7ec5f0ee0a0446abd9ab1390d6cb7b254 /src/micropub
parent2882f7d7295979549ea14040db68994ee6bc1589 (diff)
downloadkittybox-093780f094b56745ff3ef2c70ae64b2fc12b8c7a.tar.zst
micropub: flesh out query
Diffstat (limited to 'src/micropub')
-rw-r--r--src/micropub/mod.rs227
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);
+    }
 }
+