1use std::{
2 collections::{HashMap, HashSet},
3 path::{Path, PathBuf},
4};
5
6use iroh::PublicKey;
7use iroh_docs::NamespaceId;
8use iroh_gossip::{TopicId, api::GossipTopic};
9use serde::{Deserialize, Serialize};
10
11use crate::node::gossip_service::GossipService;
12use crate::{Result, SyncwebError};
13
14use super::network::{Network, NetworkId, NetworkOptions, NetworkTicket, network_topic, parse_public_key};
15
16#[derive(Clone, Debug)]
18pub struct NetworkManager {
19 path: PathBuf,
20 local_node: PublicKey,
21 networks: HashMap<NetworkId, Network>,
22}
23
24impl NetworkManager {
25 pub fn new(path_arg: impl Into<PathBuf>, local_node: PublicKey) -> Result<Self> {
31 let path = path_arg.into();
32 let networks = load_networks(&path)?
33 .into_iter()
34 .map(|network| (network.id, network))
35 .collect();
36 Ok(Self {
37 path,
38 local_node,
39 networks,
40 })
41 }
42
43 pub fn create(&mut self, name: &str, options: NetworkOptions) -> Result<NetworkId> {
49 let normalized = name.trim();
50 if normalized.is_empty() {
51 return Err(SyncwebError::InvalidConfig("network name cannot be empty".to_owned()));
52 }
53 let id = NetworkId::from_name(normalized);
54 if self.networks.contains_key(&id) {
55 return Err(SyncwebError::InvalidConfig(format!(
56 "network {normalized:?} already exists"
57 )));
58 }
59 self.networks
60 .insert(id, Network::new(normalized, self.local_node, options));
61 self.save()?;
62 Ok(id)
63 }
64
65 pub fn join(&mut self, ticket: NetworkTicket) -> Result<NetworkId> {
71 if ticket.name.trim().is_empty() || NetworkId::from_name(ticket.name.trim()) != ticket.network_id {
72 return Err(SyncwebError::InvalidTicket(
73 "network ticket ID does not match its network name".to_owned(),
74 ));
75 }
76 if ticket.invited_node.is_some_and(|invited| invited != self.local_node) {
77 return Err(SyncwebError::InvalidTicket(
78 "network ticket was issued for another device".to_owned(),
79 ));
80 }
81 let mut members = ticket.members;
82 members.insert(self.local_node);
83 let network = Network {
84 id: ticket.network_id,
85 name: ticket.name,
86 label: ticket.label,
87 topic: network_topic(ticket.network_id),
88 owner: ticket.owner,
89 members,
90 folders: ticket.folders,
91 shared_secret: ticket.shared_secret,
92 };
93 let id = network.id;
94 self.networks.insert(id, network);
95 self.save()?;
96 Ok(id)
97 }
98
99 pub fn leave(&mut self, id: NetworkId) -> Result<()> {
105 self.networks
106 .remove(&id)
107 .ok_or_else(|| SyncwebError::FolderNotFound(format!("network {id}")))?;
108 self.save()
109 }
110
111 pub fn invite(&mut self, id: NetworkId, device: PublicKey) -> Result<NetworkTicket> {
118 let network = self.network_mut_as_owner(id)?;
119 network.members.insert(device);
120 let ticket = NetworkTicket {
121 network_id: network.id,
122 name: network.name.clone(),
123 label: network.label.clone(),
124 owner: network.owner,
125 invited_node: Some(device),
126 members: network.members.clone(),
127 folders: network.folders.clone(),
128 shared_secret: network.shared_secret,
129 };
130 self.save()?;
131 Ok(ticket)
132 }
133
134 pub fn invite_any(&self, id: NetworkId) -> Result<NetworkTicket> {
140 let network = self.network_as_owner(id)?;
141 Ok(NetworkTicket {
142 network_id: network.id,
143 name: network.name.clone(),
144 label: network.label.clone(),
145 owner: network.owner,
146 invited_node: None,
147 members: network.members.clone(),
148 folders: network.folders.clone(),
149 shared_secret: network.shared_secret,
150 })
151 }
152
153 pub fn kick(&mut self, id: NetworkId, device: &PublicKey) -> Result<()> {
160 let network = self.network_mut_as_owner(id)?;
161 if network.owner == *device {
162 return Err(SyncwebError::InvalidConfig(
163 "the network owner cannot be kicked".to_owned(),
164 ));
165 }
166 if !network.members.remove(device) {
167 return Err(SyncwebError::InvalidConfig("device is not a network member".to_owned()));
168 }
169 self.save()
170 }
171
172 pub fn add_folder(&mut self, id: NetworkId, folder: NamespaceId) -> Result<()> {
178 self.network_mut(id)?.folders.insert(folder);
179 self.save()
180 }
181
182 pub fn remove_folder(&mut self, id: NetworkId, folder: NamespaceId) -> Result<()> {
188 self.network_mut(id)?.folders.remove(&folder);
189 self.save()
190 }
191
192 pub async fn subscribe(&self, id: NetworkId, gossip: &GossipService) -> Result<GossipTopic> {
202 let network = self
203 .networks
204 .get(&id)
205 .ok_or_else(|| SyncwebError::FolderNotFound(format!("network {id}")))?;
206 if !network.is_member(&self.local_node) {
207 return Err(SyncwebError::InvalidConfig(
208 "local device is not a member of this network".to_owned(),
209 ));
210 }
211 let mut bootstrap = network
212 .members
213 .iter()
214 .copied()
215 .filter(|member| *member != self.local_node)
216 .collect::<Vec<_>>();
217 bootstrap.sort_unstable();
218 gossip.subscribe(network.topic, bootstrap).await
219 }
220
221 pub fn topic(&self, id: NetworkId) -> Result<TopicId> {
227 self.networks
228 .get(&id)
229 .map(|network| network.topic)
230 .ok_or_else(|| SyncwebError::FolderNotFound(format!("network {id}")))
231 }
232
233 #[must_use]
234 pub fn list(&self) -> Vec<&Network> {
235 let mut networks = self.networks.values().collect::<Vec<_>>();
236 networks.sort_by(|left, right| left.name.cmp(&right.name));
237 networks
238 }
239
240 #[must_use]
241 pub fn get(&self, id: &NetworkId) -> Option<&Network> {
242 self.networks.get(id)
243 }
244
245 #[must_use]
246 pub fn get_by_name(&self, name: &str) -> Option<&Network> {
247 self.networks.values().find(|network| network.name == name)
248 }
249
250 fn network_mut(&mut self, id: NetworkId) -> Result<&mut Network> {
251 self.networks
252 .get_mut(&id)
253 .ok_or_else(|| SyncwebError::FolderNotFound(format!("network {id}")))
254 }
255
256 fn network_as_owner(&self, id: NetworkId) -> Result<&Network> {
257 let network = self
258 .networks
259 .get(&id)
260 .ok_or_else(|| SyncwebError::FolderNotFound(format!("network {id}")))?;
261 if network.owner != self.local_node {
262 return Err(SyncwebError::InvalidConfig(
263 "only the network owner can manage invitations".to_owned(),
264 ));
265 }
266 Ok(network)
267 }
268
269 fn network_mut_as_owner(&mut self, id: NetworkId) -> Result<&mut Network> {
270 if self.network_as_owner(id)?.owner != self.local_node {
271 return Err(SyncwebError::InvalidConfig(
272 "only the network owner can manage members".to_owned(),
273 ));
274 }
275 self.network_mut(id)
276 }
277
278 fn save(&self) -> Result<()> {
279 if let Some(parent) = self.path.parent() {
280 std::fs::create_dir_all(parent)?;
281 }
282 let records = self.networks.values().map(NetworkRecord::from).collect::<Vec<_>>();
283 let contents = serde_json::to_vec_pretty(&records)
284 .map_err(|error| SyncwebError::operation("failed to serialize networks", error))?;
285 let temporary = self.path.with_extension(format!("tmp-{}", uuid::Uuid::new_v4()));
286 std::fs::write(&temporary, contents)
287 .map_err(|error| SyncwebError::operation("failed to write network state", error))?;
288 std::fs::rename(&temporary, &self.path)
289 .map_err(|error| SyncwebError::operation("failed to persist network state", error))
290 }
291}
292
293#[derive(Deserialize, Serialize)]
294struct NetworkRecord {
295 id: String,
296 name: String,
297 label: String,
298 owner: String,
299 members: Vec<String>,
300 folders: Vec<String>,
301 shared_secret: Option<String>,
302}
303
304impl From<&Network> for NetworkRecord {
305 fn from(network: &Network) -> Self {
306 Self {
307 id: network.id.to_string(),
308 name: network.name.clone(),
309 label: network.label.clone(),
310 owner: network.owner.to_string(),
311 members: network.members.iter().map(ToString::to_string).collect(),
312 folders: network.folders.iter().map(ToString::to_string).collect(),
313 shared_secret: network.shared_secret.map(hex::encode),
314 }
315 }
316}
317
318impl TryFrom<NetworkRecord> for Network {
319 type Error = SyncwebError;
320
321 fn try_from(record: NetworkRecord) -> Result<Self> {
322 if record.name.trim().is_empty() {
323 return Err(SyncwebError::InvalidConfig("network name cannot be empty".to_owned()));
324 }
325 let id: NetworkId = record.id.parse()?;
326 if id != NetworkId::from_name(record.name.trim()) {
327 return Err(SyncwebError::InvalidConfig(
328 "network ID does not match its name".to_owned(),
329 ));
330 }
331 let members = record
332 .members
333 .into_iter()
334 .map(|member| parse_public_key(&member))
335 .collect::<Result<HashSet<_>>>()?;
336 let folders = record
337 .folders
338 .into_iter()
339 .map(|namespace| {
340 namespace
341 .parse()
342 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid folder namespace: {error}")))
343 })
344 .collect::<Result<HashSet<_>>>()?;
345 let shared_secret = record
346 .shared_secret
347 .map(|secret| {
348 let bytes = hex::decode(secret)
349 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid network secret: {error}")))?;
350 bytes.try_into().map_err(|_length_error| {
351 SyncwebError::InvalidConfig("network secret must contain 32 bytes".to_owned())
352 })
353 })
354 .transpose()?;
355 Ok(Self {
356 id,
357 name: record.name,
358 label: record.label,
359 topic: network_topic(id),
360 owner: parse_public_key(&record.owner)?,
361 members,
362 folders,
363 shared_secret,
364 })
365 }
366}
367
368fn load_networks(path: &Path) -> Result<Vec<Network>> {
369 if !path.exists() {
370 return Ok(Vec::new());
371 }
372 let bytes = std::fs::read(path).map_err(|error| SyncwebError::operation("failed to read network state", error))?;
373 let records: Vec<NetworkRecord> = serde_json::from_slice(&bytes)
374 .map_err(|error| SyncwebError::operation("failed to parse network state", error))?;
375 records.into_iter().map(Network::try_from).collect()
376}