]> Untitled Git - lemmy.git/blob - crates/apub/src/lib.rs
Activitypub crate rewrite (#2782)
[lemmy.git] / crates / apub / src / lib.rs
1 use crate::fetcher::post_or_comment::PostOrComment;
2 use activitypub_federation::config::{Data, UrlVerifier};
3 use async_trait::async_trait;
4 use lemmy_api_common::context::LemmyContext;
5 use lemmy_db_schema::{
6   source::{
7     activity::{Activity, ActivityInsertForm},
8     instance::Instance,
9     local_site::LocalSite,
10   },
11   traits::Crud,
12   utils::DbPool,
13 };
14 use lemmy_utils::{error::LemmyError, settings::structs::Settings};
15 use once_cell::sync::Lazy;
16 use serde::Serialize;
17 use url::Url;
18
19 pub mod activities;
20 pub(crate) mod activity_lists;
21 pub mod api;
22 pub(crate) mod collections;
23 pub mod fetcher;
24 pub mod http;
25 pub(crate) mod mentions;
26 pub mod objects;
27 pub mod protocol;
28
29 pub const FEDERATION_HTTP_FETCH_LIMIT: u32 = 50;
30
31 static CONTEXT: Lazy<Vec<serde_json::Value>> = Lazy::new(|| {
32   serde_json::from_str(include_str!("../assets/lemmy/context.json")).expect("parse context")
33 });
34
35 #[derive(Clone)]
36 pub struct VerifyUrlData(pub DbPool);
37
38 #[async_trait]
39 impl UrlVerifier for VerifyUrlData {
40   async fn verify(&self, url: &Url) -> Result<(), &'static str> {
41     let local_site_data = fetch_local_site_data(&self.0)
42       .await
43       .expect("read local site data");
44     check_apub_id_valid(url, &local_site_data)?;
45     Ok(())
46   }
47 }
48
49 /// Checks if the ID is allowed for sending or receiving.
50 ///
51 /// In particular, it checks for:
52 /// - federation being enabled (if its disabled, only local URLs are allowed)
53 /// - the correct scheme (either http or https)
54 /// - URL being in the allowlist (if it is active)
55 /// - URL not being in the blocklist (if it is active)
56 ///
57 /// `use_strict_allowlist` should be true only when parsing a remote community, or when parsing a
58 /// post/comment in a local community.
59 #[tracing::instrument(skip(local_site_data))]
60 fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> Result<(), &'static str> {
61   let domain = apub_id.domain().expect("apud id has domain").to_string();
62
63   if !local_site_data
64     .local_site
65     .as_ref()
66     .map(|l| l.federation_enabled)
67     .unwrap_or(true)
68   {
69     return Err("Federation disabled");
70   }
71
72   if let Some(blocked) = local_site_data.blocked_instances.as_ref() {
73     if blocked.iter().any(|i| domain.eq(&i.domain)) {
74       return Err("Domain is blocked");
75     }
76   }
77
78   if let Some(allowed) = local_site_data.allowed_instances.as_ref() {
79     if !allowed.iter().any(|i| domain.eq(&i.domain)) {
80       return Err("Domain is not in allowlist");
81     }
82   }
83
84   Ok(())
85 }
86
87 #[derive(Clone)]
88 pub(crate) struct LocalSiteData {
89   local_site: Option<LocalSite>,
90   allowed_instances: Option<Vec<Instance>>,
91   blocked_instances: Option<Vec<Instance>>,
92 }
93
94 pub(crate) async fn fetch_local_site_data(
95   pool: &DbPool,
96 ) -> Result<LocalSiteData, diesel::result::Error> {
97   // LocalSite may be missing
98   let local_site = LocalSite::read(pool).await.ok();
99   let allowed = Instance::allowlist(pool).await?;
100   let blocked = Instance::blocklist(pool).await?;
101
102   // These can return empty vectors, so convert them to options
103   let allowed_instances = (!allowed.is_empty()).then_some(allowed);
104   let blocked_instances = (!blocked.is_empty()).then_some(blocked);
105
106   Ok(LocalSiteData {
107     local_site,
108     allowed_instances,
109     blocked_instances,
110   })
111 }
112
113 #[tracing::instrument(skip(settings, local_site_data))]
114 pub(crate) fn check_apub_id_valid_with_strictness(
115   apub_id: &Url,
116   is_strict: bool,
117   local_site_data: &LocalSiteData,
118   settings: &Settings,
119 ) -> Result<(), LemmyError> {
120   let domain = apub_id.domain().expect("apud id has domain").to_string();
121   let local_instance = settings
122     .get_hostname_without_port()
123     .expect("local hostname is valid");
124   if domain == local_instance {
125     return Ok(());
126   }
127   check_apub_id_valid(apub_id, local_site_data).map_err(LemmyError::from_message)?;
128
129   if let Some(allowed) = local_site_data.allowed_instances.as_ref() {
130     // Only check allowlist if this is a community
131     if is_strict {
132       // need to allow this explicitly because apub receive might contain objects from our local
133       // instance.
134       let mut allowed_and_local = allowed
135         .iter()
136         .map(|i| i.domain.clone())
137         .collect::<Vec<String>>();
138       let local_instance = settings
139         .get_hostname_without_port()
140         .expect("local hostname is valid");
141       allowed_and_local.push(local_instance);
142
143       let domain = apub_id.domain().expect("apud id has domain").to_string();
144       if !allowed_and_local.contains(&domain) {
145         return Err(LemmyError::from_message(
146           "Federation forbidden by strict allowlist",
147         ));
148       }
149     }
150   }
151   Ok(())
152 }
153
154 /// Store a sent or received activity in the database.
155 ///
156 /// Stored activities are served over the HTTP endpoint `GET /activities/{type_}/{id}`. This also
157 /// ensures that the same activity cannot be received more than once.
158 #[tracing::instrument(skip(data, activity))]
159 async fn insert_activity<T>(
160   ap_id: &Url,
161   activity: &T,
162   local: bool,
163   sensitive: bool,
164   data: &Data<LemmyContext>,
165 ) -> Result<(), LemmyError>
166 where
167   T: Serialize,
168 {
169   let ap_id = ap_id.clone().into();
170   let form = ActivityInsertForm {
171     ap_id,
172     data: serde_json::to_value(activity)?,
173     local: Some(local),
174     sensitive: Some(sensitive),
175     updated: None,
176   };
177   Activity::create(data.pool(), &form).await?;
178   Ok(())
179 }
180
181 #[async_trait::async_trait]
182 pub trait SendActivity: Sync {
183   type Response: Sync + Send;
184
185   async fn send_activity(
186     _request: &Self,
187     _response: &Self::Response,
188     _context: &Data<LemmyContext>,
189   ) -> Result<(), LemmyError> {
190     Ok(())
191   }
192 }