Add user_inbox check that activities are really addressed to local users

This commit is contained in:
Felix Ableitner 2020-11-11 17:40:45 +01:00
parent 964d95de5c
commit fb16f47f2f
5 changed files with 97 additions and 43 deletions

View file

@ -12,7 +12,7 @@ use activitystreams::{
}; };
use actix_web::HttpRequest; use actix_web::HttpRequest;
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use lemmy_db::{activity::Activity, DbPool}; use lemmy_db::{activity::Activity, community::Community, user::User_, DbPool};
use lemmy_structs::blocking; use lemmy_structs::blocking;
use lemmy_utils::{location_info, LemmyError}; use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
@ -111,3 +111,43 @@ where
verify_signature(&request, actor.as_ref())?; verify_signature(&request, actor.as_ref())?;
Ok(actor) Ok(actor)
} }
/// Returns true if `to_and_cc` contains at least one local user.
pub(crate) async fn is_addressed_to_local_user(
to_and_cc: &[Url],
pool: &DbPool,
) -> Result<bool, LemmyError> {
for url in to_and_cc {
let url = url.to_string();
let user = blocking(&pool, move |conn| User_::read_from_actor_id(&conn, &url)).await?;
if let Ok(u) = user {
if u.local {
return Ok(true);
}
}
}
Ok(false)
}
/// If `to_and_cc` contains the followers collection of a remote community, returns this community
/// (like `https://example.com/c/main/followers`)
pub(crate) async fn is_addressed_to_community_followers(
to_and_cc: &[Url],
pool: &DbPool,
) -> Result<Option<Community>, LemmyError> {
for url in to_and_cc {
let url = url.to_string();
// TODO: extremely hacky, we should just store the followers url for each community in the db
if url.ends_with("/followers") {
let community_url = url.replace("/followers", "");
let community = blocking(&pool, move |conn| {
Community::read_from_actor_id(&conn, &community_url)
})
.await??;
if !community.local {
return Ok(Some(community));
}
}
}
Ok(None)
}

View file

@ -5,6 +5,8 @@ use crate::{
get_activity_to_and_cc, get_activity_to_and_cc,
inbox_verify_http_signature, inbox_verify_http_signature,
is_activity_already_known, is_activity_already_known,
is_addressed_to_community_followers,
is_addressed_to_local_user,
user_inbox::{user_receive_message, UserAcceptedActivities}, user_inbox::{user_receive_message, UserAcceptedActivities},
}, },
insert_activity, insert_activity,
@ -12,7 +14,7 @@ use crate::{
use activitystreams::{activity::ActorAndObject, prelude::*}; use activitystreams::{activity::ActorAndObject, prelude::*};
use actix_web::{web, HttpRequest, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
use anyhow::Context; use anyhow::Context;
use lemmy_db::{community::Community, user::User_, DbPool}; use lemmy_db::{community::Community, DbPool};
use lemmy_structs::blocking; use lemmy_structs::blocking;
use lemmy_utils::{location_info, LemmyError}; use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
@ -100,7 +102,10 @@ pub async fn shared_inbox(
} }
// If to_and_cc contains followers collection of a community, pass to receive_user_message() // If to_and_cc contains followers collection of a community, pass to receive_user_message()
if is_addressed_to_community_followers(&to_and_cc, context.pool()).await? { if is_addressed_to_community_followers(&to_and_cc, context.pool())
.await?
.is_some()
{
let user_activity = UserAcceptedActivities::from_any_base(activity_any_base.clone())? let user_activity = UserAcceptedActivities::from_any_base(activity_any_base.clone())?
.context(location_info!())?; .context(location_info!())?;
res = Some( res = Some(
@ -146,40 +151,3 @@ async fn extract_local_community_from_destinations(
} }
Ok(None) Ok(None)
} }
/// Returns true if `to_and_cc` contains at least one local user.
async fn is_addressed_to_local_user(to_and_cc: &[Url], pool: &DbPool) -> Result<bool, LemmyError> {
for url in to_and_cc {
let url = url.to_string();
let user = blocking(&pool, move |conn| User_::read_from_actor_id(&conn, &url)).await?;
if let Ok(u) = user {
if u.local {
return Ok(true);
}
}
}
Ok(false)
}
/// Returns true if `to_and_cc` contains at least one followers collection of a remote community
/// (like `https://example.com/c/main/followers`)
async fn is_addressed_to_community_followers(
to_and_cc: &[Url],
pool: &DbPool,
) -> Result<bool, LemmyError> {
for url in to_and_cc {
let url = url.to_string();
// TODO: extremely hacky, we should just store the followers url for each community in the db
if url.ends_with("/followers") {
let community_url = url.replace("/followers", "");
let community = blocking(&pool, move |conn| {
Community::read_from_actor_id(&conn, &community_url)
})
.await??;
if !community.local {
return Ok(true);
}
}
}
Ok(false)
}

View file

@ -23,6 +23,8 @@ use crate::{
get_activity_to_and_cc, get_activity_to_and_cc,
inbox_verify_http_signature, inbox_verify_http_signature,
is_activity_already_known, is_activity_already_known,
is_addressed_to_community_followers,
is_addressed_to_local_user,
is_addressed_to_public, is_addressed_to_public,
receive_for_community::{ receive_for_community::{
receive_create_for_community, receive_create_for_community,
@ -99,6 +101,7 @@ pub async fn user_inbox(
}) })
.await??; .await??;
let to_and_cc = get_activity_to_and_cc(&activity)?; let to_and_cc = get_activity_to_and_cc(&activity)?;
// TODO: we should also accept activities that are sent to community followers
if !to_and_cc.contains(&&user.actor_id()?) { if !to_and_cc.contains(&&user.actor_id()?) {
return Err(anyhow!("Activity delivered to wrong user").into()); return Err(anyhow!("Activity delivered to wrong user").into());
} }
@ -130,9 +133,7 @@ pub(crate) async fn user_receive_message(
context: &LemmyContext, context: &LemmyContext,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
// TODO: must be addressed to one or more local users, or to followers of a remote community is_for_user_inbox(context, &activity).await?;
// TODO: if it is addressed to community followers, check that at least one local user is following it
let any_base = activity.clone().into_any_base()?; let any_base = activity.clone().into_any_base()?;
let kind = activity.kind().context(location_info!())?; let kind = activity.kind().context(location_info!())?;
@ -162,6 +163,41 @@ pub(crate) async fn user_receive_message(
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
/// Returns true if the activity is addressed directly to one or more local users, or if it is
/// addressed to the followers collection of a remote community, and at least one local user follows
/// it.
async fn is_for_user_inbox(
context: &LemmyContext,
activity: &UserAcceptedActivities,
) -> Result<(), LemmyError> {
let to_and_cc = get_activity_to_and_cc(activity)?;
// Check if it is addressed directly to any local user
if is_addressed_to_local_user(&to_and_cc, context.pool()).await? {
return Ok(());
}
// Check if it is addressed to any followers collection of a remote community, and that the
// community has local followers.
let community = is_addressed_to_community_followers(&to_and_cc, context.pool()).await?;
if let Some(c) = community {
let community_id = c.id;
let has_local_followers = blocking(&context.pool(), move |conn| {
CommunityFollower::has_local_followers(conn, community_id)
})
.await??;
if c.local {
return Err(
anyhow!("Remote activity cant be addressed to followers of local community").into(),
);
}
if has_local_followers {
return Ok(());
}
}
Err(anyhow!("Not addressed for any local user").into())
}
/// Handle accepted follows. /// Handle accepted follows.
async fn receive_accept( async fn receive_accept(
context: &LemmyContext, context: &LemmyContext,

View file

@ -322,6 +322,15 @@ impl Followable<CommunityFollowerForm> for CommunityFollower {
) )
.execute(conn) .execute(conn)
} }
// TODO: this function name only makes sense if you call it with a remote community. for a local
// community, it will also return true if only remote followers exist
fn has_local_followers(conn: &PgConnection, community_id_: i32) -> Result<bool, Error> {
use crate::schema::community_follower::dsl::*;
diesel::select(exists(
community_follower.filter(community_id.eq(community_id_)),
))
.get_result(conn)
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -62,6 +62,7 @@ pub trait Followable<T> {
fn unfollow(conn: &PgConnection, form: &T) -> Result<usize, Error> fn unfollow(conn: &PgConnection, form: &T) -> Result<usize, Error>
where where
Self: Sized; Self: Sized;
fn has_local_followers(conn: &PgConnection, community_id: i32) -> Result<bool, Error>;
} }
pub trait Joinable<T> { pub trait Joinable<T> {