From 8477c07014a9cd030ac0610c1abb4a26b5fe6448 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Wed, 15 Nov 2023 01:24:50 +0100 Subject: [PATCH] actual federation types --- src/database.rs | 30 +++ src/error.rs | 2 + src/federation/activities/accept.rs | 17 +- src/federation/activities/follow.rs | 35 ++-- src/federation/activities/mod.rs | 1 + src/federation/activities/update.rs | 59 ++++++ src/federation/mod.rs | 37 ++++ .../objects/{post.rs => article.rs} | 48 ++--- src/federation/objects/instance.rs | 172 ++++++++++++++++++ src/federation/objects/mod.rs | 3 +- src/federation/objects/person.rs | 44 +---- src/federation/routes.rs | 2 +- src/instance.rs | 70 ------- src/main.rs | 7 +- 14 files changed, 360 insertions(+), 167 deletions(-) create mode 100644 src/database.rs create mode 100644 src/federation/activities/update.rs rename src/federation/objects/{post.rs => article.rs} (63%) create mode 100644 src/federation/objects/instance.rs delete mode 100644 src/instance.rs diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..9bbc807 --- /dev/null +++ b/src/database.rs @@ -0,0 +1,30 @@ +use crate::error::Error; +use crate::federation::objects::instance::DbInstance; +use crate::federation::objects::{article::DbArticle, person::DbUser}; +use anyhow::anyhow; +use std::sync::{Arc, Mutex}; + +pub type DatabaseHandle = Arc; + +/// Our "database" which contains all known posts and users (local and federated) +pub struct Database { + pub instances: Mutex>, + pub users: Mutex>, + pub posts: Mutex>, +} + +impl Database { + pub fn local_user(&self) -> DbUser { + let lock = self.users.lock().unwrap(); + lock.first().unwrap().clone() + } + + pub fn read_user(&self, name: &str) -> Result { + let db_user = self.local_user(); + if name == db_user.name { + Ok(db_user) + } else { + Err(anyhow!("Invalid user {name}").into()) + } + } +} diff --git a/src/error.rs b/src/error.rs index 52be184..a0b7a85 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,6 +2,8 @@ use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use std::fmt::{Display, Formatter}; +pub type MyResult = Result; + #[derive(Debug)] pub struct Error(pub(crate) anyhow::Error); diff --git a/src/federation/activities/accept.rs b/src/federation/activities/accept.rs index c073967..7b8aa6b 100644 --- a/src/federation/activities/accept.rs +++ b/src/federation/activities/accept.rs @@ -1,7 +1,7 @@ -use crate::{ - federation::activities::follow::Follow, federation::objects::person::DbUser, - instance::DatabaseHandle, -}; +use crate::error::MyResult; +use crate::federation::objects::instance::DbInstance; +use crate::utils::generate_object_id; +use crate::{database::DatabaseHandle, federation::activities::follow::Follow}; use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, kinds::activity::AcceptType, traits::ActivityHandler, }; @@ -11,7 +11,7 @@ use url::Url; #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Accept { - actor: ObjectId, + actor: ObjectId, object: Follow, #[serde(rename = "type")] kind: AcceptType, @@ -19,13 +19,14 @@ pub struct Accept { } impl Accept { - pub fn new(actor: ObjectId, object: Follow, id: Url) -> Accept { - Accept { + pub fn new(actor: ObjectId, object: Follow) -> MyResult { + let id = generate_object_id(actor.inner().domain().unwrap())?; + Ok(Accept { actor, object, kind: Default::default(), id, - } + }) } } diff --git a/src/federation/activities/follow.rs b/src/federation/activities/follow.rs index 6f5de8d..90d5089 100644 --- a/src/federation/activities/follow.rs +++ b/src/federation/activities/follow.rs @@ -1,7 +1,6 @@ -use crate::{ - federation::activities::accept::Accept, federation::objects::person::DbUser, - generate_object_id, instance::DatabaseHandle, -}; +use crate::error::MyResult; +use crate::federation::objects::instance::DbInstance; +use crate::{database::DatabaseHandle, federation::activities::accept::Accept, generate_object_id}; use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, @@ -14,21 +13,22 @@ use url::Url; #[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct Follow { - pub(crate) actor: ObjectId, - pub(crate) object: ObjectId, + pub(crate) actor: ObjectId, + pub(crate) object: ObjectId, #[serde(rename = "type")] kind: FollowType, id: Url, } impl Follow { - pub fn new(actor: ObjectId, object: ObjectId, id: Url) -> Follow { - Follow { + pub fn new(actor: ObjectId, object: ObjectId) -> MyResult { + let id = generate_object_id(actor.inner().domain().unwrap())?; + Ok(Follow { actor, object, kind: Default::default(), id, - } + }) } } @@ -49,22 +49,19 @@ impl ActivityHandler for Follow { Ok(()) } - // Ignore clippy false positive: https://github.com/rust-lang/rust-clippy/issues/6446 - #[allow(clippy::await_holding_lock)] async fn receive(self, data: &Data) -> Result<(), Self::Error> { // add to followers - let local_user = { - let mut users = data.users.lock().unwrap(); - let local_user = users.first_mut().unwrap(); - local_user.followers.push(self.actor.inner().clone()); - local_user.clone() + let local_instance = { + let mut instances = data.instances.lock().unwrap(); + let local_instance = instances.first_mut().unwrap(); + local_instance.followers.push(self.actor.inner().clone()); + local_instance.clone() }; // send back an accept let follower = self.actor.dereference(data).await?; - let id = generate_object_id(data.domain())?; - let accept = Accept::new(local_user.ap_id.clone(), self, id.clone()); - local_user + let accept = Accept::new(local_instance.ap_id.clone(), self)?; + local_instance .send(accept, vec![follower.shared_inbox_or_inbox()], data) .await?; Ok(()) diff --git a/src/federation/activities/mod.rs b/src/federation/activities/mod.rs index 5e2ad4b..53549be 100644 --- a/src/federation/activities/mod.rs +++ b/src/federation/activities/mod.rs @@ -1,2 +1,3 @@ pub mod accept; pub mod follow; +pub mod update; diff --git a/src/federation/activities/update.rs b/src/federation/activities/update.rs new file mode 100644 index 0000000..ebba508 --- /dev/null +++ b/src/federation/activities/update.rs @@ -0,0 +1,59 @@ +use crate::federation::objects::article::DbArticle; +use crate::{database::DatabaseHandle, federation::objects::person::DbUser}; +use activitypub_federation::kinds::activity::UpdateType; +use activitypub_federation::{config::Data, fetch::object_id::ObjectId, traits::ActivityHandler}; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::error::MyResult; +use crate::utils::generate_object_id; + +/// represents a diff between two strings +#[derive(Deserialize, Serialize, Debug)] +pub struct Diff {} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Update { + actor: ObjectId, + object: ObjectId, + result: Diff, + #[serde(rename = "type")] + kind: UpdateType, + id: Url, +} + +impl Update { + pub fn new(actor: ObjectId, object: ObjectId) -> MyResult { + let id = generate_object_id(actor.inner().domain().unwrap())?; + Ok(Update { + actor, + object, + result: Diff {}, + kind: Default::default(), + id, + }) + } +} + +#[async_trait::async_trait] +impl ActivityHandler for Update { + type DataType = DatabaseHandle; + type Error = crate::error::Error; + + fn id(&self) -> &Url { + &self.id + } + + fn actor(&self) -> &Url { + self.actor.inner() + } + + async fn verify(&self, _data: &Data) -> Result<(), Self::Error> { + Ok(()) + } + + async fn receive(self, _data: &Data) -> Result<(), Self::Error> { + Ok(()) + } +} diff --git a/src/federation/mod.rs b/src/federation/mod.rs index 67e6c0f..0033d5e 100644 --- a/src/federation/mod.rs +++ b/src/federation/mod.rs @@ -1,3 +1,40 @@ +use crate::database::{Database, DatabaseHandle}; +use crate::error::Error; +use activitypub_federation::config::{FederationConfig, UrlVerifier}; +use async_trait::async_trait; +use std::sync::{Arc, Mutex}; +use url::Url; + pub mod activities; pub mod objects; pub mod routes; + +pub async fn federation_config(hostname: &str) -> Result, Error> { + let database = Arc::new(Database { + instances: Mutex::new(vec![]), + users: Mutex::new(vec![]), + posts: Mutex::new(vec![]), + }); + let config = FederationConfig::builder() + .domain(hostname) + .app_data(database) + .debug(true) + .build() + .await?; + Ok(config) +} + +/// Use this to store your federation blocklist, or a database connection needed to retrieve it. +#[derive(Clone)] +struct MyUrlVerifier(); + +#[async_trait] +impl UrlVerifier for MyUrlVerifier { + async fn verify(&self, url: &Url) -> Result<(), &'static str> { + if url.domain() == Some("malicious.com") { + Err("malicious domain") + } else { + Ok(()) + } + } +} diff --git a/src/federation/objects/post.rs b/src/federation/objects/article.rs similarity index 63% rename from src/federation/objects/post.rs rename to src/federation/objects/article.rs index 857e4f4..0598327 100644 --- a/src/federation/objects/post.rs +++ b/src/federation/objects/article.rs @@ -1,10 +1,10 @@ -use crate::{ - error::Error, federation::objects::person::DbUser, generate_object_id, instance::DatabaseHandle, -}; +use crate::federation::objects::instance::DbInstance; +use crate::{database::DatabaseHandle, error::Error, generate_object_id}; +use activitypub_federation::kinds::object::ArticleType; use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, - kinds::{object::NoteType, public}, + kinds::public, protocol::{helpers::deserialize_one_or_many, verification::verify_domains_match}, traits::Object, }; @@ -12,20 +12,20 @@ use serde::{Deserialize, Serialize}; use url::Url; #[derive(Clone, Debug)] -pub struct DbPost { +pub struct DbArticle { pub text: String, - pub ap_id: ObjectId, - pub creator: ObjectId, + pub ap_id: ObjectId, + pub instance: ObjectId, pub local: bool, } -impl DbPost { - pub fn new(text: String, creator: ObjectId) -> Result { - let ap_id = generate_object_id(creator.inner().domain().unwrap())?.into(); - Ok(DbPost { +impl DbArticle { + pub fn new(text: String, attributed_to: ObjectId) -> Result { + let ap_id = generate_object_id(attributed_to.inner().domain().unwrap())?.into(); + Ok(DbArticle { text, ap_id, - creator, + instance: attributed_to, local: true, }) } @@ -33,20 +33,20 @@ impl DbPost { #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] -pub struct Note { +pub struct Article { #[serde(rename = "type")] - kind: NoteType, - id: ObjectId, - pub(crate) attributed_to: ObjectId, + kind: ArticleType, + id: ObjectId, + pub(crate) attributed_to: ObjectId, #[serde(deserialize_with = "deserialize_one_or_many")] pub(crate) to: Vec, content: String, } #[async_trait::async_trait] -impl Object for DbPost { +impl Object for DbArticle { type DataType = DatabaseHandle; - type Kind = Note; + type Kind = Article; type Error = Error; async fn read_from_id( @@ -62,12 +62,12 @@ impl Object for DbPost { } async fn into_json(self, data: &Data) -> Result { - let creator = self.creator.dereference_local(data).await?; - Ok(Note { + let instance = self.instance.dereference_local(data).await?; + Ok(Article { kind: Default::default(), id: self.ap_id, - attributed_to: self.creator, - to: vec![public(), creator.followers_url()?], + attributed_to: self.instance, + to: vec![public(), instance.followers_url()?], content: self.text, }) } @@ -82,10 +82,10 @@ impl Object for DbPost { } async fn from_json(json: Self::Kind, data: &Data) -> Result { - let post = DbPost { + let post = DbArticle { text: json.content, ap_id: json.id, - creator: json.attributed_to, + instance: json.attributed_to, local: false, }; diff --git a/src/federation/objects/instance.rs b/src/federation/objects/instance.rs new file mode 100644 index 0000000..1cf90e6 --- /dev/null +++ b/src/federation/objects/instance.rs @@ -0,0 +1,172 @@ +use crate::error::Error; +use crate::{ + database::DatabaseHandle, + federation::activities::{accept::Accept, follow::Follow}, +}; +use activitypub_federation::kinds::actor::ServiceType; +use activitypub_federation::{ + activity_queue::send_activity, + config::Data, + fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor}, + http_signatures::generate_actor_keypair, + protocol::{context::WithContext, public_key::PublicKey, verification::verify_domains_match}, + traits::{ActivityHandler, Actor, Object}, +}; +use chrono::{Local, NaiveDateTime}; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use url::Url; + +#[derive(Debug, Clone)] +pub struct DbInstance { + pub ap_id: ObjectId, + pub inbox: Url, + public_key: String, + private_key: Option, + last_refreshed_at: NaiveDateTime, + pub followers: Vec, + pub local: bool, +} + +/// List of all activities which this actor can receive. +#[derive(Deserialize, Serialize, Debug)] +#[serde(untagged)] +#[enum_delegate::implement(ActivityHandler)] +pub enum PersonAcceptedActivities { + Follow(Follow), + Accept(Accept), +} + +impl DbInstance { + pub fn new(hostname: &str) -> Result { + let ap_id = Url::parse(&format!("http://{}", hostname))?.into(); + let inbox = Url::parse(&format!("http://{}/inbox", hostname))?; + let keypair = generate_actor_keypair()?; + Ok(DbInstance { + ap_id, + inbox, + public_key: keypair.public_key, + private_key: Some(keypair.private_key), + last_refreshed_at: Local::now().naive_local(), + followers: vec![], + local: true, + }) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Instance { + #[serde(rename = "type")] + kind: ServiceType, + id: ObjectId, + inbox: Url, + public_key: PublicKey, +} + +impl DbInstance { + pub fn followers(&self) -> &Vec { + &self.followers + } + + pub fn followers_url(&self) -> Result { + Ok(Url::parse(&format!("{}/followers", self.ap_id.inner()))?) + } + + pub async fn follow(&self, other: &str, data: &Data) -> Result<(), Error> { + let other: DbInstance = webfinger_resolve_actor(other, data).await?; + let follow = Follow::new(self.ap_id.clone(), other.ap_id.clone())?; + self.send(follow, vec![other.shared_inbox_or_inbox()], data) + .await?; + Ok(()) + } + + pub(crate) async fn send( + &self, + activity: Activity, + recipients: Vec, + data: &Data, + ) -> Result<(), ::Error> + where + Activity: ActivityHandler + Serialize + Debug + Send + Sync, + ::Error: From + From, + { + let activity = WithContext::new_default(activity); + send_activity(activity, self, recipients, data).await?; + Ok(()) + } +} + +#[async_trait::async_trait] +impl Object for DbInstance { + type DataType = DatabaseHandle; + type Kind = Instance; + type Error = Error; + + fn last_refreshed_at(&self) -> Option { + Some(self.last_refreshed_at) + } + + async fn read_from_id( + object_id: Url, + data: &Data, + ) -> Result, Self::Error> { + let users = data.instances.lock().unwrap(); + let res = users + .clone() + .into_iter() + .find(|u| u.ap_id.inner() == &object_id); + Ok(res) + } + + async fn into_json(self, _data: &Data) -> Result { + Ok(Instance { + kind: Default::default(), + id: self.ap_id.clone(), + inbox: self.inbox.clone(), + public_key: self.public_key(), + }) + } + + async fn verify( + json: &Self::Kind, + expected_domain: &Url, + _data: &Data, + ) -> Result<(), Self::Error> { + verify_domains_match(json.id.inner(), expected_domain)?; + Ok(()) + } + + async fn from_json(json: Self::Kind, data: &Data) -> Result { + let instance = DbInstance { + ap_id: json.id, + inbox: json.inbox, + public_key: json.public_key.public_key_pem, + private_key: None, + last_refreshed_at: Local::now().naive_local(), + followers: vec![], + local: false, + }; + let mut mutex = data.instances.lock().unwrap(); + mutex.push(instance.clone()); + Ok(instance) + } +} + +impl Actor for DbInstance { + fn id(&self) -> Url { + self.ap_id.inner().clone() + } + + fn public_key_pem(&self) -> &str { + &self.public_key + } + + fn private_key_pem(&self) -> Option { + self.private_key.clone() + } + + fn inbox(&self) -> Url { + self.inbox.clone() + } +} diff --git a/src/federation/objects/mod.rs b/src/federation/objects/mod.rs index b5239ab..a25ea4c 100644 --- a/src/federation/objects/mod.rs +++ b/src/federation/objects/mod.rs @@ -1,2 +1,3 @@ +pub mod article; +pub mod instance; pub mod person; -pub mod post; diff --git a/src/federation/objects/person.rs b/src/federation/objects/person.rs index cabb5b4..aa22997 100644 --- a/src/federation/objects/person.rs +++ b/src/federation/objects/person.rs @@ -1,16 +1,14 @@ use crate::error::Error; use crate::{ + database::DatabaseHandle, federation::activities::{accept::Accept, follow::Follow}, - instance::DatabaseHandle, - utils::generate_object_id, }; use activitypub_federation::{ - activity_queue::send_activity, config::Data, - fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor}, + fetch::object_id::ObjectId, http_signatures::generate_actor_keypair, kinds::actor::PersonType, - protocol::{context::WithContext, public_key::PublicKey, verification::verify_domains_match}, + protocol::{public_key::PublicKey, verification::verify_domains_match}, traits::{ActivityHandler, Actor, Object}, }; use chrono::{Local, NaiveDateTime}; @@ -23,9 +21,7 @@ pub struct DbUser { pub name: String, pub ap_id: ObjectId, pub inbox: Url, - // exists for all users (necessary to verify http signatures) public_key: String, - // exists only for local users private_key: Option, last_refreshed_at: NaiveDateTime, pub followers: Vec, @@ -70,40 +66,6 @@ pub struct Person { public_key: PublicKey, } -impl DbUser { - pub fn followers(&self) -> &Vec { - &self.followers - } - - pub fn followers_url(&self) -> Result { - Ok(Url::parse(&format!("{}/followers", self.ap_id.inner()))?) - } - - pub async fn follow(&self, other: &str, data: &Data) -> Result<(), Error> { - let other: DbUser = webfinger_resolve_actor(other, data).await?; - let id = generate_object_id(data.domain())?; - let follow = Follow::new(self.ap_id.clone(), other.ap_id.clone(), id.clone()); - self.send(follow, vec![other.shared_inbox_or_inbox()], data) - .await?; - Ok(()) - } - - pub(crate) async fn send( - &self, - activity: Activity, - recipients: Vec, - data: &Data, - ) -> Result<(), ::Error> - where - Activity: ActivityHandler + Serialize + Debug + Send + Sync, - ::Error: From + From, - { - let activity = WithContext::new_default(activity); - send_activity(activity, self, recipients, data).await?; - Ok(()) - } -} - #[async_trait::async_trait] impl Object for DbUser { type DataType = DatabaseHandle; diff --git a/src/federation/routes.rs b/src/federation/routes.rs index 74c8abd..c95a18c 100644 --- a/src/federation/routes.rs +++ b/src/federation/routes.rs @@ -1,6 +1,6 @@ +use crate::database::DatabaseHandle; use crate::error::Error; use crate::federation::objects::person::{DbUser, Person, PersonAcceptedActivities}; -use crate::instance::DatabaseHandle; use activitypub_federation::axum::inbox::{receive_activity, ActivityData}; use activitypub_federation::axum::json::FederationJson; use activitypub_federation::config::Data; diff --git a/src/instance.rs b/src/instance.rs deleted file mode 100644 index 4add418..0000000 --- a/src/instance.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::error::Error; -use crate::federation::objects::{person::DbUser, post::DbPost}; -use activitypub_federation::config::{FederationConfig, UrlVerifier}; -use anyhow::anyhow; -use async_trait::async_trait; -use std::sync::{Arc, Mutex}; -use url::Url; - -pub async fn federation_config( - hostname: &str, - name: String, -) -> Result, Error> { - let mut system_user = DbUser::new(hostname, "system".into())?; - system_user.ap_id = Url::parse(&format!("http://{}/", hostname))?.into(); - - let local_user = DbUser::new(hostname, name)?; - let database = Arc::new(Database { - system_user: system_user.clone(), - users: Mutex::new(vec![local_user]), - posts: Mutex::new(vec![]), - }); - let config = FederationConfig::builder() - .domain(hostname) - .signed_fetch_actor(&system_user) - .app_data(database) - .debug(true) - .build() - .await?; - Ok(config) -} - -pub type DatabaseHandle = Arc; - -/// Our "database" which contains all known posts and users (local and federated) -pub struct Database { - pub system_user: DbUser, - pub users: Mutex>, - pub posts: Mutex>, -} - -/// Use this to store your federation blocklist, or a database connection needed to retrieve it. -#[derive(Clone)] -struct MyUrlVerifier(); - -#[async_trait] -impl UrlVerifier for MyUrlVerifier { - async fn verify(&self, url: &Url) -> Result<(), &'static str> { - if url.domain() == Some("malicious.com") { - Err("malicious domain") - } else { - Ok(()) - } - } -} - -impl Database { - pub fn local_user(&self) -> DbUser { - let lock = self.users.lock().unwrap(); - lock.first().unwrap().clone() - } - - pub fn read_user(&self, name: &str) -> Result { - let db_user = self.local_user(); - if name == db_user.name { - Ok(db_user) - } else { - Err(anyhow!("Invalid user {name}").into()) - } - } -} diff --git a/src/main.rs b/src/main.rs index be66f90..ba65eb2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use crate::{instance::federation_config, utils::generate_object_id}; +use crate::utils::generate_object_id; use error::Error; use tracing::log::LevelFilter; @@ -10,12 +10,13 @@ use axum::{ use crate::federation::routes::http_get_user; use crate::federation::routes::http_post_user_inbox; +use federation::federation_config; use std::net::ToSocketAddrs; use tracing::info; +mod database; mod error; mod federation; -mod instance; mod utils; #[tokio::main] @@ -26,7 +27,7 @@ async fn main() -> Result<(), Error> { .filter_module("fediwiki", LevelFilter::Info) .init(); - let config = federation_config("localhost:8001", "alpha".to_string()).await?; + let config = federation_config("localhost:8001").await?; let hostname = config.domain(); info!("Listening with axum on {hostname}");