peat_protocol/network/
peer_config.rs1#[cfg(feature = "automerge-backend")]
29use crate::security::FormationKey;
30#[cfg(feature = "automerge-backend")]
31use anyhow::{Context, Result};
32#[cfg(feature = "automerge-backend")]
33use iroh::EndpointId;
34#[cfg(feature = "automerge-backend")]
35use serde::{Deserialize, Serialize};
36#[cfg(feature = "automerge-backend")]
37use std::net::SocketAddr;
38#[cfg(feature = "automerge-backend")]
39use std::path::Path;
40
41#[cfg(feature = "automerge-backend")]
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct PeerConfig {
45 #[serde(default)]
47 pub local: LocalConfig,
48 #[serde(default)]
50 pub formation: Option<FormationConfig>,
51 #[serde(default)]
53 pub peers: Vec<PeerInfo>,
54}
55
56#[cfg(feature = "automerge-backend")]
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct FormationConfig {
63 pub id: String,
65 pub shared_key: String,
67}
68
69#[cfg(feature = "automerge-backend")]
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct LocalConfig {
73 #[serde(default = "default_bind_address")]
75 pub bind_address: String,
76 pub node_id: Option<String>,
78}
79
80#[cfg(feature = "automerge-backend")]
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct PeerInfo {
84 pub name: String,
86 pub node_id: String,
88 pub addresses: Vec<String>,
90 pub relay_url: Option<String>,
92}
93
94#[cfg(feature = "automerge-backend")]
95fn default_bind_address() -> String {
96 "0.0.0.0:0".to_string()
97}
98
99#[cfg(feature = "automerge-backend")]
100impl Default for LocalConfig {
101 fn default() -> Self {
102 Self {
103 bind_address: default_bind_address(),
104 node_id: None,
105 }
106 }
107}
108
109#[cfg(feature = "automerge-backend")]
110impl PeerConfig {
111 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
123 let contents =
124 std::fs::read_to_string(path.as_ref()).context("Failed to read peer config file")?;
125 Self::from_toml(&contents)
126 }
127
128 pub fn from_toml(toml_str: &str) -> Result<Self> {
130 toml::from_str(toml_str).context("Failed to parse TOML peer config")
131 }
132
133 pub fn empty() -> Self {
135 Self {
136 local: LocalConfig::default(),
137 formation: None,
138 peers: Vec::new(),
139 }
140 }
141
142 pub fn formation_key(&self) -> Result<Option<FormationKey>> {
147 match &self.formation {
148 Some(config) => {
149 let key = FormationKey::from_base64(&config.id, &config.shared_key)
150 .map_err(|e| anyhow::anyhow!("Invalid formation key: {}", e))?;
151 Ok(Some(key))
152 }
153 None => Ok(None),
154 }
155 }
156
157 pub fn requires_formation_auth(&self) -> bool {
159 self.formation.is_some()
160 }
161
162 pub fn bind_socket_addr(&self) -> Result<SocketAddr> {
164 self.local
165 .bind_address
166 .parse()
167 .context("Invalid bind address")
168 }
169
170 pub fn get_peer(&self, name: &str) -> Option<&PeerInfo> {
172 self.peers.iter().find(|p| p.name == name)
173 }
174}
175
176#[cfg(feature = "automerge-backend")]
177impl PeerInfo {
178 pub fn endpoint_id(&self) -> Result<EndpointId> {
180 let bytes = hex::decode(&self.node_id).context("Failed to decode node_id hex")?;
182
183 if bytes.len() != 32 {
185 anyhow::bail!(
186 "Invalid node_id length: expected 32 bytes, got {}",
187 bytes.len()
188 );
189 }
190
191 let mut array = [0u8; 32];
193 array.copy_from_slice(&bytes);
194
195 EndpointId::from_bytes(&array).context("Failed to construct EndpointId from bytes")
197 }
198
199 pub fn socket_addrs(&self) -> Result<Vec<SocketAddr>> {
201 self.addresses
202 .iter()
203 .map(|addr| {
204 addr.parse()
205 .with_context(|| format!("Invalid address: {}", addr))
206 })
207 .collect()
208 }
209}
210
211#[cfg(all(test, feature = "automerge-backend"))]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn test_parse_empty_config() {
217 let config = PeerConfig::from_toml("").unwrap();
218 assert_eq!(config.peers.len(), 0);
219 assert_eq!(config.local.bind_address, "0.0.0.0:0");
220 }
221
222 #[test]
223 fn test_parse_local_config() {
224 let toml = r#"
225 [local]
226 bind_address = "127.0.0.1:9000"
227 "#;
228
229 let config = PeerConfig::from_toml(toml).unwrap();
230 assert_eq!(config.local.bind_address, "127.0.0.1:9000");
231 assert_eq!(config.bind_socket_addr().unwrap().port(), 9000);
232 }
233
234 #[test]
235 fn test_parse_peers() {
236 let toml = r#"
237 [[peers]]
238 name = "node-1"
239 node_id = "6eb2a534751444f1353b29aa307c78c1f72acfbb06bb8696103dfeede1f4f854"
240 addresses = ["127.0.0.1:9001"]
241
242 [[peers]]
243 name = "node-2"
244 node_id = "b654917328aea8ccfae00463d63642eb4904bd276fecb4caf94dd740a76b5567"
245 addresses = ["127.0.0.1:9002", "192.168.1.100:9002"]
246 "#;
247
248 let config = PeerConfig::from_toml(toml).unwrap();
249 assert_eq!(config.peers.len(), 2);
250
251 let peer1 = &config.peers[0];
252 assert_eq!(peer1.name, "node-1");
253 assert_eq!(peer1.addresses.len(), 1);
254
255 let peer2 = &config.peers[1];
256 assert_eq!(peer2.name, "node-2");
257 assert_eq!(peer2.addresses.len(), 2);
258
259 let addrs = peer2.socket_addrs().unwrap();
261 assert_eq!(addrs.len(), 2);
262 assert_eq!(addrs[0].port(), 9002);
263 }
264
265 #[test]
266 fn test_endpoint_id_parsing() {
267 let peer = PeerInfo {
268 name: "test".to_string(),
269 node_id: "6eb2a534751444f1353b29aa307c78c1f72acfbb06bb8696103dfeede1f4f854".to_string(),
270 addresses: vec![],
271 relay_url: None,
272 };
273
274 let endpoint_id = peer.endpoint_id().unwrap();
275 assert_eq!(endpoint_id.as_bytes().len(), 32);
277 }
278
279 #[test]
280 fn test_get_peer_by_name() {
281 let toml = r#"
282 [[peers]]
283 name = "alice"
284 node_id = "6eb2a534751444f1353b29aa307c78c1f72acfbb06bb8696103dfeede1f4f854"
285 addresses = ["127.0.0.1:9001"]
286
287 [[peers]]
288 name = "bob"
289 node_id = "b654917328aea8ccfae00463d63642eb4904bd276fecb4caf94dd740a76b5567"
290 addresses = ["127.0.0.1:9002"]
291 "#;
292
293 let config = PeerConfig::from_toml(toml).unwrap();
294
295 assert!(config.get_peer("alice").is_some());
296 assert!(config.get_peer("bob").is_some());
297 assert!(config.get_peer("charlie").is_none());
298 }
299
300 #[test]
301 fn test_parse_formation_config() {
302 let secret = FormationKey::generate_secret();
304
305 let toml = format!(
306 r#"
307 [formation]
308 id = "alpha-company"
309 shared_key = "{}"
310
311 [local]
312 bind_address = "127.0.0.1:9000"
313 "#,
314 secret
315 );
316
317 let config = PeerConfig::from_toml(&toml).unwrap();
318
319 assert!(config.formation.is_some());
320 let formation = config.formation.as_ref().unwrap();
321 assert_eq!(formation.id, "alpha-company");
322 assert!(config.requires_formation_auth());
323 }
324
325 #[test]
326 fn test_formation_key_creation() {
327 let secret = FormationKey::generate_secret();
328
329 let toml = format!(
330 r#"
331 [formation]
332 id = "bravo-team"
333 shared_key = "{}"
334 "#,
335 secret
336 );
337
338 let config = PeerConfig::from_toml(&toml).unwrap();
339 let key = config.formation_key().unwrap();
340
341 assert!(key.is_some());
342 assert_eq!(key.unwrap().formation_id(), "bravo-team");
343 }
344
345 #[test]
346 fn test_no_formation_config() {
347 let config = PeerConfig::empty();
348
349 assert!(config.formation.is_none());
350 assert!(!config.requires_formation_auth());
351 assert!(config.formation_key().unwrap().is_none());
352 }
353
354 #[test]
355 fn test_invalid_formation_key() {
356 let toml = r#"
357 [formation]
358 id = "test"
359 shared_key = "not-valid-base64!!!"
360 "#;
361
362 let config = PeerConfig::from_toml(toml).unwrap();
363 assert!(config.formation_key().is_err());
364 }
365}