mirror of
https://github.com/LemmyNet/activitypub-federation-rust.git
synced 2024-06-02 13:29:36 +00:00
WIP
This commit is contained in:
parent
52971b64b3
commit
fdd16b99e0
|
@ -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 }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
50
src/core/actix/inbox.rs
Normal 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
2
src/core/actix/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod inbox;
|
||||
mod signature;
|
35
src/core/actix/signature.rs
Normal file
35
src/core/actix/signature.rs
Normal 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
50
src/core/axum/inbox.rs
Normal 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(())
|
||||
}
|
|
@ -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
|
||||
|
|
41
src/core/axum/signature.rs
Normal file
41
src/core/axum/signature.rs
Normal 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))
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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 = ();
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
63
src/lib.rs
63
src/lib.rs
|
@ -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>;
|
||||
|
|
|
@ -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;
|
||||
|
|
40
src/utils.rs
40
src/utils.rs
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue