]> Untitled Git - lemmy.git/blob - crates/apub/src/lib.rs
a5bc41d1fd0b2a192190fa074ec64cf7e2a47d0a
[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 local_site_data
73     .blocked_instances
74     .iter()
75     .any(|i| domain.eq(&i.domain))
76   {
77     return Err("Domain is blocked");
78   }
79
80   // Only check this if there are instances in the allowlist
81   if !local_site_data.allowed_instances.is_empty()
82     && !local_site_data
83       .allowed_instances
84       .iter()
85       .any(|i| domain.eq(&i.domain))
86   {
87     return Err("Domain is not in allowlist");
88   }
89
90   Ok(())
91 }
92
93 #[derive(Clone)]
94 pub(crate) struct LocalSiteData {
95   local_site: Option<LocalSite>,
96   allowed_instances: Vec<Instance>,
97   blocked_instances: Vec<Instance>,
98 }
99
100 pub(crate) async fn fetch_local_site_data(
101   pool: &DbPool,
102 ) -> Result<LocalSiteData, diesel::result::Error> {
103   // LocalSite may be missing
104   let local_site = LocalSite::read(pool).await.ok();
105   let allowed_instances = Instance::allowlist(pool).await?;
106   let blocked_instances = Instance::blocklist(pool).await?;
107
108   Ok(LocalSiteData {
109     local_site,
110     allowed_instances,
111     blocked_instances,
112   })
113 }
114
115 #[tracing::instrument(skip(settings, local_site_data))]
116 pub(crate) fn check_apub_id_valid_with_strictness(
117   apub_id: &Url,
118   is_strict: bool,
119   local_site_data: &LocalSiteData,
120   settings: &Settings,
121 ) -> Result<(), LemmyError> {
122   let domain = apub_id.domain().expect("apud id has domain").to_string();
123   let local_instance = settings
124     .get_hostname_without_port()
125     .expect("local hostname is valid");
126   if domain == local_instance {
127     return Ok(());
128   }
129   check_apub_id_valid(apub_id, local_site_data).map_err(LemmyError::from_message)?;
130
131   // Only check allowlist if this is a community, and there are instances in the allowlist
132   if is_strict && !local_site_data.allowed_instances.is_empty() {
133     // need to allow this explicitly because apub receive might contain objects from our local
134     // instance.
135     let mut allowed_and_local = local_site_data
136       .allowed_instances
137       .iter()
138       .map(|i| i.domain.clone())
139       .collect::<Vec<String>>();
140     let local_instance = settings
141       .get_hostname_without_port()
142       .expect("local hostname is valid");
143     allowed_and_local.push(local_instance);
144
145     let domain = apub_id.domain().expect("apud id has domain").to_string();
146     if !allowed_and_local.contains(&domain) {
147       return Err(LemmyError::from_message(
148         "Federation forbidden by strict allowlist",
149       ));
150     }
151   }
152   Ok(())
153 }
154
155 /// Store a sent or received activity in the database.
156 ///
157 /// Stored activities are served over the HTTP endpoint `GET /activities/{type_}/{id}`. This also
158 /// ensures that the same activity cannot be received more than once.
159 #[tracing::instrument(skip(data, activity))]
160 async fn insert_activity<T>(
161   ap_id: &Url,
162   activity: &T,
163   local: bool,
164   sensitive: bool,
165   data: &Data<LemmyContext>,
166 ) -> Result<(), LemmyError>
167 where
168   T: Serialize,
169 {
170   let ap_id = ap_id.clone().into();
171   let form = ActivityInsertForm {
172     ap_id,
173     data: serde_json::to_value(activity)?,
174     local: Some(local),
175     sensitive: Some(sensitive),
176     updated: None,
177   };
178   Activity::create(data.pool(), &form).await?;
179   Ok(())
180 }
181
182 #[async_trait::async_trait]
183 pub trait SendActivity: Sync {
184   type Response: Sync + Send;
185
186   async fn send_activity(
187     _request: &Self,
188     _response: &Self::Response,
189     _context: &Data<LemmyContext>,
190   ) -> Result<(), LemmyError> {
191     Ok(())
192   }
193 }