mirror of
https://github.com/LemmyNet/activitypub-federation-rust.git
synced 2024-06-10 01:09:27 +00:00
Implement secure mode fetch as a global config parameter
This commit is contained in:
parent
f068545df8
commit
216458eb82
|
@ -18,7 +18,7 @@ use crate::{
|
|||
activity_queue::create_activity_queue,
|
||||
error::Error,
|
||||
protocol::verification::verify_domains_match,
|
||||
traits::ActivityHandler,
|
||||
traits::{ActivityHandler, Actor},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use background_jobs::Manager;
|
||||
|
@ -75,6 +75,11 @@ pub struct FederationConfig<T: Clone> {
|
|||
/// <https://git.pleroma.social/pleroma/pleroma/-/issues/2939>
|
||||
#[builder(default = "false")]
|
||||
pub(crate) http_signature_compat: bool,
|
||||
/// Actor Id and private key to use to sign all federated fetch requests.
|
||||
/// This can be used to implement secure mode federation.
|
||||
/// <https://docs.joinmastodon.org/spec/activitypub/#secure-mode>
|
||||
#[builder(default = "None", setter(custom))]
|
||||
pub(crate) signed_fetch_actor: Option<Arc<(Url, String)>>,
|
||||
/// Queue for sending outgoing activities. Only optional to make builder work, its always
|
||||
/// present once constructed.
|
||||
#[builder(setter(skip))]
|
||||
|
@ -170,6 +175,13 @@ impl<T: Clone> FederationConfig<T> {
|
|||
}
|
||||
|
||||
impl<T: Clone> FederationConfigBuilder<T> {
|
||||
/// Sets an actor to use to sign all federated fetch requests
|
||||
pub fn signed_fetch_actor<A: Actor>(&mut self, actor: &A) -> &mut Self {
|
||||
let private_key_pem = actor.private_key_pem().unwrap();
|
||||
self.signed_fetch_actor = Some(Some(Arc::new((actor.id(), private_key_pem))));
|
||||
self
|
||||
}
|
||||
|
||||
/// Constructs a new config instance with the values supplied to builder.
|
||||
///
|
||||
/// Values which are not explicitly specified use the defaults. Also initializes the
|
||||
|
|
|
@ -7,10 +7,8 @@ use crate::{
|
|||
error::Error,
|
||||
http_signatures::sign_request,
|
||||
reqwest_shim::ResponseExt,
|
||||
traits::Actor,
|
||||
FEDERATION_CONTENT_TYPE,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use http::StatusCode;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
@ -49,60 +47,25 @@ pub async fn fetch_object_http<T: Clone, Kind: DeserializeOwned>(
|
|||
return Err(Error::RequestLimit);
|
||||
}
|
||||
|
||||
let res = config
|
||||
.client
|
||||
.get(url.as_str())
|
||||
.header("Accept", FEDERATION_CONTENT_TYPE)
|
||||
.timeout(config.request_timeout)
|
||||
.send()
|
||||
.await
|
||||
.map_err(Error::other)?;
|
||||
|
||||
if res.status() == StatusCode::GONE {
|
||||
return Err(Error::ObjectDeleted);
|
||||
}
|
||||
|
||||
res.json_limited().await
|
||||
}
|
||||
|
||||
/// Signed version of `fetch_object_http_signed`. This will sign the GET request
|
||||
/// using the private key of the given actor, which allows to implement secure
|
||||
/// federation mode.
|
||||
pub async fn fetch_object_http_signed<T: Clone, Kind: DeserializeOwned, A: Actor>(
|
||||
url: &Url,
|
||||
data: &Data<T>,
|
||||
actor: &A,
|
||||
) -> Result<Kind, Error> {
|
||||
let config = &data.config;
|
||||
// dont fetch local objects this way
|
||||
debug_assert!(url.domain() != Some(&config.domain));
|
||||
config.verify_url_valid(url).await?;
|
||||
info!("Fetching remote object {}", url.to_string());
|
||||
|
||||
let counter = data.request_counter.fetch_add(1, Ordering::SeqCst);
|
||||
if counter > config.http_fetch_limit {
|
||||
return Err(Error::RequestLimit);
|
||||
}
|
||||
|
||||
let req = config
|
||||
.client
|
||||
.get(url.as_str())
|
||||
.header("Accept", FEDERATION_CONTENT_TYPE)
|
||||
.timeout(config.request_timeout);
|
||||
|
||||
let private_key_pem = actor
|
||||
.private_key_pem()
|
||||
.ok_or(anyhow!("Actor does not have a private key to sign with"))?;
|
||||
let req = sign_request(
|
||||
req,
|
||||
actor.id(),
|
||||
String::new(),
|
||||
private_key_pem,
|
||||
data.config.http_signature_compat,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let res = config.client.execute(req).await.map_err(Error::other)?;
|
||||
let res = if let Some((actor_id, private_key_pem)) = config.signed_fetch_actor.as_deref() {
|
||||
let req = sign_request(
|
||||
req,
|
||||
actor_id.clone(),
|
||||
String::new(),
|
||||
private_key_pem.clone(),
|
||||
data.config.http_signature_compat,
|
||||
)
|
||||
.await?;
|
||||
config.client.execute(req).await.map_err(Error::other)?
|
||||
} else {
|
||||
req.send().await.map_err(Error::other)?
|
||||
};
|
||||
|
||||
if res.status() == StatusCode::GONE {
|
||||
return Err(Error::ObjectDeleted);
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
use crate::{
|
||||
config::Data,
|
||||
error::Error,
|
||||
fetch::{fetch_object_http, fetch_object_http_signed},
|
||||
traits::{Actor, Object},
|
||||
};
|
||||
use crate::{config::Data, error::Error, fetch::fetch_object_http, traits::Object};
|
||||
use anyhow::anyhow;
|
||||
use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -124,44 +119,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Fetches an activitypub object, either from local database (if possible), or over http.
|
||||
pub async fn dereference_signed<A>(
|
||||
&self,
|
||||
data: &Data<<Kind as Object>::DataType>,
|
||||
actor: &A,
|
||||
) -> Result<Kind, <Kind as Object>::Error>
|
||||
where
|
||||
<Kind as Object>::Error: From<Error> + From<anyhow::Error>,
|
||||
A: Actor,
|
||||
{
|
||||
let db_object = self.dereference_from_db(data).await?;
|
||||
|
||||
// if its a local object, only fetch it from the database and not over http
|
||||
if data.config.is_local_url(&self.0) {
|
||||
return match db_object {
|
||||
None => Err(Error::NotFound.into()),
|
||||
Some(o) => Ok(o),
|
||||
};
|
||||
}
|
||||
|
||||
// object found in database
|
||||
if let Some(object) = db_object {
|
||||
// object is old and should be refetched
|
||||
if let Some(last_refreshed_at) = object.last_refreshed_at() {
|
||||
if should_refetch_object(last_refreshed_at) {
|
||||
return self
|
||||
.dereference_from_http_signed(data, Some(object), actor)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Ok(object)
|
||||
}
|
||||
// object not found, need to fetch over http
|
||||
else {
|
||||
self.dereference_from_http_signed(data, None, actor).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch an object from the local db. Instead of falling back to http, this throws an error if
|
||||
/// the object is not found in the database.
|
||||
pub async fn dereference_local(
|
||||
|
@ -206,31 +163,6 @@ where
|
|||
Kind::verify(&res2, self.inner(), data).await?;
|
||||
Kind::from_json(res2, data).await
|
||||
}
|
||||
|
||||
async fn dereference_from_http_signed<A>(
|
||||
&self,
|
||||
data: &Data<<Kind as Object>::DataType>,
|
||||
db_object: Option<Kind>,
|
||||
actor: &A,
|
||||
) -> Result<Kind, <Kind as Object>::Error>
|
||||
where
|
||||
<Kind as Object>::Error: From<Error> + From<anyhow::Error>,
|
||||
A: Actor,
|
||||
{
|
||||
let res = fetch_object_http_signed(&self.0, data, actor).await;
|
||||
|
||||
if let Err(Error::ObjectDeleted) = &res {
|
||||
if let Some(db_object) = db_object {
|
||||
db_object.delete(data).await?;
|
||||
}
|
||||
return Err(anyhow!("Fetched remote object {} which was deleted", self).into());
|
||||
}
|
||||
|
||||
let res2 = res?;
|
||||
|
||||
Kind::verify(&res2, self.inner(), data).await?;
|
||||
Kind::from_json(res2, data).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Need to implement clone manually, to avoid requiring Kind to be Clone
|
||||
|
|
|
@ -95,7 +95,11 @@ pub(crate) async fn sign_request(
|
|||
static CONFIG2: Lazy<http_signature_normalization::Config> =
|
||||
Lazy::new(http_signature_normalization::Config::new);
|
||||
|
||||
/// Verifies the HTTP signature on an incoming inbox request.
|
||||
/// Verifies the HTTP signature on an incoming federation request
|
||||
/// for a given actor's public key.
|
||||
///
|
||||
/// Internally, this just converts the headers to a BTreeMap and passes to
|
||||
/// `verify_signature_inner` for actual signature verification.
|
||||
pub(crate) fn verify_signature<'a, H>(
|
||||
headers: H,
|
||||
method: &Method,
|
||||
|
@ -115,6 +119,10 @@ where
|
|||
verify_signature_inner(header_map, method, uri, public_key)
|
||||
}
|
||||
|
||||
/// Checks whether the given federation request has a valid signature,
|
||||
/// from any actor of type A, and returns that actor if a valid signature is found.
|
||||
/// This function will return an `Err` variant when no signature is found
|
||||
/// or if the signature could not be verified.
|
||||
pub(crate) async fn signing_actor<'a, A, H>(
|
||||
headers: H,
|
||||
method: &Method,
|
||||
|
@ -153,6 +161,8 @@ where
|
|||
Ok(actor)
|
||||
}
|
||||
|
||||
/// Verifies that the signature present in the request is valid for
|
||||
/// the specified actor's public key.
|
||||
fn verify_signature_inner(
|
||||
header_map: BTreeMap<String, String>,
|
||||
method: &Method,
|
||||
|
|
Loading…
Reference in a new issue