From 58142e64d035a55ce9462e3905d16ba2faa41999 Mon Sep 17 00:00:00 2001 From: Felix Date: Wed, 15 Apr 2020 20:12:25 +0200 Subject: [PATCH 01/13] Send activities to correct inbox, seperate community/user inboxes --- docker/federation/run-federation-test.bash | 3 +- server/src/apub/activities.rs | 26 +++--- server/src/apub/community_inbox.rs | 53 +++++++++++ server/src/apub/inbox.rs | 101 --------------------- server/src/apub/mod.rs | 3 +- server/src/apub/user_inbox.rs | 64 +++++++++++++ server/src/routes/federation.rs | 7 +- 7 files changed, 135 insertions(+), 122 deletions(-) create mode 100644 server/src/apub/community_inbox.rs delete mode 100644 server/src/apub/inbox.rs create mode 100644 server/src/apub/user_inbox.rs diff --git a/docker/federation/run-federation-test.bash b/docker/federation/run-federation-test.bash index 62bc1e8bf..2c8b681ce 100755 --- a/docker/federation/run-federation-test.bash +++ b/docker/federation/run-federation-test.bash @@ -3,6 +3,7 @@ set -e if [ "$1" = "-yarn" ]; then pushd ../../ui/ || exit + yarn yarn build popd || exit fi @@ -13,4 +14,4 @@ popd || exit sudo docker build ../../ -f Dockerfile -t lemmy-federation:latest -sudo docker-compose up \ No newline at end of file +sudo docker-compose up diff --git a/server/src/apub/activities.rs b/server/src/apub/activities.rs index a1707267c..2e35400d0 100644 --- a/server/src/apub/activities.rs +++ b/server/src/apub/activities.rs @@ -1,5 +1,5 @@ -use crate::apub::{get_apub_protocol_string, get_following_instances}; use crate::db::community::Community; +use crate::db::community_view::CommunityFollowerView; use crate::db::post::Post; use crate::db::user::User_; use crate::db::Crud; @@ -46,18 +46,14 @@ where Ok(()) } -fn get_followers(_community: &Community) -> Vec { - // TODO: this is wrong, needs to go to the (non-local) followers of the community - get_following_instances() - .iter() - .map(|i| { - format!( - "{}://{}/federation/inbox", - get_apub_protocol_string(), - i.domain - ) - }) - .collect() +fn get_followers(conn: &PgConnection, community: &Community) -> Result, Error> { + Ok( + CommunityFollowerView::for_community(conn, community.id)? + .iter() + .filter(|c| !c.user_local) + .map(|c| format!("{}/inbox", c.user_actor_id.to_owned())) + .collect(), + ) } pub fn post_create(post: &Post, creator: &User_, conn: &PgConnection) -> Result<(), Error> { @@ -73,7 +69,7 @@ pub fn post_create(post: &Post, creator: &User_, conn: &PgConnection) -> Result< .create_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_object_base_box(page)?; - send_activity(&create, get_followers(&community))?; + send_activity(&create, get_followers(conn, &community)?)?; Ok(()) } @@ -90,7 +86,7 @@ pub fn post_update(post: &Post, creator: &User_, conn: &PgConnection) -> Result< .update_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_object_base_box(page)?; - send_activity(&update, get_followers(&community))?; + send_activity(&update, get_followers(conn, &community)?)?; Ok(()) } diff --git a/server/src/apub/community_inbox.rs b/server/src/apub/community_inbox.rs new file mode 100644 index 000000000..0fe3fd709 --- /dev/null +++ b/server/src/apub/community_inbox.rs @@ -0,0 +1,53 @@ +use crate::apub::activities::accept_follow; +use crate::apub::fetcher::fetch_remote_user; +use crate::db::community::{Community, CommunityFollower, CommunityFollowerForm}; +use crate::db::Followable; +use activitystreams::activity::Follow; +use actix_web::{web, HttpResponse}; +use diesel::r2d2::{ConnectionManager, Pool}; +use diesel::PgConnection; +use failure::Error; +use url::Url; + +#[serde(untagged)] +#[derive(serde::Deserialize)] +pub enum CommunityAcceptedObjects { + Follow(Follow), +} + +pub async fn community_inbox( + input: web::Json, + db: web::Data>>, +) -> Result { + let input = input.into_inner(); + let conn = &db.get().unwrap(); + match input { + CommunityAcceptedObjects::Follow(f) => handle_follow(&f, conn), + } +} + +fn handle_follow(follow: &Follow, conn: &PgConnection) -> Result { + println!("received follow: {:?}", &follow); + + // TODO: make sure this is a local community + let community_uri = follow + .follow_props + .get_object_xsd_any_uri() + .unwrap() + .to_string(); + let community = Community::read_from_actor_id(conn, &community_uri)?; + let user_uri = follow + .follow_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + let user = fetch_remote_user(&Url::parse(&user_uri)?, conn)?; + // TODO: insert ID of the user into follows of the community + let community_follower_form = CommunityFollowerForm { + community_id: community.id, + user_id: user.id, + }; + CommunityFollower::follow(&conn, &community_follower_form)?; + accept_follow(&follow)?; + Ok(HttpResponse::Ok().finish()) +} diff --git a/server/src/apub/inbox.rs b/server/src/apub/inbox.rs deleted file mode 100644 index a2db335a8..000000000 --- a/server/src/apub/inbox.rs +++ /dev/null @@ -1,101 +0,0 @@ -use crate::apub::activities::accept_follow; -use crate::apub::fetcher::fetch_remote_user; -use crate::db::community::{Community, CommunityFollower, CommunityFollowerForm}; -use crate::db::post::{Post, PostForm}; -use crate::db::Crud; -use crate::db::Followable; -use activitystreams::activity::{Accept, Create, Follow, Update}; -use activitystreams::object::Page; -use actix_web::{web, HttpResponse}; -use diesel::r2d2::{ConnectionManager, Pool}; -use diesel::PgConnection; -use failure::Error; -use url::Url; - -// TODO: need a proper actor that has this inbox - -pub async fn inbox( - input: web::Json, - db: web::Data>>, -) -> Result { - // TODO: make sure that things are received in the correct inbox - // (by using seperate handler functions and checking the user/community name in the path) - let input = input.into_inner(); - let conn = &db.get().unwrap(); - match input { - AcceptedObjects::Create(c) => handle_create(&c, conn), - AcceptedObjects::Update(u) => handle_update(&u, conn), - AcceptedObjects::Follow(f) => handle_follow(&f, conn), - AcceptedObjects::Accept(a) => handle_accept(&a, conn), - } -} - -fn handle_create(create: &Create, conn: &PgConnection) -> Result { - let page = create - .create_props - .get_object_base_box() - .to_owned() - .unwrap() - .to_owned() - .to_concrete::()?; - let post = PostForm::from_page(&page, conn)?; - Post::create(conn, &post)?; - // TODO: send the new post out via websocket - Ok(HttpResponse::Ok().finish()) -} - -fn handle_update(update: &Update, conn: &PgConnection) -> Result { - let page = update - .update_props - .get_object_base_box() - .to_owned() - .unwrap() - .to_owned() - .to_concrete::()?; - let post = PostForm::from_page(&page, conn)?; - let id = Post::read_from_apub_id(conn, &post.ap_id)?.id; - Post::update(conn, id, &post)?; - // TODO: send the new post out via websocket - Ok(HttpResponse::Ok().finish()) -} - -fn handle_follow(follow: &Follow, conn: &PgConnection) -> Result { - println!("received follow: {:?}", &follow); - - // TODO: make sure this is a local community - let community_uri = follow - .follow_props - .get_object_xsd_any_uri() - .unwrap() - .to_string(); - let community = Community::read_from_actor_id(conn, &community_uri)?; - let user_uri = follow - .follow_props - .get_actor_xsd_any_uri() - .unwrap() - .to_string(); - let user = fetch_remote_user(&Url::parse(&user_uri)?, conn)?; - // TODO: insert ID of the user into follows of the community - let community_follower_form = CommunityFollowerForm { - community_id: community.id, - user_id: user.id, - }; - CommunityFollower::follow(&conn, &community_follower_form)?; - accept_follow(&follow)?; - Ok(HttpResponse::Ok().finish()) -} - -fn handle_accept(accept: &Accept, _conn: &PgConnection) -> Result { - println!("received accept: {:?}", &accept); - // TODO: at this point, indicate to the user that they are following the community - Ok(HttpResponse::Ok().finish()) -} - -#[serde(untagged)] -#[derive(serde::Deserialize)] -pub enum AcceptedObjects { - Create(Create), - Update(Update), - Follow(Follow), - Accept(Accept), -} diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index b2d157ae8..11b513beb 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -1,10 +1,11 @@ pub mod activities; pub mod community; +pub mod community_inbox; pub mod fetcher; -pub mod inbox; pub mod post; pub mod signatures; pub mod user; +pub mod user_inbox; use crate::apub::signatures::PublicKeyExtension; use crate::Settings; use activitystreams::actor::{properties::ApActorProperties, Group, Person}; diff --git a/server/src/apub/user_inbox.rs b/server/src/apub/user_inbox.rs new file mode 100644 index 000000000..02517afe0 --- /dev/null +++ b/server/src/apub/user_inbox.rs @@ -0,0 +1,64 @@ +use crate::db::post::{Post, PostForm}; +use crate::db::Crud; +use activitystreams::activity::{Accept, Create, Update}; +use activitystreams::object::Page; +use actix_web::{web, HttpResponse}; +use diesel::r2d2::{ConnectionManager, Pool}; +use diesel::PgConnection; +use failure::Error; + +#[serde(untagged)] +#[derive(serde::Deserialize)] +pub enum UserAcceptedObjects { + Create(Create), + Update(Update), + Accept(Accept), +} + +pub async fn user_inbox( + input: web::Json, + db: web::Data>>, +) -> Result { + let input = input.into_inner(); + let conn = &db.get().unwrap(); + match input { + UserAcceptedObjects::Create(c) => handle_create(&c, conn), + UserAcceptedObjects::Update(u) => handle_update(&u, conn), + UserAcceptedObjects::Accept(a) => handle_accept(&a, conn), + } +} + +fn handle_create(create: &Create, conn: &PgConnection) -> Result { + let page = create + .create_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .to_concrete::()?; + let post = PostForm::from_page(&page, conn)?; + Post::create(conn, &post)?; + // TODO: send the new post out via websocket + Ok(HttpResponse::Ok().finish()) +} + +fn handle_update(update: &Update, conn: &PgConnection) -> Result { + let page = update + .update_props + .get_object_base_box() + .to_owned() + .unwrap() + .to_owned() + .to_concrete::()?; + let post = PostForm::from_page(&page, conn)?; + let id = Post::read_from_apub_id(conn, &post.ap_id)?.id; + Post::update(conn, id, &post)?; + // TODO: send the new post out via websocket + Ok(HttpResponse::Ok().finish()) +} + +fn handle_accept(accept: &Accept, _conn: &PgConnection) -> Result { + println!("received accept: {:?}", &accept); + // TODO: at this point, indicate to the user that they are following the community + Ok(HttpResponse::Ok().finish()) +} diff --git a/server/src/routes/federation.rs b/server/src/routes/federation.rs index 2798e7a95..6c7cce8a8 100644 --- a/server/src/routes/federation.rs +++ b/server/src/routes/federation.rs @@ -10,15 +10,14 @@ pub fn config(cfg: &mut web::ServiceConfig) { "/federation/communities", web::get().to(apub::community::get_apub_community_list), ) - // TODO: this needs to be moved to the actors (eg /federation/u/{}/inbox) - .route("/federation/inbox", web::post().to(apub::inbox::inbox)) + // TODO: check the user/community params for these .route( "/federation/c/{_}/inbox", - web::post().to(apub::inbox::inbox), + web::post().to(apub::community_inbox::community_inbox), ) .route( "/federation/u/{_}/inbox", - web::post().to(apub::inbox::inbox), + web::post().to(apub::user_inbox::user_inbox), ) .route( "/federation/c/{community_name}", From ed77fbda554ea4cbe00a387dd4486e681efb3d4a Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 17 Apr 2020 15:46:08 +0200 Subject: [PATCH 02/13] Implement search for activitypub IDs --- docker/federation/docker-compose.yml | 2 +- server/src/api/site.rs | 23 ++++-- server/src/apub/fetcher.rs | 113 +++++++++++++++++++-------- server/src/routes/federation.rs | 2 +- 4 files changed, 96 insertions(+), 44 deletions(-) diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index 216ac9a1d..a4e5c3074 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -31,7 +31,7 @@ services: - LEMMY_SETUP__ADMIN_PASSWORD=lemmy - LEMMY_SETUP__SITE_NAME=lemmy_alpha - RUST_BACKTRACE=1 - - RUST_LOG=actix_web=debug + - RUST_LOG=debug restart: always depends_on: - postgres_alpha diff --git a/server/src/api/site.rs b/server/src/api/site.rs index 4202fea06..4e1431a57 100644 --- a/server/src/api/site.rs +++ b/server/src/api/site.rs @@ -1,9 +1,10 @@ use super::*; use crate::api::user::Register; use crate::api::{Oper, Perform}; +use crate::apub::fetcher::search_by_apub_id; use crate::settings::Settings; use diesel::PgConnection; -use log::info; +use log::{debug, info}; use std::str::FromStr; #[derive(Serialize, Deserialize)] @@ -14,7 +15,7 @@ pub struct ListCategoriesResponse { categories: Vec, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct Search { q: String, type_: String, @@ -25,13 +26,13 @@ pub struct Search { auth: Option, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct SearchResponse { - type_: String, - comments: Vec, - posts: Vec, - communities: Vec, - users: Vec, + pub type_: String, + pub comments: Vec, + pub posts: Vec, + pub communities: Vec, + pub users: Vec, } #[derive(Serialize, Deserialize)] @@ -354,6 +355,12 @@ impl Perform for Oper { fn perform(&self, conn: &PgConnection) -> Result { let data: &Search = &self.data; + dbg!(&data); + match search_by_apub_id(&data.q, conn) { + Ok(r) => return Ok(r), + Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e), + } + let user_id: Option = match &data.auth { Some(auth) => match Claims::decode(&auth) { Ok(claims) => { diff --git a/server/src/apub/fetcher.rs b/server/src/apub/fetcher.rs index 53c97b690..f29d9f19f 100644 --- a/server/src/apub/fetcher.rs +++ b/server/src/apub/fetcher.rs @@ -1,8 +1,12 @@ +use crate::api::site::SearchResponse; use crate::apub::*; use crate::db::community::{Community, CommunityForm}; +use crate::db::community_view::CommunityView; use crate::db::post::{Post, PostForm}; +use crate::db::post_view::PostView; use crate::db::user::{UserForm, User_}; -use crate::db::Crud; +use crate::db::user_view::UserView; +use crate::db::{Crud, SearchType}; use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown}; use crate::settings::Settings; use activitystreams::collection::{OrderedCollection, UnorderedCollection}; @@ -27,6 +31,36 @@ fn fetch_node_info(instance: &Instance) -> Result { Ok(fetch_remote_object::(&well_known.links.href)?) } +// TODO: move these to db +fn upsert_community( + community_form: &CommunityForm, + conn: &PgConnection, +) -> Result { + let existing = Community::read_from_actor_id(conn, &community_form.actor_id); + match existing { + Err(NotFound {}) => Ok(Community::create(conn, &community_form)?), + Ok(c) => Ok(Community::update(conn, c.id, &community_form)?), + Err(e) => Err(Error::from(e)), + } +} +fn upsert_user(user_form: &UserForm, conn: &PgConnection) -> Result { + let existing = User_::read_from_apub_id(conn, &user_form.actor_id); + Ok(match existing { + Err(NotFound {}) => User_::create(conn, &user_form)?, + Ok(u) => User_::update(conn, u.id, &user_form)?, + Err(e) => return Err(Error::from(e)), + }) +} + +fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result { + let existing = Post::read_from_apub_id(conn, &post_form.ap_id); + match existing { + Err(NotFound {}) => Ok(Post::create(conn, &post_form)?), + Ok(p) => Ok(Post::update(conn, p.id, &post_form)?), + Err(e) => Err(Error::from(e)), + } +} + fn fetch_communities_from_instance( community_list: &Url, conn: &PgConnection, @@ -39,17 +73,7 @@ fn fetch_communities_from_instance( let group = b.to_owned().to_concrete::()?; Ok(CommunityForm::from_group(&group, conn)?) }) - .map( - |cf: Result| -> Result { - let cf2 = cf?; - let existing = Community::read_from_actor_id(conn, &cf2.actor_id); - match existing { - Err(NotFound {}) => Ok(Community::create(conn, &cf2)?), - Ok(c) => Ok(Community::update(conn, c.id, &cf2)?), - Err(e) => Err(Error::from(e)), - } - }, - ) + .map(|cf| upsert_community(&cf?, conn)) .collect() } @@ -74,6 +98,45 @@ where Ok(res) } +#[serde(untagged)] +#[derive(serde::Deserialize)] +pub enum SearchAcceptedObjects { + Person(Box), + Group(Box), + Page(Box), +} + +pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result { + let query_url = Url::parse(&query)?; + let mut response = SearchResponse { + type_: SearchType::All.to_string(), + comments: vec![], + posts: vec![], + communities: vec![], + users: vec![], + }; + // test with: + // http://lemmy_alpha:8540/federation/c/main + // http://lemmy_alpha:8540/federation/u/lemmy_alpha + // http://lemmy_alpha:8540/federation/p/3 + match fetch_remote_object::(&query_url)? { + SearchAcceptedObjects::Person(p) => { + let u = upsert_user(&UserForm::from_person(&p)?, conn)?; + response.users = vec![UserView::read(conn, u.id)?]; + } + SearchAcceptedObjects::Group(g) => { + let c = upsert_community(&CommunityForm::from_group(&g, conn)?, conn)?; + response.communities = vec![CommunityView::read(conn, c.id, None)?]; + } + SearchAcceptedObjects::Page(p) => { + let p = upsert_post(&PostForm::from_page(&p, conn)?, conn)?; + response.posts = vec![PostView::read(conn, p.id, None)?]; + } + } + dbg!(&response); + Ok(response) +} + fn fetch_remote_community_posts( community: &Community, conn: &PgConnection, @@ -89,39 +152,21 @@ fn fetch_remote_community_posts( let page = obox.clone().to_concrete::()?; PostForm::from_page(&page, conn) }) - .map(|pf: Result| -> Result { - let pf2 = pf?; - let existing = Post::read_from_apub_id(conn, &pf2.ap_id); - match existing { - Err(NotFound {}) => Ok(Post::create(conn, &pf2)?), - Ok(p) => Ok(Post::update(conn, p.id, &pf2)?), - Err(e) => Err(Error::from(e)), - } - }) + .map(|pf| upsert_post(&pf?, conn)) .collect::, Error>>()?, ) } -// TODO: can probably merge these two methods? pub fn fetch_remote_user(apub_id: &Url, conn: &PgConnection) -> Result { let person = fetch_remote_object::(apub_id)?; let uf = UserForm::from_person(&person)?; - let existing = User_::read_from_apub_id(conn, &uf.actor_id); - Ok(match existing { - Err(NotFound {}) => User_::create(conn, &uf)?, - Ok(u) => User_::update(conn, u.id, &uf)?, - Err(e) => return Err(Error::from(e)), - }) + upsert_user(&uf, conn) } + pub fn fetch_remote_community(apub_id: &Url, conn: &PgConnection) -> Result { let group = fetch_remote_object::(apub_id)?; let cf = CommunityForm::from_group(&group, conn)?; - let existing = Community::read_from_actor_id(conn, &cf.actor_id); - Ok(match existing { - Err(NotFound {}) => Community::create(conn, &cf)?, - Ok(u) => Community::update(conn, u.id, &cf)?, - Err(e) => return Err(Error::from(e)), - }) + upsert_community(&cf, conn) } // TODO: in the future, this should only be done when an instance is followed for the first time diff --git a/server/src/routes/federation.rs b/server/src/routes/federation.rs index 6c7cce8a8..4be4e54b5 100644 --- a/server/src/routes/federation.rs +++ b/server/src/routes/federation.rs @@ -37,7 +37,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { ) .route( "/federation/p/{post_id}", - web::get().to(apub::user::get_apub_user), + web::get().to(apub::post::get_apub_post), ); } } From 5ddbe8c6273e70e8c364324b9b51556b83e9483f Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 17 Apr 2020 16:39:03 +0200 Subject: [PATCH 03/13] Remove instance follows --- docker/federation/docker-compose.yml | 2 -- server/config/defaults.hjson | 2 -- server/src/apub/fetcher.rs | 50 +++------------------------- server/src/apub/mod.rs | 15 --------- server/src/main.rs | 15 --------- server/src/routes/federation.rs | 4 --- server/src/routes/nodeinfo.rs | 13 -------- server/src/settings.rs | 1 - 8 files changed, 5 insertions(+), 97 deletions(-) diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index a4e5c3074..61db932cc 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -24,7 +24,6 @@ services: - LEMMY_JWT_SECRET=changeme - LEMMY_FRONT_END_DIR=/app/dist - LEMMY_FEDERATION__ENABLED=true - - LEMMY_FEDERATION__FOLLOWED_INSTANCES=lemmy_beta:8550 - LEMMY_FEDERATION__TLS_ENABLED=false - LEMMY_PORT=8540 - LEMMY_SETUP__ADMIN_USERNAME=lemmy_alpha @@ -58,7 +57,6 @@ services: - LEMMY_JWT_SECRET=changeme - LEMMY_FRONT_END_DIR=/app/dist - LEMMY_FEDERATION__ENABLED=true - - LEMMY_FEDERATION__FOLLOWED_INSTANCES=lemmy_alpha:8540 - LEMMY_FEDERATION__TLS_ENABLED=false - LEMMY_PORT=8550 - LEMMY_SETUP__ADMIN_USERNAME=lemmy_beta diff --git a/server/config/defaults.hjson b/server/config/defaults.hjson index 8603e49a8..509bef47d 100644 --- a/server/config/defaults.hjson +++ b/server/config/defaults.hjson @@ -54,8 +54,6 @@ federation: { # whether to enable activitypub federation. this feature is in alpha, do not enable in production. enabled: false - # comma seperated list of instances to follow - followed_instances: "" # whether tls is required for activitypub. only disable this for debugging, never for producion. tls_enabled: true } diff --git a/server/src/apub/fetcher.rs b/server/src/apub/fetcher.rs index f29d9f19f..3be53ea72 100644 --- a/server/src/apub/fetcher.rs +++ b/server/src/apub/fetcher.rs @@ -9,23 +9,22 @@ use crate::db::user_view::UserView; use crate::db::{Crud, SearchType}; use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown}; use crate::settings::Settings; -use activitystreams::collection::{OrderedCollection, UnorderedCollection}; +use activitystreams::collection::OrderedCollection; use activitystreams::object::Page; use activitystreams::BaseBox; use diesel::result::Error::NotFound; use diesel::PgConnection; use failure::Error; use isahc::prelude::*; -use log::warn; use serde::Deserialize; use std::time::Duration; use url::Url; -fn fetch_node_info(instance: &Instance) -> Result { +fn _fetch_node_info(domain: &str) -> Result { let well_known_uri = Url::parse(&format!( "{}://{}/.well-known/nodeinfo", get_apub_protocol_string(), - instance.domain + domain ))?; let well_known = fetch_remote_object::(&well_known_uri)?; Ok(fetch_remote_object::(&well_known.links.href)?) @@ -61,22 +60,6 @@ fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result } } -fn fetch_communities_from_instance( - community_list: &Url, - conn: &PgConnection, -) -> Result, Error> { - fetch_remote_object::(community_list)? - .collection_props - .get_many_items_base_boxes() - .unwrap() - .map(|b| -> Result { - let group = b.to_owned().to_concrete::()?; - Ok(CommunityForm::from_group(&group, conn)?) - }) - .map(|cf| upsert_community(&cf?, conn)) - .collect() -} - // TODO: add an optional param last_updated and only fetch if its too old pub fn fetch_remote_object(url: &Url) -> Result where @@ -126,6 +109,7 @@ pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result { let c = upsert_community(&CommunityForm::from_group(&g, conn)?, conn)?; + fetch_community_outbox(&c, conn)?; response.communities = vec![CommunityView::read(conn, c.id, None)?]; } SearchAcceptedObjects::Page(p) => { @@ -133,14 +117,10 @@ pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result Result, Error> { +fn fetch_community_outbox(community: &Community, conn: &PgConnection) -> Result, Error> { let outbox_url = Url::parse(&community.get_outbox_url())?; let outbox = fetch_remote_object::(&outbox_url)?; let items = outbox.collection_props.get_many_items_base_boxes(); @@ -168,23 +148,3 @@ pub fn fetch_remote_community(apub_id: &Url, conn: &PgConnection) -> Result Result<(), Error> { - for instance in &get_following_instances() { - let node_info = fetch_node_info(instance)?; - if let Some(community_list) = node_info.metadata.community_list_url { - let communities = fetch_communities_from_instance(&community_list, conn)?; - for c in communities { - fetch_remote_community_posts(&c, conn)?; - } - } else { - warn!( - "{} is not a Lemmy instance, federation is not supported", - instance.domain - ); - } - } - Ok(()) -} diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index 11b513beb..04b462bff 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -27,10 +27,6 @@ pub enum EndpointType { Comment, } -pub struct Instance { - domain: String, -} - fn create_apub_response(json: &T) -> HttpResponse where T: serde::ser::Serialize, @@ -92,14 +88,3 @@ pub fn gen_keypair_str() -> (String, String) { fn vec_bytes_to_str(bytes: Vec) -> String { String::from_utf8_lossy(&bytes).into_owned() } - -pub fn get_following_instances() -> Vec { - Settings::get() - .federation - .followed_instances - .split(',') - .map(|i| Instance { - domain: i.to_string(), - }) - .collect() -} diff --git a/server/src/main.rs b/server/src/main.rs index 2b4e8b081..a4acf1ec0 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -7,15 +7,10 @@ use actix_web::*; use diesel::r2d2::{ConnectionManager, Pool}; use diesel::PgConnection; use failure::Error; -use lemmy_server::apub::fetcher::fetch_all; use lemmy_server::db::code_migrations::run_advanced_migrations; use lemmy_server::routes::{api, federation, feeds, index, nodeinfo, webfinger, websocket}; use lemmy_server::settings::Settings; use lemmy_server::websocket::server::*; -use log::warn; -use std::thread; -use std::thread::sleep; -use std::time::Duration; embed_migrations!(); @@ -39,16 +34,6 @@ async fn main() -> Result<(), Error> { // Set up websocket server let server = ChatServer::startup(pool.clone()).start(); - thread::spawn(move || { - // some work here - sleep(Duration::from_secs(5)); - println!("Fetching apub data"); - match fetch_all(&conn) { - Ok(_) => {} - Err(e) => warn!("Error during apub fetch: {}", e), - } - }); - println!( "Starting http server at {}:{}", settings.bind, settings.port diff --git a/server/src/routes/federation.rs b/server/src/routes/federation.rs index 4be4e54b5..ef7ba56f8 100644 --- a/server/src/routes/federation.rs +++ b/server/src/routes/federation.rs @@ -6,10 +6,6 @@ pub fn config(cfg: &mut web::ServiceConfig) { if Settings::get().federation.enabled { println!("federation enabled, host is {}", Settings::get().hostname); cfg - .route( - "/federation/communities", - web::get().to(apub::community::get_apub_community_list), - ) // TODO: check the user/community params for these .route( "/federation/c/{_}/inbox", diff --git a/server/src/routes/nodeinfo.rs b/server/src/routes/nodeinfo.rs index a8ae21388..8f50fbc79 100644 --- a/server/src/routes/nodeinfo.rs +++ b/server/src/routes/nodeinfo.rs @@ -61,13 +61,6 @@ async fn node_info( local_comments: site_view.number_of_comments, open_registrations: site_view.open_registration, }, - metadata: NodeInfoMetadata { - community_list_url: Some(Url::parse(&format!( - "{}://{}/federation/communities", - get_apub_protocol_string(), - Settings::get().hostname - ))?), - }, }) }) .await @@ -93,7 +86,6 @@ pub struct NodeInfo { pub software: NodeInfoSoftware, pub protocols: Vec, pub usage: NodeInfoUsage, - pub metadata: NodeInfoMetadata, } #[derive(Serialize, Deserialize, Debug)] @@ -115,8 +107,3 @@ pub struct NodeInfoUsage { pub struct NodeInfoUsers { pub total: i64, } - -#[derive(Serialize, Deserialize, Debug)] -pub struct NodeInfoMetadata { - pub community_list_url: Option, -} diff --git a/server/src/settings.rs b/server/src/settings.rs index d9d7a2291..a82e47860 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -63,7 +63,6 @@ pub struct Database { #[derive(Debug, Deserialize, Clone)] pub struct Federation { pub enabled: bool, - pub followed_instances: String, pub tls_enabled: bool, } From 7d152c81bebd5c06fe9010512adadf9f29c96a53 Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 17 Apr 2020 16:55:28 +0200 Subject: [PATCH 04/13] Some code cleanup and better logging --- server/src/apub/activities.rs | 7 ++++--- server/src/apub/community_inbox.rs | 17 +++++++++++++---- server/src/apub/user_inbox.rs | 16 +++++++++++++--- server/src/routes/federation.rs | 4 ++-- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/server/src/apub/activities.rs b/server/src/apub/activities.rs index 2e35400d0..8885f5558 100644 --- a/server/src/apub/activities.rs +++ b/server/src/apub/activities.rs @@ -11,6 +11,7 @@ use diesel::PgConnection; use failure::Error; use failure::_core::fmt::Debug; use isahc::prelude::*; +use log::debug; use serde::Serialize; fn populate_object_props( @@ -34,14 +35,14 @@ where A: Serialize + Debug, { let json = serde_json::to_string(&activity)?; - println!("sending data {}", json); + debug!("Sending activitypub activity {}", json); for t in to { - println!("to: {}", t); + debug!("Sending activity to: {}", t); let res = Request::post(t) .header("Content-Type", "application/json") .body(json.to_owned())? .send()?; - dbg!(res); + debug!("Result for activity send: {:?}", res); } Ok(()) } diff --git a/server/src/apub/community_inbox.rs b/server/src/apub/community_inbox.rs index 0fe3fd709..caadecf13 100644 --- a/server/src/apub/community_inbox.rs +++ b/server/src/apub/community_inbox.rs @@ -7,28 +7,38 @@ use actix_web::{web, HttpResponse}; use diesel::r2d2::{ConnectionManager, Pool}; use diesel::PgConnection; use failure::Error; +use log::debug; +use serde::Deserialize; use url::Url; #[serde(untagged)] -#[derive(serde::Deserialize)] +#[derive(Deserialize, Debug)] pub enum CommunityAcceptedObjects { Follow(Follow), } +#[derive(Deserialize)] +pub struct Params { + community_name: String, +} + pub async fn community_inbox( input: web::Json, + params: web::Query, db: web::Data>>, ) -> Result { let input = input.into_inner(); let conn = &db.get().unwrap(); + debug!( + "Community {} received activity {:?}", + ¶ms.community_name, &input + ); match input { CommunityAcceptedObjects::Follow(f) => handle_follow(&f, conn), } } fn handle_follow(follow: &Follow, conn: &PgConnection) -> Result { - println!("received follow: {:?}", &follow); - // TODO: make sure this is a local community let community_uri = follow .follow_props @@ -42,7 +52,6 @@ fn handle_follow(follow: &Follow, conn: &PgConnection) -> Result, + params: web::Query, db: web::Data>>, ) -> Result { let input = input.into_inner(); let conn = &db.get().unwrap(); + debug!("User {} received activity: {:?}", ¶ms.user_name, &input); + match input { UserAcceptedObjects::Create(c) => handle_create(&c, conn), UserAcceptedObjects::Update(u) => handle_update(&u, conn), @@ -57,8 +67,8 @@ fn handle_update(update: &Update, conn: &PgConnection) -> Result Result { - println!("received accept: {:?}", &accept); +fn handle_accept(_accept: &Accept, _conn: &PgConnection) -> Result { + // TODO: make sure that we actually requested a follow // TODO: at this point, indicate to the user that they are following the community Ok(HttpResponse::Ok().finish()) } diff --git a/server/src/routes/federation.rs b/server/src/routes/federation.rs index ef7ba56f8..d80759c3e 100644 --- a/server/src/routes/federation.rs +++ b/server/src/routes/federation.rs @@ -8,11 +8,11 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg // TODO: check the user/community params for these .route( - "/federation/c/{_}/inbox", + "/federation/c/{community_name}/inbox", web::post().to(apub::community_inbox::community_inbox), ) .route( - "/federation/u/{_}/inbox", + "/federation/u/{user_name}/inbox", web::post().to(apub::user_inbox::user_inbox), ) .route( From 8189425ad9cb9ff4bc12216dd82f08451193c3c4 Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 17 Apr 2020 17:33:55 +0200 Subject: [PATCH 05/13] Added documentation for most functions --- server/src/apub/activities.rs | 12 +++++++--- server/src/apub/community.rs | 29 ++++------------------- server/src/apub/community_inbox.rs | 3 +++ server/src/apub/fetcher.rs | 19 +++++++++++---- server/src/apub/mod.rs | 38 +++++++++++++++--------------- server/src/apub/post.rs | 3 +++ server/src/apub/signatures.rs | 5 ++-- server/src/apub/user.rs | 2 ++ server/src/apub/user_inbox.rs | 4 ++++ 9 files changed, 62 insertions(+), 53 deletions(-) diff --git a/server/src/apub/activities.rs b/server/src/apub/activities.rs index 8885f5558..f31be9db3 100644 --- a/server/src/apub/activities.rs +++ b/server/src/apub/activities.rs @@ -30,6 +30,7 @@ fn populate_object_props( Ok(()) } +/// Send an activity to a list of recipients, using the correct headers etc. fn send_activity(activity: &A, to: Vec) -> Result<(), Error> where A: Serialize + Debug, @@ -47,7 +48,8 @@ where Ok(()) } -fn get_followers(conn: &PgConnection, community: &Community) -> Result, Error> { +/// For a given community, returns the inboxes of all followers. +fn get_follower_inboxes(conn: &PgConnection, community: &Community) -> Result, Error> { Ok( CommunityFollowerView::for_community(conn, community.id)? .iter() @@ -57,6 +59,7 @@ fn get_followers(conn: &PgConnection, community: &Community) -> Result Result<(), Error> { let page = post.as_page(conn)?; let community = Community::read(conn, post.community_id)?; @@ -70,10 +73,11 @@ pub fn post_create(post: &Post, creator: &User_, conn: &PgConnection) -> Result< .create_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_object_base_box(page)?; - send_activity(&create, get_followers(conn, &community)?)?; + send_activity(&create, get_follower_inboxes(conn, &community)?)?; Ok(()) } +/// Send out information about an edited post, to the followers of the community. pub fn post_update(post: &Post, creator: &User_, conn: &PgConnection) -> Result<(), Error> { let page = post.as_page(conn)?; let community = Community::read(conn, post.community_id)?; @@ -87,10 +91,11 @@ pub fn post_update(post: &Post, creator: &User_, conn: &PgConnection) -> Result< .update_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_object_base_box(page)?; - send_activity(&update, get_followers(conn, &community)?)?; + send_activity(&update, get_follower_inboxes(conn, &community)?)?; Ok(()) } +/// As a given local user, send out a follow request to a remote community. pub fn follow_community( community: &Community, user: &User_, @@ -111,6 +116,7 @@ pub fn follow_community( Ok(()) } +/// As a local community, accept the follow request from a remote user. pub fn accept_follow(follow: &Follow) -> Result<(), Error> { let mut accept = Accept::new(); accept diff --git a/server/src/apub/community.rs b/server/src/apub/community.rs index 0bea47055..a49357a86 100644 --- a/server/src/apub/community.rs +++ b/server/src/apub/community.rs @@ -7,7 +7,6 @@ use crate::db::establish_unpooled_connection; use crate::db::post::Post; use crate::db::user::User_; use crate::db::Crud; -use crate::settings::Settings; use crate::{convert_datetime, naive_now}; use activitystreams::actor::properties::ApActorProperties; use activitystreams::collection::OrderedCollection; @@ -30,30 +29,8 @@ pub struct CommunityQuery { community_name: String, } -pub async fn get_apub_community_list( - db: web::Data>>, -) -> Result, Error> { - // TODO: implement pagination - let communities = Community::list_local(&db.get().unwrap())? - .iter() - .map(|c| c.as_group(&db.get().unwrap())) - .collect::, Error>>()?; - let mut collection = UnorderedCollection::default(); - let oprops: &mut ObjectProperties = collection.as_mut(); - oprops.set_context_xsd_any_uri(context())?.set_id(format!( - "{}://{}/federation/communities", - get_apub_protocol_string(), - Settings::get().hostname - ))?; - - collection - .collection_props - .set_total_items(communities.len() as u64)? - .set_many_items_base_boxes(communities)?; - Ok(create_apub_response(&collection)) -} - impl Community { + // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network. fn as_group(&self, conn: &PgConnection) -> Result { let mut group = Group::default(); let oprops: &mut ObjectProperties = group.as_mut(); @@ -104,6 +81,7 @@ impl Community { } impl CommunityForm { + /// Parse an ActivityPub group received from another instance into a Lemmy community. pub fn from_group(group: &GroupExt, conn: &PgConnection) -> Result { let oprops = &group.base.base.object_props; let aprops = &group.base.extension; @@ -142,6 +120,7 @@ impl CommunityForm { } } +/// Return the community json over HTTP. pub async fn get_apub_community_http( info: Path, db: web::Data>>, @@ -151,6 +130,7 @@ pub async fn get_apub_community_http( Ok(create_apub_response(&c)) } +/// Returns an empty followers collection, only populating the siz (for privacy). pub async fn get_apub_community_followers( info: Path, db: web::Data>>, @@ -173,6 +153,7 @@ pub async fn get_apub_community_followers( Ok(create_apub_response(&collection)) } +/// Returns an UnorderedCollection with the latest posts from the community. pub async fn get_apub_community_outbox( info: Path, db: web::Data>>, diff --git a/server/src/apub/community_inbox.rs b/server/src/apub/community_inbox.rs index caadecf13..ea0d91054 100644 --- a/server/src/apub/community_inbox.rs +++ b/server/src/apub/community_inbox.rs @@ -22,6 +22,7 @@ pub struct Params { community_name: String, } +/// Handler for all incoming activities to community inboxes. pub async fn community_inbox( input: web::Json, params: web::Query, @@ -38,6 +39,8 @@ pub async fn community_inbox( } } +/// Handle a follow request from a remote user, adding it to the local database and returning an +/// Accept activity. fn handle_follow(follow: &Follow, conn: &PgConnection) -> Result { // TODO: make sure this is a local community let community_uri = follow diff --git a/server/src/apub/fetcher.rs b/server/src/apub/fetcher.rs index 3be53ea72..368aa4dc4 100644 --- a/server/src/apub/fetcher.rs +++ b/server/src/apub/fetcher.rs @@ -20,6 +20,7 @@ use serde::Deserialize; use std::time::Duration; use url::Url; +// Fetch nodeinfo metadata from a remote instance. fn _fetch_node_info(domain: &str) -> Result { let well_known_uri = Url::parse(&format!( "{}://{}/.well-known/nodeinfo", @@ -60,7 +61,9 @@ fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result } } -// TODO: add an optional param last_updated and only fetch if its too old +/// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation, +/// timeouts etc. +/// TODO: add an optional param last_updated and only fetch if its too old pub fn fetch_remote_object(url: &Url) -> Result where Response: for<'de> Deserialize<'de>, @@ -81,6 +84,7 @@ where Ok(res) } +/// The types of ActivityPub objects that can be fetched directly by searching for their ID. #[serde(untagged)] #[derive(serde::Deserialize)] pub enum SearchAcceptedObjects { @@ -89,6 +93,12 @@ pub enum SearchAcceptedObjects { Page(Box), } +/// Attempt to parse the query as URL, and fetch an ActivityPub object from it. +/// +/// Some working examples for use with the docker/federation/ setup: +/// http://lemmy_alpha:8540/federation/c/main +/// http://lemmy_alpha:8540/federation/u/lemmy_alpha +/// http://lemmy_alpha:8540/federation/p/3 pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result { let query_url = Url::parse(&query)?; let mut response = SearchResponse { @@ -98,10 +108,6 @@ pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result(&query_url)? { SearchAcceptedObjects::Person(p) => { let u = upsert_user(&UserForm::from_person(&p)?, conn)?; @@ -120,6 +126,7 @@ pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result Result, Error> { let outbox_url = Url::parse(&community.get_outbox_url())?; let outbox = fetch_remote_object::(&outbox_url)?; @@ -137,12 +144,14 @@ fn fetch_community_outbox(community: &Community, conn: &PgConnection) -> Result< ) } +/// Fetch a user, insert/update it in the database and return the user. pub fn fetch_remote_user(apub_id: &Url, conn: &PgConnection) -> Result { let person = fetch_remote_object::(apub_id)?; let uf = UserForm::from_person(&person)?; upsert_user(&uf, conn) } +/// Fetch a community, insert/update it in the database and return the community. pub fn fetch_remote_community(apub_id: &Url, conn: &PgConnection) -> Result { let group = fetch_remote_object::(apub_id)?; let cf = CommunityForm::from_group(&group, conn)?; diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index 04b462bff..a7f0668a3 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -13,6 +13,7 @@ use activitystreams::ext::Ext; use actix_web::body::Body; use actix_web::HttpResponse; use openssl::{pkey::PKey, rsa::Rsa}; +use serde::ser::Serialize; use url::Url; type GroupExt = Ext, PublicKeyExtension>; @@ -27,18 +28,22 @@ pub enum EndpointType { Comment, } -fn create_apub_response(json: &T) -> HttpResponse +/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub +/// headers. +fn create_apub_response(data: &T) -> HttpResponse where - T: serde::ser::Serialize, + T: Serialize, { HttpResponse::Ok() .content_type(APUB_JSON_CONTENT_TYPE) - .json(json) + .json(data) } -// TODO: we will probably need to change apub endpoint urls so that html and activity+json content -// types are handled at the same endpoint, so that you can copy the url into mastodon search -// and have it fetch the object. +/// Generates the ActivityPub ID for a given object type and name. +/// +/// TODO: we will probably need to change apub endpoint urls so that html and activity+json content +/// types are handled at the same endpoint, so that you can copy the url into mastodon search +/// and have it fetch the object. pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url { let point = match endpoint_type { EndpointType::Community => "c", @@ -67,21 +72,16 @@ pub fn get_apub_protocol_string() -> &'static str { } } -pub fn gen_keypair() -> (Vec, Vec) { +/// Generate the asymmetric keypair for ActivityPub HTTP signatures. +pub fn gen_keypair_str() -> (String, String) { let rsa = Rsa::generate(2048).expect("sign::gen_keypair: key generation error"); let pkey = PKey::from_rsa(rsa).expect("sign::gen_keypair: parsing error"); - ( - pkey - .public_key_to_pem() - .expect("sign::gen_keypair: public key encoding error"), - pkey - .private_key_to_pem_pkcs8() - .expect("sign::gen_keypair: private key encoding error"), - ) -} - -pub fn gen_keypair_str() -> (String, String) { - let (public_key, private_key) = gen_keypair(); + let public_key = pkey + .public_key_to_pem() + .expect("sign::gen_keypair: public key encoding error"); + let private_key = pkey + .private_key_to_pem_pkcs8() + .expect("sign::gen_keypair: private key encoding error"); (vec_bytes_to_str(public_key), vec_bytes_to_str(private_key)) } diff --git a/server/src/apub/post.rs b/server/src/apub/post.rs index b574d09c0..edae92d06 100644 --- a/server/src/apub/post.rs +++ b/server/src/apub/post.rs @@ -20,6 +20,7 @@ pub struct PostQuery { post_id: String, } +/// Return the post json over HTTP. pub async fn get_apub_post( info: Path, db: web::Data>>, @@ -30,6 +31,7 @@ pub async fn get_apub_post( } impl Post { + // Turn a Lemmy post into an ActivityPub page that can be sent out over the network. pub fn as_page(&self, conn: &PgConnection) -> Result { let mut page = Page::default(); let oprops: &mut ObjectProperties = page.as_mut(); @@ -67,6 +69,7 @@ impl Post { } impl PostForm { + /// Parse an ActivityPub page received from another instance into a Lemmy post. pub fn from_page(page: &Page, conn: &PgConnection) -> Result { let oprops = &page.object_props; let creator_id = Url::parse(&oprops.get_attributed_to_xsd_any_uri().unwrap().to_string())?; diff --git a/server/src/apub/signatures.rs b/server/src/apub/signatures.rs index 5bb3c5344..0348acb85 100644 --- a/server/src/apub/signatures.rs +++ b/server/src/apub/signatures.rs @@ -1,11 +1,12 @@ // For this example, we'll use the Extensible trait, the Extension trait, the Actor trait, and // the Person type use activitystreams::{actor::Actor, ext::Extension}; +use serde::{Deserialize, Serialize}; // The following is taken from here: // https://docs.rs/activitystreams/0.5.0-alpha.17/activitystreams/ext/index.html -#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct PublicKey { pub id: String, @@ -13,7 +14,7 @@ pub struct PublicKey { pub public_key_pem: String, } -#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct PublicKeyExtension { pub public_key: PublicKey, diff --git a/server/src/apub/user.rs b/server/src/apub/user.rs index d10093d4c..b5a819114 100644 --- a/server/src/apub/user.rs +++ b/server/src/apub/user.rs @@ -22,6 +22,7 @@ pub struct UserQuery { user_name: String, } +// Turn a Lemmy user into an ActivityPub person and return it as json. pub async fn get_apub_user( info: Path, db: web::Data>>, @@ -64,6 +65,7 @@ pub async fn get_apub_user( } impl UserForm { + /// Parse an ActivityPub person received from another instance into a Lemmy user. pub fn from_person(person: &PersonExt) -> Result { let oprops = &person.base.base.object_props; let aprops = &person.base.extension; diff --git a/server/src/apub/user_inbox.rs b/server/src/apub/user_inbox.rs index 3b8d1df35..7d1463083 100644 --- a/server/src/apub/user_inbox.rs +++ b/server/src/apub/user_inbox.rs @@ -22,6 +22,7 @@ pub struct Params { user_name: String, } +/// Handler for all incoming activities to user inboxes. pub async fn user_inbox( input: web::Json, params: web::Query, @@ -38,6 +39,7 @@ pub async fn user_inbox( } } +/// Handle create activities and insert them in the database. fn handle_create(create: &Create, conn: &PgConnection) -> Result { let page = create .create_props @@ -52,6 +54,7 @@ fn handle_create(create: &Create, conn: &PgConnection) -> Result Result { let page = update .update_props @@ -67,6 +70,7 @@ fn handle_update(update: &Update, conn: &PgConnection) -> Result Result { // TODO: make sure that we actually requested a follow // TODO: at this point, indicate to the user that they are following the community From 19fd788e7206688413348b6c4131de5cd761a73f Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 17 Apr 2020 19:34:18 +0200 Subject: [PATCH 06/13] Implement instance whitelist --- docker/federation/docker-compose.yml | 2 ++ server/config/defaults.hjson | 2 ++ server/src/apub/activities.rs | 8 ++++++-- server/src/apub/community_inbox.rs | 10 +++------- server/src/apub/fetcher.rs | 7 +++---- server/src/apub/mod.rs | 23 +++++++++++++++++++++++ server/src/apub/user_inbox.rs | 13 ++++++------- server/src/settings.rs | 1 + 8 files changed, 46 insertions(+), 20 deletions(-) diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index 61db932cc..b7aec47ba 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -25,6 +25,7 @@ services: - LEMMY_FRONT_END_DIR=/app/dist - LEMMY_FEDERATION__ENABLED=true - LEMMY_FEDERATION__TLS_ENABLED=false + - LEMMY_FEDERATION__INSTANCE_WHITELIST=lemmy_beta - LEMMY_PORT=8540 - LEMMY_SETUP__ADMIN_USERNAME=lemmy_alpha - LEMMY_SETUP__ADMIN_PASSWORD=lemmy @@ -58,6 +59,7 @@ services: - LEMMY_FRONT_END_DIR=/app/dist - LEMMY_FEDERATION__ENABLED=true - LEMMY_FEDERATION__TLS_ENABLED=false + - LEMMY_FEDERATION__INSTANCE_WHITELIST=lemmy_alpha - LEMMY_PORT=8550 - LEMMY_SETUP__ADMIN_USERNAME=lemmy_beta - LEMMY_SETUP__ADMIN_PASSWORD=lemmy diff --git a/server/config/defaults.hjson b/server/config/defaults.hjson index 509bef47d..09db4c020 100644 --- a/server/config/defaults.hjson +++ b/server/config/defaults.hjson @@ -56,6 +56,8 @@ enabled: false # whether tls is required for activitypub. only disable this for debugging, never for producion. tls_enabled: true + # comma seperated list of instances with which federation is allowed + instance_whitelist: "" } # # email sending configuration # email: { diff --git a/server/src/apub/activities.rs b/server/src/apub/activities.rs index f31be9db3..d49008064 100644 --- a/server/src/apub/activities.rs +++ b/server/src/apub/activities.rs @@ -1,3 +1,4 @@ +use crate::apub::is_apub_id_valid; use crate::db::community::Community; use crate::db::community_view::CommunityFollowerView; use crate::db::post::Post; @@ -36,9 +37,12 @@ where A: Serialize + Debug, { let json = serde_json::to_string(&activity)?; - debug!("Sending activitypub activity {}", json); + debug!("Sending activitypub activity {} to {:?}", json, to); for t in to { - debug!("Sending activity to: {}", t); + if is_apub_id_valid(&t) { + debug!("Not sending activity to {} (invalid or blacklisted)", t); + continue; + } let res = Request::post(t) .header("Content-Type", "application/json") .body(json.to_owned())? diff --git a/server/src/apub/community_inbox.rs b/server/src/apub/community_inbox.rs index ea0d91054..65d7bec1f 100644 --- a/server/src/apub/community_inbox.rs +++ b/server/src/apub/community_inbox.rs @@ -17,22 +17,18 @@ pub enum CommunityAcceptedObjects { Follow(Follow), } -#[derive(Deserialize)] -pub struct Params { - community_name: String, -} - /// Handler for all incoming activities to community inboxes. pub async fn community_inbox( input: web::Json, - params: web::Query, + path: web::Path, db: web::Data>>, ) -> Result { let input = input.into_inner(); let conn = &db.get().unwrap(); debug!( "Community {} received activity {:?}", - ¶ms.community_name, &input + &path.into_inner(), + &input ); match input { CommunityAcceptedObjects::Follow(f) => handle_follow(&f, conn), diff --git a/server/src/apub/fetcher.rs b/server/src/apub/fetcher.rs index 368aa4dc4..d44fdcb5f 100644 --- a/server/src/apub/fetcher.rs +++ b/server/src/apub/fetcher.rs @@ -8,7 +8,6 @@ use crate::db::user::{UserForm, User_}; use crate::db::user_view::UserView; use crate::db::{Crud, SearchType}; use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown}; -use crate::settings::Settings; use activitystreams::collection::OrderedCollection; use activitystreams::object::Page; use activitystreams::BaseBox; @@ -68,8 +67,8 @@ pub fn fetch_remote_object(url: &Url) -> Result where Response: for<'de> Deserialize<'de>, { - if Settings::get().federation.tls_enabled && url.scheme() != "https" { - return Err(format_err!("Activitypub uri is insecure: {}", url)); + if !is_apub_id_valid(&url.to_string()) { + return Err(format_err!("Activitypub uri invalid or blocked: {}", url)); } // TODO: this function should return a future let timeout = Duration::from_secs(60); @@ -86,7 +85,7 @@ where /// The types of ActivityPub objects that can be fetched directly by searching for their ID. #[serde(untagged)] -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, Debug)] pub enum SearchAcceptedObjects { Person(Box), Group(Box), diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index a7f0668a3..634f35101 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -88,3 +88,26 @@ pub fn gen_keypair_str() -> (String, String) { fn vec_bytes_to_str(bytes: Vec) -> String { String::from_utf8_lossy(&bytes).into_owned() } + +// Checks if the ID has a valid format, correct scheme, and is in the whitelist. +fn is_apub_id_valid(apub_id: &str) -> bool { + let url = match Url::parse(apub_id) { + Ok(u) => u, + Err(_) => return false, + }; + + if url.scheme() != get_apub_protocol_string() { + return false; + } + + let whitelist: Vec = Settings::get() + .federation + .instance_whitelist + .split(',') + .map(|d| d.to_string()) + .collect(); + match url.domain() { + Some(d) => whitelist.contains(&d.to_owned()), + None => false, + } +} diff --git a/server/src/apub/user_inbox.rs b/server/src/apub/user_inbox.rs index 7d1463083..75cd4e479 100644 --- a/server/src/apub/user_inbox.rs +++ b/server/src/apub/user_inbox.rs @@ -17,20 +17,19 @@ pub enum UserAcceptedObjects { Accept(Accept), } -#[derive(Deserialize)] -pub struct Params { - user_name: String, -} - /// Handler for all incoming activities to user inboxes. pub async fn user_inbox( input: web::Json, - params: web::Query, + path: web::Path, db: web::Data>>, ) -> Result { let input = input.into_inner(); let conn = &db.get().unwrap(); - debug!("User {} received activity: {:?}", ¶ms.user_name, &input); + debug!( + "User {} received activity: {:?}", + &path.into_inner(), + &input + ); match input { UserAcceptedObjects::Create(c) => handle_create(&c, conn), diff --git a/server/src/settings.rs b/server/src/settings.rs index a82e47860..c19cb7175 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -64,6 +64,7 @@ pub struct Database { pub struct Federation { pub enabled: bool, pub tls_enabled: bool, + pub instance_whitelist: String, } lazy_static! { From cd2d4dee744921d881ab30e289f652aea074583d Mon Sep 17 00:00:00 2001 From: Felix Date: Sat, 18 Apr 2020 17:17:25 +0200 Subject: [PATCH 07/13] Fix bug in whitelist implementation --- server/src/apub/activities.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/apub/activities.rs b/server/src/apub/activities.rs index d49008064..73b7293d7 100644 --- a/server/src/apub/activities.rs +++ b/server/src/apub/activities.rs @@ -39,7 +39,7 @@ where let json = serde_json::to_string(&activity)?; debug!("Sending activitypub activity {} to {:?}", json, to); for t in to { - if is_apub_id_valid(&t) { + if !is_apub_id_valid(&t) { debug!("Not sending activity to {} (invalid or blacklisted)", t); continue; } From 615c2b7df6f2d6eea5212351cf4442668d443db3 Mon Sep 17 00:00:00 2001 From: Felix Date: Sat, 18 Apr 2020 17:24:55 +0200 Subject: [PATCH 08/13] Use debug logging --- docker/federation/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index b7aec47ba..4f23cdcc2 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -65,7 +65,7 @@ services: - LEMMY_SETUP__ADMIN_PASSWORD=lemmy - LEMMY_SETUP__SITE_NAME=lemmy_beta - RUST_BACKTRACE=1 - - RUST_LOG=actix_web=debug + - RUST_LOG=debug restart: always depends_on: - postgres_beta From c23b49502ea75170d22d6e2668cafef9572e462d Mon Sep 17 00:00:00 2001 From: Felix Date: Sat, 18 Apr 2020 20:54:20 +0200 Subject: [PATCH 09/13] Add http signature to outgoing apub requests --- server/Cargo.lock | 27 ++++++++---- server/Cargo.toml | 3 ++ server/src/api/community.rs | 9 ++-- server/src/api/user.rs | 15 +++---- server/src/apub/activities.rs | 62 ++++++++++++++++++++-------- server/src/apub/community_inbox.rs | 2 +- server/src/apub/fetcher.rs | 2 +- server/src/apub/mod.rs | 29 ++----------- server/src/apub/signatures.rs | 66 +++++++++++++++++++++++++++++- server/src/db/code_migrations.rs | 15 +++---- server/src/db/community.rs | 16 ++++++++ server/src/db/user.rs | 16 ++++++++ 12 files changed, 191 insertions(+), 71 deletions(-) diff --git a/server/Cargo.lock b/server/Cargo.lock index b17f4d60b..577c6d317 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -74,7 +74,7 @@ dependencies = [ "derive_more 0.99.3 (registry+https://github.com/rust-lang/crates.io-index)", "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "trust-dns-proto 0.18.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)", "trust-dns-resolver 0.18.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -126,7 +126,7 @@ dependencies = [ "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", "fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "h2 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", "indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -160,7 +160,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bytestring 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1176,7 +1176,7 @@ dependencies = [ "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1237,7 +1237,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "http" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1245,6 +1245,15 @@ dependencies = [ "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "http-signature-normalization" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "httparse" version = "1.3.4" @@ -1314,7 +1323,7 @@ dependencies = [ "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", "futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1387,6 +1396,7 @@ dependencies = [ "actix-rt 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "actix-web 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "actix-web-actors 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "bcrypt 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", "comrak 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1398,6 +1408,8 @@ dependencies = [ "failure 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "hjson 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", "htmlescape 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "http-signature-normalization 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "isahc 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", "jsonwebtoken 7.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3070,7 +3082,8 @@ dependencies = [ "checksum hostname 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e" "checksum hostname 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" "checksum htmlescape 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" -"checksum http 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b708cc7f06493459026f53b9a61a7a121a5d1ec6238dee58ea4941132b30156b" +"checksum http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +"checksum http-signature-normalization 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "257835255b5d40c6de712d90e56dc874ca5da2816121e7b9f3cfc7b3a55a5714" "checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" "checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" "checksum ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" diff --git a/server/Cargo.toml b/server/Cargo.toml index 03bbfbee5..8bd170efb 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -39,3 +39,6 @@ percent-encoding = "2.1.0" isahc = "0.9" comrak = "0.7" openssl = "0.10" +http = "0.2.1" +http-signature-normalization = "0.4.1" +base64 = "0.12.0" diff --git a/server/src/api/community.rs b/server/src/api/community.rs index 40d8afe39..30c8aa205 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -1,6 +1,7 @@ use super::*; use crate::apub::activities::follow_community; -use crate::apub::{gen_keypair_str, make_apub_endpoint, EndpointType}; +use crate::apub::signatures::generate_actor_keypair; +use crate::apub::{make_apub_endpoint, EndpointType}; use diesel::PgConnection; use std::str::FromStr; @@ -200,7 +201,7 @@ impl Perform for Oper { } // When you create a community, make sure the user becomes a moderator and a follower - let (community_public_key, community_private_key) = gen_keypair_str(); + let keypair = generate_actor_keypair(); let community_form = CommunityForm { name: data.name.to_owned(), @@ -214,8 +215,8 @@ impl Perform for Oper { updated: None, actor_id: make_apub_endpoint(EndpointType::Community, &data.name).to_string(), local: true, - private_key: Some(community_private_key), - public_key: Some(community_public_key), + private_key: Some(keypair.private_key), + public_key: Some(keypair.public_key), last_refreshed_at: None, published: None, }; diff --git a/server/src/api/user.rs b/server/src/api/user.rs index fbdead53b..35bdd33ab 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -1,5 +1,6 @@ use super::*; -use crate::apub::{gen_keypair_str, make_apub_endpoint, EndpointType}; +use crate::apub::signatures::generate_actor_keypair; +use crate::apub::{make_apub_endpoint, EndpointType}; use crate::settings::Settings; use crate::{generate_random_string, send_email}; use bcrypt::verify; @@ -251,7 +252,7 @@ impl Perform for Oper { return Err(APIError::err("admin_already_created").into()); } - let (user_public_key, user_private_key) = gen_keypair_str(); + let keypair = generate_actor_keypair(); // Register the new user let user_form = UserForm { @@ -274,8 +275,8 @@ impl Perform for Oper { actor_id: make_apub_endpoint(EndpointType::User, &data.username).to_string(), bio: None, local: true, - private_key: Some(user_private_key), - public_key: Some(user_public_key), + private_key: Some(keypair.private_key), + public_key: Some(keypair.public_key), last_refreshed_at: None, }; @@ -295,7 +296,7 @@ impl Perform for Oper { } }; - let (community_public_key, community_private_key) = gen_keypair_str(); + let keypair = generate_actor_keypair(); // Create the main community if it doesn't exist let main_community: Community = match Community::read(&conn, 2) { @@ -314,8 +315,8 @@ impl Perform for Oper { updated: None, actor_id: make_apub_endpoint(EndpointType::Community, default_community_name).to_string(), local: true, - private_key: Some(community_private_key), - public_key: Some(community_public_key), + private_key: Some(keypair.private_key), + public_key: Some(keypair.public_key), last_refreshed_at: None, published: None, }; diff --git a/server/src/apub/activities.rs b/server/src/apub/activities.rs index 73b7293d7..a7f63ec85 100644 --- a/server/src/apub/activities.rs +++ b/server/src/apub/activities.rs @@ -1,4 +1,5 @@ use crate::apub::is_apub_id_valid; +use crate::apub::signatures::{sign, Keypair}; use crate::db::community::Community; use crate::db::community_view::CommunityFollowerView; use crate::db::post::Post; @@ -14,6 +15,7 @@ use failure::_core::fmt::Debug; use isahc::prelude::*; use log::debug; use serde::Serialize; +use url::Url; fn populate_object_props( props: &mut ObjectProperties, @@ -32,18 +34,27 @@ fn populate_object_props( } /// Send an activity to a list of recipients, using the correct headers etc. -fn send_activity(activity: &A, to: Vec) -> Result<(), Error> +fn send_activity( + activity: &A, + keypair: &Keypair, + sender_id: &str, + to: Vec, +) -> Result<(), Error> where A: Serialize + Debug, { let json = serde_json::to_string(&activity)?; debug!("Sending activitypub activity {} to {:?}", json, to); for t in to { - if !is_apub_id_valid(&t) { + let to_url = Url::parse(&t)?; + if !is_apub_id_valid(&to_url) { debug!("Not sending activity to {} (invalid or blacklisted)", t); continue; } - let res = Request::post(t) + let request = Request::post(t).header("Host", to_url.domain().unwrap()); + let signature = sign(&request, keypair, sender_id)?; + let res = request + .header("Signature", signature) .header("Content-Type", "application/json") .body(json.to_owned())? .send()?; @@ -77,7 +88,12 @@ pub fn post_create(post: &Post, creator: &User_, conn: &PgConnection) -> Result< .create_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_object_base_box(page)?; - send_activity(&create, get_follower_inboxes(conn, &community)?)?; + send_activity( + &create, + &creator.get_keypair().unwrap(), + &creator.actor_id, + get_follower_inboxes(conn, &community)?, + )?; Ok(()) } @@ -95,7 +111,12 @@ pub fn post_update(post: &Post, creator: &User_, conn: &PgConnection) -> Result< .update_props .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_object_base_box(page)?; - send_activity(&update, get_follower_inboxes(conn, &community)?)?; + send_activity( + &update, + &creator.get_keypair().unwrap(), + &creator.actor_id, + get_follower_inboxes(conn, &community)?, + )?; Ok(()) } @@ -116,12 +137,23 @@ pub fn follow_community( .set_actor_xsd_any_uri(user.actor_id.clone())? .set_object_xsd_any_uri(community.actor_id.clone())?; let to = format!("{}/inbox", community.actor_id); - send_activity(&follow, vec![to])?; + send_activity( + &follow, + &community.get_keypair().unwrap(), + &community.actor_id, + vec![to], + )?; Ok(()) } /// As a local community, accept the follow request from a remote user. -pub fn accept_follow(follow: &Follow) -> Result<(), Error> { +pub fn accept_follow(follow: &Follow, conn: &PgConnection) -> Result<(), Error> { + let community_uri = follow + .follow_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + let community = Community::read_from_actor_id(conn, &community_uri)?; let mut accept = Accept::new(); accept .object_props @@ -137,14 +169,12 @@ pub fn accept_follow(follow: &Follow) -> Result<(), Error> { accept .accept_props .set_object_base_box(BaseBox::from_concrete(follow.clone())?)?; - let to = format!( - "{}/inbox", - follow - .follow_props - .get_actor_xsd_any_uri() - .unwrap() - .to_string() - ); - send_activity(&accept, vec![to])?; + let to = format!("{}/inbox", community_uri); + send_activity( + &accept, + &community.get_keypair().unwrap(), + &community.actor_id, + vec![to], + )?; Ok(()) } diff --git a/server/src/apub/community_inbox.rs b/server/src/apub/community_inbox.rs index 65d7bec1f..a60d8c683 100644 --- a/server/src/apub/community_inbox.rs +++ b/server/src/apub/community_inbox.rs @@ -56,6 +56,6 @@ fn handle_follow(follow: &Follow, conn: &PgConnection) -> Result(url: &Url) -> Result where Response: for<'de> Deserialize<'de>, { - if !is_apub_id_valid(&url.to_string()) { + if !is_apub_id_valid(&url) { return Err(format_err!("Activitypub uri invalid or blocked: {}", url)); } // TODO: this function should return a future diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index 634f35101..8d5df8a8b 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -12,7 +12,6 @@ use activitystreams::actor::{properties::ApActorProperties, Group, Person}; use activitystreams::ext::Ext; use actix_web::body::Body; use actix_web::HttpResponse; -use openssl::{pkey::PKey, rsa::Rsa}; use serde::ser::Serialize; use url::Url; @@ -72,31 +71,9 @@ pub fn get_apub_protocol_string() -> &'static str { } } -/// Generate the asymmetric keypair for ActivityPub HTTP signatures. -pub fn gen_keypair_str() -> (String, String) { - let rsa = Rsa::generate(2048).expect("sign::gen_keypair: key generation error"); - let pkey = PKey::from_rsa(rsa).expect("sign::gen_keypair: parsing error"); - let public_key = pkey - .public_key_to_pem() - .expect("sign::gen_keypair: public key encoding error"); - let private_key = pkey - .private_key_to_pem_pkcs8() - .expect("sign::gen_keypair: private key encoding error"); - (vec_bytes_to_str(public_key), vec_bytes_to_str(private_key)) -} - -fn vec_bytes_to_str(bytes: Vec) -> String { - String::from_utf8_lossy(&bytes).into_owned() -} - // Checks if the ID has a valid format, correct scheme, and is in the whitelist. -fn is_apub_id_valid(apub_id: &str) -> bool { - let url = match Url::parse(apub_id) { - Ok(u) => u, - Err(_) => return false, - }; - - if url.scheme() != get_apub_protocol_string() { +fn is_apub_id_valid(apub_id: &Url) -> bool { + if apub_id.scheme() != get_apub_protocol_string() { return false; } @@ -106,7 +83,7 @@ fn is_apub_id_valid(apub_id: &str) -> bool { .split(',') .map(|d| d.to_string()) .collect(); - match url.domain() { + match apub_id.domain() { Some(d) => whitelist.contains(&d.to_owned()), None => false, } diff --git a/server/src/apub/signatures.rs b/server/src/apub/signatures.rs index 0348acb85..e0734e7be 100644 --- a/server/src/apub/signatures.rs +++ b/server/src/apub/signatures.rs @@ -1,7 +1,69 @@ -// For this example, we'll use the Extensible trait, the Extension trait, the Actor trait, and -// the Person type use activitystreams::{actor::Actor, ext::Extension}; +use failure::Error; +use http::request::Builder; +use http_signature_normalization::Config; +use openssl::hash::MessageDigest; +use openssl::sign::Signer; +use openssl::{pkey::PKey, rsa::Rsa}; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +pub struct Keypair { + pub private_key: String, + pub public_key: String, +} + +/// Generate the asymmetric keypair for ActivityPub HTTP signatures. +pub fn generate_actor_keypair() -> Keypair { + let rsa = Rsa::generate(2048).expect("sign::gen_keypair: key generation error"); + let pkey = PKey::from_rsa(rsa).expect("sign::gen_keypair: parsing error"); + let public_key = pkey + .public_key_to_pem() + .expect("sign::gen_keypair: public key encoding error"); + let private_key = pkey + .private_key_to_pem_pkcs8() + .expect("sign::gen_keypair: private key encoding error"); + Keypair { + private_key: String::from_utf8_lossy(&private_key).into_owned(), + public_key: String::from_utf8_lossy(&public_key).into_owned(), + } +} + +/// Signs request headers with the given keypair. +pub fn sign(request: &Builder, keypair: &Keypair, sender_id: &str) -> Result { + let signing_key_id = format!("{}#main-key", sender_id); + let config = Config::new(); + + let headers = request + .headers_ref() + .unwrap() + .iter() + .map(|h| -> Result<(String, String), Error> { + Ok((h.0.as_str().to_owned(), h.1.to_str()?.to_owned())) + }) + .collect::, Error>>()?; + + let signature_header_value = config + .begin_sign( + request.method_ref().unwrap().as_str(), + request + .uri_ref() + .unwrap() + .path_and_query() + .unwrap() + .as_str(), + headers, + ) + .sign(signing_key_id, |signing_string| { + let private_key = PKey::private_key_from_pem(keypair.private_key.as_bytes())?; + let mut signer = Signer::new(MessageDigest::sha256(), &private_key).unwrap(); + signer.update(signing_string.as_bytes()).unwrap(); + Ok(base64::encode(signer.sign_to_vec()?)) as Result<_, Error> + })? + .signature_header(); + + Ok(signature_header_value) +} // The following is taken from here: // https://docs.rs/activitystreams/0.5.0-alpha.17/activitystreams/ext/index.html diff --git a/server/src/db/code_migrations.rs b/server/src/db/code_migrations.rs index a13a99647..a72de685e 100644 --- a/server/src/db/code_migrations.rs +++ b/server/src/db/code_migrations.rs @@ -4,7 +4,8 @@ use super::community::{Community, CommunityForm}; use super::post::Post; use super::user::{UserForm, User_}; use super::*; -use crate::apub::{gen_keypair_str, make_apub_endpoint, EndpointType}; +use crate::apub::signatures::generate_actor_keypair; +use crate::apub::{make_apub_endpoint, EndpointType}; use crate::naive_now; use log::info; @@ -29,7 +30,7 @@ fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> { .load::(conn)?; for cuser in &incorrect_users { - let (user_public_key, user_private_key) = gen_keypair_str(); + let keypair = generate_actor_keypair(); let form = UserForm { name: cuser.name.to_owned(), @@ -51,8 +52,8 @@ fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> { actor_id: make_apub_endpoint(EndpointType::User, &cuser.name).to_string(), bio: cuser.bio.to_owned(), local: cuser.local, - private_key: Some(user_private_key), - public_key: Some(user_public_key), + private_key: Some(keypair.private_key), + public_key: Some(keypair.public_key), last_refreshed_at: Some(naive_now()), }; @@ -76,7 +77,7 @@ fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> { .load::(conn)?; for ccommunity in &incorrect_communities { - let (community_public_key, community_private_key) = gen_keypair_str(); + let keypair = generate_actor_keypair(); let form = CommunityForm { name: ccommunity.name.to_owned(), @@ -90,8 +91,8 @@ fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> { updated: None, actor_id: make_apub_endpoint(EndpointType::Community, &ccommunity.name).to_string(), local: ccommunity.local, - private_key: Some(community_private_key), - public_key: Some(community_public_key), + private_key: Some(keypair.private_key), + public_key: Some(keypair.public_key), last_refreshed_at: Some(naive_now()), published: None, }; diff --git a/server/src/db/community.rs b/server/src/db/community.rs index ca2fc120a..7a706557b 100644 --- a/server/src/db/community.rs +++ b/server/src/db/community.rs @@ -1,4 +1,5 @@ use super::*; +use crate::apub::signatures::Keypair; use crate::schema::{community, community_follower, community_moderator, community_user_ban}; #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] @@ -95,6 +96,21 @@ impl Community { pub fn get_url(&self) -> String { format!("https://{}/c/{}", Settings::get().hostname, self.name) } + + pub fn get_keypair(&self) -> Option { + if let Some(private) = self.private_key.to_owned() { + if let Some(public) = self.public_key.to_owned() { + Some(Keypair { + private_key: private, + public_key: public, + }) + } else { + None + } + } else { + None + } + } } #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] diff --git a/server/src/db/user.rs b/server/src/db/user.rs index 3a079f091..dfce9f2cb 100644 --- a/server/src/db/user.rs +++ b/server/src/db/user.rs @@ -1,4 +1,5 @@ use super::*; +use crate::apub::signatures::Keypair; use crate::schema::user_; use crate::schema::user_::dsl::*; use crate::{is_email_regex, naive_now, Settings}; @@ -124,6 +125,21 @@ impl User_ { use crate::schema::user_::dsl::*; user_.filter(actor_id.eq(object_id)).first::(conn) } + + pub fn get_keypair(&self) -> Option { + if let Some(private) = self.private_key.to_owned() { + if let Some(public) = self.public_key.to_owned() { + Some(Keypair { + private_key: private, + public_key: public, + }) + } else { + None + } + } else { + None + } + } } #[derive(Debug, Serialize, Deserialize)] From 5557f384365f9f4dda9f32a93ff96fc7fcc2414b Mon Sep 17 00:00:00 2001 From: Felix Date: Sun, 19 Apr 2020 13:44:44 +0200 Subject: [PATCH 10/13] Simplify signing code --- server/src/api/community.rs | 2 +- server/src/api/user.rs | 4 ++-- server/src/apub/activities.rs | 14 +++++++------- server/src/apub/signatures.rs | 27 ++++++++++++--------------- server/src/db/code_migrations.rs | 5 +++-- server/src/db/community.rs | 16 ---------------- server/src/db/user.rs | 16 ---------------- 7 files changed, 25 insertions(+), 59 deletions(-) diff --git a/server/src/api/community.rs b/server/src/api/community.rs index 30c8aa205..0c0ddceec 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -201,7 +201,7 @@ impl Perform for Oper { } // When you create a community, make sure the user becomes a moderator and a follower - let keypair = generate_actor_keypair(); + let keypair = generate_actor_keypair()?; let community_form = CommunityForm { name: data.name.to_owned(), diff --git a/server/src/api/user.rs b/server/src/api/user.rs index 35bdd33ab..f42ea03a5 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -252,7 +252,7 @@ impl Perform for Oper { return Err(APIError::err("admin_already_created").into()); } - let keypair = generate_actor_keypair(); + let keypair = generate_actor_keypair()?; // Register the new user let user_form = UserForm { @@ -296,7 +296,7 @@ impl Perform for Oper { } }; - let keypair = generate_actor_keypair(); + let keypair = generate_actor_keypair()?; // Create the main community if it doesn't exist let main_community: Community = match Community::read(&conn, 2) { diff --git a/server/src/apub/activities.rs b/server/src/apub/activities.rs index a7f63ec85..9d2a06680 100644 --- a/server/src/apub/activities.rs +++ b/server/src/apub/activities.rs @@ -1,5 +1,5 @@ use crate::apub::is_apub_id_valid; -use crate::apub::signatures::{sign, Keypair}; +use crate::apub::signatures::sign; use crate::db::community::Community; use crate::db::community_view::CommunityFollowerView; use crate::db::post::Post; @@ -36,7 +36,7 @@ fn populate_object_props( /// Send an activity to a list of recipients, using the correct headers etc. fn send_activity( activity: &A, - keypair: &Keypair, + private_key: &str, sender_id: &str, to: Vec, ) -> Result<(), Error> @@ -52,7 +52,7 @@ where continue; } let request = Request::post(t).header("Host", to_url.domain().unwrap()); - let signature = sign(&request, keypair, sender_id)?; + let signature = sign(&request, private_key, sender_id)?; let res = request .header("Signature", signature) .header("Content-Type", "application/json") @@ -90,7 +90,7 @@ pub fn post_create(post: &Post, creator: &User_, conn: &PgConnection) -> Result< .set_object_base_box(page)?; send_activity( &create, - &creator.get_keypair().unwrap(), + &creator.private_key.as_ref().unwrap(), &creator.actor_id, get_follower_inboxes(conn, &community)?, )?; @@ -113,7 +113,7 @@ pub fn post_update(post: &Post, creator: &User_, conn: &PgConnection) -> Result< .set_object_base_box(page)?; send_activity( &update, - &creator.get_keypair().unwrap(), + &creator.private_key.as_ref().unwrap(), &creator.actor_id, get_follower_inboxes(conn, &community)?, )?; @@ -139,7 +139,7 @@ pub fn follow_community( let to = format!("{}/inbox", community.actor_id); send_activity( &follow, - &community.get_keypair().unwrap(), + &community.private_key.as_ref().unwrap(), &community.actor_id, vec![to], )?; @@ -172,7 +172,7 @@ pub fn accept_follow(follow: &Follow, conn: &PgConnection) -> Result<(), Error> let to = format!("{}/inbox", community_uri); send_activity( &accept, - &community.get_keypair().unwrap(), + &community.private_key.unwrap(), &community.actor_id, vec![to], )?; diff --git a/server/src/apub/signatures.rs b/server/src/apub/signatures.rs index e0734e7be..4181e11f7 100644 --- a/server/src/apub/signatures.rs +++ b/server/src/apub/signatures.rs @@ -14,23 +14,20 @@ pub struct Keypair { } /// Generate the asymmetric keypair for ActivityPub HTTP signatures. -pub fn generate_actor_keypair() -> Keypair { - let rsa = Rsa::generate(2048).expect("sign::gen_keypair: key generation error"); - let pkey = PKey::from_rsa(rsa).expect("sign::gen_keypair: parsing error"); - let public_key = pkey - .public_key_to_pem() - .expect("sign::gen_keypair: public key encoding error"); - let private_key = pkey - .private_key_to_pem_pkcs8() - .expect("sign::gen_keypair: private key encoding error"); - Keypair { - private_key: String::from_utf8_lossy(&private_key).into_owned(), - public_key: String::from_utf8_lossy(&public_key).into_owned(), - } +pub fn generate_actor_keypair() -> Result { + let rsa = Rsa::generate(2048)?; + let pkey = PKey::from_rsa(rsa)?; + let public_key = pkey.public_key_to_pem()?; + let private_key = pkey.private_key_to_pem_pkcs8()?; + Ok(Keypair { + private_key: String::from_utf8(private_key)?, + public_key: String::from_utf8(public_key)?, + }) } /// Signs request headers with the given keypair. -pub fn sign(request: &Builder, keypair: &Keypair, sender_id: &str) -> Result { +/// TODO: would be nice to pass the sending actor in, instead of raw privatekey/id strings +pub fn sign(request: &Builder, private_key: &str, sender_id: &str) -> Result { let signing_key_id = format!("{}#main-key", sender_id); let config = Config::new(); @@ -55,7 +52,7 @@ pub fn sign(request: &Builder, keypair: &Keypair, sender_id: &str) -> Result diff --git a/server/src/db/code_migrations.rs b/server/src/db/code_migrations.rs index a72de685e..605971996 100644 --- a/server/src/db/code_migrations.rs +++ b/server/src/db/code_migrations.rs @@ -7,6 +7,7 @@ use super::*; use crate::apub::signatures::generate_actor_keypair; use crate::apub::{make_apub_endpoint, EndpointType}; use crate::naive_now; +use failure::Error; use log::info; pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), Error> { @@ -30,7 +31,7 @@ fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> { .load::(conn)?; for cuser in &incorrect_users { - let keypair = generate_actor_keypair(); + let keypair = generate_actor_keypair()?; let form = UserForm { name: cuser.name.to_owned(), @@ -77,7 +78,7 @@ fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> { .load::(conn)?; for ccommunity in &incorrect_communities { - let keypair = generate_actor_keypair(); + let keypair = generate_actor_keypair()?; let form = CommunityForm { name: ccommunity.name.to_owned(), diff --git a/server/src/db/community.rs b/server/src/db/community.rs index 7a706557b..ca2fc120a 100644 --- a/server/src/db/community.rs +++ b/server/src/db/community.rs @@ -1,5 +1,4 @@ use super::*; -use crate::apub::signatures::Keypair; use crate::schema::{community, community_follower, community_moderator, community_user_ban}; #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] @@ -96,21 +95,6 @@ impl Community { pub fn get_url(&self) -> String { format!("https://{}/c/{}", Settings::get().hostname, self.name) } - - pub fn get_keypair(&self) -> Option { - if let Some(private) = self.private_key.to_owned() { - if let Some(public) = self.public_key.to_owned() { - Some(Keypair { - private_key: private, - public_key: public, - }) - } else { - None - } - } else { - None - } - } } #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] diff --git a/server/src/db/user.rs b/server/src/db/user.rs index dfce9f2cb..3a079f091 100644 --- a/server/src/db/user.rs +++ b/server/src/db/user.rs @@ -1,5 +1,4 @@ use super::*; -use crate::apub::signatures::Keypair; use crate::schema::user_; use crate::schema::user_::dsl::*; use crate::{is_email_regex, naive_now, Settings}; @@ -125,21 +124,6 @@ impl User_ { use crate::schema::user_::dsl::*; user_.filter(actor_id.eq(object_id)).first::(conn) } - - pub fn get_keypair(&self) -> Option { - if let Some(private) = self.private_key.to_owned() { - if let Some(public) = self.public_key.to_owned() { - Some(Keypair { - private_key: private, - public_key: public, - }) - } else { - None - } - } else { - None - } - } } #[derive(Debug, Serialize, Deserialize)] From 5e7a60630c33f75cb1be41ddd0936f1d1074d463 Mon Sep 17 00:00:00 2001 From: Felix Date: Sun, 19 Apr 2020 19:35:40 +0200 Subject: [PATCH 11/13] Verifyt http signatures --- server/src/apub/activities.rs | 4 +-- server/src/apub/community_inbox.rs | 26 +++++++++----- server/src/apub/signatures.rs | 48 +++++++++++++++++++++++-- server/src/apub/user_inbox.rs | 56 ++++++++++++++++++++++++++---- 4 files changed, 113 insertions(+), 21 deletions(-) diff --git a/server/src/apub/activities.rs b/server/src/apub/activities.rs index 9d2a06680..e5980e293 100644 --- a/server/src/apub/activities.rs +++ b/server/src/apub/activities.rs @@ -139,7 +139,7 @@ pub fn follow_community( let to = format!("{}/inbox", community.actor_id); send_activity( &follow, - &community.private_key.as_ref().unwrap(), + &user.private_key.as_ref().unwrap(), &community.actor_id, vec![to], )?; @@ -150,7 +150,7 @@ pub fn follow_community( pub fn accept_follow(follow: &Follow, conn: &PgConnection) -> Result<(), Error> { let community_uri = follow .follow_props - .get_actor_xsd_any_uri() + .get_object_xsd_any_uri() .unwrap() .to_string(); let community = Community::read_from_actor_id(conn, &community_uri)?; diff --git a/server/src/apub/community_inbox.rs b/server/src/apub/community_inbox.rs index a60d8c683..e7fc856e8 100644 --- a/server/src/apub/community_inbox.rs +++ b/server/src/apub/community_inbox.rs @@ -1,9 +1,10 @@ use crate::apub::activities::accept_follow; use crate::apub::fetcher::fetch_remote_user; +use crate::apub::signatures::verify; use crate::db::community::{Community, CommunityFollower, CommunityFollowerForm}; use crate::db::Followable; use activitystreams::activity::Follow; -use actix_web::{web, HttpResponse}; +use actix_web::{web, HttpRequest, HttpResponse}; use diesel::r2d2::{ConnectionManager, Pool}; use diesel::PgConnection; use failure::Error; @@ -19,6 +20,7 @@ pub enum CommunityAcceptedObjects { /// Handler for all incoming activities to community inboxes. pub async fn community_inbox( + request: HttpRequest, input: web::Json, path: web::Path, db: web::Data>>, @@ -31,13 +33,25 @@ pub async fn community_inbox( &input ); match input { - CommunityAcceptedObjects::Follow(f) => handle_follow(&f, conn), + CommunityAcceptedObjects::Follow(f) => handle_follow(&f, &request, conn), } } /// Handle a follow request from a remote user, adding it to the local database and returning an /// Accept activity. -fn handle_follow(follow: &Follow, conn: &PgConnection) -> Result { +fn handle_follow( + follow: &Follow, + request: &HttpRequest, + conn: &PgConnection, +) -> Result { + let user_uri = follow + .follow_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + let user = fetch_remote_user(&Url::parse(&user_uri)?, conn)?; + verify(&request, &user.public_key.unwrap())?; + // TODO: make sure this is a local community let community_uri = follow .follow_props @@ -45,12 +59,6 @@ fn handle_follow(follow: &Follow, conn: &PgConnection) -> Result Result { /// TODO: would be nice to pass the sending actor in, instead of raw privatekey/id strings pub fn sign(request: &Builder, private_key: &str, sender_id: &str) -> Result { let signing_key_id = format!("{}#main-key", sender_id); - let config = Config::new(); let headers = request .headers_ref() @@ -40,7 +45,7 @@ pub fn sign(request: &Builder, private_key: &str, sender_id: &str) -> Result, Error>>()?; - let signature_header_value = config + let signature_header_value = HTTP_SIG_CONFIG .begin_sign( request.method_ref().unwrap().as_str(), request @@ -62,6 +67,43 @@ pub fn sign(request: &Builder, private_key: &str, sender_id: &str) -> Result Result<(), Error> { + let headers = request + .headers() + .iter() + .map(|h| -> Result<(String, String), Error> { + Ok((h.0.as_str().to_owned(), h.1.to_str()?.to_owned())) + }) + .collect::, Error>>()?; + + let verified = HTTP_SIG_CONFIG + .begin_verify( + request.method().as_str(), + request.uri().path_and_query().unwrap().as_str(), + headers, + )? + .verify(|signature, signing_string| -> Result { + debug!( + "Verifying with key {}, message {}", + &public_key, &signing_string + ); + let public_key = PKey::public_key_from_pem(public_key.as_bytes())?; + let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key).unwrap(); + verifier.update(&signing_string.as_bytes()).unwrap(); + Ok(verifier.verify(&base64::decode(signature)?)?) + })?; + + if verified { + debug!("verified signature for {}", &request.uri()); + Ok(()) + } else { + Err(format_err!( + "Invalid signature on request: {}", + &request.uri() + )) + } +} + // The following is taken from here: // https://docs.rs/activitystreams/0.5.0-alpha.17/activitystreams/ext/index.html diff --git a/server/src/apub/user_inbox.rs b/server/src/apub/user_inbox.rs index 75cd4e479..f9faa0f09 100644 --- a/server/src/apub/user_inbox.rs +++ b/server/src/apub/user_inbox.rs @@ -1,13 +1,16 @@ +use crate::apub::fetcher::{fetch_remote_community, fetch_remote_user}; +use crate::apub::signatures::verify; use crate::db::post::{Post, PostForm}; use crate::db::Crud; use activitystreams::activity::{Accept, Create, Update}; use activitystreams::object::Page; -use actix_web::{web, HttpResponse}; +use actix_web::{web, HttpRequest, HttpResponse}; use diesel::r2d2::{ConnectionManager, Pool}; use diesel::PgConnection; use failure::Error; use log::debug; use serde::Deserialize; +use url::Url; #[serde(untagged)] #[derive(Deserialize, Debug)] @@ -19,10 +22,12 @@ pub enum UserAcceptedObjects { /// Handler for all incoming activities to user inboxes. pub async fn user_inbox( + request: HttpRequest, input: web::Json, path: web::Path, db: web::Data>>, ) -> Result { + // TODO: would be nice if we could do the signature check here, but we cant access the actor property let input = input.into_inner(); let conn = &db.get().unwrap(); debug!( @@ -32,14 +37,27 @@ pub async fn user_inbox( ); match input { - UserAcceptedObjects::Create(c) => handle_create(&c, conn), - UserAcceptedObjects::Update(u) => handle_update(&u, conn), - UserAcceptedObjects::Accept(a) => handle_accept(&a, conn), + UserAcceptedObjects::Create(c) => handle_create(&c, &request, conn), + UserAcceptedObjects::Update(u) => handle_update(&u, &request, conn), + UserAcceptedObjects::Accept(a) => handle_accept(&a, &request, conn), } } /// Handle create activities and insert them in the database. -fn handle_create(create: &Create, conn: &PgConnection) -> Result { +fn handle_create( + create: &Create, + request: &HttpRequest, + conn: &PgConnection, +) -> Result { + let community_uri = create + .create_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + // TODO: should do this in a generic way so we dont need to know if its a user or a community + let user = fetch_remote_user(&Url::parse(&community_uri)?, conn)?; + verify(request, &user.public_key.unwrap())?; + let page = create .create_props .get_object_base_box() @@ -54,7 +72,19 @@ fn handle_create(create: &Create, conn: &PgConnection) -> Result Result { +fn handle_update( + update: &Update, + request: &HttpRequest, + conn: &PgConnection, +) -> Result { + let community_uri = update + .update_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + let user = fetch_remote_user(&Url::parse(&community_uri)?, conn)?; + verify(request, &user.public_key.unwrap())?; + let page = update .update_props .get_object_base_box() @@ -70,7 +100,19 @@ fn handle_update(update: &Update, conn: &PgConnection) -> Result Result { +fn handle_accept( + accept: &Accept, + request: &HttpRequest, + conn: &PgConnection, +) -> Result { + let community_uri = accept + .accept_props + .get_actor_xsd_any_uri() + .unwrap() + .to_string(); + let community = fetch_remote_community(&Url::parse(&community_uri)?, conn)?; + verify(request, &community.public_key.unwrap())?; + // TODO: make sure that we actually requested a follow // TODO: at this point, indicate to the user that they are following the community Ok(HttpResponse::Ok().finish()) From abd0601f08fffbd73461bf5cc24361e67fba2b20 Mon Sep 17 00:00:00 2001 From: Felix Date: Tue, 21 Apr 2020 13:24:08 +0200 Subject: [PATCH 12/13] Update federation dev instructions --- .../contributing_federation_development.md | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/docs/src/contributing_federation_development.md b/docs/src/contributing_federation_development.md index 13a047d08..bcac4caa7 100644 --- a/docs/src/contributing_federation_development.md +++ b/docs/src/contributing_federation_development.md @@ -5,17 +5,17 @@ If you don't have a local clone of the Lemmy repo yet, just run the following command: ```bash -git clone https://yerbamate.dev/nutomic/lemmy.git -b federation +git clone https://yerbamate.dev/LemmyNet/lemmy.git -b federation ``` If you already have the Lemmy repo cloned, you need to add a new remote: ```bash -git remote add federation https://yerbamate.dev/nutomic/lemmy.git +git remote add federation https://yerbamate.dev/LemmyNet/lemmy.git git checkout federation git pull federation federation ``` -## Running +## Running locally You need to have the following packages installed, the Docker service needs to be running. @@ -31,7 +31,30 @@ cd dev/federation-test ``` After the build is finished and the docker-compose setup is running, open [127.0.0.1:8540](http://127.0.0.1:8540) and -[127.0.0.1:8541](http://127.0.0.1:8541) in your browser to use the test instances. You can login as admin with -username `lemmy` and password `lemmy`, or create new accounts. +[127.0.0.1:8550](http://127.0.0.1:8550) in your browser to use the test instances. You can login as admin with +username `lemmy_alpha` and `lemmy_beta` respectively, with password `lemmy`. -Please get in touch if you want to contribute to this, so we can coordinate things and avoid duplicate work. \ No newline at end of file +## Running on a server + +Note that federation is currently in alpha. Only use it for testing, not on any production server, and be aware +that you might have to wipe the instance data at one point or another. + +Follow the normal installation instructions, either with [Ansible](administration_install_ansible.md) or +[manually](administration_install_docker.md). Then replace the line `image: dessalines/lemmy:v0.x.x` in +`/lemmy/docker-compose.yml` with `image: dessalines/lemmy:federation`. Also add the following in +`/lemmy/lemmy.hjson`: + +``` + federation: { + enabled: true + instance_whitelist: example.com + } +``` + +Afterwards, and whenver you want to update to the latest version, run these commands on the server: + +``` +cd /lemmy/ +sudo docker-compose pull +sudo docker-compose up -d +``` From b396ed407ecd786e5ae60c5aa2236f8e6c14f145 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 21 Apr 2020 09:21:29 -0400 Subject: [PATCH 13/13] Change local unique username constraints. --- .../2020-04-21-123957_remove_unique_user_constraints/down.sql | 4 ++++ .../2020-04-21-123957_remove_unique_user_constraints/up.sql | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 server/migrations/2020-04-21-123957_remove_unique_user_constraints/down.sql create mode 100644 server/migrations/2020-04-21-123957_remove_unique_user_constraints/up.sql diff --git a/server/migrations/2020-04-21-123957_remove_unique_user_constraints/down.sql b/server/migrations/2020-04-21-123957_remove_unique_user_constraints/down.sql new file mode 100644 index 000000000..a172581a1 --- /dev/null +++ b/server/migrations/2020-04-21-123957_remove_unique_user_constraints/down.sql @@ -0,0 +1,4 @@ +-- The username index +drop index idx_user_name_lower_actor_id; +create unique index idx_user_name_lower on user_ (lower(name)); + diff --git a/server/migrations/2020-04-21-123957_remove_unique_user_constraints/up.sql b/server/migrations/2020-04-21-123957_remove_unique_user_constraints/up.sql new file mode 100644 index 000000000..969eab0b6 --- /dev/null +++ b/server/migrations/2020-04-21-123957_remove_unique_user_constraints/up.sql @@ -0,0 +1,2 @@ +drop index idx_user_name_lower; +create unique index idx_user_name_lower_actor_id on user_ (lower(name), lower(actor_id));