From 251e0d3b82fd21688c0d0cc2566454a1a9111a78 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Tue, 20 Jul 2021 09:00:20 +0200 Subject: [PATCH] Move resolving of activitypub objects to separate api endpoint (fixes #1584) --- Cargo.lock | 4 + crates/api/src/lib.rs | 3 + crates/api/src/site.rs | 20 ++- crates/api_common/src/lib.rs | 18 --- crates/api_common/src/site.rs | 14 ++ crates/apub/src/activities/comment/mod.rs | 5 +- crates/apub/src/fetcher/search.rs | 149 +++++++++++----------- crates/apub_lib/Cargo.toml | 3 + crates/apub_lib/src/lib.rs | 2 + crates/apub_lib/src/webfinger.rs | 72 +++++++++++ crates/routes/Cargo.toml | 1 + crates/routes/src/webfinger.rs | 9 +- crates/websocket/src/lib.rs | 1 + src/api_routes.rs | 5 + 14 files changed, 202 insertions(+), 104 deletions(-) create mode 100644 crates/apub_lib/src/webfinger.rs diff --git a/Cargo.lock b/Cargo.lock index 4cdcc47f6..624897b38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1736,10 +1736,13 @@ name = "lemmy_apub_lib" version = "0.11.3" dependencies = [ "activitystreams", + "anyhow", "async-trait", "lemmy_apub_lib_derive", "lemmy_utils", "lemmy_websocket", + "log", + "reqwest", "serde", "serde_json", "url", @@ -1836,6 +1839,7 @@ dependencies = [ "diesel", "lazy_static", "lemmy_api_common", + "lemmy_apub_lib", "lemmy_db_queries", "lemmy_db_schema", "lemmy_db_views", diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index bf3a813b3..83d98bde5 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -87,6 +87,9 @@ pub async fn match_websocket_operation( do_websocket_operation::(context, id, op, data).await } UserOperation::Search => do_websocket_operation::(context, id, op, data).await, + UserOperation::ResolveObject => { + do_websocket_operation::(context, id, op, data).await + } UserOperation::TransferCommunity => { do_websocket_operation::(context, id, op, data).await } diff --git a/crates/api/src/site.rs b/crates/api/src/site.rs index 221b9870f..315cf72da 100644 --- a/crates/api/src/site.rs +++ b/crates/api/src/site.rs @@ -50,7 +50,6 @@ use lemmy_utils::{ LemmyError, }; use lemmy_websocket::LemmyContext; -use log::debug; #[async_trait::async_trait(?Send)] impl Perform for GetModlog { @@ -143,11 +142,6 @@ impl Perform for Search { ) -> Result { let data: &Search = self; - match search_by_apub_id(&data.q, context).await { - Ok(r) => return Ok(r), - Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e), - } - let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?; let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw); @@ -372,6 +366,20 @@ impl Perform for Search { } } +#[async_trait::async_trait(?Send)] +impl Perform for ResolveObject { + type Response = ResolveObjectResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let local_user_view = get_local_user_view_from_jwt_opt(&self.auth, context.pool()).await?; + search_by_apub_id(&self.q, local_user_view, context).await + } +} + #[async_trait::async_trait(?Send)] impl Perform for TransferSite { type Response = GetSiteResponse; diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index e7d5181bc..a79e842ee 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -46,26 +46,8 @@ use lemmy_utils::{ LemmyError, }; use log::error; -use serde::{Deserialize, Serialize}; use url::Url; -#[derive(Serialize, Deserialize, Debug)] -pub struct WebFingerLink { - pub rel: Option, - #[serde(rename(serialize = "type", deserialize = "type"))] - pub type_: Option, - pub href: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub template: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct WebFingerResponse { - pub subject: String, - pub aliases: Vec, - pub links: Vec, -} - pub async fn blocking(pool: &DbPool, f: F) -> Result where F: FnOnce(&diesel::PgConnection) -> T + Send + 'static, diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index af63854d3..ed6211781 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -50,6 +50,20 @@ pub struct SearchResponse { pub users: Vec, } +#[derive(Deserialize, Debug)] +pub struct ResolveObject { + pub q: String, + pub auth: Option, +} + +#[derive(Serialize, Debug)] +pub enum ResolveObjectResponse { + Comment(CommentView), + Post(PostView), + Community(CommunityView), + Person(PersonViewSafe), +} + #[derive(Deserialize)] pub struct GetModlog { pub mod_person_id: Option, diff --git a/crates/apub/src/activities/comment/mod.rs b/crates/apub/src/activities/comment/mod.rs index 228a5f804..44b8d245a 100644 --- a/crates/apub/src/activities/comment/mod.rs +++ b/crates/apub/src/activities/comment/mod.rs @@ -5,7 +5,8 @@ use activitystreams::{ }; use anyhow::anyhow; use itertools::Itertools; -use lemmy_api_common::{blocking, send_local_notifs, WebFingerResponse}; +use lemmy_api_common::{blocking, send_local_notifs}; +use lemmy_apub_lib::webfinger::WebfingerResponse; use lemmy_db_queries::{Crud, DbPool}; use lemmy_db_schema::{ source::{comment::Comment, community::Community, person::Person, post::Post}, @@ -128,7 +129,7 @@ async fn fetch_webfinger_url(mention: &MentionData, client: &Client) -> Result, context: &LemmyContext, -) -> Result { - // Parse the shorthand query url - let query_url = if query.contains('@') { - debug!("Search for {}", query); - let split = query.split('@').collect::>(); - - // Person type will look like ['', username, instance] - // Community will look like [!community, instance] - let (name, instance) = if split.len() == 3 { - (format!("/u/{}", split[1]), split[2]) - } else if split.len() == 2 { - if split[0].contains('!') { - let split2 = split[0].split('!').collect::>(); - (format!("/c/{}", split2[1]), split[1]) - } else { - return Err(anyhow!("Invalid search query: {}", query).into()); +) -> Result { + let query_url = match Url::parse(query) { + Ok(u) => u, + Err(_) => { + let (kind, name) = query.split_at(1); + let kind = match kind { + "@" => WebfingerType::Person, + "!" => WebfingerType::Group, + _ => return Err(anyhow!("invalid query").into()), + }; + // remote actor, use webfinger to resolve url + if name.contains('@') { + let (name, domain) = name.splitn(2, '@').collect_tuple().expect("invalid query"); + webfinger_resolve_actor(name, domain, kind, context.client()).await? } - } else { - return Err(anyhow!("Invalid search query: {}", query).into()); - }; - - let url = format!( - "{}://{}{}", - Settings::get().get_protocol_string(), - instance, - name - ); - Url::parse(&url)? - } else { - Url::parse(query)? + // local actor, read from database and return + else { + let name: String = name.into(); + return match kind { + WebfingerType::Group => { + let res = blocking(context.pool(), move |conn| { + let community = Community::read_from_name(conn, &name)?; + CommunityView::read(conn, community.id, local_user_view.map(|l| l.person.id)) + }) + .await??; + Ok(ResolveObjectResponse::Community(res)) + } + WebfingerType::Person => { + let res = blocking(context.pool(), move |conn| { + let person = Person::find_by_name(conn, &name)?; + PersonViewSafe::read(conn, person.id) + }) + .await??; + Ok(ResolveObjectResponse::Person(res)) + } + }; + } + } }; - let recursion_counter = &mut 0; + let request_counter = &mut 0; + // this does a fetch (even for local objects), just to determine its type and fetch it again + // below. we need to fix this when rewriting the fetcher. let fetch_response = - fetch_remote_object::(context.client(), &query_url, recursion_counter) + fetch_remote_object::(context.client(), &query_url, request_counter) .await; if is_deleted(&fetch_response) { delete_object_locally(&query_url, context).await?; + return Err(anyhow!("Object was deleted").into()); } // Necessary because we get a stack overflow using FetchError let fet_res = fetch_response.map_err(|e| LemmyError::from(e.inner))?; - build_response(fet_res, query_url, recursion_counter, context).await + build_response(fet_res, query_url, request_counter, context).await } async fn build_response( @@ -105,58 +119,45 @@ async fn build_response( query_url: Url, recursion_counter: &mut i32, context: &LemmyContext, -) -> Result { - let mut response = SearchResponse { - type_: SearchType::All.to_string(), - comments: vec![], - posts: vec![], - communities: vec![], - users: vec![], - }; - - match fetch_response { +) -> Result { + use ResolveObjectResponse as ROR; + Ok(match fetch_response { SearchAcceptedObjects::Person(p) => { - let person_id = p.id(&query_url)?; - let person = get_or_fetch_and_upsert_person(person_id, context, recursion_counter).await?; + let person_uri = p.id(&query_url)?; - response.users = vec![ + let person = get_or_fetch_and_upsert_person(person_uri, context, recursion_counter).await?; + ROR::Person( blocking(context.pool(), move |conn| { PersonViewSafe::read(conn, person.id) }) .await??, - ]; + ) } SearchAcceptedObjects::Group(g) => { let community_uri = g.id(&query_url)?; let community = get_or_fetch_and_upsert_community(community_uri, context, recursion_counter).await?; - - response.communities = vec![ + ROR::Community( blocking(context.pool(), move |conn| { CommunityView::read(conn, community.id, None) }) .await??, - ]; + ) } SearchAcceptedObjects::Page(p) => { let p = Post::from_apub(&p, context, &query_url, recursion_counter).await?; - - response.posts = - vec![blocking(context.pool(), move |conn| PostView::read(conn, p.id, None)).await??]; + ROR::Post(blocking(context.pool(), move |conn| PostView::read(conn, p.id, None)).await??) } SearchAcceptedObjects::Comment(c) => { let c = Comment::from_apub(&c, context, &query_url, recursion_counter).await?; - - response.comments = vec![ + ROR::Comment( blocking(context.pool(), move |conn| { CommentView::read(conn, c.id, None) }) .await??, - ]; + ) } - }; - - Ok(response) + }) } async fn delete_object_locally(query_url: &Url, context: &LemmyContext) -> Result<(), LemmyError> { @@ -194,5 +195,5 @@ async fn delete_object_locally(query_url: &Url, context: &LemmyContext) -> Resul .await??; } } - Err(anyhow!("Object was deleted").into()) + Ok(()) } diff --git a/crates/apub_lib/Cargo.toml b/crates/apub_lib/Cargo.toml index 2593cd94e..7ea84c1f8 100644 --- a/crates/apub_lib/Cargo.toml +++ b/crates/apub_lib/Cargo.toml @@ -14,3 +14,6 @@ serde = { version = "1.0.127", features = ["derive"] } async-trait = "0.1.51" url = { version = "2.2.2", features = ["serde"] } serde_json = { version = "1.0.66", features = ["preserve_order"] } +anyhow = "1.0.41" +reqwest = { version = "0.11.4", features = ["json"] } +log = "0.4.14" diff --git a/crates/apub_lib/src/lib.rs b/crates/apub_lib/src/lib.rs index cc88b79c9..846666ed2 100644 --- a/crates/apub_lib/src/lib.rs +++ b/crates/apub_lib/src/lib.rs @@ -6,6 +6,8 @@ use lemmy_utils::LemmyError; use lemmy_websocket::LemmyContext; use url::Url; +pub mod webfinger; + pub trait ActivityFields { fn id_unchecked(&self) -> &Url; fn actor(&self) -> &Url; diff --git a/crates/apub_lib/src/webfinger.rs b/crates/apub_lib/src/webfinger.rs new file mode 100644 index 000000000..ebd49ea67 --- /dev/null +++ b/crates/apub_lib/src/webfinger.rs @@ -0,0 +1,72 @@ +use anyhow::anyhow; +use lemmy_utils::{ + request::{retry, RecvError}, + settings::structs::Settings, + LemmyError, +}; +use log::debug; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Serialize, Deserialize, Debug)] +pub struct WebfingerLink { + pub rel: Option, + #[serde(rename(serialize = "type", deserialize = "type"))] + pub type_: Option, + pub href: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub template: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct WebfingerResponse { + pub subject: String, + pub aliases: Vec, + pub links: Vec, +} + +pub enum WebfingerType { + Person, + Group, +} + +/// Turns a person id like `@name@example.com` into an apub ID, like `https://example.com/user/name`, +/// using webfinger. +pub async fn webfinger_resolve_actor( + name: &str, + domain: &str, + webfinger_type: WebfingerType, + client: &Client, +) -> Result { + let webfinger_type = match webfinger_type { + WebfingerType::Person => "acct", + WebfingerType::Group => "group", + }; + let fetch_url = format!( + "{}://{}/.well-known/webfinger?resource={}:{}@{}", + Settings::get().get_protocol_string(), + domain, + webfinger_type, + name, + domain + ); + debug!("Fetching webfinger url: {}", &fetch_url); + + let response = retry(|| client.get(&fetch_url).send()).await?; + + let res: WebfingerResponse = response + .json() + .await + .map_err(|e| RecvError(e.to_string()))?; + + let link = res + .links + .iter() + .find(|l| l.type_.eq(&Some("application/activity+json".to_string()))) + .ok_or_else(|| anyhow!("No application/activity+json link found."))?; + link + .href + .to_owned() + .ok_or_else(|| anyhow!("No href found.").into()) +} diff --git a/crates/routes/Cargo.toml b/crates/routes/Cargo.toml index a29172ac2..e47e7a36e 100644 --- a/crates/routes/Cargo.toml +++ b/crates/routes/Cargo.toml @@ -16,6 +16,7 @@ lemmy_db_views = { version = "=0.11.3", path = "../db_views" } lemmy_db_views_actor = { version = "=0.11.3", path = "../db_views_actor" } lemmy_db_schema = { version = "=0.11.3", path = "../db_schema" } lemmy_api_common = { version = "=0.11.3", path = "../api_common" } +lemmy_apub_lib = { version = "=0.11.3", path = "../apub_lib" } diesel = "1.4.7" actix = "0.12.0" actix-web = { version = "4.0.0-beta.8", default-features = false, features = ["rustls"] } diff --git a/crates/routes/src/webfinger.rs b/crates/routes/src/webfinger.rs index 5d4be74c4..677321887 100644 --- a/crates/routes/src/webfinger.rs +++ b/crates/routes/src/webfinger.rs @@ -1,6 +1,7 @@ use actix_web::{error::ErrorBadRequest, web::Query, *}; use anyhow::anyhow; -use lemmy_api_common::{blocking, WebFingerLink, WebFingerResponse}; +use lemmy_api_common::blocking; +use lemmy_apub_lib::webfinger::{WebfingerLink, WebfingerResponse}; use lemmy_db_queries::source::{community::Community_, person::Person_}; use lemmy_db_schema::source::{community::Community, person::Person}; use lemmy_utils::{ @@ -68,17 +69,17 @@ async fn get_webfinger_response( return Err(ErrorBadRequest(LemmyError::from(anyhow!("not_found")))); }; - let json = WebFingerResponse { + let json = WebfingerResponse { subject: info.resource.to_owned(), aliases: vec![url.to_owned().into()], links: vec![ - WebFingerLink { + WebfingerLink { rel: Some("http://webfinger.net/rel/profile-page".to_string()), type_: Some("text/html".to_string()), href: Some(url.to_owned().into()), template: None, }, - WebFingerLink { + WebfingerLink { rel: Some("self".to_string()), type_: Some("application/activity+json".to_string()), href: Some(url.into()), diff --git a/crates/websocket/src/lib.rs b/crates/websocket/src/lib.rs index fbb2bb512..d501e9611 100644 --- a/crates/websocket/src/lib.rs +++ b/crates/websocket/src/lib.rs @@ -110,6 +110,7 @@ pub enum UserOperation { AddAdmin, BanPerson, Search, + ResolveObject, MarkAllAsRead, SaveUserSettings, TransferCommunity, diff --git a/src/api_routes.rs b/src/api_routes.rs index feaf0e235..1d76af002 100644 --- a/src/api_routes.rs +++ b/src/api_routes.rs @@ -33,6 +33,11 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .wrap(rate_limit.message()) .route(web::get().to(route_get::)), ) + .service( + web::resource("/resolve_object") + .wrap(rate_limit.message()) + .route(web::get().to(route_get::)), + ) // Community .service( web::resource("/community")