diff --git a/.drone.yml b/.drone.yml index 5641110..7f72434 100644 --- a/.drone.yml +++ b/.drone.yml @@ -48,11 +48,11 @@ steps: CARGO_HOME: .cargo RUST_BACKTRACE: 1 commands: - - cargo run --example simple_federation_actix + - cargo run --example simple_federation --features actix-web - name: cargo run axum image: rust:1.65-bullseye environment: CARGO_HOME: .cargo RUST_BACKTRACE: 1 commands: - - cargo run --example simple_federation_axum --features axum + - cargo run --example simple_federation --features axum diff --git a/Cargo.toml b/Cargo.toml index 6724f50..6d17e80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,24 +37,21 @@ bytes = "1.3.0" futures-core = { version = "0.3.25", default-features = false } pin-project-lite = "0.2.9" +# Actix-web actix-web = { version = "4.2.1", default-features = false, optional = true } -axum = { version = "0.6.0", features = ["json", "headers", "macros", "original-uri"], optional = true } # Axum -tower-http = { version = "0.3", features = ["map-request-body", "util", "trace"], optional = true } +axum = { version = "0.6.0", features = ["json", "headers", "macros", "original-uri"], optional = true } tower = { version = "0.4.13", optional = true } hyper = { version = "0.14", optional = true } -tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true } [features] -default = ["actix"] -actix = ["dep:actix-web"] +default = [] +actix-web = ["dep:actix-web"] axum = [ "dep:axum", - "dep:tower-http", "dep:tower", "dep:hyper", - "dep:tracing-subscriber", ] [dev-dependencies] @@ -63,14 +60,13 @@ rand = "0.8.5" actix-rt = "2.7.0" tokio = { version = "1.21.2", features = ["full"] } env_logger = { version = "0.9.3", default-features = false } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tower-http = { version = "0.3", features = ["map-request-body", "util", "trace"] } + +[profile.dev] +strip = "symbols" +debug = 0 [[example]] -name = "simple_federation_actix" -path = "examples/federation-actix/main.rs" -required-features = ["actix"] - -[[example]] -name = "simple_federation_axum" -path = "examples/federation-axum/main.rs" -required-features = ["axum"] - +name = "simple_federation" +path = "examples/simple_federation/main.rs" \ No newline at end of file diff --git a/examples/federation-actix/instance.rs b/examples/federation-actix/instance.rs deleted file mode 100644 index 7ee2e47..0000000 --- a/examples/federation-actix/instance.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::{ - error::Error, - generate_object_id, - objects::{ - note::MyPost, - person::{MyUser, PersonAcceptedActivities}, - }, -}; -use activitypub_federation::{ - core::{ - actix::inbox::receive_activity, - object_id::ObjectId, - signatures::generate_actor_keypair, - }, - deser::context::WithContext, - request_data::{ApubContext, ApubMiddleware, RequestData}, - traits::ApubObject, - FederationSettings, - InstanceConfig, - UrlVerifier, - APUB_JSON_CONTENT_TYPE, -}; -use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer}; -use async_trait::async_trait; -use reqwest::Client; -use std::sync::{Arc, Mutex}; -use tokio::task; -use url::Url; - -pub type DatabaseHandle = Arc; - -/// Our "database" which contains all known posts users (local and federated) -pub struct Database { - 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 new(hostname: String) -> Result, Error> { - let settings = FederationSettings::builder() - .debug(true) - .url_verifier(Box::new(MyUrlVerifier())) - .build()?; - let local_instance = - InstanceConfig::new(hostname.clone(), Client::default().into(), settings); - let local_user = MyUser::new(generate_object_id(&hostname)?, generate_actor_keypair()?); - let instance = Arc::new(Database { - users: Mutex::new(vec![local_user]), - posts: Mutex::new(vec![]), - }); - Ok(ApubContext::new(instance, local_instance)) - } - - pub fn local_user(&self) -> MyUser { - self.users.lock().unwrap().first().cloned().unwrap() - } - - pub fn listen(data: &ApubContext) -> Result<(), Error> { - let hostname = data.local_instance().hostname(); - let data = data.clone(); - let server = HttpServer::new(move || { - App::new() - .wrap(ApubMiddleware::new(data.clone())) - .route("/objects/{user_name}", web::get().to(http_get_user)) - .service( - web::scope("") - // Just a single, global inbox for simplicity - .route("/inbox", web::post().to(http_post_user_inbox)), - ) - }) - .bind(hostname)? - .run(); - task::spawn(server); - Ok(()) - } -} - -/// Handles requests to fetch user json over HTTP -async fn http_get_user( - request: HttpRequest, - data: RequestData, -) -> Result { - let hostname: String = data.local_instance().hostname().to_string(); - let request_url = format!("http://{}{}", hostname, &request.uri().to_string()); - let url = Url::parse(&request_url)?; - let user = ObjectId::::new(url) - .dereference_local(&data) - .await? - .into_apub(&data) - .await?; - - Ok(HttpResponse::Ok() - .content_type(APUB_JSON_CONTENT_TYPE) - .json(WithContext::new_default(user))) -} - -/// Handles messages received in user inbox -async fn http_post_user_inbox( - request: HttpRequest, - payload: String, - data: RequestData, -) -> Result { - let activity = serde_json::from_str(&payload)?; - receive_activity::, MyUser, DatabaseHandle>( - request, activity, &data, - ) - .await -} diff --git a/examples/federation-actix/objects/note.rs b/examples/federation-actix/objects/note.rs deleted file mode 100644 index 4d7db9d..0000000 --- a/examples/federation-actix/objects/note.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::{generate_object_id, instance::DatabaseHandle, objects::person::MyUser}; -use activitypub_federation::{ - core::object_id::ObjectId, - deser::helpers::deserialize_one_or_many, - request_data::RequestData, - traits::ApubObject, -}; -use activitystreams_kinds::{object::NoteType, public}; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Clone, Debug)] -pub struct MyPost { - pub text: String, - pub ap_id: ObjectId, - pub creator: ObjectId, - pub local: bool, -} - -impl MyPost { - pub fn new(text: String, creator: ObjectId) -> MyPost { - MyPost { - text, - ap_id: ObjectId::new(generate_object_id(creator.inner().domain().unwrap()).unwrap()), - creator, - local: true, - } - } -} - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct Note { - #[serde(rename = "type")] - kind: NoteType, - 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 ApubObject for MyPost { - type DataType = DatabaseHandle; - type ApubType = Note; - type DbType = (); - type Error = crate::error::Error; - - async fn read_from_apub_id( - _object_id: Url, - _data: &RequestData, - ) -> Result, Self::Error> { - todo!() - } - - async fn into_apub( - self, - data: &RequestData, - ) -> Result { - let creator = self.creator.dereference_local(data).await?; - Ok(Note { - kind: Default::default(), - id: self.ap_id, - attributed_to: self.creator, - to: vec![public(), creator.followers_url()?], - content: self.text, - }) - } - - async fn from_apub( - apub: Self::ApubType, - data: &RequestData, - ) -> Result { - let post = MyPost { - text: apub.content, - ap_id: apub.id, - creator: apub.attributed_to, - local: false, - }; - - let mut lock = data.posts.lock().unwrap(); - lock.push(post.clone()); - Ok(post) - } -} diff --git a/examples/federation-axum/activities/accept.rs b/examples/federation-axum/activities/accept.rs deleted file mode 100644 index f3e1243..0000000 --- a/examples/federation-axum/activities/accept.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::{activities::follow::Follow, instance::DatabaseHandle, objects::person::MyUser}; -use activitypub_federation::{ - core::object_id::ObjectId, - request_data::RequestData, - traits::ActivityHandler, -}; -use activitystreams_kinds::activity::AcceptType; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct Accept { - actor: ObjectId, - object: Follow, - #[serde(rename = "type")] - kind: AcceptType, - id: Url, -} - -impl Accept { - pub fn new(actor: ObjectId, object: Follow, id: Url) -> Accept { - Accept { - actor, - object, - kind: Default::default(), - id, - } - } -} - -#[async_trait::async_trait] -impl ActivityHandler for Accept { - type DataType = DatabaseHandle; - type Error = crate::error::Error; - - fn id(&self) -> &Url { - &self.id - } - - fn actor(&self) -> &Url { - self.actor.inner() - } - - async fn receive(self, _data: &RequestData) -> Result<(), Self::Error> { - Ok(()) - } -} diff --git a/examples/federation-axum/activities/create_note.rs b/examples/federation-axum/activities/create_note.rs deleted file mode 100644 index ac35979..0000000 --- a/examples/federation-axum/activities/create_note.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::{ - instance::DatabaseHandle, - objects::{note::Note, person::MyUser}, - MyPost, -}; -use activitypub_federation::{ - core::object_id::ObjectId, - deser::helpers::deserialize_one_or_many, - request_data::RequestData, - traits::{ActivityHandler, ApubObject}, -}; -use activitystreams_kinds::activity::CreateType; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct CreateNote { - pub(crate) actor: ObjectId, - #[serde(deserialize_with = "deserialize_one_or_many")] - pub(crate) to: Vec, - pub(crate) object: Note, - #[serde(rename = "type")] - pub(crate) kind: CreateType, - pub(crate) id: Url, -} - -impl CreateNote { - pub fn new(note: Note, id: Url) -> CreateNote { - CreateNote { - actor: note.attributed_to.clone(), - to: note.to.clone(), - object: note, - kind: CreateType::Create, - id, - } - } -} - -#[async_trait::async_trait] -impl ActivityHandler for CreateNote { - type DataType = DatabaseHandle; - type Error = crate::error::Error; - - fn id(&self) -> &Url { - &self.id - } - - fn actor(&self) -> &Url { - self.actor.inner() - } - - async fn receive(self, data: &RequestData) -> Result<(), Self::Error> { - MyPost::from_apub(self.object, data).await?; - Ok(()) - } -} diff --git a/examples/federation-axum/activities/follow.rs b/examples/federation-axum/activities/follow.rs deleted file mode 100644 index b430299..0000000 --- a/examples/federation-axum/activities/follow.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::{ - activities::accept::Accept, - generate_object_id, - instance::DatabaseHandle, - objects::person::MyUser, -}; -use activitypub_federation::{ - core::object_id::ObjectId, - request_data::RequestData, - traits::{ActivityHandler, Actor}, -}; -use activitystreams_kinds::activity::FollowType; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(rename_all = "camelCase")] -pub struct Follow { - 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 { - actor, - object, - kind: Default::default(), - id, - } - } -} - -#[async_trait::async_trait] -impl ActivityHandler for Follow { - type DataType = DatabaseHandle; - type Error = crate::error::Error; - - fn id(&self) -> &Url { - &self.id - } - - fn actor(&self) -> &Url { - self.actor.inner() - } - - // Ignore clippy false positive: https://github.com/rust-lang/rust-clippy/issues/6446 - #[allow(clippy::await_holding_lock)] - async fn receive(self, data: &RequestData) -> 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() - }; - - // send back an accept - let follower = self.actor.dereference(data).await?; - let id = generate_object_id(data.local_instance().hostname())?; - let accept = Accept::new(local_user.ap_id.clone(), self, id.clone()); - local_user - .send(accept, vec![follower.shared_inbox_or_inbox()], data) - .await?; - Ok(()) - } -} diff --git a/examples/federation-axum/activities/mod.rs b/examples/federation-axum/activities/mod.rs deleted file mode 100644 index 59d2fb0..0000000 --- a/examples/federation-axum/activities/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod accept; -pub mod create_note; -pub mod follow; diff --git a/examples/federation-axum/error.rs b/examples/federation-axum/error.rs deleted file mode 100644 index 0b4302d..0000000 --- a/examples/federation-axum/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -/// Necessary because of this issue: https://github.com/actix/actix-web/issues/1711 -#[derive(Debug)] -pub struct Error(anyhow::Error); - -impl From for Error -where - T: Into, -{ - fn from(t: T) -> Self { - Error(t.into()) - } -} - -mod axum { - use super::Error; - use axum::response::{IntoResponse, Response}; - use http::StatusCode; - - impl IntoResponse for Error { - fn into_response(self) -> Response { - (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", self.0)).into_response() - } - } -} diff --git a/examples/federation-axum/instance.rs b/examples/federation-axum/instance.rs deleted file mode 100644 index c425111..0000000 --- a/examples/federation-axum/instance.rs +++ /dev/null @@ -1,148 +0,0 @@ -use crate::{ - error::Error, - generate_object_id, - objects::{ - note::MyPost, - person::{MyUser, Person, PersonAcceptedActivities}, - }, -}; -use activitypub_federation::{ - core::{ - axum::{inbox::receive_activity, json::ApubJson, verify_request_payload, DigestVerified}, - object_id::ObjectId, - signatures::generate_actor_keypair, - }, - deser::context::WithContext, - request_data::{ApubContext, ApubMiddleware, RequestData}, - traits::ApubObject, - FederationSettings, - InstanceConfig, - UrlVerifier, -}; -use async_trait::async_trait; -use axum::{ - body, - body::Body, - extract::{Json, OriginalUri}, - middleware, - response::IntoResponse, - routing::{get, post}, - Extension, - Router, -}; -use http::{HeaderMap, Method, Request}; -use reqwest::Client; -use std::{ - net::ToSocketAddrs, - sync::{Arc, Mutex}, -}; -use tokio::task; -use tower::ServiceBuilder; -use tower_http::{trace::TraceLayer, ServiceBuilderExt}; -use url::Url; - -pub type DatabaseHandle = Arc; - -/// Our "database" which contains all known posts and users (local and federated) -pub struct Database { - 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 new(hostname: String) -> Result, Error> { - let settings = FederationSettings::builder() - .debug(true) - .url_verifier(Box::new(MyUrlVerifier())) - .build()?; - let local_instance = - InstanceConfig::new(hostname.clone(), Client::default().into(), settings); - let local_user = MyUser::new(generate_object_id(&hostname)?, generate_actor_keypair()?); - let instance = Arc::new(Database { - users: Mutex::new(vec![local_user]), - posts: Mutex::new(vec![]), - }); - Ok(ApubContext::new(instance, local_instance)) - } - - pub fn local_user(&self) -> MyUser { - self.users.lock().unwrap().first().cloned().unwrap() - } - - pub fn listen(data: &ApubContext) -> Result<(), Error> { - let hostname = data.local_instance().hostname(); - let data = data.clone(); - let app = Router::new() - .route("/inbox", post(http_post_user_inbox)) - .layer( - ServiceBuilder::new() - .map_request_body(body::boxed) - .layer(middleware::from_fn(verify_request_payload)), - ) - .route("/objects/:user_name", get(http_get_user)) - .layer(ApubMiddleware::new(data)) - .layer(TraceLayer::new_for_http()); - - // run it - let addr = hostname - .to_socket_addrs()? - .next() - .expect("Failed to lookup domain name"); - let server = axum::Server::bind(&addr).serve(app.into_make_service()); - - task::spawn(server); - Ok(()) - } -} - -async fn http_get_user( - data: RequestData, - request: Request, -) -> Result>, Error> { - let hostname: String = data.local_instance().hostname().to_string(); - let request_url = format!("http://{}{}", hostname, &request.uri()); - - let url = Url::parse(&request_url).expect("Failed to parse url"); - - let user = ObjectId::::new(url) - .dereference_local(&data) - .await? - .into_apub(&data) - .await?; - - Ok(ApubJson(WithContext::new_default(user))) -} - -async fn http_post_user_inbox( - headers: HeaderMap, - method: Method, - OriginalUri(uri): OriginalUri, - data: RequestData, - Extension(digest_verified): Extension, - Json(activity): Json>, -) -> impl IntoResponse { - receive_activity::, MyUser, DatabaseHandle>( - digest_verified, - activity, - &data, - headers, - method, - uri, - ) - .await -} diff --git a/examples/federation-axum/main.rs b/examples/federation-axum/main.rs deleted file mode 100644 index 8b01f9c..0000000 --- a/examples/federation-axum/main.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::{error::Error, instance::Database, objects::note::MyPost, utils::generate_object_id}; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - -mod activities; -mod error; -mod instance; -mod objects; -mod utils; - -#[actix_rt::main] -async fn main() -> Result<(), Error> { - tracing_subscriber::registry() - .with(tracing_subscriber::EnvFilter::new( - std::env::var("RUST_LOG").unwrap_or_else(|_| { - "activitypub_federation=debug,federation-axum=debug,tower_http=debug".into() - }), - )) - .with(tracing_subscriber::fmt::layer()) - .init(); - - let alpha = Database::new("localhost:8001".to_string())?; - let beta = Database::new("localhost:8002".to_string())?; - Database::listen(&alpha)?; - Database::listen(&beta)?; - - // alpha user follows beta user - alpha - .local_user() - .follow(&beta.local_user(), &alpha.to_request_data()) - .await?; - - // assert that follow worked correctly - assert_eq!( - beta.local_user().followers(), - &vec![alpha.local_user().ap_id.inner().clone()] - ); - - // beta sends a post to its followers - let sent_post = MyPost::new("hello world!".to_string(), beta.local_user().ap_id); - beta.local_user() - .post(sent_post.clone(), &beta.to_request_data()) - .await?; - let received_post = alpha.posts.lock().unwrap().first().cloned().unwrap(); - - // assert that alpha received the post - assert_eq!(received_post.text, sent_post.text); - assert_eq!(received_post.ap_id.inner(), sent_post.ap_id.inner()); - assert_eq!(received_post.creator.inner(), sent_post.creator.inner()); - Ok(()) -} diff --git a/examples/federation-axum/objects/mod.rs b/examples/federation-axum/objects/mod.rs deleted file mode 100644 index 20b1a49..0000000 --- a/examples/federation-axum/objects/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod note; -pub mod person; diff --git a/examples/federation-axum/objects/person.rs b/examples/federation-axum/objects/person.rs deleted file mode 100644 index 3476d93..0000000 --- a/examples/federation-axum/objects/person.rs +++ /dev/null @@ -1,188 +0,0 @@ -use crate::{ - activities::{accept::Accept, create_note::CreateNote, follow::Follow}, - error::Error, - instance::DatabaseHandle, - objects::note::MyPost, - utils::generate_object_id, -}; -use activitypub_federation::{ - core::{ - activity_queue::send_activity, - object_id::ObjectId, - signatures::{Keypair, PublicKey}, - }, - deser::context::WithContext, - request_data::RequestData, - traits::{ActivityHandler, Actor, ApubObject}, -}; -use activitystreams_kinds::actor::PersonType; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Debug, Clone)] -pub struct MyUser { - 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, - 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), - CreateNote(CreateNote), -} - -impl MyUser { - pub fn new(ap_id: Url, keypair: Keypair) -> MyUser { - let mut inbox = ap_id.clone(); - inbox.set_path("/inbox"); - let ap_id = ObjectId::new(ap_id); - MyUser { - ap_id, - inbox, - public_key: keypair.public_key, - private_key: Some(keypair.private_key), - followers: vec![], - local: true, - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Person { - #[serde(rename = "type")] - kind: PersonType, - id: ObjectId, - inbox: Url, - public_key: PublicKey, -} - -impl MyUser { - pub fn followers(&self) -> &Vec { - &self.followers - } - - pub fn followers_url(&self) -> Result { - Ok(Url::parse(&format!("{}/followers", self.ap_id.inner()))?) - } - - fn public_key(&self) -> PublicKey { - PublicKey::new_main_key(self.ap_id.clone().into_inner(), self.public_key.clone()) - } - - pub async fn follow( - &self, - other: &MyUser, - data: &RequestData, - ) -> Result<(), Error> { - let id = generate_object_id(data.local_instance().hostname())?; - 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 async fn post( - &self, - post: MyPost, - data: &RequestData, - ) -> Result<(), Error> { - let id = generate_object_id(data.local_instance().hostname())?; - let create = CreateNote::new(post.into_apub(data).await?, id.clone()); - let mut inboxes = vec![]; - for f in self.followers.clone() { - let user: MyUser = ObjectId::new(f).dereference(data).await?; - inboxes.push(user.shared_inbox_or_inbox()); - } - self.send(create, inboxes, data).await?; - Ok(()) - } - - pub(crate) async fn send( - &self, - activity: Activity, - recipients: Vec, - data: &RequestData, - ) -> Result<(), ::Error> - where - Activity: ActivityHandler + Serialize + Send + Sync, - ::Error: From + From, - { - let activity = WithContext::new_default(activity); - send_activity( - activity, - self.public_key(), - self.private_key.clone().expect("has private key"), - recipients, - data.local_instance(), - ) - .await?; - Ok(()) - } -} - -#[async_trait::async_trait] -impl ApubObject for MyUser { - type DataType = DatabaseHandle; - type ApubType = Person; - type DbType = MyUser; - type Error = crate::error::Error; - - async fn read_from_apub_id( - object_id: Url, - data: &RequestData, - ) -> Result, Self::Error> { - let users = data.users.lock().unwrap(); - let res = users - .clone() - .into_iter() - .find(|u| u.ap_id.inner() == &object_id); - Ok(res) - } - - async fn into_apub( - self, - _data: &RequestData, - ) -> Result { - Ok(Person { - kind: Default::default(), - id: self.ap_id.clone(), - inbox: self.inbox.clone(), - public_key: self.public_key(), - }) - } - - async fn from_apub( - apub: Self::ApubType, - _data: &RequestData, - ) -> Result { - Ok(MyUser { - ap_id: apub.id, - inbox: apub.inbox, - public_key: apub.public_key.public_key_pem, - private_key: None, - followers: vec![], - local: false, - }) - } -} - -impl Actor for MyUser { - fn public_key(&self) -> &str { - &self.public_key - } - - fn inbox(&self) -> Url { - self.inbox.clone() - } -} diff --git a/examples/federation-axum/utils.rs b/examples/federation-axum/utils.rs deleted file mode 100644 index 87c421e..0000000 --- a/examples/federation-axum/utils.rs +++ /dev/null @@ -1,13 +0,0 @@ -use rand::{distributions::Alphanumeric, thread_rng, Rng}; -use url::{ParseError, Url}; - -/// Just generate random url as object id. In a real project, you probably want to use -/// an url which contains the database id for easy retrieval (or store the random id in db). -pub fn generate_object_id(hostname: &str) -> Result { - let id: String = thread_rng() - .sample_iter(&Alphanumeric) - .take(7) - .map(char::from) - .collect(); - Url::parse(&format!("http://{}/objects/{}", hostname, id)) -} diff --git a/examples/federation-actix/activities/accept.rs b/examples/simple_federation/activities/accept.rs similarity index 100% rename from examples/federation-actix/activities/accept.rs rename to examples/simple_federation/activities/accept.rs diff --git a/examples/federation-actix/activities/create_note.rs b/examples/simple_federation/activities/create_note.rs similarity index 100% rename from examples/federation-actix/activities/create_note.rs rename to examples/simple_federation/activities/create_note.rs diff --git a/examples/federation-actix/activities/follow.rs b/examples/simple_federation/activities/follow.rs similarity index 100% rename from examples/federation-actix/activities/follow.rs rename to examples/simple_federation/activities/follow.rs diff --git a/examples/federation-actix/activities/mod.rs b/examples/simple_federation/activities/mod.rs similarity index 100% rename from examples/federation-actix/activities/mod.rs rename to examples/simple_federation/activities/mod.rs diff --git a/examples/simple_federation/actix_web/http.rs b/examples/simple_federation/actix_web/http.rs new file mode 100644 index 0000000..1c2850d --- /dev/null +++ b/examples/simple_federation/actix_web/http.rs @@ -0,0 +1,66 @@ +use crate::{ + error::Error, + instance::DatabaseHandle, + objects::person::{MyUser, PersonAcceptedActivities}, +}; +use activitypub_federation::{ + core::{actix_web::inbox::receive_activity, object_id::ObjectId}, + deser::context::WithContext, + request_data::{ApubContext, ApubMiddleware, RequestData}, + traits::ApubObject, + APUB_JSON_CONTENT_TYPE, +}; +use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer}; +use tokio::task; +use url::Url; + +pub fn listen(data: &ApubContext) -> Result<(), Error> { + let hostname = data.local_instance().hostname(); + let data = data.clone(); + let server = HttpServer::new(move || { + App::new() + .wrap(ApubMiddleware::new(data.clone())) + .route("/objects/{user_name}", web::get().to(http_get_user)) + .service( + web::scope("") + // Just a single, global inbox for simplicity + .route("/inbox", web::post().to(http_post_user_inbox)), + ) + }) + .bind(hostname)? + .run(); + task::spawn(server); + Ok(()) +} + +/// Handles requests to fetch user json over HTTP +pub async fn http_get_user( + request: HttpRequest, + data: RequestData, +) -> Result { + let hostname: String = data.local_instance().hostname().to_string(); + let request_url = format!("http://{}{}", hostname, &request.uri().to_string()); + let url = Url::parse(&request_url)?; + let user = ObjectId::::new(url) + .dereference_local(&data) + .await? + .into_apub(&data) + .await?; + + Ok(HttpResponse::Ok() + .content_type(APUB_JSON_CONTENT_TYPE) + .json(WithContext::new_default(user))) +} + +/// Handles messages received in user inbox +pub async fn http_post_user_inbox( + request: HttpRequest, + payload: String, + data: RequestData, +) -> Result { + let activity = serde_json::from_str(&payload)?; + receive_activity::, MyUser, DatabaseHandle>( + request, activity, &data, + ) + .await +} diff --git a/examples/simple_federation/actix_web/mod.rs b/examples/simple_federation/actix_web/mod.rs new file mode 100644 index 0000000..ae11164 --- /dev/null +++ b/examples/simple_federation/actix_web/mod.rs @@ -0,0 +1,6 @@ +use crate::error::Error; +use actix_web::ResponseError; + +pub(crate) mod http; + +impl ResponseError for Error {} diff --git a/examples/simple_federation/axum/http.rs b/examples/simple_federation/axum/http.rs new file mode 100644 index 0000000..7040f83 --- /dev/null +++ b/examples/simple_federation/axum/http.rs @@ -0,0 +1,93 @@ +use crate::{ + error::Error, + instance::DatabaseHandle, + objects::person::{MyUser, Person, PersonAcceptedActivities}, +}; +use activitypub_federation::{ + core::{ + axum::{inbox::receive_activity, json::ApubJson, verify_request_payload, DigestVerified}, + object_id::ObjectId, + }, + deser::context::WithContext, + request_data::{ApubContext, ApubMiddleware, RequestData}, + traits::ApubObject, +}; +use axum::{ + body, + extract::OriginalUri, + middleware, + response::IntoResponse, + routing::{get, post}, + Extension, + Json, + Router, +}; +use http::{HeaderMap, Method, Request}; +use hyper::Body; +use std::net::ToSocketAddrs; +use tokio::task; +use tower::ServiceBuilder; +use tower_http::{trace::TraceLayer, ServiceBuilderExt}; +use url::Url; + +pub fn listen(data: &ApubContext) -> Result<(), Error> { + let hostname = data.local_instance().hostname(); + let data = data.clone(); + let app = Router::new() + .route("/inbox", post(http_post_user_inbox)) + .layer( + ServiceBuilder::new() + .map_request_body(body::boxed) + .layer(middleware::from_fn(verify_request_payload)), + ) + .route("/objects/:user_name", get(http_get_user)) + .layer(ApubMiddleware::new(data)) + .layer(TraceLayer::new_for_http()); + + // run it + let addr = hostname + .to_socket_addrs()? + .next() + .expect("Failed to lookup domain name"); + let server = axum::Server::bind(&addr).serve(app.into_make_service()); + + task::spawn(server); + Ok(()) +} + +async fn http_get_user( + data: RequestData, + request: Request, +) -> Result>, Error> { + let hostname: String = data.local_instance().hostname().to_string(); + let request_url = format!("http://{}{}", hostname, &request.uri()); + + let url = Url::parse(&request_url).expect("Failed to parse url"); + + let user = ObjectId::::new(url) + .dereference_local(&data) + .await? + .into_apub(&data) + .await?; + + Ok(ApubJson(WithContext::new_default(user))) +} + +async fn http_post_user_inbox( + headers: HeaderMap, + method: Method, + OriginalUri(uri): OriginalUri, + data: RequestData, + Extension(digest_verified): Extension, + Json(activity): Json>, +) -> impl IntoResponse { + receive_activity::, MyUser, DatabaseHandle>( + digest_verified, + activity, + &data, + headers, + method, + uri, + ) + .await +} diff --git a/examples/simple_federation/axum/mod.rs b/examples/simple_federation/axum/mod.rs new file mode 100644 index 0000000..7d70cd6 --- /dev/null +++ b/examples/simple_federation/axum/mod.rs @@ -0,0 +1,11 @@ +use crate::error::Error; +use ::http::StatusCode; +use axum::response::{IntoResponse, Response}; + +pub mod http; + +impl IntoResponse for Error { + fn into_response(self) -> Response { + (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", self.0)).into_response() + } +} diff --git a/examples/federation-actix/error.rs b/examples/simple_federation/error.rs similarity index 80% rename from examples/federation-actix/error.rs rename to examples/simple_federation/error.rs index b460545..3ef1819 100644 --- a/examples/federation-actix/error.rs +++ b/examples/simple_federation/error.rs @@ -1,9 +1,8 @@ -use actix_web::ResponseError; use std::fmt::{Display, Formatter}; /// Necessary because of this issue: https://github.com/actix/actix-web/issues/1711 #[derive(Debug)] -pub struct Error(anyhow::Error); +pub struct Error(pub(crate) anyhow::Error); impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { @@ -19,5 +18,3 @@ where Error(t.into()) } } - -impl ResponseError for Error {} diff --git a/examples/simple_federation/instance.rs b/examples/simple_federation/instance.rs new file mode 100644 index 0000000..f7cb37f --- /dev/null +++ b/examples/simple_federation/instance.rs @@ -0,0 +1,68 @@ +use crate::{ + generate_object_id, + objects::{note::MyPost, person::MyUser}, + Error, +}; +use activitypub_federation::{ + core::signatures::generate_actor_keypair, + request_data::ApubContext, + FederationSettings, + InstanceConfig, + UrlVerifier, +}; +use async_trait::async_trait; +use reqwest::Client; +use std::sync::{Arc, Mutex}; +use url::Url; + +pub type DatabaseHandle = Arc; + +/// Our "database" which contains all known posts users (local and federated) +pub struct Database { + 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(()) + } + } +} + +pub fn listen(data: &ApubContext) -> Result<(), Error> { + #[cfg(feature = "actix-web")] + crate::actix_web::http::listen(data)?; + #[cfg(feature = "axum")] + crate::axum::http::listen(data)?; + Ok(()) +} + +impl Database { + pub fn new(hostname: String) -> Result, Error> { + let settings = FederationSettings::builder() + .debug(true) + .url_verifier(Box::new(MyUrlVerifier())) + .build()?; + let local_instance = + InstanceConfig::new(hostname.clone(), Client::default().into(), settings); + let local_user = MyUser::new(generate_object_id(&hostname)?, generate_actor_keypair()?); + let instance = Arc::new(Database { + users: Mutex::new(vec![local_user]), + posts: Mutex::new(vec![]), + }); + Ok(ApubContext::new(instance, local_instance)) + } + + pub fn local_user(&self) -> MyUser { + self.users.lock().unwrap().first().cloned().unwrap() + } +} diff --git a/examples/federation-actix/main.rs b/examples/simple_federation/main.rs similarity index 83% rename from examples/federation-actix/main.rs rename to examples/simple_federation/main.rs index dc95653..2bf5b1b 100644 --- a/examples/federation-actix/main.rs +++ b/examples/simple_federation/main.rs @@ -1,7 +1,16 @@ -use crate::{error::Error, instance::Database, objects::note::MyPost, utils::generate_object_id}; +use crate::{ + instance::{listen, Database}, + objects::note::MyPost, + utils::generate_object_id, +}; +use error::Error; use tracing::log::LevelFilter; mod activities; +#[cfg(feature = "actix-web")] +mod actix_web; +#[cfg(feature = "axum")] +mod axum; mod error; mod instance; mod objects; @@ -15,8 +24,8 @@ async fn main() -> Result<(), Error> { let alpha = Database::new("localhost:8001".to_string())?; let beta = Database::new("localhost:8002".to_string())?; - Database::listen(&alpha)?; - Database::listen(&beta)?; + listen(&alpha)?; + listen(&beta)?; // alpha user follows beta user alpha diff --git a/examples/federation-actix/objects/mod.rs b/examples/simple_federation/objects/mod.rs similarity index 100% rename from examples/federation-actix/objects/mod.rs rename to examples/simple_federation/objects/mod.rs diff --git a/examples/federation-axum/objects/note.rs b/examples/simple_federation/objects/note.rs similarity index 94% rename from examples/federation-axum/objects/note.rs rename to examples/simple_federation/objects/note.rs index 4d7db9d..51bcee8 100644 --- a/examples/federation-axum/objects/note.rs +++ b/examples/simple_federation/objects/note.rs @@ -1,4 +1,4 @@ -use crate::{generate_object_id, instance::DatabaseHandle, objects::person::MyUser}; +use crate::{error::Error, generate_object_id, instance::DatabaseHandle, objects::person::MyUser}; use activitypub_federation::{ core::object_id::ObjectId, deser::helpers::deserialize_one_or_many, @@ -45,7 +45,7 @@ impl ApubObject for MyPost { type DataType = DatabaseHandle; type ApubType = Note; type DbType = (); - type Error = crate::error::Error; + type Error = Error; async fn read_from_apub_id( _object_id: Url, diff --git a/examples/federation-actix/objects/person.rs b/examples/simple_federation/objects/person.rs similarity index 99% rename from examples/federation-actix/objects/person.rs rename to examples/simple_federation/objects/person.rs index fdf4442..078cf74 100644 --- a/examples/federation-actix/objects/person.rs +++ b/examples/simple_federation/objects/person.rs @@ -136,7 +136,7 @@ impl ApubObject for MyUser { type DataType = DatabaseHandle; type ApubType = Person; type DbType = MyUser; - type Error = crate::error::Error; + type Error = Error; async fn read_from_apub_id( object_id: Url, diff --git a/examples/federation-actix/utils.rs b/examples/simple_federation/utils.rs similarity index 100% rename from examples/federation-actix/utils.rs rename to examples/simple_federation/utils.rs diff --git a/src/core/actix/inbox.rs b/src/core/actix_web/inbox.rs similarity index 100% rename from src/core/actix/inbox.rs rename to src/core/actix_web/inbox.rs diff --git a/src/core/actix/middleware.rs b/src/core/actix_web/middleware.rs similarity index 100% rename from src/core/actix/middleware.rs rename to src/core/actix_web/middleware.rs diff --git a/src/core/actix/mod.rs b/src/core/actix_web/mod.rs similarity index 100% rename from src/core/actix/mod.rs rename to src/core/actix_web/mod.rs diff --git a/src/core/mod.rs b/src/core/mod.rs index 0f5c1dd..6b23ac4 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -5,5 +5,5 @@ pub mod signatures; #[cfg(feature = "axum")] pub mod axum; -#[cfg(feature = "actix")] -pub mod actix; +#[cfg(feature = "actix-web")] +pub mod actix_web;