1use std::collections::{BTreeMap, BTreeSet};
15use std::sync::Arc;
16
17use zeroize::Zeroizing;
18
19use crate::error::{AgentError, Result};
20use crate::vault::KeyVault;
21
22#[cfg(feature = "bridge-loopback")]
23pub mod loopback;
24
25#[derive(Clone)]
27pub struct LocalEvent {
28 pub room_id: String,
29 pub sender: String,
30 pub body: String,
31 pub event_id: String,
32}
33
34impl std::fmt::Debug for LocalEvent {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 f.debug_struct("LocalEvent").field("room_id", &self.room_id).field("event_id", &self.event_id).field("body", &"<plaintext hidden>").finish()
37 }
38}
39
40pub struct RemoteMessage {
42 pub room_id: String,
43 pub author: String,
44 pub body: String,
45}
46
47#[derive(Clone)]
49pub struct ScopedVault {
50 inner: Arc<dyn KeyVault>,
51 prefix: String,
52}
53
54impl ScopedVault {
55 fn label(&self, l: &str) -> Result<String> {
56 if l.is_empty() || l.len() > 64 || !l.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')) {
57 return Err(AgentError::Vault("bridge credential label must be 1-64 characters of [A-Za-z0-9._-]".into()));
58 }
59 Ok(format!("{}{l}", self.prefix))
60 }
61 pub fn get(&self, l: &str) -> Result<Option<Zeroizing<Vec<u8>>>> {
62 self.inner.get(&self.label(l)?)
63 }
64 pub fn put(&self, l: &str, v: &[u8]) -> Result<()> {
65 self.inner.put(&self.label(l)?, v)
66 }
67 pub fn delete(&self, l: &str) -> Result<bool> {
68 self.inner.delete(&self.label(l)?)
69 }
70}
71
72pub trait Bridge: Send {
73 fn name(&self) -> &'static str;
75 fn connect(&mut self, vault: &ScopedVault) -> Result<()>;
77 fn outgoing(&mut self, remote_room: &str, ev: &LocalEvent) -> Result<()>;
80 fn incoming(&mut self) -> Result<Vec<(String, String, String)>>; }
84
85pub struct ConsentStore {
87 vault: Arc<dyn KeyVault>,
88}
89
90fn consent_label(bridge: &str) -> String {
91 format!("bridge-consent/{bridge}")
92}
93
94impl ConsentStore {
95 pub fn new(vault: Arc<dyn KeyVault>) -> Self {
96 Self { vault }
97 }
98
99 fn load(&self, bridge: &str) -> Result<BTreeMap<String, String>> {
100 match self.vault.get(&consent_label(bridge))? {
101 Some(raw) => serde_json::from_slice(&raw).map_err(|e| AgentError::Vault(format!("bridge consent: {e}"))),
102 None => Ok(BTreeMap::new()),
103 }
104 }
105
106 fn store(&self, bridge: &str, m: &BTreeMap<String, String>) -> Result<()> {
107 self.vault.put(&consent_label(bridge), &serde_json::to_vec(m).map_err(|e| AgentError::Vault(e.to_string()))?)
108 }
109
110 pub fn grant(&self, bridge: &str, room_id: &str, remote_room: &str) -> Result<()> {
112 let mut m = self.load(bridge)?;
113 m.insert(room_id.to_string(), remote_room.to_string());
114 self.store(bridge, &m)
115 }
116
117 pub fn revoke(&self, bridge: &str, room_id: &str) -> Result<bool> {
119 let mut m = self.load(bridge)?;
120 let had = m.remove(room_id).is_some();
121 if had {
122 self.store(bridge, &m)?;
123 }
124 Ok(had)
125 }
126
127 pub fn remote_of(&self, bridge: &str, room_id: &str) -> Result<Option<String>> {
128 Ok(self.load(bridge)?.get(room_id).cloned())
129 }
130
131 pub fn local_of(&self, bridge: &str, remote_room: &str) -> Result<Option<String>> {
132 Ok(self.load(bridge)?.into_iter().find(|(_, r)| r == remote_room).map(|(l, _)| l))
133 }
134
135 pub fn rooms(&self, bridge: &str) -> Result<BTreeSet<String>> {
136 Ok(self.load(bridge)?.into_keys().collect())
137 }
138}
139
140#[derive(Debug, Default, PartialEq, Eq)]
141pub struct ForwardReport {
142 pub forwarded: Vec<String>,
143 pub failed: Vec<(String, String)>,
145}
146
147pub struct BridgeRegistry {
149 vault: Arc<dyn KeyVault>,
150 consent: ConsentStore,
151 bridges: BTreeMap<&'static str, Box<dyn Bridge>>,
152}
153
154impl BridgeRegistry {
155 pub fn new(vault: Arc<dyn KeyVault>) -> Self {
156 Self { consent: ConsentStore::new(vault.clone()), vault, bridges: BTreeMap::new() }
157 }
158
159 pub fn consent(&self) -> &ConsentStore {
160 &self.consent
161 }
162
163 pub fn register(&mut self, mut bridge: Box<dyn Bridge>) -> Result<()> {
165 let name = bridge.name();
166 if self.bridges.contains_key(name) {
167 return Err(AgentError::Identity(format!("bridge {name} is already registered")));
168 }
169 bridge.connect(&ScopedVault { inner: self.vault.clone(), prefix: format!("bridge/{name}/") })?;
170 self.bridges.insert(name, bridge);
171 Ok(())
172 }
173
174 pub fn names(&self) -> Vec<&'static str> {
175 self.bridges.keys().copied().collect()
176 }
177
178 pub fn forward(&mut self, ev: &LocalEvent) -> ForwardReport {
180 let mut report = ForwardReport::default();
181 for (name, bridge) in self.bridges.iter_mut() {
182 match self.consent.remote_of(name, &ev.room_id) {
183 Ok(Some(remote)) => match bridge.outgoing(&remote, ev) {
184 Ok(()) => report.forwarded.push(name.to_string()),
185 Err(e) => report.failed.push((name.to_string(), e.to_string())),
186 },
187 Ok(None) => {}
188 Err(e) => report.failed.push((name.to_string(), e.to_string())),
189 }
190 }
191 report
192 }
193
194 pub fn collect(&mut self) -> Vec<(String, &'static str, String, String)> {
197 let mut out = Vec::new();
198 for (name, bridge) in self.bridges.iter_mut() {
199 let Ok(msgs) = bridge.incoming() else { continue };
200 for (remote, author, body) in msgs {
201 if let Ok(Some(local)) = self.consent.local_of(name, &remote) {
202 out.push((local, *name, author, body));
203 }
204 }
205 }
206 out
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::vault::MemoryVault;
214
215 #[cfg(feature = "bridge-loopback")]
216 #[test]
217 fn only_consented_rooms_reach_the_bridge_and_credentials_are_scoped() {
218 use loopback::{LoopbackBridge, LoopbackRemote, TOKEN_LABEL};
219 let vault: Arc<dyn KeyVault> = Arc::new(MemoryVault::new());
220 vault.put("identity-key/s1", b"never-visible").unwrap();
221 let remote = Arc::new(LoopbackRemote::default());
222 let mut reg = BridgeRegistry::new(vault.clone());
223 assert!(reg.register(Box::new(LoopbackBridge::new(remote.clone()))).is_err());
225 vault.put(&format!("bridge/loopback/{TOKEN_LABEL}"), b"t").unwrap();
226 reg.register(Box::new(LoopbackBridge::new(remote.clone()))).unwrap();
227 assert!(reg.register(Box::new(LoopbackBridge::new(remote.clone()))).is_err(), "twice");
228
229 let ev = |room: &str, body: &str| LocalEvent { room_id: room.into(), sender: "@a:x".into(), body: body.into(), event_id: "$1".into() };
230 assert_eq!(reg.forward(&ev("!r1", "secret")), ForwardReport::default());
232 assert!(remote.sent.lock().unwrap().is_empty());
233 reg.consent().grant("loopback", "!r1", "remote-1").unwrap();
234 assert_eq!(reg.forward(&ev("!r1", "hello")).forwarded, vec!["loopback"]);
235 assert_eq!(reg.forward(&ev("!r2", "other room")), ForwardReport::default());
236 assert_eq!(remote.sent.lock().unwrap().as_slice(), &[("remote-1".to_string(), "@a:x: hello".to_string())]);
237
238 remote.inbox.lock().unwrap().push_back(("remote-1".into(), "bob".into(), "hi".into()));
240 remote.inbox.lock().unwrap().push_back(("unlinked".into(), "eve".into(), "x".into()));
241 assert_eq!(reg.collect(), vec![("!r1".to_string(), "loopback", "bob".to_string(), "hi".to_string())]);
242 assert!(reg.consent().revoke("loopback", "!r1").unwrap());
243 remote.inbox.lock().unwrap().push_back(("remote-1".into(), "bob".into(), "again".into()));
244 assert!(reg.collect().is_empty());
245 assert_eq!(reg.forward(&ev("!r1", "after")), ForwardReport::default());
246 assert_eq!(remote.sent.lock().unwrap().len(), 1);
247
248 let sv = ScopedVault { inner: vault.clone(), prefix: "bridge/loopback/".into() };
250 assert!(sv.get("../identity-key/s1").is_err() && sv.get("a/b").is_err());
251 assert!(sv.get("identity-key").unwrap().is_none());
252 }
253
254 #[test]
255 fn plaintext_does_not_print() {
256 let e = LocalEvent { room_id: "!r".into(), sender: "@a:x".into(), body: "very secret words".into(), event_id: "$1".into() };
257 assert!(!format!("{e:?}").contains("secret"));
258 let _ = MemoryVault::new();
259 }
260}