syncweb_core/net/
network.rs1use std::{
2 collections::HashSet,
3 fmt::{Display, Formatter},
4 str::FromStr,
5};
6
7use iroh::PublicKey;
8use iroh_docs::NamespaceId;
9use iroh_gossip::TopicId;
10use serde::{Deserialize, Serialize};
11
12use crate::{Result, SyncwebError};
13
14#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
16pub struct NetworkId([u8; 32]);
17
18impl NetworkId {
19 #[must_use]
20 pub fn from_name(name: &str) -> Self {
21 let mut hasher = blake3::Hasher::new();
22 hasher.update(b"network:");
23 hasher.update(name.as_bytes());
24 Self(*hasher.finalize().as_bytes())
25 }
26
27 #[must_use]
28 pub const fn as_bytes(&self) -> &[u8; 32] {
29 &self.0
30 }
31}
32
33impl Display for NetworkId {
34 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
35 formatter.write_str(&hex::encode(self.0))
36 }
37}
38
39impl FromStr for NetworkId {
40 type Err = SyncwebError;
41
42 fn from_str(value: &str) -> Result<Self> {
43 let bytes =
44 hex::decode(value).map_err(|error| SyncwebError::InvalidConfig(format!("invalid network ID: {error}")))?;
45 let array: [u8; 32] = bytes
46 .try_into()
47 .map_err(|_length_error| SyncwebError::InvalidConfig("network ID must contain 32 bytes".to_owned()))?;
48 Ok(Self(array))
49 }
50}
51
52#[derive(Clone, Debug, Default, Eq, PartialEq)]
54#[non_exhaustive]
55pub struct NetworkOptions {
56 pub label: String,
57 pub invite_only: bool,
58}
59
60impl NetworkOptions {
61 #[must_use]
62 pub fn with_label(mut self, label: impl Into<String>) -> Self {
63 self.label = label.into();
64 self
65 }
66
67 #[must_use]
68 pub const fn invite_only(mut self, invite_only: bool) -> Self {
69 self.invite_only = invite_only;
70 self
71 }
72}
73
74#[derive(Clone, Debug)]
76#[non_exhaustive]
77pub struct Network {
78 pub id: NetworkId,
79 pub name: String,
80 pub label: String,
81 pub topic: TopicId,
82 pub owner: PublicKey,
83 pub members: HashSet<PublicKey>,
84 pub folders: HashSet<NamespaceId>,
85 pub(crate) shared_secret: Option<[u8; 32]>,
86}
87
88impl Network {
89 #[must_use]
90 pub fn new(name_arg: impl Into<String>, owner: PublicKey, options: NetworkOptions) -> Self {
91 let name = name_arg.into().trim().to_owned();
92 let id = NetworkId::from_name(&name);
93 let topic = network_topic(id);
94 let mut members = HashSet::new();
95 members.insert(owner);
96 Self {
97 id,
98 name,
99 label: options.label,
100 topic,
101 owner,
102 members,
103 folders: HashSet::new(),
104 shared_secret: options.invite_only.then(rand::random),
105 }
106 }
107
108 #[must_use]
109 pub fn is_member(&self, node_id: &PublicKey) -> bool {
110 self.members.contains(node_id)
111 }
112
113 #[must_use]
114 pub const fn topic(&self) -> TopicId {
115 self.topic
116 }
117
118 #[must_use]
119 pub const fn is_invite_only(&self) -> bool {
120 self.shared_secret.is_some()
121 }
122}
123
124#[derive(Clone, Debug, Eq, PartialEq)]
126pub struct NetworkTicket {
127 pub network_id: NetworkId,
128 pub name: String,
129 pub label: String,
130 pub owner: PublicKey,
131 pub invited_node: Option<PublicKey>,
132 pub members: HashSet<PublicKey>,
133 pub folders: HashSet<NamespaceId>,
134 pub(crate) shared_secret: Option<[u8; 32]>,
135}
136
137impl NetworkTicket {
138 #[must_use]
139 pub const fn is_invite_only(&self) -> bool {
140 self.shared_secret.is_some()
141 }
142}
143
144impl Display for NetworkTicket {
145 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
146 let wire = TicketWire::from(self);
147 let bytes = serde_json::to_vec(&wire).map_err(|_serialization_error| std::fmt::Error)?;
148 write!(formatter, "syncweb://network/{}", hex::encode(bytes))
149 }
150}
151
152impl FromStr for NetworkTicket {
153 type Err = SyncwebError;
154
155 fn from_str(value: &str) -> Result<Self> {
156 let encoded = value.strip_prefix("syncweb://network/").ok_or_else(|| {
157 SyncwebError::InvalidTicket("network ticket must start with syncweb://network/".to_owned())
158 })?;
159 let bytes = hex::decode(encoded)
160 .map_err(|error| SyncwebError::InvalidTicket(format!("invalid network ticket encoding: {error}")))?;
161 let wire: TicketWire = serde_json::from_slice(&bytes)
162 .map_err(|error| SyncwebError::InvalidTicket(format!("invalid network ticket: {error}")))?;
163 wire.try_into()
164 }
165}
166
167#[derive(Deserialize, Serialize)]
168struct TicketWire {
169 network_id: String,
170 name: String,
171 label: String,
172 owner: String,
173 invited_node: Option<String>,
174 members: Vec<String>,
175 folders: Vec<String>,
176 shared_secret: Option<String>,
177}
178
179impl From<&NetworkTicket> for TicketWire {
180 fn from(ticket: &NetworkTicket) -> Self {
181 let mut members = ticket.members.iter().map(ToString::to_string).collect::<Vec<_>>();
182 members.sort_unstable();
183 let mut folders = ticket.folders.iter().map(ToString::to_string).collect::<Vec<_>>();
184 folders.sort_unstable();
185 Self {
186 network_id: ticket.network_id.to_string(),
187 name: ticket.name.clone(),
188 label: ticket.label.clone(),
189 owner: ticket.owner.to_string(),
190 invited_node: ticket.invited_node.map(|node| node.to_string()),
191 members,
192 folders,
193 shared_secret: ticket.shared_secret.map(hex::encode),
194 }
195 }
196}
197
198impl TryFrom<TicketWire> for NetworkTicket {
199 type Error = SyncwebError;
200
201 fn try_from(wire: TicketWire) -> Result<Self> {
202 if wire.name.trim().is_empty() {
203 return Err(SyncwebError::InvalidTicket("network name cannot be empty".to_owned()));
204 }
205 let network_id: NetworkId = wire.network_id.parse()?;
206 if network_id != NetworkId::from_name(wire.name.trim()) {
207 return Err(SyncwebError::InvalidTicket(
208 "network ticket ID does not match its network name".to_owned(),
209 ));
210 }
211 let members = wire
212 .members
213 .into_iter()
214 .map(|member| parse_public_key(&member))
215 .collect::<Result<HashSet<_>>>()?;
216 let folders = wire
217 .folders
218 .into_iter()
219 .map(|folder| {
220 folder
221 .parse()
222 .map_err(|error| SyncwebError::InvalidTicket(format!("invalid folder namespace: {error}")))
223 })
224 .collect::<Result<HashSet<_>>>()?;
225 let shared_secret = wire
226 .shared_secret
227 .map(|secret| {
228 let bytes = hex::decode(secret)
229 .map_err(|error| SyncwebError::InvalidTicket(format!("invalid network secret: {error}")))?;
230 bytes.try_into().map_err(|_length_error| {
231 SyncwebError::InvalidTicket("network secret must contain 32 bytes".to_owned())
232 })
233 })
234 .transpose()?;
235 Ok(Self {
236 network_id,
237 name: wire.name,
238 label: wire.label,
239 owner: parse_public_key(&wire.owner)?,
240 invited_node: wire.invited_node.map(|node| parse_public_key(&node)).transpose()?,
241 members,
242 folders,
243 shared_secret,
244 })
245 }
246}
247
248pub(crate) fn network_topic(id: NetworkId) -> TopicId {
249 let digest = blake3::hash(format!("syncweb/net/{id}").as_bytes());
250 TopicId::from_bytes(*digest.as_bytes())
251}
252
253pub(crate) fn parse_public_key(value: &str) -> Result<PublicKey> {
254 value
255 .parse()
256 .map_err(|error| SyncwebError::InvalidTicket(format!("invalid node ID: {error}")))
257}