feat: expose signed_fetch for authorized-fetch / Secure Mode
Builder: .signed_fetch_actor_id(uuid) sets instance-level signing actor. Service: .signed_fetch(&url) performs a signed GET returning raw JSON. Closes #2
This commit is contained in:
@@ -133,21 +133,36 @@ pub async fn get_local_actor(
|
|||||||
user_id: uuid::Uuid,
|
user_id: uuid::Uuid,
|
||||||
data: &Data<FederationData>,
|
data: &Data<FederationData>,
|
||||||
) -> Result<DbActor, Error> {
|
) -> Result<DbActor, Error> {
|
||||||
let user = data
|
build_local_actor(
|
||||||
.user_repo
|
user_id,
|
||||||
.find_by_id(user_id)
|
&data.base_url,
|
||||||
.await
|
data.user_repo.as_ref(),
|
||||||
.map_err(Error::from)?
|
data.actor_repo.as_ref(),
|
||||||
.ok_or_else(|| Error::not_found(anyhow::anyhow!("user not found: {}", user_id)))?;
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::not_found(anyhow::anyhow!("{e}")))
|
||||||
|
}
|
||||||
|
|
||||||
let (public_key, private_key) = match data.actor_repo.get_local_actor_keypair(user_id).await? {
|
/// Build a local actor's `DbActor` from repository data. Generates a keypair
|
||||||
|
/// if one doesn't exist yet. Usable outside of a `FederationData` context
|
||||||
|
/// (e.g. during service construction).
|
||||||
|
pub async fn build_local_actor(
|
||||||
|
user_id: uuid::Uuid,
|
||||||
|
base_url: &str,
|
||||||
|
user_repo: &dyn crate::user::ApUserRepository,
|
||||||
|
actor_repo: &dyn crate::repository::ActorRepository,
|
||||||
|
) -> anyhow::Result<DbActor> {
|
||||||
|
let user = user_repo
|
||||||
|
.find_by_id(user_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("user not found: {}", user_id))?;
|
||||||
|
|
||||||
|
let (public_key, private_key) = match actor_repo.get_local_actor_keypair(user_id).await? {
|
||||||
Some(kp) => kp,
|
Some(kp) => kp,
|
||||||
None => {
|
None => {
|
||||||
let kp = generate_actor_keypair()?;
|
let kp = generate_actor_keypair()?;
|
||||||
// Zeroize the private key after storing it so the plaintext doesn't
|
|
||||||
// linger in memory beyond this scope.
|
|
||||||
let private_zeroized = Zeroizing::new(kp.private_key.clone());
|
let private_zeroized = Zeroizing::new(kp.private_key.clone());
|
||||||
data.actor_repo
|
actor_repo
|
||||||
.save_local_actor_keypair(
|
.save_local_actor_keypair(
|
||||||
user_id,
|
user_id,
|
||||||
kp.public_key.clone(),
|
kp.public_key.clone(),
|
||||||
@@ -166,7 +181,7 @@ pub async fn get_local_actor(
|
|||||||
outbox_url,
|
outbox_url,
|
||||||
followers_url,
|
followers_url,
|
||||||
following_url,
|
following_url,
|
||||||
} = ActorUrls::build(&data.base_url, user_id);
|
} = ActorUrls::build(base_url, user_id);
|
||||||
|
|
||||||
Ok(DbActor {
|
Ok(DbActor {
|
||||||
user_id,
|
user_id,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use activitypub_federation::config::{Data, FederationConfig, FederationMiddlewar
|
|||||||
use activitypub_federation::error::Error as FedError;
|
use activitypub_federation::error::Error as FedError;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::actors::DbActor;
|
||||||
use crate::data::FederationData;
|
use crate::data::FederationData;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -27,7 +28,15 @@ impl ApFederationConfig {
|
|||||||
/// and accepts any URL. **Never use in production.**
|
/// and accepts any URL. **Never use in production.**
|
||||||
///
|
///
|
||||||
/// Outbound signing always uses Mastodon compat mode regardless of this flag.
|
/// Outbound signing always uses Mastodon compat mode regardless of this flag.
|
||||||
pub async fn new(data: FederationData, debug: bool) -> anyhow::Result<Self> {
|
///
|
||||||
|
/// When `signing_actor` is provided, all outgoing fetch requests (GETs) are
|
||||||
|
/// signed with that actor's keypair — required for instances with
|
||||||
|
/// authorized-fetch / Secure Mode enabled.
|
||||||
|
pub async fn new(
|
||||||
|
data: FederationData,
|
||||||
|
debug: bool,
|
||||||
|
signing_actor: Option<&DbActor>,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
let config = if debug {
|
let config = if debug {
|
||||||
FederationConfig::builder()
|
FederationConfig::builder()
|
||||||
.domain(&data.domain)
|
.domain(&data.domain)
|
||||||
@@ -38,12 +47,12 @@ impl ApFederationConfig {
|
|||||||
.build()
|
.build()
|
||||||
.await?
|
.await?
|
||||||
} else {
|
} else {
|
||||||
FederationConfig::builder()
|
let mut builder = FederationConfig::builder();
|
||||||
.domain(&data.domain)
|
builder.domain(&data.domain).app_data(data).debug(false);
|
||||||
.app_data(data)
|
if let Some(actor) = signing_actor {
|
||||||
.debug(false)
|
builder.signed_fetch_actor(actor);
|
||||||
.build()
|
}
|
||||||
.await?
|
builder.build().await?
|
||||||
};
|
};
|
||||||
Ok(Self(config))
|
Ok(Self(config))
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/service/fetch.rs
Normal file
20
src/service/fetch.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use super::ActivityPubService;
|
||||||
|
|
||||||
|
impl ActivityPubService {
|
||||||
|
/// Fetch a remote ActivityPub resource with HTTP Signatures.
|
||||||
|
///
|
||||||
|
/// Requires `signed_fetch_actor_id` to have been set on the builder.
|
||||||
|
/// Returns the raw JSON value of the remote resource.
|
||||||
|
pub async fn signed_fetch(&self, url: &Url) -> anyhow::Result<serde_json::Value> {
|
||||||
|
let data = self.federation_config.to_request_data();
|
||||||
|
let res = activitypub_federation::fetch::fetch_object_http::<
|
||||||
|
crate::data::FederationData,
|
||||||
|
serde_json::Value,
|
||||||
|
>(url, &data)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||||
|
Ok(res.object)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ use crate::{
|
|||||||
mod backfill;
|
mod backfill;
|
||||||
pub(crate) mod broadcast;
|
pub(crate) mod broadcast;
|
||||||
pub(super) mod delivery;
|
pub(super) mod delivery;
|
||||||
|
mod fetch;
|
||||||
mod follow;
|
mod follow;
|
||||||
|
|
||||||
/// Default max delivery retries per inbox (used as the builder default).
|
/// Default max delivery retries per inbox (used as the builder default).
|
||||||
@@ -57,6 +58,7 @@ pub struct ActivityPubServiceBuilder {
|
|||||||
event_publisher: Option<Arc<dyn crate::data::EventPublisher>>,
|
event_publisher: Option<Arc<dyn crate::data::EventPublisher>>,
|
||||||
delivery_max_attempts: u32,
|
delivery_max_attempts: u32,
|
||||||
delivery_initial_delay_secs: u64,
|
delivery_initial_delay_secs: u64,
|
||||||
|
signed_fetch_actor_id: Option<uuid::Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActivityPubServiceBuilder {
|
impl ActivityPubServiceBuilder {
|
||||||
@@ -113,6 +115,14 @@ impl ActivityPubServiceBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set a local actor whose keypair signs all outgoing fetch requests
|
||||||
|
/// (HTTP Signature on GETs). Required for federating with instances
|
||||||
|
/// that enforce authorized-fetch / Secure Mode.
|
||||||
|
pub fn signed_fetch_actor_id(mut self, v: uuid::Uuid) -> Self {
|
||||||
|
self.signed_fetch_actor_id = Some(v);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn build(self) -> anyhow::Result<ActivityPubService> {
|
pub async fn build(self) -> anyhow::Result<ActivityPubService> {
|
||||||
let activity_repo = self
|
let activity_repo = self
|
||||||
.activity_repo
|
.activity_repo
|
||||||
@@ -138,9 +148,9 @@ impl ActivityPubServiceBuilder {
|
|||||||
let data = FederationData::new(
|
let data = FederationData::new(
|
||||||
activity_repo,
|
activity_repo,
|
||||||
follow_repo,
|
follow_repo,
|
||||||
actor_repo,
|
actor_repo.clone(),
|
||||||
blocklist_repo,
|
blocklist_repo,
|
||||||
user_repo,
|
user_repo.clone(),
|
||||||
content_reader,
|
content_reader,
|
||||||
object_handler,
|
object_handler,
|
||||||
self.base_url.clone(),
|
self.base_url.clone(),
|
||||||
@@ -148,7 +158,20 @@ impl ActivityPubServiceBuilder {
|
|||||||
self.software_name,
|
self.software_name,
|
||||||
self.event_publisher,
|
self.event_publisher,
|
||||||
);
|
);
|
||||||
let federation_config = ApFederationConfig::new(data, self.debug).await?;
|
let signing_actor = if let Some(uid) = self.signed_fetch_actor_id {
|
||||||
|
let actor = crate::actors::build_local_actor(
|
||||||
|
uid,
|
||||||
|
&self.base_url,
|
||||||
|
user_repo.as_ref(),
|
||||||
|
actor_repo.as_ref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Some(actor)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let federation_config =
|
||||||
|
ApFederationConfig::new(data, self.debug, signing_actor.as_ref()).await?;
|
||||||
Ok(ActivityPubService {
|
Ok(ActivityPubService {
|
||||||
federation_config,
|
federation_config,
|
||||||
base_url: self.base_url,
|
base_url: self.base_url,
|
||||||
@@ -175,6 +198,7 @@ impl ActivityPubService {
|
|||||||
event_publisher: None,
|
event_publisher: None,
|
||||||
delivery_max_attempts: DELIVERY_MAX_ATTEMPTS,
|
delivery_max_attempts: DELIVERY_MAX_ATTEMPTS,
|
||||||
delivery_initial_delay_secs: DELIVERY_INITIAL_DELAY_SECS,
|
delivery_initial_delay_secs: DELIVERY_INITIAL_DELAY_SECS,
|
||||||
|
signed_fetch_actor_id: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user