about summary refs log tree commit diff
path: root/src/indieauth.rs
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2021-05-04 17:05:51 +0300
committerVika <vika@fireburn.ru>2021-05-04 17:07:25 +0300
commit08c09aaa055c05228855eed8cded9fdfe4939c0f (patch)
tree792ba1d2a3b3af7a837135aa90620d8f689d7ebd /src/indieauth.rs
downloadkittybox-08c09aaa055c05228855eed8cded9fdfe4939c0f.tar.zst
Initial commit
Working features:
 - Sending posts from the database
 - Reading posts from the database
 - Responding with MF2-JSON (only in debug mode!)
 - Not locking the database when not needed
 - All database actions are atomic (except for a small race where UIDs
   can clash, but that's not gonna happen often)

TODOs:
 - Send webmentions
 - Send syndication requests
 - Send WebSub notifications
 - Make tombstones for deleted posts (update adding dt-deleted)
 - Rich reply contexts (possibly on the frontend part?)
 - Frontend?
 - Fix UID race

Code maintenance TODOs:
 - Split the database module
 - Finish implementing the in-memory test database
 - Make RedisDatabase unit tests launch their own Redis instances (see
   redis-rs/tests/support/mod.rs for more info)
 - Write more unit-tests!!!
Diffstat (limited to 'src/indieauth.rs')
-rw-r--r--src/indieauth.rs116
1 files changed, 116 insertions, 0 deletions
diff --git a/src/indieauth.rs b/src/indieauth.rs
new file mode 100644
index 0000000..8d41577
--- /dev/null
+++ b/src/indieauth.rs
@@ -0,0 +1,116 @@
+use log::{error,info};
+use std::future::Future;
+use std::pin::Pin;
+use url::Url;
+use tide::prelude::*;
+use tide::{Request, Response, Next, Result};
+
+use crate::database;
+use crate::ApplicationState;
+
+#[derive(Deserialize, Serialize, Debug, PartialEq)]
+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<User>) {
+    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::<User>().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)
+        }
+    }
+}
+
+// TODO: Figure out how to cache these authorization values - they can potentially take a lot of processing time
+pub fn check_auth<'a, Backend>(mut req: Request<ApplicationState<Backend>>, next: Next<'a, ApplicationState<Backend>>) -> Pin<Box<dyn Future<Output = Result> + Send + 'a>>
+where
+    Backend: database::Storage + Send + Sync + Clone
+{
+    Box::pin(async {
+        let header = req.header("Authorization");
+        match header {
+            None => {
+                Ok(Response::builder(401).body(json!({
+                    "error": "unauthorized",
+                    "error_description": "Please provide an access token."
+                })).build())
+            },
+            Some(value) => {
+                // TODO check the token
+                let endpoint = &req.state().token_endpoint;
+                let http_client = &req.state().http_client;
+                match get_token_data(value.last().to_string(), endpoint, http_client).await {
+                    (http_types::StatusCode::Ok, Some(user)) => {
+                        req.set_ext(user);
+                        Ok(next.run(req).await)
+                    },
+                    (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"));
+    }
+}
\ No newline at end of file