ap delivery worker
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user