feat: production hardening — security, scale, protocol, DX
Breaking changes to FederationRepository, ApObjectHandler, ApUser:
FederationRepository:
- add is_activity_processed / mark_activity_processed (inbox idempotency)
- add get_accepted_follower_inboxes (DB-side dedup/filtering, replaces in-memory load-all)
ApObjectHandler:
- add on_announce_of_remote (cross-server boosts, previously silently dropped)
ApUser:
- add manually_approves_followers: bool
- add actor_type: ApActorType (was hardcoded Person)
Security:
- block check before actor HTTP fetch in Follow (prevents SSRF on blocked actors)
- 4xx responses use generic "not found"/"bad request" (no internal leak)
- 1 MB DefaultBodyLimit on inbox routes
- zeroize private key after generation
Delivery:
- all broadcasts are now non-blocking (tokio::spawn fallback, or EventPublisher queue)
- EventPublisher redesigned with typed FederationEvent enum (DeliveryRequested/DeliveryFailed)
- new deliver_to_inbox() public method for queue consumers
- configurable delivery_max_attempts and delivery_initial_delay_secs via builder
- Follow saved as Pending BEFORE delivery (race condition fix)
Router:
- GET /users/{id} (actor), GET /users/{id}/followers, GET /users/{id}/following now mounted
Protocol:
- mention extraction from Create/Update tag arrays → on_mention() dispatched
- WebFinger: add aliases field (acct: URI + AP actor URL)
- outbox: add last link, use count_local_posts for totalItems
- idempotency guard added to every inbound activity receive()
- actor serializes display_name and configurable actor_type/manually_approves_followers
Bump: 0.1.10 → 0.2.0
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
use activitypub_federation::{
|
||||
config::Data,
|
||||
fetch::webfinger::{Webfinger, build_webfinger_response, extract_webfinger_name},
|
||||
fetch::webfinger::{extract_webfinger_name},
|
||||
};
|
||||
use axum::{
|
||||
extract::Query,
|
||||
http::header,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::data::FederationData;
|
||||
use crate::error::Error;
|
||||
@@ -17,6 +17,23 @@ pub struct WebfingerQuery {
|
||||
resource: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct WebfingerLink {
|
||||
rel: String,
|
||||
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
||||
kind: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
href: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct WebfingerResponse {
|
||||
subject: String,
|
||||
/// Canonical URIs for the same account (acct: URI + AP actor URL).
|
||||
aliases: Vec<String>,
|
||||
links: Vec<WebfingerLink>,
|
||||
}
|
||||
|
||||
pub async fn webfinger_handler(
|
||||
Query(query): Query<WebfingerQuery>,
|
||||
data: Data<FederationData>,
|
||||
@@ -31,8 +48,25 @@ pub async fn webfinger_handler(
|
||||
.ok_or_else(|| Error::not_found(anyhow::anyhow!("user not found")))?;
|
||||
|
||||
let ap_id = crate::urls::actor_url(&data.base_url, user.id);
|
||||
let acct_uri = format!("acct:{}@{}", user.username, data.domain);
|
||||
|
||||
let wf = WebfingerResponse {
|
||||
subject: query.resource.clone(),
|
||||
aliases: vec![acct_uri, ap_id.to_string()],
|
||||
links: vec![
|
||||
WebfingerLink {
|
||||
rel: "http://webfinger.net/rel/profile-page".to_string(),
|
||||
kind: Some("text/html".to_string()),
|
||||
href: Some(ap_id.to_string()),
|
||||
},
|
||||
WebfingerLink {
|
||||
rel: "self".to_string(),
|
||||
kind: Some("application/activity+json".to_string()),
|
||||
href: Some(ap_id.to_string()),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let wf: Webfinger = build_webfinger_response(query.resource, ap_id);
|
||||
let body = serde_json::to_string(&wf).map_err(|e| Error::from(anyhow::anyhow!(e)))?;
|
||||
Ok(([(header::CONTENT_TYPE, "application/jrd+json")], body).into_response())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user