Async verify (#4)

* Add date header when sending activity

* Version 0.2.2

* Make verify url function async
This commit is contained in:
Nutomic 2022-10-28 10:37:51 +00:00 committed by GitHub
parent 356b98bb97
commit 472d55a69e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 66 additions and 11 deletions

9
Cargo.lock generated
View file

@ -4,7 +4,7 @@ version = 3
[[package]] [[package]]
name = "activitypub_federation" name = "activitypub_federation"
version = "0.2.1" version = "0.2.2"
dependencies = [ dependencies = [
"activitypub_federation_derive", "activitypub_federation_derive",
"activitystreams-kinds", "activitystreams-kinds",
@ -16,6 +16,7 @@ dependencies = [
"base64", "base64",
"chrono", "chrono",
"derive_builder", "derive_builder",
"dyn-clone",
"env_logger", "env_logger",
"http", "http",
"http-signature-normalization-actix", "http-signature-normalization-actix",
@ -530,6 +531,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c97b9233581d84b8e1e689cdd3a47b6f69770084fc246e86a7f78b0d9c1d4a5" checksum = "8c97b9233581d84b8e1e689cdd3a47b6f69770084fc246e86a7f78b0d9c1d4a5"
[[package]]
name = "dyn-clone"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2"
[[package]] [[package]]
name = "either" name = "either"
version = "1.8.0" version = "1.8.0"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "activitypub_federation" name = "activitypub_federation"
version = "0.2.1" version = "0.2.2"
edition = "2021" edition = "2021"
description = "High-level Activitypub framework" description = "High-level Activitypub framework"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -33,6 +33,7 @@ background-jobs = "0.13.0"
thiserror = "1.0.37" thiserror = "1.0.37"
derive_builder = "0.11.2" derive_builder = "0.11.2"
itertools = "0.10.5" itertools = "0.10.5"
dyn-clone = "1.0.9"
[dev-dependencies] [dev-dependencies]
activitystreams-kinds = "0.2.1" activitystreams-kinds = "0.2.1"

View file

@ -13,9 +13,11 @@ use activitypub_federation::{
traits::ApubObject, traits::ApubObject,
InstanceSettings, InstanceSettings,
LocalInstance, LocalInstance,
UrlVerifier,
APUB_JSON_CONTENT_TYPE, APUB_JSON_CONTENT_TYPE,
}; };
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer}; use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
use async_trait::async_trait;
use http_signature_normalization_actix::prelude::VerifyDigest; use http_signature_normalization_actix::prelude::VerifyDigest;
use reqwest::Client; use reqwest::Client;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@ -37,9 +39,27 @@ pub struct Instance {
pub posts: Mutex<Vec<MyPost>>, pub posts: Mutex<Vec<MyPost>>,
} }
/// Use this to store your federation blocklist, or a database connection needed to retrieve it.
#[derive(Clone)]
struct MyUrlVerifier();
#[async_trait]
impl UrlVerifier for MyUrlVerifier {
async fn verify(&self, url: &Url) -> Result<(), &'static str> {
if url.domain() == Some("malicious.com") {
Err("malicious domain")
} else {
Ok(())
}
}
}
impl Instance { impl Instance {
pub fn new(hostname: String) -> Result<InstanceHandle, Error> { pub fn new(hostname: String) -> Result<InstanceHandle, Error> {
let settings = InstanceSettings::builder().debug(true).build()?; let settings = InstanceSettings::builder()
.debug(true)
.url_verifier(Box::new(MyUrlVerifier()))
.build()?;
let local_instance = let local_instance =
LocalInstance::new(hostname.clone(), Client::default().into(), settings); LocalInstance::new(hostname.clone(), Client::default().into(), settings);
let local_user = MyUser::new(generate_object_id(&hostname)?, generate_actor_keypair()?); let local_user = MyUser::new(generate_object_id(&hostname)?, generate_actor_keypair()?);

View file

@ -16,6 +16,7 @@ use background_jobs::{
MaxRetries, MaxRetries,
WorkerConfig, WorkerConfig,
}; };
use chrono::Utc;
use http::{header::HeaderName, HeaderMap, HeaderValue}; use http::{header::HeaderName, HeaderMap, HeaderValue};
use itertools::Itertools; use itertools::Itertools;
use reqwest_middleware::ClientWithMiddleware; use reqwest_middleware::ClientWithMiddleware;
@ -50,12 +51,11 @@ where
.into_iter() .into_iter()
.unique() .unique()
.filter(|i| !instance.is_local_url(i)) .filter(|i| !instance.is_local_url(i))
.filter(|i| verify_url_valid(i, &instance.settings).is_ok())
.collect(); .collect();
let activity_queue = &instance.activity_queue; let activity_queue = &instance.activity_queue;
for inbox in inboxes { for inbox in inboxes {
if verify_url_valid(&inbox, &instance.settings).is_err() { if verify_url_valid(&inbox, &instance.settings).await.is_err() {
continue; continue;
} }
let message = SendActivityTask { let message = SendActivityTask {
@ -187,6 +187,10 @@ fn generate_request_headers(inbox_url: &Url) -> HeaderMap {
HeaderName::from_static("host"), HeaderName::from_static("host"),
HeaderValue::from_str(&host).expect("Hostname is valid"), HeaderValue::from_str(&host).expect("Hostname is valid"),
); );
headers.insert(
"date",
HeaderValue::from_str(&Utc::now().to_rfc2822()).expect("Hostname is valid"),
);
headers headers
} }

View file

@ -26,7 +26,7 @@ where
<ActorT as ApubObject>::Error: From<Error> + From<anyhow::Error>, <ActorT as ApubObject>::Error: From<Error> + From<anyhow::Error>,
{ {
verify_domains_match(activity.id(), activity.actor())?; verify_domains_match(activity.id(), activity.actor())?;
verify_url_valid(activity.id(), &local_instance.settings)?; verify_url_valid(activity.id(), &local_instance.settings).await?;
if local_instance.is_local_url(activity.id()) { if local_instance.is_local_url(activity.id()) {
return Err(Error::UrlVerificationError("Activity was sent from local instance").into()); return Err(Error::UrlVerificationError("Activity was sent from local instance").into());
} }

View file

@ -1,6 +1,8 @@
use crate::core::activity_queue::create_activity_queue; use crate::core::activity_queue::create_activity_queue;
use async_trait::async_trait;
use background_jobs::Manager; use background_jobs::Manager;
use derive_builder::Builder; use derive_builder::Builder;
use dyn_clone::{clone_trait_object, DynClone};
use reqwest_middleware::ClientWithMiddleware; use reqwest_middleware::ClientWithMiddleware;
use std::time::Duration; use std::time::Duration;
use url::Url; use url::Url;
@ -23,6 +25,12 @@ pub struct LocalInstance {
settings: InstanceSettings, settings: InstanceSettings,
} }
#[async_trait]
pub trait UrlVerifier: DynClone + Send {
async fn verify(&self, url: &Url) -> Result<(), &'static str>;
}
clone_trait_object!(UrlVerifier);
// Use InstanceSettingsBuilder to initialize this // Use InstanceSettingsBuilder to initialize this
#[derive(Builder)] #[derive(Builder)]
pub struct InstanceSettings { pub struct InstanceSettings {
@ -45,12 +53,13 @@ pub struct InstanceSettings {
/// Function used to verify that urls are valid, used when receiving activities or fetching remote /// Function used to verify that urls are valid, used when receiving activities or fetching remote
/// objects. Use this to implement functionality like federation blocklists. In case verification /// objects. Use this to implement functionality like federation blocklists. In case verification
/// fails, it should return an error message. /// fails, it should return an error message.
#[builder(default = "|_| { Ok(()) }")] #[builder(default = "Box::new(DefaultUrlVerifier())")]
verify_url_function: fn(&Url) -> Result<(), &'static str>, url_verifier: Box<dyn UrlVerifier + Sync>,
/// Enable to sign HTTP signatures according to draft 10, which does not include (created) and /// Enable to sign HTTP signatures according to draft 10, which does not include (created) and
/// (expires) fields. This is required for compatibility with some software like Pleroma. /// (expires) fields. This is required for compatibility with some software like Pleroma.
/// https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-10 /// https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-10
/// https://git.pleroma.social/pleroma/pleroma/-/issues/2939 /// https://git.pleroma.social/pleroma/pleroma/-/issues/2939
#[builder(default = "false")]
http_signature_compat: bool, http_signature_compat: bool,
} }
@ -61,6 +70,16 @@ impl InstanceSettings {
} }
} }
#[derive(Clone)]
struct DefaultUrlVerifier();
#[async_trait]
impl UrlVerifier for DefaultUrlVerifier {
async fn verify(&self, _url: &Url) -> Result<(), &'static str> {
Ok(())
}
}
impl LocalInstance { impl LocalInstance {
pub fn new(domain: String, client: ClientWithMiddleware, settings: InstanceSettings) -> Self { pub fn new(domain: String, client: ClientWithMiddleware, settings: InstanceSettings) -> Self {
let activity_queue = create_activity_queue(client.clone(), &settings); let activity_queue = create_activity_queue(client.clone(), &settings);

View file

@ -11,7 +11,7 @@ pub async fn fetch_object_http<Kind: DeserializeOwned>(
) -> Result<Kind, Error> { ) -> Result<Kind, Error> {
// dont fetch local objects this way // dont fetch local objects this way
debug_assert!(url.domain() != Some(&instance.hostname)); debug_assert!(url.domain() != Some(&instance.hostname));
verify_url_valid(url, &instance.settings)?; verify_url_valid(url, &instance.settings).await?;
info!("Fetching remote object {}", url.to_string()); info!("Fetching remote object {}", url.to_string());
*request_counter += 1; *request_counter += 1;
@ -55,7 +55,7 @@ pub fn verify_urls_match(a: &Url, b: &Url) -> Result<(), Error> {
/// [`InstanceSettings.verify_url_function`]. /// [`InstanceSettings.verify_url_function`].
/// ///
/// https://www.w3.org/TR/activitypub/#security-considerations /// https://www.w3.org/TR/activitypub/#security-considerations
pub fn verify_url_valid(url: &Url, settings: &InstanceSettings) -> Result<(), Error> { pub async fn verify_url_valid(url: &Url, settings: &InstanceSettings) -> Result<(), Error> {
match url.scheme() { match url.scheme() {
"https" => {} "https" => {}
"http" => { "http" => {
@ -78,7 +78,11 @@ pub fn verify_url_valid(url: &Url, settings: &InstanceSettings) -> Result<(), Er
)); ));
} }
(settings.verify_url_function)(url).map_err(Error::UrlVerificationError)?; settings
.url_verifier
.verify(url)
.await
.map_err(Error::UrlVerificationError)?;
Ok(()) Ok(())
} }