#[cfg(feature = "automerge-backend")]
use crate::security::FormationKey;
#[cfg(feature = "automerge-backend")]
use anyhow::{Context, Result};
#[cfg(feature = "automerge-backend")]
use iroh::EndpointId;
#[cfg(feature = "automerge-backend")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "automerge-backend")]
use std::net::SocketAddr;
#[cfg(feature = "automerge-backend")]
use std::path::Path;
#[cfg(feature = "automerge-backend")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerConfig {
#[serde(default)]
pub local: LocalConfig,
#[serde(default)]
pub formation: Option<FormationConfig>,
#[serde(default)]
pub peers: Vec<PeerInfo>,
}
#[cfg(feature = "automerge-backend")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FormationConfig {
pub id: String,
pub shared_key: String,
}
#[cfg(feature = "automerge-backend")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalConfig {
#[serde(default = "default_bind_address")]
pub bind_address: String,
pub node_id: Option<String>,
}
#[cfg(feature = "automerge-backend")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerInfo {
pub name: String,
pub node_id: String,
pub addresses: Vec<String>,
pub relay_url: Option<String>,
}
#[cfg(feature = "automerge-backend")]
fn default_bind_address() -> String {
"0.0.0.0:0".to_string()
}
#[cfg(feature = "automerge-backend")]
impl Default for LocalConfig {
fn default() -> Self {
Self {
bind_address: default_bind_address(),
node_id: None,
}
}
}
#[cfg(feature = "automerge-backend")]
impl PeerConfig {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let contents =
std::fs::read_to_string(path.as_ref()).context("Failed to read peer config file")?;
Self::from_toml(&contents)
}
pub fn from_toml(toml_str: &str) -> Result<Self> {
toml::from_str(toml_str).context("Failed to parse TOML peer config")
}
pub fn empty() -> Self {
Self {
local: LocalConfig::default(),
formation: None,
peers: Vec::new(),
}
}
pub fn formation_key(&self) -> Result<Option<FormationKey>> {
match &self.formation {
Some(config) => {
let key = FormationKey::from_base64(&config.id, &config.shared_key)
.map_err(|e| anyhow::anyhow!("Invalid formation key: {}", e))?;
Ok(Some(key))
}
None => Ok(None),
}
}
pub fn requires_formation_auth(&self) -> bool {
self.formation.is_some()
}
pub fn bind_socket_addr(&self) -> Result<SocketAddr> {
self.local
.bind_address
.parse()
.context("Invalid bind address")
}
pub fn get_peer(&self, name: &str) -> Option<&PeerInfo> {
self.peers.iter().find(|p| p.name == name)
}
}
#[cfg(feature = "automerge-backend")]
impl PeerInfo {
pub fn endpoint_id(&self) -> Result<EndpointId> {
let bytes = hex::decode(&self.node_id).context("Failed to decode node_id hex")?;
if bytes.len() != 32 {
anyhow::bail!(
"Invalid node_id length: expected 32 bytes, got {}",
bytes.len()
);
}
let mut array = [0u8; 32];
array.copy_from_slice(&bytes);
EndpointId::from_bytes(&array).context("Failed to construct EndpointId from bytes")
}
pub fn socket_addrs(&self) -> Result<Vec<SocketAddr>> {
self.addresses
.iter()
.map(|addr| {
addr.parse()
.with_context(|| format!("Invalid address: {}", addr))
})
.collect()
}
}
#[cfg(all(test, feature = "automerge-backend"))]
mod tests {
use super::*;
#[test]
fn test_parse_empty_config() {
let config = PeerConfig::from_toml("").unwrap();
assert_eq!(config.peers.len(), 0);
assert_eq!(config.local.bind_address, "0.0.0.0:0");
}
#[test]
fn test_parse_local_config() {
let toml = r#"
[local]
bind_address = "127.0.0.1:9000"
"#;
let config = PeerConfig::from_toml(toml).unwrap();
assert_eq!(config.local.bind_address, "127.0.0.1:9000");
assert_eq!(config.bind_socket_addr().unwrap().port(), 9000);
}
#[test]
fn test_parse_peers() {
let toml = r#"
[[peers]]
name = "node-1"
node_id = "6eb2a534751444f1353b29aa307c78c1f72acfbb06bb8696103dfeede1f4f854"
addresses = ["127.0.0.1:9001"]
[[peers]]
name = "node-2"
node_id = "b654917328aea8ccfae00463d63642eb4904bd276fecb4caf94dd740a76b5567"
addresses = ["127.0.0.1:9002", "192.168.1.100:9002"]
"#;
let config = PeerConfig::from_toml(toml).unwrap();
assert_eq!(config.peers.len(), 2);
let peer1 = &config.peers[0];
assert_eq!(peer1.name, "node-1");
assert_eq!(peer1.addresses.len(), 1);
let peer2 = &config.peers[1];
assert_eq!(peer2.name, "node-2");
assert_eq!(peer2.addresses.len(), 2);
let addrs = peer2.socket_addrs().unwrap();
assert_eq!(addrs.len(), 2);
assert_eq!(addrs[0].port(), 9002);
}
#[test]
fn test_endpoint_id_parsing() {
let peer = PeerInfo {
name: "test".to_string(),
node_id: "6eb2a534751444f1353b29aa307c78c1f72acfbb06bb8696103dfeede1f4f854".to_string(),
addresses: vec![],
relay_url: None,
};
let endpoint_id = peer.endpoint_id().unwrap();
assert_eq!(endpoint_id.as_bytes().len(), 32);
}
#[test]
fn test_get_peer_by_name() {
let toml = r#"
[[peers]]
name = "alice"
node_id = "6eb2a534751444f1353b29aa307c78c1f72acfbb06bb8696103dfeede1f4f854"
addresses = ["127.0.0.1:9001"]
[[peers]]
name = "bob"
node_id = "b654917328aea8ccfae00463d63642eb4904bd276fecb4caf94dd740a76b5567"
addresses = ["127.0.0.1:9002"]
"#;
let config = PeerConfig::from_toml(toml).unwrap();
assert!(config.get_peer("alice").is_some());
assert!(config.get_peer("bob").is_some());
assert!(config.get_peer("charlie").is_none());
}
#[test]
fn test_parse_formation_config() {
let secret = FormationKey::generate_secret();
let toml = format!(
r#"
[formation]
id = "alpha-company"
shared_key = "{}"
[local]
bind_address = "127.0.0.1:9000"
"#,
secret
);
let config = PeerConfig::from_toml(&toml).unwrap();
assert!(config.formation.is_some());
let formation = config.formation.as_ref().unwrap();
assert_eq!(formation.id, "alpha-company");
assert!(config.requires_formation_auth());
}
#[test]
fn test_formation_key_creation() {
let secret = FormationKey::generate_secret();
let toml = format!(
r#"
[formation]
id = "bravo-team"
shared_key = "{}"
"#,
secret
);
let config = PeerConfig::from_toml(&toml).unwrap();
let key = config.formation_key().unwrap();
assert!(key.is_some());
assert_eq!(key.unwrap().formation_id(), "bravo-team");
}
#[test]
fn test_no_formation_config() {
let config = PeerConfig::empty();
assert!(config.formation.is_none());
assert!(!config.requires_formation_auth());
assert!(config.formation_key().unwrap().is_none());
}
#[test]
fn test_invalid_formation_key() {
let toml = r#"
[formation]
id = "test"
shared_key = "not-valid-base64!!!"
"#;
let config = PeerConfig::from_toml(toml).unwrap();
assert!(config.formation_key().is_err());
}
}