This commit is contained in:
Paul Delafosse 2022-11-27 16:54:02 +01:00
parent 52971b64b3
commit fdd16b99e0
28 changed files with 315 additions and 295 deletions

View file

@ -34,7 +34,7 @@ http-signature-normalization = "0.6.0"
actix-web = { version = "4.2.1", default-features = false, optional = true }
http-signature-normalization-actix = { version = "0.6.1", default-features = false, features = ["server", "sha-2"], optional = true }
axum = { version = "0.6.0-rc.5", features = ["json", "headers", "macros"], optional = true }
axum = { version = "0.6.0-rc.5", features = ["json", "headers", "macros", "original-uri"], optional = true }
# Axum
tower-http = { version = "0.3", features = ["map-request-body", "util"], optional = true }

View file

@ -25,7 +25,7 @@ impl Accept {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for Accept {
type DataType = InstanceHandle;
type Error = crate::error::Error;

View file

@ -37,7 +37,7 @@ impl CreateNote {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for CreateNote {
type DataType = InstanceHandle;
type Error = crate::error::Error;

View file

@ -34,7 +34,7 @@ impl Follow {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for Follow {
type DataType = InstanceHandle;
type Error = crate::error::Error;
@ -63,11 +63,12 @@ impl ActivityHandler for Follow {
request_counter: &mut i32,
) -> Result<(), Self::Error> {
// add to followers
let mut users = data.users.lock().unwrap();
let local_user = users.first_mut().unwrap();
local_user.followers.push(self.actor.inner().clone());
let local_user = local_user.clone();
drop(users);
let local_user = {
let mut users = data.users.lock().unwrap();
let local_user = users.first_mut().unwrap();
local_user.followers.push(self.actor.inner().clone());
local_user.clone()
};
// send back an accept
let follower = self

View file

@ -8,7 +8,11 @@ use crate::{
};
use activitypub_federation::{
core::{inbox::receive_activity, object_id::ObjectId, signatures::generate_actor_keypair},
core::{
actix::inbox::receive_activity,
object_id::ObjectId,
signatures::generate_actor_keypair,
},
data::Data,
deser::context::WithContext,
traits::ApubObject,

View file

@ -39,7 +39,7 @@ pub struct Note {
content: String,
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ApubObject for MyPost {
type DataType = InstanceHandle;
type ApubType = Note;

View file

@ -115,7 +115,7 @@ impl MyUser {
local_instance: &LocalInstance,
) -> Result<(), <Activity as ActivityHandler>::Error>
where
Activity: ActivityHandler + Serialize,
Activity: ActivityHandler + Serialize + Send + Sync,
<Activity as ActivityHandler>::Error: From<anyhow::Error> + From<serde_json::Error>,
{
let activity = WithContext::new_default(activity);
@ -131,7 +131,7 @@ impl MyUser {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ApubObject for MyUser {
type DataType = InstanceHandle;
type ApubType = Person;

View file

@ -25,7 +25,7 @@ impl Accept {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for Accept {
type DataType = InstanceHandle;
type Error = crate::error::Error;

View file

@ -37,7 +37,7 @@ impl CreateNote {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for CreateNote {
type DataType = InstanceHandle;
type Error = crate::error::Error;

View file

@ -34,7 +34,7 @@ impl Follow {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for Follow {
type DataType = InstanceHandle;
type Error = crate::error::Error;
@ -63,11 +63,12 @@ impl ActivityHandler for Follow {
request_counter: &mut i32,
) -> Result<(), Self::Error> {
// add to followers
let mut users = data.users.lock().unwrap();
let local_user = users.first_mut().unwrap();
local_user.followers.push(self.actor.inner().clone());
let local_user = local_user.clone();
drop(users);
let local_user = {
let mut users = data.users.lock().unwrap();
let local_user = users.first_mut().unwrap();
local_user.followers.push(self.actor.inner().clone());
local_user.clone()
};
// send back an accept
let follower = self

View file

@ -8,7 +8,7 @@ use crate::{
};
use activitypub_federation::{
core::{inbox::receive_activity, object_id::ObjectId, signatures::generate_actor_keypair},
core::{object_id::ObjectId, receive_activity, signatures::generate_actor_keypair},
data::Data,
deser::context::WithContext,
traits::ApubObject,
@ -22,16 +22,15 @@ use activitypub_federation::core::axum::{verify_request_payload, DigestVerified}
use async_trait::async_trait;
use axum::{
body,
body::Body,
extract::State,
body::{Body, BoxBody},
extract::{Json, OriginalUri, State},
middleware,
response::IntoResponse,
routing::get,
routing::{get, post},
Extension,
Json,
Router,
};
use http::{header::CONTENT_TYPE, Request, Response};
use http::{header::CONTENT_TYPE, HeaderMap, Request, Response};
use reqwest::Client;
use std::{
net::SocketAddr,
@ -98,12 +97,13 @@ impl Instance {
let hostname = instance.local_instance.hostname();
let instance = instance.clone();
let app = Router::new()
.route("/objects/:user_name", get(http_get_user))
.route("/inbox", post(http_post_user_inbox))
.layer(
ServiceBuilder::new()
.map_request_body(body::boxed)
.layer(middleware::from_fn(verify_request_payload)),
)
.route("/objects/:user_name", get(http_get_user))
.with_state(instance);
// run it
@ -117,6 +117,7 @@ impl Instance {
/// FIXME
use axum_macros::debug_handler;
#[debug_handler(body = Body)]
/// Handles requests to fetch user json over HTTP
async fn http_get_user(
@ -131,7 +132,9 @@ async fn http_get_user(
let user = ObjectId::<MyUser>::new(url)
.dereference_local(&data)
.await
.expect("Failed to dereference user")
.expect("Failed to dereference user");
let user = user
.into_apub(&data)
.await
.expect("Failed to convert to apub user");
@ -146,18 +149,21 @@ async fn http_get_user(
}
/// Handles messages received in user inbox
#[debug_handler(body = BoxBody)]
async fn http_post_user_inbox(
request: Request<Body>,
activity: Json<WithContext<PersonAcceptedActivities>>,
Extension(data): Extension<InstanceHandle>,
headers: HeaderMap,
OriginalUri(uri): OriginalUri,
State(data): State<InstanceHandle>,
Extension(digest_verified): Extension<DigestVerified>,
Json(activity): Json<WithContext<PersonAcceptedActivities>>,
) -> impl IntoResponse {
receive_activity::<WithContext<PersonAcceptedActivities>, MyUser, InstanceHandle>(
digest_verified,
request,
activity,
&data.clone().local_instance,
&Data::new(data),
headers,
uri,
)
.await
}

View file

@ -39,7 +39,7 @@ pub struct Note {
content: String,
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ApubObject for MyPost {
type DataType = InstanceHandle;
type ApubType = Note;

View file

@ -115,7 +115,7 @@ impl MyUser {
local_instance: &LocalInstance,
) -> Result<(), <Activity as ActivityHandler>::Error>
where
Activity: ActivityHandler + Serialize,
Activity: ActivityHandler + Serialize + Send + Sync,
<Activity as ActivityHandler>::Error: From<anyhow::Error> + From<serde_json::Error>,
{
let activity = WithContext::new_default(activity);
@ -131,7 +131,7 @@ impl MyUser {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ApubObject for MyUser {
type DataType = InstanceHandle;
type ApubType = Person;

View file

@ -1,7 +1,6 @@
use crate::{
core::signatures::{sign_request, PublicKey},
traits::ActivityHandler,
utils::verify_url_valid,
Error,
InstanceSettings,
LocalInstance,
@ -60,9 +59,10 @@ where
let activity_queue = &instance.activity_queue;
for inbox in inboxes {
if verify_url_valid(&inbox, &instance.settings).await.is_err() {
if instance.verify_url_valid(&inbox).await.is_err() {
continue;
}
let message = SendActivityTask {
activity_id: activity_id.clone(),
inbox,

50
src/core/actix/inbox.rs Normal file
View file

@ -0,0 +1,50 @@
use crate::{
core::object_id::ObjectId,
data::Data,
traits::{ActivityHandler, Actor, ApubObject},
Error,
LocalInstance,
};
use crate::core::actix::signature::verify_signature;
use actix_web::{dev::Payload, FromRequest, HttpRequest, HttpResponse};
use http_signature_normalization_actix::prelude::DigestVerified;
use serde::de::DeserializeOwned;
use tracing::debug;
/// Receive an activity and perform some basic checks, including HTTP signature verification.
pub async fn receive_activity<Activity, ActorT, Datatype>(
request: HttpRequest,
activity: Activity,
local_instance: &LocalInstance,
data: &Data<Datatype>,
) -> Result<HttpResponse, <Activity as ActivityHandler>::Error>
where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static,
ActorT: ApubObject<DataType = Datatype> + Actor + Send + 'static,
for<'de2> <ActorT as ApubObject>::ApubType: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<anyhow::Error>
+ From<Error>
+ From<<ActorT as ApubObject>::Error>
+ From<serde_json::Error>
+ From<http_signature_normalization_actix::digest::middleware::VerifyError>,
<ActorT as ApubObject>::Error: From<Error> + From<anyhow::Error>,
{
// ensure that payload hash was checked against digest header by middleware
DigestVerified::from_request(&request, &mut Payload::None).await?;
local_instance.verify_url_and_domain(&activity).await?;
let request_counter = &mut 0;
let actor = ObjectId::<ActorT>::new(activity.actor().clone())
.dereference(data, local_instance, request_counter)
.await?;
verify_signature(&request, actor.public_key())?;
debug!("Verifying activity {}", activity.id().to_string());
activity.verify(data, request_counter).await?;
debug!("Receiving activity {}", activity.id().to_string());
activity.receive(data, request_counter).await?;
Ok(HttpResponse::Ok().finish())
}

2
src/core/actix/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod inbox;
mod signature;

View file

@ -0,0 +1,35 @@
use actix_web::HttpRequest;
use anyhow::anyhow;
use http_signature_normalization_actix::Config as ConfigActix;
use once_cell::sync::Lazy;
use openssl::{hash::MessageDigest, pkey::PKey, sign::Verifier};
use tracing::debug;
static CONFIG2: Lazy<ConfigActix> = Lazy::new(ConfigActix::new);
/// Verifies the HTTP signature on an incoming inbox request.
pub fn verify_signature(request: &HttpRequest, public_key: &str) -> Result<(), anyhow::Error> {
let verified = CONFIG2
.begin_verify(
request.method(),
request.uri().path_and_query(),
request.headers().clone(),
)?
.verify(|signature, signing_string| -> anyhow::Result<bool> {
debug!(
"Verifying with key {}, message {}",
&public_key, &signing_string
);
let public_key = PKey::public_key_from_pem(public_key.as_bytes())?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key)?;
verifier.update(signing_string.as_bytes())?;
Ok(verifier.verify(&base64::decode(signature)?)?)
})?;
if verified {
debug!("verified signature for {}", &request.uri());
Ok(())
} else {
Err(anyhow!("Invalid signature on request: {}", &request.uri()))
}
}

50
src/core/axum/inbox.rs Normal file
View file

@ -0,0 +1,50 @@
use crate::{
core::{
axum::{signature::verify_signature, DigestVerified},
object_id::ObjectId,
},
data::Data,
traits::{ActivityHandler, Actor, ApubObject},
Error,
LocalInstance,
};
use http::{HeaderMap, Uri};
use serde::de::DeserializeOwned;
use tracing::debug;
/// Receive an activity and perform some basic checks, including HTTP signature verification.
pub async fn receive_activity<Activity, ActorT, Datatype>(
_digest_verified: DigestVerified,
activity: Activity,
local_instance: &LocalInstance,
data: &Data<Datatype>,
headers: HeaderMap,
uri: Uri,
) -> Result<(), <Activity as ActivityHandler>::Error>
where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static,
ActorT: ApubObject<DataType = Datatype> + Actor + Send + 'static,
for<'de2> <ActorT as ApubObject>::ApubType: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<anyhow::Error>
+ From<Error>
+ From<<ActorT as ApubObject>::Error>
+ From<serde_json::Error>,
// + From<http_signature_normalization_actix::digest::middleware::VerifyError>,
<ActorT as ApubObject>::Error: From<Error> + From<anyhow::Error>,
{
local_instance.verify_url_and_domain(&activity).await?;
let request_counter = &mut 0;
let actor = ObjectId::<ActorT>::new(activity.actor().clone())
.dereference(data, local_instance, request_counter)
.await?;
verify_signature(&headers, uri, actor.public_key())?;
debug!("Verifying activity {}", activity.id().to_string());
activity.verify(data, request_counter).await?;
debug!("Receiving activity {}", activity.id().to_string());
activity.receive(data, request_counter).await?;
Ok(())
}

View file

@ -9,11 +9,16 @@ use axum::{
use digest::{verify_sha256, DigestPart};
mod digest;
pub mod inbox;
mod signature;
/// A request guard to ensure digest has been verified request has been
/// see [`receive_activity`]
#[derive(Clone)]
pub struct DigestVerified;
pub struct BufferRequestBody(pub Bytes);
pub async fn verify_request_payload(
request: Request<BoxBody>,
next: Next<BoxBody>,
@ -50,8 +55,6 @@ async fn verify_payload(request: Request<BoxBody>) -> Result<Request<BoxBody>, R
}
}
struct BufferRequestBody(Bytes);
#[async_trait]
impl<S> FromRequest<S, BoxBody> for BufferRequestBody
where

View file

@ -0,0 +1,41 @@
use anyhow::anyhow;
use http::{HeaderMap, Uri};
use http_signature_normalization::Config;
use openssl::{hash::MessageDigest, pkey::PKey, sign::Verifier};
use std::collections::BTreeMap;
use tracing::debug;
/// Verifies the HTTP signature on an incoming inbox request.
pub fn verify_signature(
headers: &HeaderMap,
uri: Uri,
public_key: &str,
) -> Result<(), anyhow::Error> {
let config = Config::default();
let mut header_map = BTreeMap::new();
for (name, value) in headers {
if let Ok(value) = value.to_str() {
header_map.insert(name.to_string(), value.to_string());
}
}
let verified = config
.begin_verify("GET", "/foo?bar=baz", header_map)?
.verify(|signature, signing_string| -> anyhow::Result<bool> {
debug!(
"Verifying with key {}, message {}",
&public_key, &signing_string
);
let public_key = PKey::public_key_from_pem(public_key.as_bytes())?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key)?;
verifier.update(signing_string.as_bytes())?;
Ok(verifier.verify(&base64::decode(signature)?)?)
})?;
if verified {
debug!("verified signature for {}", uri);
Ok(())
} else {
Err(anyhow!("Invalid signature on request: {}", uri))
}
}

View file

@ -1,123 +0,0 @@
#[cfg(feature = "actix")]
pub use actix_imp::receive_activity;
#[cfg(feature = "axum")]
pub use axum_imp::receive_activity;
#[cfg(feature = "actix")]
mod actix_imp {
use crate::{
core::{object_id::ObjectId, signatures::actix::verify_signature},
data::Data,
traits::{ActivityHandler, Actor, ApubObject},
utils::{verify_domains_match, verify_url_valid},
Error,
LocalInstance,
};
use actix_web::{dev::Payload, FromRequest, HttpRequest, HttpResponse};
use http_signature_normalization_actix::prelude::DigestVerified;
use serde::de::DeserializeOwned;
use tracing::debug;
/// Receive an activity and perform some basic checks, including HTTP signature verification.
pub async fn receive_activity<Activity, ActorT, Datatype>(
request: HttpRequest,
activity: Activity,
local_instance: &LocalInstance,
data: &Data<Datatype>,
) -> Result<HttpResponse, <Activity as ActivityHandler>::Error>
where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static,
ActorT: ApubObject<DataType = Datatype> + Actor + Send + 'static,
for<'de2> <ActorT as ApubObject>::ApubType: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<anyhow::Error>
+ From<Error>
+ From<<ActorT as ApubObject>::Error>
+ From<serde_json::Error>
+ From<http_signature_normalization_actix::digest::middleware::VerifyError>,
<ActorT as ApubObject>::Error: From<Error> + From<anyhow::Error>,
{
// ensure that payload hash was checked against digest header by middleware
DigestVerified::from_request(&request, &mut Payload::None).await?;
verify_domains_match(activity.id(), activity.actor())?;
verify_url_valid(activity.id(), &local_instance.settings).await?;
if local_instance.is_local_url(activity.id()) {
return Err(
Error::UrlVerificationError("Activity was sent from local instance").into(),
);
}
let request_counter = &mut 0;
let actor = ObjectId::<ActorT>::new(activity.actor().clone())
.dereference(data, local_instance, request_counter)
.await?;
verify_signature(&request, actor.public_key())?;
debug!("Verifying activity {}", activity.id().to_string());
activity.verify(data, request_counter).await?;
debug!("Receiving activity {}", activity.id().to_string());
activity.receive(data, request_counter).await?;
Ok(HttpResponse::Ok().finish())
}
}
#[cfg(feature = "axum")]
mod axum_imp {
use crate::{
core::{axum::DigestVerified, object_id::ObjectId, signatures::axum::verify_signature},
data::Data,
traits::{ActivityHandler, Actor, ApubObject},
utils::{verify_domains_match, verify_url_valid},
Error,
LocalInstance,
};
use axum::{http::Request, Json};
use hyper::Body;
use serde::de::DeserializeOwned;
use tracing::debug;
/// Receive an activity and perform some basic checks, including HTTP signature verification.
pub async fn receive_activity<Activity, ActorT, Datatype>(
_digest_verified: DigestVerified,
request: Request<Body>,
Json(activity): Json<Activity>,
local_instance: &LocalInstance,
data: &Data<Datatype>,
) -> Result<(), <Activity as ActivityHandler>::Error>
where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static,
ActorT: ApubObject<DataType = Datatype> + Actor + Send + 'static,
for<'de2> <ActorT as ApubObject>::ApubType: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<anyhow::Error>
+ From<Error>
+ From<<ActorT as ApubObject>::Error>
+ From<serde_json::Error>,
// + From<http_signature_normalization_actix::digest::middleware::VerifyError>,
<ActorT as ApubObject>::Error: From<Error> + From<anyhow::Error>,
{
verify_domains_match(activity.id(), activity.actor())?;
verify_url_valid(activity.id(), &local_instance.settings).await?;
if local_instance.is_local_url(activity.id()) {
return Err(
Error::UrlVerificationError("Activity was sent from local instance").into(),
);
}
let request_counter = &mut 0;
let actor = ObjectId::<ActorT>::new(activity.actor().clone())
.dereference(data, local_instance, request_counter)
.await?;
verify_signature(&request, actor.public_key())?;
debug!("Verifying activity {}", activity.id().to_string());
activity.verify(data, request_counter).await?;
debug!("Receiving activity {}", activity.id().to_string());
activity.receive(data, request_counter).await?;
Ok(())
}
}

View file

@ -1,7 +1,15 @@
pub mod activity_queue;
pub mod inbox;
pub mod object_id;
pub mod signatures;
#[cfg(feature = "axum")]
pub use self::axum::inbox::receive_activity;
#[cfg(feature = "axum")]
pub mod axum;
#[cfg(feature = "actix")]
pub use actix::inbox::receive_activity;
#[cfg(feature = "actix")]
pub mod actix;

View file

@ -195,7 +195,7 @@ mod tests {
#[derive(Debug)]
struct TestObject {}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ApubObject for TestObject {
type DataType = TestObject;
type ApubType = ();

View file

@ -94,86 +94,3 @@ impl PublicKey {
}
}
}
#[cfg(feature = "actix")]
pub mod actix {
use actix_web::HttpRequest;
use anyhow::anyhow;
use http_signature_normalization_actix::Config as ConfigActix;
use once_cell::sync::Lazy;
use openssl::{hash::MessageDigest, pkey::PKey, sign::Verifier};
use tracing::debug;
static CONFIG2: Lazy<ConfigActix> = Lazy::new(ConfigActix::new);
/// Verifies the HTTP signature on an incoming inbox request.
pub fn verify_signature(request: &HttpRequest, public_key: &str) -> Result<(), anyhow::Error> {
let verified = CONFIG2
.begin_verify(
request.method(),
request.uri().path_and_query(),
request.headers().clone(),
)?
.verify(|signature, signing_string| -> anyhow::Result<bool> {
debug!(
"Verifying with key {}, message {}",
&public_key, &signing_string
);
let public_key = PKey::public_key_from_pem(public_key.as_bytes())?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key)?;
verifier.update(signing_string.as_bytes())?;
Ok(verifier.verify(&base64::decode(signature)?)?)
})?;
if verified {
debug!("verified signature for {}", &request.uri());
Ok(())
} else {
Err(anyhow!("Invalid signature on request: {}", &request.uri()))
}
}
}
#[cfg(feature = "axum")]
pub mod axum {
use anyhow::anyhow;
use axum::http::Request;
use http_signature_normalization::Config;
use openssl::{hash::MessageDigest, pkey::PKey, sign::Verifier};
use std::collections::BTreeMap;
use tracing::debug;
/// Verifies the HTTP signature on an incoming inbox request.
pub fn verify_signature<B>(
request: &Request<B>,
public_key: &str,
) -> Result<(), anyhow::Error> {
let config = Config::default();
let mut header_map = BTreeMap::new();
for (name, value) in request.headers() {
if let Ok(value) = value.to_str() {
header_map.insert(name.to_string(), value.to_string());
}
}
let verified = config
.begin_verify("GET", "/foo?bar=baz", header_map)?
.verify(|signature, signing_string| -> anyhow::Result<bool> {
debug!(
"Verifying with key {}, message {}",
&public_key, &signing_string
);
let public_key = PKey::public_key_from_pem(public_key.as_bytes())?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key)?;
verifier.update(signing_string.as_bytes())?;
Ok(verifier.verify(&base64::decode(signature)?)?)
})?;
if verified {
debug!("verified signature for {}", &request.uri());
Ok(())
} else {
Err(anyhow!("Invalid signature on request: {}", &request.uri()))
}
}
}

View file

@ -28,10 +28,10 @@ impl<T> WithContext<T> {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl<T> ActivityHandler for WithContext<T>
where
T: ActivityHandler,
T: ActivityHandler + Send + Sync,
{
type DataType = <T as ActivityHandler>::DataType;
type Error = <T as ActivityHandler>::Error;

View file

@ -1,9 +1,14 @@
use crate::core::activity_queue::create_activity_queue;
use crate::{
core::activity_queue::create_activity_queue,
traits::ActivityHandler,
utils::verify_domains_match,
};
use async_trait::async_trait;
use background_jobs::Manager;
use derive_builder::Builder;
use dyn_clone::{clone_trait_object, DynClone};
use reqwest_middleware::ClientWithMiddleware;
use serde::de::DeserializeOwned;
use std::time::Duration;
use url::Url;
@ -25,6 +30,62 @@ pub struct LocalInstance {
settings: InstanceSettings,
}
impl LocalInstance {
async fn verify_url_and_domain<Activity, Datatype>(
&self,
activity: &Activity,
) -> Result<(), Error>
where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static,
{
verify_domains_match(activity.id(), activity.actor())?;
self.verify_url_valid(activity.id()).await?;
if self.is_local_url(activity.id()) {
return Err(Error::UrlVerificationError(
"Activity was sent from local instance",
));
}
Ok(())
}
/// Perform some security checks on URLs as mentioned in activitypub spec, and call user-supplied
/// [`InstanceSettings.verify_url_function`].
///
/// https://www.w3.org/TR/activitypub/#security-considerations
async fn verify_url_valid(&self, url: &Url) -> Result<(), Error> {
match url.scheme() {
"https" => {}
"http" => {
if !self.settings.debug {
return Err(Error::UrlVerificationError(
"Http urls are only allowed in debug mode",
));
}
}
_ => return Err(Error::UrlVerificationError("Invalid url scheme")),
};
if url.domain().is_none() {
return Err(Error::UrlVerificationError("Url must have a domain"));
}
if url.domain() == Some("localhost") && !self.settings.debug {
return Err(Error::UrlVerificationError(
"Localhost is only allowed in debug mode",
));
}
self.settings
.url_verifier
.verify(url)
.await
.map_err(Error::UrlVerificationError)?;
Ok(())
}
}
#[async_trait]
pub trait UrlVerifier: DynClone + Send {
async fn verify(&self, url: &Url) -> Result<(), &'static str>;

View file

@ -4,10 +4,10 @@ use std::ops::Deref;
use url::Url;
/// Trait which allows verification and reception of incoming activities.
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
#[enum_delegate::register]
pub trait ActivityHandler {
type DataType;
type DataType: Send + Sync;
type Error;
/// `id` field of the activity
@ -34,10 +34,10 @@ pub trait ActivityHandler {
}
/// Allow for boxing of enum variants
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl<T> ActivityHandler for Box<T>
where
T: ActivityHandler,
T: ActivityHandler + Send + Sync,
{
type DataType = T::DataType;
type Error = T::Error;
@ -67,9 +67,9 @@ where
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
pub trait ApubObject {
type DataType;
type DataType: Send + Sync;
type ApubType;
type DbType;
type Error;

View file

@ -1,4 +1,4 @@
use crate::{Error, InstanceSettings, LocalInstance, APUB_JSON_CONTENT_TYPE};
use crate::{Error, LocalInstance, APUB_JSON_CONTENT_TYPE};
use http::StatusCode;
use serde::de::DeserializeOwned;
use tracing::info;
@ -11,7 +11,7 @@ pub async fn fetch_object_http<Kind: DeserializeOwned>(
) -> Result<Kind, Error> {
// dont fetch local objects this way
debug_assert!(url.domain() != Some(&instance.hostname));
verify_url_valid(url, &instance.settings).await?;
instance.verify_url_valid(url).await?;
info!("Fetching remote object {}", url.to_string());
*request_counter += 1;
@ -50,39 +50,3 @@ pub fn verify_urls_match(a: &Url, b: &Url) -> Result<(), Error> {
}
Ok(())
}
/// Perform some security checks on URLs as mentioned in activitypub spec, and call user-supplied
/// [`InstanceSettings.verify_url_function`].
///
/// https://www.w3.org/TR/activitypub/#security-considerations
pub async fn verify_url_valid(url: &Url, settings: &InstanceSettings) -> Result<(), Error> {
match url.scheme() {
"https" => {}
"http" => {
if !settings.debug {
return Err(Error::UrlVerificationError(
"Http urls are only allowed in debug mode",
));
}
}
_ => return Err(Error::UrlVerificationError("Invalid url scheme")),
};
if url.domain().is_none() {
return Err(Error::UrlVerificationError("Url must have a domain"));
}
if url.domain() == Some("localhost") && !settings.debug {
return Err(Error::UrlVerificationError(
"Localhost is only allowed in debug mode",
));
}
settings
.url_verifier
.verify(url)
.await
.map_err(Error::UrlVerificationError)?;
Ok(())
}