Skip to main content

mail4agent_server/http/
fed_net.rs

1//! Outgoing federation: signed requests, outbox delivery, PDU verification,
2//! remote join, and the remote halves of key query/claim and to-device.
3
4use std::collections::BTreeMap;
5use std::sync::Arc;
6
7use serde_json::{json, Value};
8
9use super::{with_conn_pub, Homeserver};
10use crate::error::MatrixError;
11use crate::fed_rooms as fr;
12use crate::federation::{self as fed, enc};
13
14fn internal<E>(_: E) -> MatrixError {
15    MatrixError::internal()
16}
17
18fn now_rfc3339() -> String {
19    chrono::Utc::now().to_rfc3339()
20}
21
22/// Send one signed request to `dest` (path without the `/_matrix` prefix, query included).
23pub(crate) async fn fed_request(state: &Arc<Homeserver>, dest: &str, method: &str, path: &str, body: Option<Value>) -> Result<(u16, Value), MatrixError> {
24    let transport = state.fed_transport.get().cloned().ok_or_else(|| MatrixError::unknown("federation transport is not configured"))?;
25    let local = crate::store::matrix_server_name();
26    let uri = format!("/_matrix{path}");
27    let (key_id, key) = with_conn_pub(state, |c| fed::active_signing_key(c, fed::now_ms()).map_err(internal)).await?;
28    let auth = fed::build_x_matrix_header(local, dest, &key_id, &key, method, &uri, body.as_ref());
29    transport.request(dest, method, &uri, &auth, body.as_ref()).await.map_err(|e| MatrixError::unknown(format!("federation to {dest}: {e}")))
30}
31
32/// Verify a PDU's signature with its signer's keys. `Ok(false)`: signature
33/// good, content hash wrong (keep only the redacted form).
34pub(crate) async fn verify_pdu(state: &Arc<Homeserver>, pdu: &Value) -> Result<bool, MatrixError> {
35    let (signer, key_id) = fr::pdu_signer(pdu).ok_or_else(|| MatrixError::bad_json("pdu is not signed by its sender's server"))?;
36    if crate::store::is_local_server_name(&signer) {
37        let id = pdu.get("event_id").and_then(Value::as_str).unwrap_or_default().to_string();
38        let known = with_conn_pub(state, move |c| Ok(crate::store::get_event(c, &id)?.is_some())).await?;
39        return if known { Ok(true) } else { Err(MatrixError::forbidden("pdu claims to be ours but is unknown")) };
40    }
41    let pk = super::federation::remote_key(state, &signer, &key_id).await?;
42    fr::verify_pdu_with_key(pdu, &signer, &key_id, &pk).map_err(|_| MatrixError::forbidden("bad pdu signature"))
43}
44
45fn backoff_ms(attempts: i64) -> i64 {
46    (1000i64 << attempts.clamp(0, 10)).min(600_000)
47}
48
49struct Row {
50    id: i64,
51    kind: String,
52    room_id: String,
53    event_id: String,
54    payload: Value,
55    attempts: i64,
56}
57
58/// Export new local events and deliver everything due. Returns the number of delivered items.
59pub async fn drain_outbox(state: &Arc<Homeserver>) -> usize {
60    if state.federation_enabled.get().is_none() || state.fed_transport.get().is_none() {
61        return 0;
62    }
63    let local = crate::store::matrix_server_name().to_string();
64    let now = fed::now_ms();
65    let l2 = local.clone();
66    let rows = with_conn_pub(state, move |c| {
67        if let Err(e) = fr::export_local_events(c, &l2, now) {
68            tracing::warn!("federation export: {e}");
69        }
70        let mut stmt = c
71            .prepare("SELECT id, destination, kind, room_id, event_id, payload, attempts FROM fed_outbox WHERE next_try_ms <= ?1 ORDER BY id LIMIT 300")
72            .map_err(internal)?;
73        let it = stmt
74            .query_map([now], |r| {
75                Ok((
76                    r.get::<_, String>(1)?,
77                    Row { id: r.get(0)?, kind: r.get(2)?, room_id: r.get(3)?, event_id: r.get(4)?, payload: serde_json::from_str(&r.get::<_, String>(5)?).unwrap_or(Value::Null), attempts: r.get(6)? },
78                ))
79            })
80            .map_err(internal)?;
81        it.collect::<Result<Vec<_>, _>>().map_err(internal)
82    })
83    .await
84    .unwrap_or_default();
85    let mut by_dest: BTreeMap<String, Vec<Row>> = BTreeMap::new();
86    for (d, r) in rows {
87        by_dest.entry(d).or_default().push(r);
88    }
89    let mut delivered = 0;
90    for (dest, items) in by_dest {
91        let mut batch: Vec<Row> = Vec::new();
92        let mut failed_from: Option<usize> = None;
93        let mut idx = 0;
94        let total = items.len();
95        let mut items = items.into_iter().peekable();
96        while let Some(item) = items.next() {
97            let is_invite = item.kind == "invite";
98            if is_invite {
99                // flush the batch first to keep order
100                if !batch.is_empty() {
101                    let b = std::mem::take(&mut batch);
102                    match send_batch(state, &local, &dest, &b).await {
103                        Ok(n) => delivered += n,
104                        Err(_) => {
105                            mark_failed(state, &b).await;
106                            mark_failed(state, std::slice::from_ref(&item)).await;
107                            failed_from = Some(idx);
108                            break;
109                        }
110                    }
111                }
112                let body = json!({ "room_version": "11", "event": item.payload["event"], "room_info": item.payload["room_info"], "invite_room_state": item.payload["state"] });
113                let path = format!("/federation/v2/invite/{}/{}", enc(&item.room_id), enc(&item.event_id));
114                match fed_request(state, &dest, "PUT", &path, Some(body)).await {
115                    Ok((200, _)) | Ok((403, _)) => {
116                        delete_rows(state, &[item.id]).await;
117                        delivered += 1;
118                    }
119                    _ => {
120                        mark_failed(state, std::slice::from_ref(&item)).await;
121                        failed_from = Some(idx);
122                        break;
123                    }
124                }
125            } else {
126                batch.push(item);
127                if batch.len() >= 20 || items.peek().is_none_or(|n| n.kind == "invite") {
128                    let b = std::mem::take(&mut batch);
129                    match send_batch(state, &local, &dest, &b).await {
130                        Ok(n) => delivered += n,
131                        Err(_) => {
132                            mark_failed(state, &b).await;
133                            failed_from = Some(idx);
134                            break;
135                        }
136                    }
137                }
138            }
139            idx += 1;
140        }
141        let _ = (failed_from, total);
142    }
143    delivered
144}
145
146async fn delete_rows(state: &Arc<Homeserver>, ids: &[i64]) {
147    let ids = ids.to_vec();
148    let _ = with_conn_pub(state, move |c| {
149        for id in ids {
150            c.execute("DELETE FROM fed_outbox WHERE id = ?1", [id]).map_err(internal)?;
151        }
152        Ok(())
153    })
154    .await;
155}
156
157async fn mark_failed(state: &Arc<Homeserver>, rows: &[Row]) {
158    let now = fed::now_ms();
159    let data: Vec<(i64, i64)> = rows.iter().map(|r| (r.id, r.attempts)).collect();
160    let _ = with_conn_pub(state, move |c| {
161        for (id, attempts) in data {
162            c.execute("UPDATE fed_outbox SET attempts = attempts + 1, next_try_ms = ?2 WHERE id = ?1", rusqlite::params![id, now + backoff_ms(attempts)]).map_err(internal)?;
163        }
164        Ok(())
165    })
166    .await;
167}
168
169async fn send_batch(state: &Arc<Homeserver>, local: &str, dest: &str, rows: &[Row]) -> Result<usize, MatrixError> {
170    let (mut pdus, mut edus) = (Vec::new(), Vec::new());
171    for r in rows {
172        if r.kind == "edu" {
173            edus.push(r.payload.clone());
174        } else {
175            pdus.push(r.payload.clone());
176        }
177    }
178    let txn = format!("m4a{}", rows[0].id);
179    let body = json!({ "origin": local, "origin_server_ts": fed::now_ms(), "pdus": pdus, "edus": edus });
180    let (status, _) = fed_request(state, dest, "PUT", &format!("/federation/v1/send/{}", enc(&txn)), Some(body)).await?;
181    if status != 200 {
182        return Err(MatrixError::unknown(format!("destination answered {status}")));
183    }
184    delete_rows(state, &rows.iter().map(|r| r.id).collect::<Vec<_>>()).await;
185    Ok(rows.len())
186}
187
188/// Background worker: drains whenever poked or every few seconds.
189pub fn spawn_outbox_worker(state: Arc<Homeserver>) {
190    tokio::spawn(async move {
191        loop {
192            drain_outbox(&state).await;
193            let _ = tokio::time::timeout(std::time::Duration::from_secs(3), state.fed_notify.notified()).await;
194        }
195    });
196}
197
198// -------------------------------------------------------------- remote join
199
200/// Whether `room_id` belongs to another server's namespace.
201pub(crate) fn room_domain_is_remote(room_id: &str) -> bool {
202    room_id.split_once(':').is_some_and(|(_, d)| !crate::store::is_local_server_name(d))
203}
204
205fn map_status(status: u16, what: &str) -> MatrixError {
206    match status {
207        403 => MatrixError::forbidden(format!("{what}: refused by the remote server")),
208        404 => MatrixError::not_found(format!("{what}: not found on the remote server")),
209        _ => MatrixError::unknown(format!("{what}: remote server answered {status}")),
210    }
211}
212
213/// Join a room that lives on another server (make_join, send_join, ingest).
214pub(crate) async fn federated_join(state: &Arc<Homeserver>, caller: &super::Caller, room_id: &str) -> Result<Vec<i64>, MatrixError> {
215    let dest = room_id.split_once(':').map(|(_, d)| d.to_string()).ok_or_else(|| MatrixError::invalid_param("room id"))?;
216    let local = crate::store::matrix_server_name().to_string();
217    let (status, tmpl) = fed_request(state, &dest, "GET", &format!("/federation/v1/make_join/{}/{}", enc(room_id), enc(&caller.mxid)), None).await?;
218    if status != 200 {
219        return Err(map_status(status, "make_join"));
220    }
221    let mut ev = tmpl.get("event").and_then(Value::as_object).cloned().ok_or_else(|| MatrixError::unknown("make_join: no event"))?;
222    let uid = caller.user_id;
223    let label = with_conn_pub(state, move |c| crate::nick::effective_label(c, uid).map_err(internal)).await?;
224    let event_id = crate::store::new_event_id();
225    ev.insert("event_id".into(), json!(event_id));
226    ev.insert("origin".into(), json!(local));
227    let mut content = ev.get("content").cloned().unwrap_or_else(|| json!({}));
228    if !label.is_empty() {
229        content["displayname"] = json!(label);
230    }
231    content["membership"] = json!("join");
232    ev.insert("content".into(), content);
233    if ev.get("sender").and_then(Value::as_str) != Some(caller.mxid.as_str()) || ev.get("room_id").and_then(Value::as_str) != Some(room_id) {
234        return Err(MatrixError::unknown("make_join: template does not match the request"));
235    }
236    let l2 = local.clone();
237    let pdu = with_conn_pub(state, move |c| fr::finalize_pdu(c, ev, &l2, fed::now_ms()).map_err(internal)).await?;
238    let (status, resp) = fed_request(state, &dest, "PUT", &format!("/federation/v2/send_join/{}/{}", enc(room_id), enc(&event_id)), Some(pdu.clone())).await?;
239    if status != 200 {
240        return Err(map_status(status, "send_join"));
241    }
242    let list = |k: &str| resp.get(k).and_then(Value::as_array).cloned().unwrap_or_default();
243    let (state_pdus, timeline) = (list("state"), list("m4a_timeline"));
244    let info = resp.get("m4a_room").cloned().ok_or_else(|| MatrixError::unknown("send_join: no room info"))?;
245    let mut verified: Vec<(Value, bool)> = Vec::new();
246    for p in state_pdus.iter().chain(timeline.iter()) {
247        if p.get("event_id").and_then(Value::as_str) == Some(event_id.as_str()) {
248            continue; // our own join, echoed back; stored below
249        }
250        let ok = verify_pdu(state, p).await?;
251        verified.push((p.clone(), ok));
252    }
253    let room = room_id.to_string();
254    let (mxid, user_id) = (caller.mxid.clone(), caller.user_id);
255    let ids = with_conn_pub(state, move |c| {
256        let now = now_rfc3339();
257        fr::create_replica_room(c, &room, &info, &now)?;
258        for (p, ok) in &verified {
259            fr::ingest_pdu(c, p, *ok, fr::Mode::Trusted, &now)?;
260        }
261        fr::store_own_pdu(c, &pdu, user_id, &mxid, &now)?;
262        let ids = crate::rooms::member_and_invited_ids(c, &room).map_err(internal)?;
263        Ok(ids.into_iter().collect::<Vec<_>>())
264    })
265    .await?;
266    Ok(ids)
267}
268
269// ------------------------------------------------------------ remote keys
270
271/// Split `{mxid: v}` into (local part, per-remote-domain part).
272pub(crate) fn split_by_domain<V: Clone>(map: &BTreeMap<String, V>) -> (BTreeMap<String, V>, BTreeMap<String, BTreeMap<String, V>>) {
273    let (mut local, mut remote): (BTreeMap<String, V>, BTreeMap<String, BTreeMap<String, V>>) = (BTreeMap::new(), BTreeMap::new());
274    for (k, v) in map {
275        if fr::is_remote_mxid(k) {
276            if let Some(d) = fr::domain_of(k) {
277                remote.entry(d.to_string()).or_default().insert(k.clone(), v.clone());
278            }
279        } else {
280            local.insert(k.clone(), v.clone());
281        }
282    }
283    (local, remote)
284}
285
286/// Query remote servers for device keys and merge into `out` (a client keys/query response).
287pub(crate) async fn merge_remote_keys_query(state: &Arc<Homeserver>, remote: BTreeMap<String, BTreeMap<String, Vec<String>>>, out: &mut Value) {
288    for (domain, users) in remote {
289        let body = json!({ "device_keys": users });
290        match fed_request(state, &domain, "POST", "/federation/v1/user/keys/query", Some(body)).await {
291            Ok((200, resp)) => {
292                for k in ["device_keys", "master_keys", "self_signing_keys"] {
293                    if let (Some(dst), Some(src)) = (out[k].as_object_mut(), resp.get(k).and_then(Value::as_object)) {
294                        for (u, v) in src {
295                            dst.insert(u.clone(), v.clone());
296                        }
297                    }
298                }
299            }
300            _ => {
301                out["failures"][&domain] = json!({ "status": 502, "errcode": "M_UNREACHABLE" });
302            }
303        }
304    }
305}
306
307/// Claim one-time keys from remote servers and merge into `out`.
308pub(crate) async fn merge_remote_keys_claim(state: &Arc<Homeserver>, remote: BTreeMap<String, BTreeMap<String, BTreeMap<String, String>>>, out: &mut Value) {
309    for (domain, users) in remote {
310        let body = json!({ "one_time_keys": users });
311        match fed_request(state, &domain, "POST", "/federation/v1/user/keys/claim", Some(body)).await {
312            Ok((200, resp)) => {
313                if let (Some(dst), Some(src)) = (out["one_time_keys"].as_object_mut(), resp.get("one_time_keys").and_then(Value::as_object)) {
314                    for (u, v) in src {
315                        dst.insert(u.clone(), v.clone());
316                    }
317                }
318            }
319            _ => {
320                out["failures"][&domain] = json!({ "status": 502, "errcode": "M_UNREACHABLE" });
321            }
322        }
323    }
324}
325
326/// Queue a `m.direct_to_device` EDU for a remote server.
327pub(crate) fn enqueue_to_device_edu(conn: &rusqlite::Connection, domain: &str, sender: &str, event_type: &str, message_id: &str, messages: &Value) -> Result<(), MatrixError> {
328    let edu = json!({ "edu_type": "m.direct_to_device", "content": { "sender": sender, "type": event_type, "message_id": message_id, "messages": messages } });
329    fr::enqueue(conn, domain, "edu", "", "", &edu, fed::now_ms()).map_err(internal)
330}