From 9c7c2858cc603cde9d5336c670c57dceabd53b9e Mon Sep 17 00:00:00 2001 From: nullishamy Date: Thu, 24 Apr 2025 20:19:54 +0100 Subject: [PATCH 1/2] feat: auth basics --- Cargo.lock | 1 + Cargo.toml | 3 +- ferri-main/Cargo.toml | 2 +- ferri-main/src/lib.rs | 9 ++ ferri-server/Cargo.toml | 1 + ferri-server/src/endpoints/api/apps.rs | 36 ++++++- ferri-server/src/endpoints/api/timeline.rs | 9 +- ferri-server/src/endpoints/oauth.rs | 111 +++++++++++++++++++-- ferri-server/src/lib.rs | 44 ++++++-- ferri-server/src/types/oauth.rs | 2 +- migrations/20250423182916_add_auth.sql | 26 +++++ 11 files changed, 215 insertions(+), 29 deletions(-) create mode 100644 migrations/20250423182916_add_auth.sql diff --git a/Cargo.lock b/Cargo.lock index 402385e..f1201d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2156,6 +2156,7 @@ version = "0.1.0" dependencies = [ "chrono", "main", + "rand 0.8.5", "reqwest", "rocket", "rocket_db_pools", diff --git a/Cargo.toml b/Cargo.toml index b0d9a9e..e039c64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,5 @@ serde = "1.0.219" rocket = { version = "0.5.1", features = ["json"] } sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite", "macros" ], default-features = false } uuid = { version = "1.16.0", features = ["v4"] } -chrono = "0.4.40" \ No newline at end of file +chrono = "0.4.40" +rand = "0.8" \ No newline at end of file diff --git a/ferri-main/Cargo.toml b/ferri-main/Cargo.toml index 855afc8..0d91f57 100644 --- a/ferri-main/Cargo.toml +++ b/ferri-main/Cargo.toml @@ -13,5 +13,5 @@ uuid = { workspace = true } base64 = "0.22.1" rsa = { version = "0.9.8", features = ["sha2"] } -rand = "0.8" +rand = { workspace = true } url = "2.5.4" diff --git a/ferri-main/src/lib.rs b/ferri-main/src/lib.rs index 4d8826e..620300a 100644 --- a/ferri-main/src/lib.rs +++ b/ferri-main/src/lib.rs @@ -1,2 +1,11 @@ pub mod ap; pub mod config; +use rand::{Rng, distributions::Alphanumeric}; + +pub fn gen_token(len: usize) -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(len) + .map(char::from) + .collect() +} diff --git a/ferri-server/Cargo.toml b/ferri-server/Cargo.toml index 5ff5b60..fba127f 100644 --- a/ferri-server/Cargo.toml +++ b/ferri-server/Cargo.toml @@ -11,5 +11,6 @@ reqwest = { workspace = true } sqlx = { workspace = true } uuid = { workspace = true } chrono = { workspace = true } +rand = { workspace = true } url = "2.5.4" \ No newline at end of file diff --git a/ferri-server/src/endpoints/api/apps.rs b/ferri-server/src/endpoints/api/apps.rs index 40425cf..1ed5001 100644 --- a/ferri-server/src/endpoints/api/apps.rs +++ b/ferri-server/src/endpoints/api/apps.rs @@ -1,14 +1,44 @@ use rocket::{form::Form, post, serde::json::Json}; +use crate::Db; use crate::types::oauth::{App, CredentialApplication}; +use rocket_db_pools::Connection; #[post("/apps", data = "")] -pub async fn new_app(app: Form) -> Json { +pub async fn new_app(app: Form, mut db: Connection) -> Json { + let secret = main::gen_token(15); + + // Abort when we encounter a duplicate + let is_app_present = sqlx::query!( + r#" + INSERT INTO app (client_id, client_secret, scopes) + VALUES (?1, ?2, ?3) + "#, + app.client_name, + app.scopes, + secret + ) + .execute(&mut **db) + .await + .is_err(); + + let mut app: App = app.clone(); + + if is_app_present { + let existing_app = sqlx::query!("SELECT * FROM app WHERE client_id = ?1", app.client_name) + .fetch_one(&mut **db) + .await + .unwrap(); + + app.client_name = existing_app.client_id; + app.scopes = existing_app.scopes; + } + Json(CredentialApplication { name: app.client_name.clone(), scopes: app.scopes.clone(), redirect_uris: app.redirect_uris.clone(), - client_id: format!("id-for-{}", app.client_name), - client_secret: format!("secret-for-{}", app.client_name), + client_id: app.client_name.clone(), + client_secret: secret, }) } diff --git a/ferri-server/src/endpoints/api/timeline.rs b/ferri-server/src/endpoints/api/timeline.rs index 385c0de..3a3ec13 100644 --- a/ferri-server/src/endpoints/api/timeline.rs +++ b/ferri-server/src/endpoints/api/timeline.rs @@ -1,4 +1,4 @@ -use crate::{Db, endpoints::api::user::CredentialAcount}; +use crate::{AuthenticatedUser, Db, endpoints::api::user::CredentialAcount}; use rocket::{ get, serde::{Deserialize, Serialize, json::Json}, @@ -32,7 +32,12 @@ pub struct TimelineStatus { } #[get("/timelines/home?")] -pub async fn home(mut db: Connection, limit: i64) -> Json> { +pub async fn home( + mut db: Connection, + limit: i64, + user: AuthenticatedUser, +) -> Json> { + dbg!(user); let posts = sqlx::query!( r#" SELECT p.id as "post_id", u.id as "user_id", p.content, p.uri as "post_uri", diff --git a/ferri-server/src/endpoints/oauth.rs b/ferri-server/src/endpoints/oauth.rs index 5b48ebc..7045ec1 100644 --- a/ferri-server/src/endpoints/oauth.rs +++ b/ferri-server/src/endpoints/oauth.rs @@ -1,8 +1,12 @@ +use crate::Db; use rocket::{ + FromForm, + form::Form, get, post, response::Redirect, serde::{Deserialize, Serialize, json::Json}, }; +use rocket_db_pools::Connection; #[get("/oauth/authorize?&&&")] pub async fn authorize( @@ -10,11 +14,45 @@ pub async fn authorize( scope: &str, redirect_uri: &str, response_type: &str, + mut db: Connection, ) -> Redirect { - Redirect::temporary(format!( - "{}?code=code-for-{}&state=state-for-{}", - redirect_uri, client_id, client_id - )) + // For now, we will always authorize the request and assign it to an admin user + let user_id = "9b9d497b-2731-435f-a929-e609ca69dac9"; + let code = main::gen_token(15); + + // This will act as a token for the user, but we will in future say that it expires very shortly + // and can only be used for obtaining an access token etc + sqlx::query!( + r#" + INSERT INTO auth (token, user_id) + VALUES (?1, ?2) + "#, + code, + user_id + ) + .execute(&mut **db) + .await + .unwrap(); + + let id_token = main::gen_token(10); + + // Add an oauth entry for the `code` which /oauth/token will rewrite + sqlx::query!( + r#" + INSERT INTO oauth (id_token, client_id, expires_in, scope, access_token) + VALUES (?1, ?2, ?3, ?4, ?5) + "#, + id_token, + client_id, + 3600, + scope, + code + ) + .execute(&mut **db) + .await + .unwrap(); + + Redirect::temporary(format!("{}?code={}", redirect_uri, code)) } #[derive(Serialize, Deserialize, Debug)] @@ -27,13 +65,66 @@ pub struct Token { pub id_token: String, } -#[post("/oauth/token")] -pub async fn new_token() -> Json { +#[derive(Deserialize, Debug, FromForm)] +#[serde(crate = "rocket::serde")] +struct NewTokenRequest { + client_id: String, + redirect_uri: String, + grant_type: String, + code: String, + client_secret: String, +} + +#[post("/oauth/token", data = "")] +pub async fn new_token(req: Form, mut db: Connection) -> Json { + let oauth = sqlx::query!( + r#" + SELECT o.*, a.* + FROM oauth o + INNER JOIN auth a ON a.token = ?2 + WHERE o.access_token = ?1 + "#, + req.code, + req.code + ) + .fetch_one(&mut **db) + .await + .unwrap(); + + let access_token = main::gen_token(15); + + // Important: setup 'auth' first + sqlx::query!( + r#" + INSERT INTO auth (token, user_id) + VALUES (?1, ?2) + "#, + access_token, + oauth.user_id + ) + .execute(&mut **db) + .await + .unwrap(); + + sqlx::query!( + "UPDATE oauth SET access_token = ?1 WHERE access_token = ?2", + access_token, + req.code + ) + .execute(&mut **db) + .await + .unwrap(); + + sqlx::query!("DELETE FROM auth WHERE token = ?1", req.code) + .execute(&mut **db) + .await + .unwrap(); + Json(Token { - access_token: "9b9d497b-2731-435f-a929-e609ca69dac9".to_string(), + access_token: access_token.to_string(), token_type: "Bearer".to_string(), - expires_in: 3600, - scope: "read write follow push".to_string(), - id_token: "id-token".to_string(), + expires_in: oauth.expires_in, + scope: oauth.scope.to_string(), + id_token: oauth.id_token, }) } diff --git a/ferri-server/src/lib.rs b/ferri-server/src/lib.rs index 176de74..baebb31 100644 --- a/ferri-server/src/lib.rs +++ b/ferri-server/src/lib.rs @@ -2,15 +2,17 @@ use endpoints::{ api::{self, timeline}, custom, inbox, oauth, user, well_known, }; + use main::ap::http; use main::config::Config; use rocket::{ Build, Request, Rocket, build, get, - http::ContentType, + http::{ContentType, Status}, + outcome::IntoOutcome, request::{FromRequest, Outcome}, routes, }; -use rocket_db_pools::{Database, sqlx}; +use rocket_db_pools::{Connection, Database, sqlx}; mod cors; mod endpoints; @@ -34,6 +36,7 @@ async fn activity_endpoint(activity: String) { #[derive(Debug)] struct AuthenticatedUser { username: String, + token: String, actor_id: String, } @@ -48,15 +51,34 @@ enum LoginError { impl<'a> FromRequest<'a> for AuthenticatedUser { type Error = LoginError; async fn from_request(request: &'a Request<'_>) -> Outcome { - let token = request.headers().get_one("Authorization").unwrap(); - let token = token - .strip_prefix("Bearer") - .map(|s| s.trim()) - .unwrap_or(token); - Outcome::Success(AuthenticatedUser { - username: token.to_string(), - actor_id: format!("https://ferri.amy.mov/users/{}", token), - }) + let token = request.headers().get_one("Authorization"); + + if let Some(token) = token { + let token = token + .strip_prefix("Bearer") + .map(|s| s.trim()) + .unwrap_or(token); + + let mut conn = request.guard::>().await.unwrap(); + let auth = sqlx::query!(r#" + SELECT * + FROM auth a + INNER JOIN user u ON a.user_id = u.id + WHERE token = ?1 + "#, token) + .fetch_one(&mut **conn) + .await; + + if let Ok(auth) = auth { + return Outcome::Success(AuthenticatedUser { + token: auth.token, + username: auth.display_name, + actor_id: auth.actor_id, + }) + } + } + + Outcome::Forward(Status::Unauthorized) } } diff --git a/ferri-server/src/types/oauth.rs b/ferri-server/src/types/oauth.rs index 567dd19..8791fe3 100644 --- a/ferri-server/src/types/oauth.rs +++ b/ferri-server/src/types/oauth.rs @@ -3,7 +3,7 @@ use rocket::{ serde::{Deserialize, Serialize}, }; -#[derive(Serialize, Deserialize, Debug, FromForm)] +#[derive(Serialize, Deserialize, Debug, FromForm, Clone)] #[serde(crate = "rocket::serde")] pub struct App { pub client_name: String, diff --git a/migrations/20250423182916_add_auth.sql b/migrations/20250423182916_add_auth.sql new file mode 100644 index 0000000..d3807cb --- /dev/null +++ b/migrations/20250423182916_add_auth.sql @@ -0,0 +1,26 @@ +CREATE TABLE IF NOT EXISTS auth +( + token TEXT PRIMARY KEY NOT NULL, + user_id TEXT NOT NULL, + + FOREIGN KEY(user_id) REFERENCES user(id) +); + +CREATE TABLE IF NOT EXISTS app +( + client_id TEXT PRIMARY KEY NOT NULL, + client_secret TEXT NOT NULL, + scopes TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS oauth +( + id_token TEXT PRIMARY KEY NOT NULL, + client_id TEXT NOT NULL, + expires_in INTEGER NOT NULL, + scope TEXT NOT NULL, + access_token TEXT NOT NULL, + + FOREIGN KEY(access_token) REFERENCES auth(token), + FOREIGN KEY(client_id) REFERENCES app(client_id) +); From 3719fae10253541dd63e9fbb7be79f1592f7d494 Mon Sep 17 00:00:00 2001 From: nullishamy Date: Fri, 25 Apr 2025 16:46:47 +0100 Subject: [PATCH 2/2] feat: logging; fixes; error handloing --- Cargo.lock | 17 +++++++ Cargo.toml | 8 ++- ferri-main/Cargo.toml | 6 ++- ferri-main/src/ap/activity.rs | 21 ++++---- ferri-main/src/ap/http.rs | 4 +- ferri-main/src/ap/mod.rs | 2 +- ferri-main/src/ap/post.rs | 2 +- ferri-main/src/ap/user.rs | 25 +++++++--- ferri-server/Cargo.toml | 8 +-- ferri-server/src/endpoints/api/status.rs | 28 +++++++++-- ferri-server/src/endpoints/api/timeline.rs | 1 - ferri-server/src/endpoints/api/user.rs | 28 +++++++---- ferri-server/src/endpoints/custom.rs | 2 - ferri-server/src/endpoints/inbox.rs | 58 +++++++++++++++------- ferri-server/src/endpoints/oauth.rs | 26 ++++------ ferri-server/src/endpoints/user.rs | 37 ++++++++------ ferri-server/src/endpoints/well_known.rs | 8 +-- ferri-server/src/lib.rs | 53 ++++++++++++++------ 18 files changed, 228 insertions(+), 106 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f1201d4..fc47954 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1264,6 +1264,8 @@ dependencies = [ "serde", "serde_json", "sqlx", + "thiserror 2.0.12", + "tracing", "url", "uuid", ] @@ -2161,6 +2163,8 @@ dependencies = [ "rocket", "rocket_db_pools", "sqlx", + "tracing", + "tracing-subscriber", "url", "uuid", ] @@ -2740,6 +2744,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.19" @@ -2750,12 +2764,15 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e039c64..0f7cb23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,10 @@ rocket = { version = "0.5.1", features = ["json"] } sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite", "macros" ], default-features = false } uuid = { version = "1.16.0", features = ["v4"] } chrono = "0.4.40" -rand = "0.8" \ No newline at end of file +rand = "0.8" +thiserror = "2.0.12" + +tracing = "0.1.40" +tracing-appender = "0.2.3" +tracing-log = "0.2.0" +tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "registry", "smallvec"] } \ No newline at end of file diff --git a/ferri-main/Cargo.toml b/ferri-main/Cargo.toml index 0d91f57..cca1ac5 100644 --- a/ferri-main/Cargo.toml +++ b/ferri-main/Cargo.toml @@ -5,13 +5,15 @@ edition = "2024" [dependencies] serde = { workspace = true } -serde_json = "1.0.140" sqlx = { workspace = true } chrono = { workspace = true } reqwest = { workspace = true } uuid = { workspace = true } +rand = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } base64 = "0.22.1" rsa = { version = "0.9.8", features = ["sha2"] } -rand = { workspace = true } url = "2.5.4" +serde_json = "1.0.140" \ No newline at end of file diff --git a/ferri-main/src/ap/activity.rs b/ferri-main/src/ap/activity.rs index bf834f1..59af769 100644 --- a/ferri-main/src/ap/activity.rs +++ b/ferri-main/src/ap/activity.rs @@ -1,8 +1,9 @@ use crate::ap::{Actor, User, http}; -use chrono::{DateTime, Local}; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::Sqlite; use std::fmt::Debug; +use tracing::{event, Level}; #[derive(Debug, Clone)] pub enum ActivityType { @@ -28,7 +29,7 @@ pub struct Activity { pub id: String, pub ty: ActivityType, pub object: T, - pub published: DateTime, + pub published: DateTime, pub to: Vec, pub cc: Vec, } @@ -39,7 +40,7 @@ impl Default for Activity { id: Default::default(), ty: ActivityType::Unknown, object: Default::default(), - published: Local::now(), + published: Utc::now(), to: Default::default(), cc: Default::default(), } @@ -102,19 +103,18 @@ impl<'a> Outbox<'a> { } pub async fn post(&self, activity: OutgoingActivity) { - dbg!(&activity); + event!(Level::INFO, ?activity, "activity in outbox"); + let raw = RawActivity { context: "https://www.w3.org/ns/activitystreams".to_string(), - id: activity.req.id, + id: activity.req.id.clone(), ty: activity.req.ty.to_raw(), actor: self.user.actor().id().to_string(), object: activity.req.object, published: activity.req.published.to_rfc3339(), }; - dbg!(&raw); - - let follow_res = self + let outbox_res = self .transport .post(activity.to.inbox()) .activity() @@ -127,7 +127,10 @@ impl<'a> Outbox<'a> { .await .unwrap(); - dbg!(follow_res); + event!(Level::DEBUG, + outbox_res, activity = activity.req.id, + "got response for outbox dispatch" + ); } pub fn for_user(user: User, transport: &'a OutboxTransport) -> Outbox<'a> { diff --git a/ferri-main/src/ap/http.rs b/ferri-main/src/ap/http.rs index 5c45cca..ca0e026 100644 --- a/ferri-main/src/ap/http.rs +++ b/ferri-main/src/ap/http.rs @@ -12,6 +12,7 @@ use rsa::{ use base64::prelude::*; use chrono::Utc; +use tracing::{event, Level}; pub struct HttpClient { client: reqwest::Client, @@ -59,7 +60,8 @@ impl RequestBuilder { } pub async fn send(self) -> Result { - dbg!(&self.inner); + event!(Level::DEBUG, ?self.inner, "sending an http request"); + self.inner.send().await } diff --git a/ferri-main/src/ap/mod.rs b/ferri-main/src/ap/mod.rs index 44ae578..f86ba50 100644 --- a/ferri-main/src/ap/mod.rs +++ b/ferri-main/src/ap/mod.rs @@ -12,7 +12,7 @@ pub use user::*; mod post; pub use post::*; -pub const AS_CONTEXT: &'static str = "https://www.w3.org/ns/activitystreams"; +pub const AS_CONTEXT: &str = "https://www.w3.org/ns/activitystreams"; pub fn new_id() -> String { Uuid::new_v4().to_string() diff --git a/ferri-main/src/ap/post.rs b/ferri-main/src/ap/post.rs index 53bfe03..53c33e0 100644 --- a/ferri-main/src/ap/post.rs +++ b/ferri-main/src/ap/post.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::Serialize; use sqlx::Sqlite; -const POST_TYPE: &'static str = "Post"; +const POST_TYPE: &str = "Post"; #[derive(Clone)] pub struct Post { diff --git a/ferri-main/src/ap/user.rs b/ferri-main/src/ap/user.rs index 4178cfb..b080181 100644 --- a/ferri-main/src/ap/user.rs +++ b/ferri-main/src/ap/user.rs @@ -1,5 +1,6 @@ use sqlx::Sqlite; use std::fmt::Debug; +use thiserror::Error; #[derive(Debug, Clone)] pub struct Actor { @@ -20,6 +21,10 @@ impl Actor { pub fn inbox(&self) -> &str { &self.inbox } + + pub fn outbox(&self) -> &str { + &self.outbox + } } #[derive(Debug, Clone)] @@ -30,6 +35,13 @@ pub struct User { display_name: String, } + +#[derive(Error, Debug)] +pub enum UserError { + #[error("user `{0}` not found")] + NotFound(String), +} + impl User { pub fn id(&self) -> &str { &self.id @@ -55,7 +67,7 @@ impl User { format!("https://ferri.amy.mov/users/{}", self.id()) } - pub async fn from_id(uuid: &str, conn: impl sqlx::Executor<'_, Database = Sqlite>) -> User { + pub async fn from_id(uuid: &str, conn: impl sqlx::Executor<'_, Database = Sqlite>) -> Result { let user = sqlx::query!( r#" SELECT u.*, a.id as "actor_own_id", a.inbox, a.outbox @@ -65,10 +77,11 @@ impl User { "#, uuid ) - .fetch_one(conn) - .await - .unwrap(); - User { + .fetch_one(conn) + .await + .map_err(|_| UserError::NotFound(uuid.to_string()))?; + + Ok(User { id: user.id, username: user.username, actor: Actor { @@ -77,7 +90,7 @@ impl User { outbox: user.outbox, }, display_name: user.display_name, - } + }) } pub async fn from_username( diff --git a/ferri-server/Cargo.toml b/ferri-server/Cargo.toml index fba127f..91b72fe 100644 --- a/ferri-server/Cargo.toml +++ b/ferri-server/Cargo.toml @@ -5,12 +5,14 @@ edition = "2024" [dependencies] main = { path = "../ferri-main/" } -rocket = { workspace = true } rocket_db_pools = { version = "0.2.0", features = ["sqlx_sqlite"] } +url = "2.5.4" + +rocket = { workspace = true } reqwest = { workspace = true } sqlx = { workspace = true } uuid = { workspace = true } chrono = { workspace = true } rand = { workspace = true } - -url = "2.5.4" \ No newline at end of file +tracing = { workspace = true } +tracing-subscriber = { workspace = true } \ No newline at end of file diff --git a/ferri-server/src/endpoints/api/status.rs b/ferri-server/src/endpoints/api/status.rs index 92cb6ec..c0e6898 100644 --- a/ferri-server/src/endpoints/api/status.rs +++ b/ferri-server/src/endpoints/api/status.rs @@ -1,17 +1,17 @@ use crate::timeline::TimelineStatus; -use chrono::Local; use main::ap::{self, http::HttpClient}; use rocket::{ FromForm, State, form::Form, post, + get, serde::{Deserialize, Serialize, json::Json}, }; use rocket_db_pools::Connection; use uuid::Uuid; use crate::api::user::CredentialAcount; -use crate::{AuthenticatedUser, Db, types::content}; +use crate::{AuthenticatedUser, Db}; #[derive(Serialize, Deserialize, Debug, FromForm)] #[serde(crate = "rocket::serde")] @@ -19,17 +19,36 @@ pub struct Status { status: String, } +#[derive(Serialize, Deserialize, Debug, FromForm)] +#[serde(crate = "rocket::serde")] +pub struct StatusContext { + ancestors: Vec, + descendants: Vec +} + +#[get("/statuses//context")] +pub async fn status_context( + status: &str, + user: AuthenticatedUser, + mut db: Connection +) -> Json { + Json(StatusContext { + ancestors: vec![], + descendants: vec![], + }) +} + async fn create_status( user: AuthenticatedUser, mut db: Connection, http: &HttpClient, status: &Status, ) -> TimelineStatus { - let user = ap::User::from_id(&user.username, &mut **db).await; + let user = ap::User::from_id(&user.id, &mut **db).await.unwrap(); let outbox = ap::Outbox::for_user(user.clone(), http); let post_id = ap::new_id(); - let now = ap::new_ts(); + let now = ap::now(); let post = ap::Post::from_parts(post_id, status.status.clone(), user.clone()) .to(format!("{}/followers", user.uri())) @@ -55,6 +74,7 @@ async fn create_status( ty: ap::ActivityType::Create, object: post.clone().to_ap(), to: vec![format!("{}/followers", user.uri())], + published: now, cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()], ..Default::default() }; diff --git a/ferri-server/src/endpoints/api/timeline.rs b/ferri-server/src/endpoints/api/timeline.rs index 3a3ec13..ae1f771 100644 --- a/ferri-server/src/endpoints/api/timeline.rs +++ b/ferri-server/src/endpoints/api/timeline.rs @@ -37,7 +37,6 @@ pub async fn home( limit: i64, user: AuthenticatedUser, ) -> Json> { - dbg!(user); let posts = sqlx::query!( r#" SELECT p.id as "post_id", u.id as "user_id", p.content, p.uri as "post_uri", diff --git a/ferri-server/src/endpoints/api/user.rs b/ferri-server/src/endpoints/api/user.rs index edf84f2..a6be6aa 100644 --- a/ferri-server/src/endpoints/api/user.rs +++ b/ferri-server/src/endpoints/api/user.rs @@ -5,6 +5,7 @@ use rocket::{ }; use rocket_db_pools::Connection; use uuid::Uuid; +use rocket::response::status::NotFound; use crate::timeline::{TimelineAccount, TimelineStatus}; use crate::{AuthenticatedUser, Db, http::HttpClient}; @@ -62,9 +63,12 @@ pub async fn new_follow( http: &State, uuid: &str, user: AuthenticatedUser, -) { +) -> Result<(), NotFound> { let follower = ap::User::from_actor_id(&user.actor_id, &mut **db).await; - let followed = ap::User::from_id(uuid, &mut **db).await; + + let followed = ap::User::from_id(uuid, &mut **db) + .await + .map_err(|e| NotFound(e.to_string()))?; let outbox = ap::Outbox::for_user(follower.clone(), http.inner()); @@ -83,6 +87,8 @@ pub async fn new_follow( req.save(&mut **db).await; outbox.post(req).await; + + Ok(()) } #[get("/accounts/")] @@ -90,10 +96,12 @@ pub async fn account( mut db: Connection, uuid: &str, user: AuthenticatedUser, -) -> Json { - let user = ap::User::from_id(uuid, &mut **db).await; +) -> Result, NotFound> { + let user = ap::User::from_id(uuid, &mut **db) + .await + .map_err(|e| NotFound(e.to_string()))?; let user_uri = format!("https://ferri.amy.mov/users/{}", user.username()); - Json(CredentialAcount { + Ok(Json(CredentialAcount { id: user.id().to_string(), username: user.username().to_string(), acct: user.username().to_string(), @@ -112,7 +120,7 @@ pub async fn account( following_count: 1, statuses_count: 1, last_status_at: "2025-04-10T22:14:34Z".to_string(), - }) + })) } #[get("/accounts//statuses?")] @@ -121,8 +129,10 @@ pub async fn statuses( uuid: &str, limit: Option, user: AuthenticatedUser, -) -> Json> { - let user = ap::User::from_id(uuid, &mut **db).await; +) -> Result>, NotFound> { + let user = ap::User::from_id(uuid, &mut **db) + .await + .map_err(|e| NotFound(e.to_string()))?; let uid = user.id(); let posts = sqlx::query!( @@ -181,5 +191,5 @@ pub async fn statuses( }); } - Json(out) + Ok(Json(out)) } diff --git a/ferri-server/src/endpoints/custom.rs b/ferri-server/src/endpoints/custom.rs index 25dca75..ff3bea8 100644 --- a/ferri-server/src/endpoints/custom.rs +++ b/ferri-server/src/endpoints/custom.rs @@ -118,7 +118,5 @@ pub async fn test(http: &State) -> &'static str { .await .unwrap(); - dbg!(follow); - "Hello, world!" } diff --git a/ferri-server/src/endpoints/inbox.rs b/ferri-server/src/endpoints/inbox.rs index 3291799..5d1972a 100644 --- a/ferri-server/src/endpoints/inbox.rs +++ b/ferri-server/src/endpoints/inbox.rs @@ -6,6 +6,7 @@ use rocket_db_pools::Connection; use sqlx::Sqlite; use url::Url; use uuid::Uuid; +use tracing::{event, span, Level, debug, warn, info}; use crate::{ Db, @@ -14,12 +15,12 @@ use crate::{ }; fn handle_delete_activity(activity: activity::DeleteActivity) { - dbg!(activity); + warn!(?activity, "unimplemented delete activity"); } async fn create_actor( user: &Person, - actor: String, + actor: &str, conn: impl sqlx::Executor<'_, Database = Sqlite>, ) { sqlx::query!( @@ -39,7 +40,7 @@ async fn create_actor( async fn create_user( user: &Person, - actor: String, + actor: &str, conn: impl sqlx::Executor<'_, Database = Sqlite>, ) { // HACK: Allow us to formulate a `user@host` username by assuming the actor is on the same host as the user @@ -84,7 +85,7 @@ async fn create_follow( } async fn handle_follow_activity( - followed_account: String, + followed_account: &str, activity: activity::FollowActivity, http: &HttpClient, mut db: Connection, @@ -99,8 +100,8 @@ async fn handle_follow_activity( .await .unwrap(); - create_actor(&user, activity.actor.clone(), &mut **db).await; - create_user(&user, activity.actor.clone(), &mut **db).await; + create_actor(&user, &activity.actor, &mut **db).await; + create_user(&user, &activity.actor, &mut **db).await; create_follow(&activity, &mut **db).await; let follower = ap::User::from_actor_id(&activity.actor, &mut **db).await; @@ -128,11 +129,17 @@ async fn handle_follow_activity( } async fn handle_like_activity(activity: activity::LikeActivity, mut db: Connection) { + warn!(?activity, "unimplemented like activity"); + let target_post = sqlx::query!("SELECT * FROM post WHERE uri = ?1", activity.object) .fetch_one(&mut **db) - .await - .unwrap(); - dbg!(&target_post); + .await; + + if let Ok(post) = target_post { + warn!(?post, "tried to like post"); + } else { + warn!(post = ?activity.object, "could not find post"); + } } async fn handle_create_activity( @@ -141,6 +148,8 @@ async fn handle_create_activity( mut db: Connection, ) { assert!(&activity.object.ty == "Note"); + debug!("resolving user {}", activity.actor); + let user = http .get(&activity.actor) .activity() @@ -151,10 +160,14 @@ async fn handle_create_activity( .await .unwrap(); - create_actor(&user, activity.actor.clone(), &mut **db).await; - create_user(&user, activity.actor.clone(), &mut **db).await; + debug!("creating actor {}", activity.actor); + create_actor(&user, &activity.actor, &mut **db).await; + + debug!("creating user {}", activity.actor); + create_user(&user, &activity.actor, &mut **db).await; let user = ap::User::from_actor_id(&activity.actor, &mut **db).await; + debug!("user created {:?}", user); let user_id = user.id(); let now = Local::now().to_rfc3339(); @@ -162,6 +175,8 @@ async fn handle_create_activity( let post_id = Uuid::new_v4().to_string(); let uri = activity.id; + info!(post_id, "creating post"); + sqlx::query!( r#" INSERT INTO post (id, uri, user_id, content, created_at) @@ -173,14 +188,19 @@ async fn handle_create_activity( content, now ) - .execute(&mut **db) - .await - .unwrap(); + .execute(&mut **db) + .await + .unwrap(); } #[post("/users//inbox", data = "")] -pub async fn inbox(db: Connection, http: &State, user: String, body: String) { +pub async fn inbox(db: Connection, http: &State, user: &str, body: String) { let min = serde_json::from_str::(&body).unwrap(); + let inbox_span = span!(Level::INFO, "inbox-post", user_id = user); + + let _enter = inbox_span.enter(); + event!(Level::INFO, ?min, "received an activity"); + match min.ty.as_str() { "Delete" => { let activity = serde_json::from_str::(&body).unwrap(); @@ -198,11 +218,11 @@ pub async fn inbox(db: Connection, http: &State, user: String, b let activity = serde_json::from_str::(&body).unwrap(); handle_like_activity(activity, db).await; } - unknown => { - eprintln!("WARN: Unknown activity '{}' - {}", unknown, body); + act => { + warn!(act, body, "unknown activity"); } } - dbg!(min); - println!("Body in inbox: {}", body); + debug!("body in inbox: {}", body); + drop(_enter) } diff --git a/ferri-server/src/endpoints/oauth.rs b/ferri-server/src/endpoints/oauth.rs index 7045ec1..81c8d55 100644 --- a/ferri-server/src/endpoints/oauth.rs +++ b/ferri-server/src/endpoints/oauth.rs @@ -23,10 +23,10 @@ pub async fn authorize( // This will act as a token for the user, but we will in future say that it expires very shortly // and can only be used for obtaining an access token etc sqlx::query!( - r#" - INSERT INTO auth (token, user_id) - VALUES (?1, ?2) - "#, + r#" + INSERT INTO auth (token, user_id) + VALUES (?1, ?2) + "#, code, user_id ) @@ -35,7 +35,7 @@ pub async fn authorize( .unwrap(); let id_token = main::gen_token(10); - + // Add an oauth entry for the `code` which /oauth/token will rewrite sqlx::query!( r#" @@ -67,7 +67,7 @@ pub struct Token { #[derive(Deserialize, Debug, FromForm)] #[serde(crate = "rocket::serde")] -struct NewTokenRequest { +pub struct NewTokenRequest { client_id: String, redirect_uri: String, grant_type: String, @@ -77,19 +77,15 @@ struct NewTokenRequest { #[post("/oauth/token", data = "")] pub async fn new_token(req: Form, mut db: Connection) -> Json { - let oauth = sqlx::query!( - r#" + let oauth = sqlx::query!(" SELECT o.*, a.* FROM oauth o INNER JOIN auth a ON a.token = ?2 WHERE o.access_token = ?1 - "#, - req.code, - req.code - ) - .fetch_one(&mut **db) - .await - .unwrap(); + ", req.code, req.code) + .fetch_one(&mut **db) + .await + .unwrap(); let access_token = main::gen_token(15); diff --git a/ferri-server/src/endpoints/user.rs b/ferri-server/src/endpoints/user.rs index 7cc0550..3963926 100644 --- a/ferri-server/src/endpoints/user.rs +++ b/ferri-server/src/endpoints/user.rs @@ -1,6 +1,7 @@ use main::ap; use rocket::{get, http::ContentType, serde::json::Json}; use rocket_db_pools::Connection; +use rocket::response::status::NotFound; use crate::{ Db, @@ -20,7 +21,6 @@ pub async fn inbox(user: String) -> Json { #[get("/users//outbox")] pub async fn outbox(user: String) -> Json { - dbg!(&user); Json(OrderedCollection { ty: "OrderedCollection".to_string(), total_items: 0, @@ -29,8 +29,11 @@ pub async fn outbox(user: String) -> Json { } #[get("/users//followers")] -pub async fn followers(mut db: Connection, uuid: &str) -> Json { - let target = ap::User::from_id(uuid, &mut **db).await; +pub async fn followers(mut db: Connection, uuid: &str) -> Result, NotFound> { + let target = ap::User::from_id(uuid, &mut **db) + .await + .map_err(|e| NotFound(e.to_string()))?; + let actor_id = target.actor_id(); let followers = sqlx::query!( @@ -44,19 +47,22 @@ pub async fn followers(mut db: Connection, uuid: &str) -> Json>(), - }) + })) } #[get("/users//following")] -pub async fn following(mut db: Connection, uuid: &str) -> Json { - let target = ap::User::from_id(uuid, &mut **db).await; +pub async fn following(mut db: Connection, uuid: &str) -> Result, NotFound> { + let target = ap::User::from_id(uuid, &mut **db) + .await + .map_err(|e| NotFound(e.to_string()))?; + let actor_id = target.actor_id(); let following = sqlx::query!( @@ -70,14 +76,14 @@ pub async fn following(mut db: Connection, uuid: &str) -> Json>(), - }) + })) } #[get("/users//posts/")] @@ -111,14 +117,17 @@ pub async fn post( } #[get("/users/")] -pub async fn user(mut db: Connection, uuid: &str) -> (ContentType, Json) { - let user = ap::User::from_id(uuid, &mut **db).await; - ( +pub async fn user(mut db: Connection, uuid: &str) -> Result<(ContentType, Json), NotFound> { + let user = ap::User::from_id(uuid, &mut **db) + .await + .map_err(|e| NotFound(e.to_string()))?; + + Ok(( activity_type(), Json(Person { context: "https://www.w3.org/ns/activitystreams".to_string(), ty: "Person".to_string(), - id: user.id().to_string(), + id: format!("https://ferri.amy.mov/users/{}", user.id()), name: user.username().to_string(), preferred_username: user.display_name().to_string(), followers: format!("https://ferri.amy.mov/users/{}/followers", uuid), @@ -132,5 +141,5 @@ pub async fn user(mut db: Connection, uuid: &str) -> (ContentType, Json &'static str { // https://mastodon.social/.well-known/webfinger?resource=acct:gargron@mastodon.social #[get("/.well-known/webfinger?")] pub async fn webfinger(mut db: Connection, resource: &str) -> Json { - println!("Webfinger request for {}", resource); + info!(?resource, "incoming webfinger request"); + let acct = resource.strip_prefix("acct:").unwrap(); let (user, _) = acct.split_once("@").unwrap(); let user = ap::User::from_username(user, &mut **db).await; @@ -29,13 +31,13 @@ pub async fn webfinger(mut db: Connection, resource: &str) -> Json) -> (ContentType, &'static str) { - dbg!(cfg); (ContentType::HTML, "

hello

") } #[get("/activities/")] async fn activity_endpoint(activity: String) { - dbg!(activity); + } #[derive(Debug)] struct AuthenticatedUser { - username: String, - token: String, - actor_id: String, + pub username: String, + pub id: String, + pub token: String, + pub actor_id: String, } #[derive(Debug)] @@ -52,7 +54,7 @@ impl<'a> FromRequest<'a> for AuthenticatedUser { type Error = LoginError; async fn from_request(request: &'a Request<'_>) -> Outcome { let token = request.headers().get_one("Authorization"); - + if let Some(token) = token { let token = token .strip_prefix("Bearer") @@ -60,29 +62,49 @@ impl<'a> FromRequest<'a> for AuthenticatedUser { .unwrap_or(token); let mut conn = request.guard::>().await.unwrap(); - let auth = sqlx::query!(r#" + let auth = sqlx::query!( + r#" SELECT * FROM auth a INNER JOIN user u ON a.user_id = u.id WHERE token = ?1 - "#, token) - .fetch_one(&mut **conn) - .await; + "#, + token + ) + .fetch_one(&mut **conn) + .await; if let Ok(auth) = auth { return Outcome::Success(AuthenticatedUser { token: auth.token, + id: auth.id, username: auth.display_name, actor_id: auth.actor_id, - }) - } - } - + }); + } + } + Outcome::Forward(Status::Unauthorized) } } + + pub fn launch(cfg: Config) -> Rocket { + let format = fmt::format() + .with_ansi(true) + .without_time() + .with_level(true) + .with_target(false) + .with_thread_names(false) + .with_source_location(false) + .compact(); + + tracing_subscriber::fmt() + .event_format(format) + .with_writer(std::io::stdout) + .init(); + let http_client = http::HttpClient::new(); build() .manage(cfg) @@ -114,6 +136,7 @@ pub fn launch(cfg: Config) -> Rocket { .mount( "/api/v1", routes![ + api::status::status_context, api::status::new_status, api::status::new_status_json, api::user::new_follow,