ap delivery worker

This commit is contained in:
2026-05-09 22:59:51 +03:00
parent 917f2936ea
commit f2eb74d34f
+125
View File
@@ -0,0 +1,125 @@
use std::sync::Arc;
use std::time::Duration;
use crate::state::AppState;
use super::signatures;
pub fn start_delivery_worker(state: Arc<AppState>) {
tokio::spawn(async move {
loop {
if state.ap.is_some() {
// #16: skip processing when federation is frozen
if !super::is_frozen() {
process_queue(&state).await;
}
}
tokio::time::sleep(Duration::from_secs(30)).await;
}
});
}
async fn process_queue(state: &Arc<AppState>) {
let ap = match state.ap.as_ref() {
Some(ap) => ap,
None => return,
};
let now = chrono::Utc::now().to_rfc3339();
let deliveries: Vec<(i64, String, String, i64)> = {
let dbi = state.db.lock().await;
let mut stmt = dbi.db.prepare(
"SELECT id, inbox_uri, payload, attempts FROM ap_delivery_queue
WHERE next_retry <= ?1 ORDER BY next_retry ASC LIMIT 20"
).unwrap();
stmt.query_map([&now], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
}).unwrap().filter_map(|r| r.ok()).collect()
};
if deliveries.is_empty() { return; }
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.build()
.unwrap();
for (id, inbox_uri, payload, attempts) in deliveries {
// #8: DNS rebinding protection on delivery targets
if crate::security::is_internal_url(&inbox_uri) {
tracing::warn!("refusing delivery to internal URL: {}", inbox_uri);
let dbi = state.db.lock().await;
dbi.db.execute("DELETE FROM ap_delivery_queue WHERE id = ?1", [id]).ok();
continue;
}
let host = inbox_uri.split("//").nth(1)
.and_then(|s| s.split('/').next())
.unwrap_or("");
let path = inbox_uri.split("//").nth(1)
.and_then(|s| s.find('/').map(|i| &s[i..]))
.unwrap_or("/inbox");
let key_id = format!("{}#main-key", ap.actor_uri);
let sig_headers = signatures::sign_request(
&ap.keys.private_key,
&key_id,
"POST",
path,
host,
Some(payload.as_bytes()),
);
let mut req = client.post(&inbox_uri)
.header("Content-Type", "application/activity+json");
for (name, value) in &sig_headers {
req = req.header(name.as_str(), value.as_str());
}
match req.body(payload.clone()).send().await {
Ok(resp) if resp.status().is_success() || resp.status().as_u16() == 202 => {
let dbi = state.db.lock().await;
dbi.db.execute("DELETE FROM ap_delivery_queue WHERE id = ?1", [id]).ok();
crate::db::track_federation_activity(&dbi.db, host, "outbound");
tracing::info!("delivered to {}", inbox_uri);
}
Ok(resp) => {
tracing::warn!("delivery to {} failed: {}", inbox_uri, resp.status());
handle_failure(state, id, attempts).await;
}
Err(e) => {
tracing::warn!("delivery to {} error: {}", inbox_uri, e);
handle_failure(state, id, attempts).await;
}
}
}
}
async fn handle_failure(state: &Arc<AppState>, id: i64, attempts: i64) {
let dbi = state.db.lock().await;
let new_attempts = attempts + 1;
if new_attempts >= 10 {
// give up after 10 attempts
dbi.db.execute("DELETE FROM ap_delivery_queue WHERE id = ?1", [id]).ok();
tracing::info!("dropping delivery {} after {} attempts", id, new_attempts);
return;
}
// exponential backoff: 1m, 5m, 30m, 2h, 12h, 24h cap
let delay_secs = match new_attempts {
1 => 60,
2 => 300,
3 => 1800,
4 => 7200,
5 => 43200,
_ => 86400,
};
let next_retry = (chrono::Utc::now() + chrono::Duration::seconds(delay_secs)).to_rfc3339();
dbi.db.execute(
"UPDATE ap_delivery_queue SET attempts = ?1, next_retry = ?2 WHERE id = ?3",
rusqlite::params![new_attempts, next_retry, id],
).ok();
}