iroh_http_discovery/
lib.rs1#![deny(unsafe_code)]
13
14#[cfg(feature = "mdns")]
15use iroh::address_lookup::{DiscoveryEvent, MdnsAddressLookup};
16#[cfg(feature = "mdns")]
17use std::sync::Arc;
18
19#[derive(Debug)]
26pub enum DiscoveryError {
27 Setup(String),
30 InvalidServiceName(String),
32}
33
34impl std::fmt::Display for DiscoveryError {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 match self {
37 DiscoveryError::Setup(msg) => write!(f, "mDNS setup failed: {msg}"),
38 DiscoveryError::InvalidServiceName(msg) => {
39 write!(f, "invalid mDNS service name: {msg}")
40 }
41 }
42 }
43}
44
45impl std::error::Error for DiscoveryError {}
46
47#[derive(Debug, Clone)]
51pub struct PeerDiscoveryEvent {
52 pub is_active: bool,
54 pub node_id: String,
56 pub addrs: Vec<String>,
58}
59
60#[cfg(feature = "mdns")]
69pub struct BrowseSession {
70 rx: tokio::sync::mpsc::Receiver<DiscoveryEvent>,
71 _mdns: Arc<MdnsAddressLookup>,
72}
73
74#[cfg(feature = "mdns")]
75impl BrowseSession {
76 pub async fn next_event(&mut self) -> Option<PeerDiscoveryEvent> {
78 use iroh::TransportAddr;
79
80 let ev = self.rx.recv().await?;
81 Some(match ev {
82 DiscoveryEvent::Discovered { endpoint_info, .. } => {
83 let node_id = endpoint_info.endpoint_id.to_string();
84 let mut addrs = Vec::new();
85 for a in endpoint_info.data.addrs() {
86 match a {
87 TransportAddr::Ip(sock) => addrs.push(sock.to_string()),
88 TransportAddr::Relay(url) => addrs.push(url.to_string()),
89 other => addrs.push(format!("{:?}", other)),
90 }
91 }
92 PeerDiscoveryEvent {
93 is_active: true,
94 node_id,
95 addrs,
96 }
97 }
98 DiscoveryEvent::Expired { endpoint_id } => PeerDiscoveryEvent {
99 is_active: false,
100 node_id: endpoint_id.to_string(),
101 addrs: Vec::new(),
102 },
103 _ => return None,
104 })
105 }
106}
107
108#[cfg(feature = "mdns")]
113pub async fn start_browse(
114 ep: &iroh::Endpoint,
115 service_name: &str,
116) -> Result<BrowseSession, DiscoveryError> {
117 let mdns = Arc::new(
118 MdnsAddressLookup::builder()
119 .advertise(false)
120 .service_name(service_name)
121 .build(ep.id())
122 .map_err(|e| DiscoveryError::Setup(e.to_string()))?,
123 );
124 ep.address_lookup()
125 .map_err(|e| DiscoveryError::Setup(e.to_string()))?
126 .add(Arc::clone(&mdns));
127
128 use futures::StreamExt;
131 let mut stream = mdns.subscribe().await;
132 let (tx, rx) = tokio::sync::mpsc::channel(64);
133 tokio::spawn(async move {
134 while let Some(ev) = stream.next().await {
135 if tx.send(ev).await.is_err() {
136 break;
137 }
138 }
139 });
140
141 Ok(BrowseSession { rx, _mdns: mdns })
142}
143
144#[cfg(feature = "mdns")]
151pub struct AdvertiseSession {
152 _mdns: Arc<MdnsAddressLookup>,
153}
154
155#[cfg(feature = "mdns")]
159pub fn start_advertise(
160 ep: &iroh::Endpoint,
161 service_name: &str,
162) -> Result<AdvertiseSession, DiscoveryError> {
163 let mdns = Arc::new(
164 MdnsAddressLookup::builder()
165 .advertise(true)
166 .service_name(service_name)
167 .build(ep.id())
168 .map_err(|e| DiscoveryError::Setup(e.to_string()))?,
169 );
170 ep.address_lookup()
171 .map_err(|e| DiscoveryError::Setup(e.to_string()))?
172 .add(Arc::clone(&mdns));
173 Ok(AdvertiseSession { _mdns: mdns })
174}
175
176#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn peer_discovery_event_active_construction() {
184 let ev = PeerDiscoveryEvent {
185 is_active: true,
186 node_id: "node123".to_string(),
187 addrs: vec![
188 "127.0.0.1:4000".to_string(),
189 "relay://r.example.com".to_string(),
190 ],
191 };
192 assert!(ev.is_active);
193 assert_eq!(ev.node_id, "node123");
194 assert_eq!(ev.addrs.len(), 2);
195 assert!(ev.addrs.iter().any(|a| a.contains("127.0.0.1")));
196 }
197
198 #[test]
199 fn peer_discovery_event_expired_construction() {
200 let ev = PeerDiscoveryEvent {
201 is_active: false,
202 node_id: "expired_node".to_string(),
203 addrs: vec![],
204 };
205 assert!(!ev.is_active);
206 assert_eq!(ev.node_id, "expired_node");
207 assert!(ev.addrs.is_empty(), "expired events carry no addresses");
208 }
209
210 #[test]
211 fn peer_discovery_event_clone_preserves_all_fields() {
212 let original = PeerDiscoveryEvent {
213 is_active: true,
214 node_id: "abc".to_string(),
215 addrs: vec!["10.0.0.1:1234".to_string()],
216 };
217 let cloned = original.clone();
218 assert_eq!(cloned.is_active, original.is_active);
219 assert_eq!(cloned.node_id, original.node_id);
220 assert_eq!(cloned.addrs, original.addrs);
221 }
222
223 #[tokio::test]
227 async fn channel_close_on_sender_drop() {
228 let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(1);
229 drop(tx);
230 assert!(
231 rx.recv().await.is_none(),
232 "recv() must return None when all senders are dropped"
233 );
234 }
235}