Skip to main content

deltachat/
peer_channels.rs

1//! Peer channels for realtime communication in webxdcs.
2//!
3//! We use Iroh as an ephemeral peer channels provider to create direct communication
4//! channels between webxdcs. See [here](https://webxdc.org/docs/spec/joinRealtimeChannel.html) for the webxdc specs.
5//!
6//! Ephemeral channels should be established lazily, to avoid bootstrapping p2p connectivity
7//! when it's not required. Only when a webxdc subscribes to realtime data or when a reatlime message is sent,
8//! the p2p machinery should be started.
9//!
10//! Adding peer channels to webxdc needs upfront negotiation of a topic and sharing of public keys so that
11//! nodes can connect to each other. The explicit approach is as follows:
12//!
13//! 1. We introduce a new [`IrohGossipTopic`](crate::headerdef::HeaderDef::IrohGossipTopic) message header with a random 32-byte TopicId,
14//!    securely generated on the initial webxdc sender's device. This message header is encrypted
15//!    and sent in the same message as the webxdc application.
16//! 2. Whenever `joinRealtimeChannel().setListener()` or `joinRealtimeChannel().send()` is called by the webxdc application,
17//!    we start a routine to establish p2p connectivity and join the gossip swarm with Iroh.
18//! 3. The first step of this routine is to introduce yourself with a regular message containing the [`IrohNodeAddr`](crate::headerdef::HeaderDef::IrohNodeAddr).
19//!    This message contains the users relay-server and public key.
20//!    Direct IP address is not included as this information can be persisted by email providers.
21//! 4. After the announcement, the sending peer joins the gossip swarm with an empty list of peer IDs (as they don't know anyone yet).
22//! 5. Upon receiving an announcement message, other peers store the sender's [NodeAddr] in the database
23//!    (scoped per WebXDC app instance/message-id). The other peers can then join the gossip with `joinRealtimeChannel().setListener()`
24//!    and `joinRealtimeChannel().send()` just like the other peers.
25
26use anyhow::{Context as _, Result, anyhow, bail};
27use data_encoding::BASE32_NOPAD;
28use futures_lite::StreamExt;
29use iroh::{Endpoint, NodeAddr, NodeId, PublicKey, RelayMode, RelayUrl, SecretKey};
30use iroh_gossip::net::{Event, GOSSIP_ALPN, Gossip, GossipEvent, JoinOptions};
31use iroh_gossip::proto::TopicId;
32use parking_lot::Mutex;
33use std::collections::{BTreeSet, HashMap};
34use std::env;
35use tokio::sync::{RwLock, oneshot};
36use tokio::task::JoinHandle;
37use url::Url;
38
39use crate::EventType;
40use crate::chat::send_msg;
41use crate::config::Config;
42use crate::context::Context;
43use crate::log::warn;
44use crate::message::{Message, MsgId, Viewtype};
45use crate::mimeparser::SystemMessage;
46
47/// The length of an ed25519 `PublicKey`, in bytes.
48const PUBLIC_KEY_LENGTH: usize = 32;
49const PUBLIC_KEY_STUB: &[u8] = "static_string".as_bytes();
50
51/// Store Iroh peer channels for the context.
52#[derive(Debug)]
53pub struct Iroh {
54    /// Iroh router  needed for Iroh peer channels.
55    pub(crate) router: iroh::protocol::Router,
56
57    /// [Gossip] needed for Iroh peer channels.
58    pub(crate) gossip: Gossip,
59
60    /// Sequence numbers for gossip channels.
61    pub(crate) sequence_numbers: Mutex<HashMap<TopicId, i32>>,
62
63    /// Topics for which an advertisement has already been sent.
64    pub(crate) iroh_channels: RwLock<HashMap<TopicId, ChannelState>>,
65
66    /// Currently used Iroh public key.
67    ///
68    /// This is attached to every message to work around `iroh_gossip` deduplication.
69    pub(crate) public_key: PublicKey,
70}
71
72impl Iroh {
73    /// Notify the endpoint that the network has changed.
74    pub(crate) async fn network_change(&self) {
75        self.router.endpoint().network_change().await
76    }
77
78    /// Closes the QUIC endpoint.
79    pub(crate) async fn close(self) -> Result<()> {
80        self.router.shutdown().await.context("Closing iroh failed")
81    }
82
83    /// Join a topic and create the subscriber loop for it.
84    ///
85    /// If there is no gossip, create it.
86    ///
87    /// The returned future resolves when the swarm becomes operational.
88    async fn join_and_subscribe_gossip(
89        &self,
90        ctx: &Context,
91        msg_id: MsgId,
92    ) -> Result<Option<oneshot::Receiver<()>>> {
93        let topic = get_iroh_topic_for_msg(ctx, msg_id)
94            .await?
95            .with_context(|| format!("Message {msg_id} has no gossip topic"))?;
96
97        // Take exclusive lock to make sure
98        // no other thread can create a second gossip subscription
99        // after we check that it does not exist and before we create a new one.
100        // Otherwise we would receive every message twice or more times.
101        let mut iroh_channels = self.iroh_channels.write().await;
102
103        if iroh_channels.contains_key(&topic) {
104            return Ok(None);
105        }
106
107        let peers = get_iroh_gossip_peers(ctx, msg_id).await?;
108        let node_ids = peers.iter().map(|p| p.node_id).collect::<Vec<_>>();
109
110        info!(
111            ctx,
112            "IROH_REALTIME: Joining gossip {topic} with peers: {:?}.", node_ids,
113        );
114
115        // Inform iroh of potentially new node addresses
116        for node_addr in &peers {
117            if !node_addr.is_empty() {
118                self.router.endpoint().add_node_addr(node_addr.clone())?;
119            }
120        }
121
122        let (join_tx, join_rx) = oneshot::channel();
123
124        let (gossip_sender, gossip_receiver) = self
125            .gossip
126            .subscribe_with_opts(topic, JoinOptions::with_bootstrap(node_ids))
127            .split();
128
129        let ctx = ctx.clone();
130        let subscribe_loop = tokio::spawn(async move {
131            if let Err(e) = subscribe_loop(&ctx, gossip_receiver, topic, msg_id, join_tx).await {
132                warn!(ctx, "subscribe_loop failed: {e}")
133            }
134        });
135
136        iroh_channels.insert(topic, ChannelState::new(subscribe_loop, gossip_sender));
137
138        Ok(Some(join_rx))
139    }
140
141    /// Add gossip peer to realtime channel if it is already active.
142    pub async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: NodeAddr) -> Result<()> {
143        if self.iroh_channels.read().await.get(&topic).is_some() {
144            self.router.endpoint().add_node_addr(peer.clone())?;
145            self.gossip.subscribe(topic, vec![peer.node_id])?;
146        }
147        Ok(())
148    }
149
150    /// Send realtime data to the gossip swarm.
151    pub async fn send_webxdc_realtime_data(
152        &self,
153        ctx: &Context,
154        msg_id: MsgId,
155        mut data: Vec<u8>,
156    ) -> Result<()> {
157        let topic = get_iroh_topic_for_msg(ctx, msg_id)
158            .await?
159            .with_context(|| format!("Message {msg_id} has no gossip topic"))?;
160        self.join_and_subscribe_gossip(ctx, msg_id).await?;
161
162        let seq_num = self.get_and_incr(&topic);
163
164        let mut iroh_channels = self.iroh_channels.write().await;
165        let state = iroh_channels
166            .get_mut(&topic)
167            .context("Just created state does not exist")?;
168        data.extend(seq_num.to_le_bytes());
169        data.extend(self.public_key.as_bytes());
170
171        state.sender.broadcast(data.into()).await?;
172
173        if env::var("REALTIME_DEBUG").is_ok() {
174            info!(ctx, "Sent realtime data");
175        }
176
177        Ok(())
178    }
179
180    fn get_and_incr(&self, topic: &TopicId) -> i32 {
181        let mut sequence_numbers = self.sequence_numbers.lock();
182        let entry = sequence_numbers.entry(*topic).or_default();
183        *entry = entry.wrapping_add(1);
184        *entry
185    }
186
187    /// Get the iroh [NodeAddr] without direct IP addresses.
188    ///
189    /// The address is guaranteed to have home relay URL set
190    /// as it is the only way to reach the node
191    /// without global discovery mechanisms.
192    pub(crate) async fn get_node_addr(&self) -> Result<NodeAddr> {
193        let mut addr = self.router.endpoint().node_addr().await?;
194        addr.direct_addresses = BTreeSet::new();
195        debug_assert!(addr.relay_url().is_some());
196        Ok(addr)
197    }
198
199    /// Leave the realtime channel for a given topic.
200    pub(crate) async fn leave_realtime(&self, topic: TopicId) -> Result<()> {
201        if let Some(channel) = self.iroh_channels.write().await.remove(&topic) {
202            // Dropping the last GossipTopic results in quitting the topic.
203            // It is split into GossipReceiver and GossipSender.
204            // GossipSender (`channel.sender`) is dropped automatically.
205
206            // Subscribe loop owns GossipReceiver.
207            // Aborting it and waiting for it to be dropped
208            // drops the receiver.
209            channel.subscribe_loop.abort();
210            let _ = channel.subscribe_loop.await;
211        }
212        Ok(())
213    }
214}
215
216/// Single gossip channel state.
217#[derive(Debug)]
218pub(crate) struct ChannelState {
219    /// The subscribe loop handle.
220    subscribe_loop: JoinHandle<()>,
221
222    sender: iroh_gossip::net::GossipSender,
223}
224
225impl ChannelState {
226    fn new(subscribe_loop: JoinHandle<()>, sender: iroh_gossip::net::GossipSender) -> Self {
227        Self {
228            subscribe_loop,
229            sender,
230        }
231    }
232}
233
234impl Context {
235    /// Create iroh endpoint and gossip.
236    async fn init_peer_channels(&self) -> Result<Iroh> {
237        info!(self, "Initializing peer channels.");
238        let secret_key = SecretKey::generate(rand_old::rngs::OsRng);
239        let public_key = secret_key.public();
240
241        let relay_mode = if let Some(relay_url) = self
242            .metadata
243            .read()
244            .await
245            .as_ref()
246            .and_then(|conf| conf.iroh_relay.clone())
247        {
248            RelayMode::Custom(RelayUrl::from(relay_url).into())
249        } else {
250            // FIXME: this should be RelayMode::Disabled instead.
251            // Currently using default relays because otherwise Rust tests fail.
252            RelayMode::Default
253        };
254
255        let endpoint = Endpoint::builder()
256            .tls_x509() // For compatibility with iroh <0.34.0
257            .secret_key(secret_key)
258            .alpns(vec![GOSSIP_ALPN.to_vec()])
259            .relay_mode(relay_mode)
260            .bind()
261            .await?;
262
263        // create gossip
264        // Allow messages up to 128 KB in size.
265        // We set the limit to 128 KiB to account for internal overhead,
266        // but only guarantee 128 KB of payload to WebXDC developers.
267
268        let gossip = Gossip::builder()
269            .max_message_size(128 * 1024)
270            .spawn(endpoint.clone())
271            .await?;
272
273        let router = iroh::protocol::Router::builder(endpoint)
274            .accept(GOSSIP_ALPN, gossip.clone())
275            .spawn();
276
277        Ok(Iroh {
278            router,
279            gossip,
280            sequence_numbers: Mutex::new(HashMap::new()),
281            iroh_channels: RwLock::new(HashMap::new()),
282            public_key,
283        })
284    }
285
286    /// Returns [`None`] if the peer channels has not been initialized.
287    pub async fn get_peer_channels(&self) -> Option<tokio::sync::RwLockReadGuard<'_, Iroh>> {
288        tokio::sync::RwLockReadGuard::<'_, std::option::Option<Iroh>>::try_map(
289            self.iroh.read().await,
290            |opt_iroh| opt_iroh.as_ref(),
291        )
292        .ok()
293    }
294
295    /// Get or initialize the iroh peer channel.
296    pub async fn get_or_try_init_peer_channel(
297        &self,
298    ) -> Result<tokio::sync::RwLockReadGuard<'_, Iroh>> {
299        if !self.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
300            bail!("Attempt to initialize Iroh when realtime is disabled");
301        }
302
303        if let Some(lock) = self.get_peer_channels().await {
304            return Ok(lock);
305        }
306
307        let lock = self.iroh.write().await;
308        match tokio::sync::RwLockWriteGuard::<'_, std::option::Option<Iroh>>::try_downgrade_map(
309            lock,
310            |opt_iroh| opt_iroh.as_ref(),
311        ) {
312            Ok(lock) => Ok(lock),
313            Err(mut lock) => {
314                let iroh = self.init_peer_channels().await?;
315                *lock = Some(iroh);
316                tokio::sync::RwLockWriteGuard::<'_, std::option::Option<Iroh>>::try_downgrade_map(
317                    lock,
318                    |opt_iroh| opt_iroh.as_ref(),
319                )
320                .map_err(|_| anyhow!("Downgrade should succeed as we just stored `Some` value"))
321            }
322        }
323    }
324
325    pub(crate) async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: NodeAddr) -> Result<()> {
326        if let Some(iroh) = &*self.iroh.read().await {
327            info!(
328                self,
329                "Adding (maybe existing) peer with id {} to {topic}.", peer.node_id
330            );
331            iroh.maybe_add_gossip_peer(topic, peer).await?;
332        }
333        Ok(())
334    }
335}
336
337/// Cache a peers [NodeId] for one topic.
338pub(crate) async fn iroh_add_peer_for_topic(
339    ctx: &Context,
340    msg_id: MsgId,
341    topic: TopicId,
342    peer: NodeId,
343    relay_server: Option<&str>,
344) -> Result<()> {
345    ctx.sql
346        .execute(
347            "INSERT OR REPLACE INTO iroh_gossip_peers (msg_id, public_key, topic, relay_server) VALUES (?, ?, ?, ?)",
348            (msg_id, peer.as_bytes(), topic.as_bytes(), relay_server),
349        )
350        .await?;
351    Ok(())
352}
353
354/// Add gossip peer from `Iroh-Node-Addr` header to WebXDC message identified by `instance_id`.
355pub async fn add_gossip_peer_from_header(
356    context: &Context,
357    instance_id: MsgId,
358    node_addr: &str,
359) -> Result<()> {
360    if !context
361        .get_config_bool(Config::WebxdcRealtimeEnabled)
362        .await?
363    {
364        return Ok(());
365    }
366
367    let node_addr =
368        serde_json::from_str::<NodeAddr>(node_addr).context("Failed to parse node address")?;
369
370    info!(
371        context,
372        "Adding iroh peer with node id {} to the topic of {instance_id}.", node_addr.node_id
373    );
374
375    context.emit_event(EventType::WebxdcRealtimeAdvertisementReceived {
376        msg_id: instance_id,
377    });
378
379    let Some(topic) = get_iroh_topic_for_msg(context, instance_id).await? else {
380        warn!(
381            context,
382            "Could not add iroh peer because {instance_id} has no topic."
383        );
384        return Ok(());
385    };
386
387    let node_id = node_addr.node_id;
388    let relay_server = node_addr.relay_url().map(|relay| relay.as_str());
389    iroh_add_peer_for_topic(context, instance_id, topic, node_id, relay_server).await?;
390
391    context.maybe_add_gossip_peer(topic, node_addr).await?;
392    Ok(())
393}
394
395/// Insert topicId into the database so that we can use it to retrieve the topic.
396pub(crate) async fn insert_topic_stub(ctx: &Context, msg_id: MsgId, topic: TopicId) -> Result<()> {
397    ctx.sql
398        .execute(
399            "INSERT OR REPLACE INTO iroh_gossip_peers (msg_id, public_key, topic, relay_server) VALUES (?, ?, ?, ?)",
400            (msg_id, PUBLIC_KEY_STUB, topic.as_bytes(), Option::<&str>::None),
401        )
402        .await?;
403    Ok(())
404}
405
406/// Get a list of [NodeAddr]s for one webxdc.
407async fn get_iroh_gossip_peers(ctx: &Context, msg_id: MsgId) -> Result<Vec<NodeAddr>> {
408    ctx.sql
409        .query_map(
410            "SELECT public_key, relay_server FROM iroh_gossip_peers WHERE msg_id = ? AND public_key != ?",
411            (msg_id, PUBLIC_KEY_STUB),
412            |row| {
413                let key:  Vec<u8> = row.get(0)?;
414                let server: Option<String> = row.get(1)?;
415                Ok((key, server))
416            },
417            |g| {
418                g.map(|data| {
419                    let (key, server) = data?;
420                    let server = server.map(|data| Ok::<_, url::ParseError>(RelayUrl::from(Url::parse(&data)?))).transpose()?;
421                    let id = NodeId::from_bytes(&key.try_into()
422                    .map_err(|_| anyhow!("Can't convert sql data to [u8; 32]"))?)?;
423                    Ok::<_, anyhow::Error>(NodeAddr::from_parts(
424                        id, server, vec![]
425                    ))
426                })
427                .collect::<std::result::Result<Vec<_>, _>>()
428            },
429        )
430        .await
431}
432
433/// Get the topic for a given [MsgId].
434pub(crate) async fn get_iroh_topic_for_msg(
435    ctx: &Context,
436    msg_id: MsgId,
437) -> Result<Option<TopicId>> {
438    if let Some(bytes) = ctx
439        .sql
440        .query_get_value::<Vec<u8>>(
441            "SELECT topic FROM iroh_gossip_peers WHERE msg_id = ? LIMIT 1",
442            (msg_id,),
443        )
444        .await
445        .context("Couldn't restore topic from db")?
446    {
447        let topic_id = TopicId::from_bytes(
448            bytes
449                .try_into()
450                .map_err(|_| anyhow!("Could not convert stored topic ID"))?,
451        );
452        Ok(Some(topic_id))
453    } else {
454        Ok(None)
455    }
456}
457
458/// Send a gossip advertisement to the chat that [MsgId] belongs to.
459/// This method should be called from the frontend when `joinRealtimeChannel` is called.
460pub async fn send_webxdc_realtime_advertisement(
461    ctx: &Context,
462    msg_id: MsgId,
463) -> Result<Option<oneshot::Receiver<()>>> {
464    if !ctx.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
465        return Ok(None);
466    }
467
468    let iroh = ctx.get_or_try_init_peer_channel().await?;
469    let conn = iroh.join_and_subscribe_gossip(ctx, msg_id).await?;
470
471    let webxdc = Message::load_from_db(ctx, msg_id).await?;
472    let mut msg = Message::new(Viewtype::Text);
473    msg.hidden = true;
474    msg.param.set_cmd(SystemMessage::IrohNodeAddr);
475    msg.in_reply_to = Some(webxdc.rfc724_mid.clone());
476    send_msg(ctx, webxdc.chat_id, &mut msg).await?;
477    info!(ctx, "IROH_REALTIME: Sent realtime advertisement");
478    Ok(conn)
479}
480
481/// Send realtime data to other peers using iroh.
482pub async fn send_webxdc_realtime_data(ctx: &Context, msg_id: MsgId, data: Vec<u8>) -> Result<()> {
483    if !ctx.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
484        return Ok(());
485    }
486
487    let iroh = ctx.get_or_try_init_peer_channel().await?;
488    iroh.send_webxdc_realtime_data(ctx, msg_id, data).await?;
489    Ok(())
490}
491
492/// Leave the gossip of the webxdc with given [MsgId].
493///
494/// NB: When this is called before closing a webxdc app in UIs, it must be guaranteed that
495/// `send_webxdc_realtime_*()` functions aren't called for the given `msg_id` anymore until the app
496/// is open again.
497pub async fn leave_webxdc_realtime(ctx: &Context, msg_id: MsgId) -> Result<()> {
498    let Some(iroh) = ctx.get_peer_channels().await else {
499        return Ok(());
500    };
501    let Some(topic) = get_iroh_topic_for_msg(ctx, msg_id).await? else {
502        return Ok(());
503    };
504    iroh.leave_realtime(topic).await?;
505    info!(ctx, "IROH_REALTIME: Left gossip for message {msg_id}");
506
507    Ok(())
508}
509
510/// Creates a new random gossip topic.
511fn create_random_topic() -> TopicId {
512    TopicId::from_bytes(rand::random())
513}
514
515/// Creates `Iroh-Gossip-Header` with a new random topic
516/// and stores the topic for the message.
517pub(crate) async fn create_iroh_header(ctx: &Context, msg_id: MsgId) -> Result<String> {
518    let topic = create_random_topic();
519    insert_topic_stub(ctx, msg_id, topic).await?;
520    let topic_string = BASE32_NOPAD.encode(topic.as_bytes()).to_ascii_lowercase();
521    Ok(topic_string)
522}
523
524/// Converts `Iroh-Gossip-Header` contents to iroh topic ID.
525pub(crate) fn iroh_topic_from_str(topic: &str) -> Result<TopicId> {
526    let mut topic_raw = [0u8; 32];
527    BASE32_NOPAD
528        .decode_mut(topic.to_ascii_uppercase().as_bytes(), &mut topic_raw)
529        .map_err(|e| e.error)
530        .context("Wrong gossip topic header")?;
531
532    let topic = TopicId::from_bytes(topic_raw);
533    Ok(topic)
534}
535
536#[expect(clippy::arithmetic_side_effects)]
537async fn subscribe_loop(
538    context: &Context,
539    mut stream: iroh_gossip::net::GossipReceiver,
540    topic: TopicId,
541    msg_id: MsgId,
542    join_tx: oneshot::Sender<()>,
543) -> Result<()> {
544    let mut join_tx = Some(join_tx);
545
546    while let Some(event) = stream.try_next().await? {
547        match event {
548            Event::Gossip(event) => match event {
549                GossipEvent::Joined(nodes) => {
550                    if let Some(join_tx) = join_tx.take() {
551                        // Try to notify that at least one peer joined,
552                        // but ignore the error if receiver is dropped and nobody listens.
553                        join_tx.send(()).ok();
554                    }
555
556                    for node in nodes {
557                        iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
558                    }
559                }
560                GossipEvent::NeighborUp(node) => {
561                    info!(context, "IROH_REALTIME: NeighborUp: {}", node.to_string());
562                    iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
563                }
564                GossipEvent::NeighborDown(_node) => {}
565                GossipEvent::Received(message) => {
566                    info!(context, "IROH_REALTIME: Received realtime data");
567                    context.emit_event(EventType::WebxdcRealtimeData {
568                        msg_id,
569                        data: message
570                            .content
571                            .get(0..message.content.len() - 4 - PUBLIC_KEY_LENGTH)
572                            .context("too few bytes in iroh message")?
573                            .into(),
574                    });
575                }
576            },
577            Event::Lagged => {
578                warn!(context, "Gossip lost some messages");
579            }
580        };
581    }
582    Ok(())
583}
584
585#[cfg(test)]
586mod tests {
587    use super::*;
588    use crate::{
589        EventType,
590        chat::{self, ChatId, add_contact_to_chat, resend_msgs, send_msg},
591        message::{Message, Viewtype},
592        receive_imf::receive_imf,
593        test_utils::{TestContext, TestContextManager},
594    };
595
596    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
597    async fn test_can_communicate() {
598        let mut tcm = TestContextManager::new();
599        let alice = &mut tcm.alice().await;
600        let bob = &mut tcm.bob().await;
601
602        // Alice sends webxdc to bob
603        let alice_chat = alice.create_chat(bob).await;
604        let mut instance = Message::new(Viewtype::File);
605        instance
606            .set_file_from_bytes(
607                alice,
608                "minimal.xdc",
609                include_bytes!("../test-data/webxdc/minimal.xdc"),
610                None,
611            )
612            .unwrap();
613
614        send_msg(alice, alice_chat.id, &mut instance).await.unwrap();
615        let alice_webxdc = alice.get_last_msg().await;
616        assert_eq!(alice_webxdc.get_viewtype(), Viewtype::Webxdc);
617
618        let webxdc = alice.pop_sent_msg().await;
619        let bob_webxdc = bob.recv_msg(&webxdc).await;
620        assert_eq!(bob_webxdc.get_viewtype(), Viewtype::Webxdc);
621
622        bob_webxdc.chat_id.accept(bob).await.unwrap();
623
624        // Alice advertises herself.
625        send_webxdc_realtime_advertisement(alice, alice_webxdc.id)
626            .await
627            .unwrap();
628
629        bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
630        loop {
631            let event = bob.evtracker.recv().await.unwrap();
632            if let EventType::WebxdcRealtimeAdvertisementReceived { msg_id } = event.typ {
633                assert!(msg_id == bob_webxdc.id);
634                break;
635            }
636        }
637
638        // Bob adds alice to gossip peers.
639        let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
640            .await
641            .unwrap()
642            .into_iter()
643            .map(|addr| addr.node_id)
644            .collect::<Vec<_>>();
645
646        assert_eq!(
647            members,
648            vec![
649                alice
650                    .get_or_try_init_peer_channel()
651                    .await
652                    .unwrap()
653                    .get_node_addr()
654                    .await
655                    .unwrap()
656                    .node_id
657            ]
658        );
659
660        bob.get_or_try_init_peer_channel()
661            .await
662            .unwrap()
663            .join_and_subscribe_gossip(bob, bob_webxdc.id)
664            .await
665            .unwrap()
666            .unwrap()
667            .await
668            .unwrap();
669
670        // Alice sends ephemeral message
671        alice
672            .get_or_try_init_peer_channel()
673            .await
674            .unwrap()
675            .send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
676            .await
677            .unwrap();
678
679        loop {
680            let event = bob.evtracker.recv().await.unwrap();
681            if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
682                if data == "alice -> bob".as_bytes() {
683                    break;
684                } else {
685                    panic!(
686                        "Unexpected status update: {}",
687                        String::from_utf8_lossy(&data)
688                    );
689                }
690            }
691        }
692        // Bob sends ephemeral message
693        bob.get_or_try_init_peer_channel()
694            .await
695            .unwrap()
696            .send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
697            .await
698            .unwrap();
699
700        loop {
701            let event = alice.evtracker.recv().await.unwrap();
702            if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
703                if data == "bob -> alice".as_bytes() {
704                    break;
705                } else {
706                    panic!(
707                        "Unexpected status update: {}",
708                        String::from_utf8_lossy(&data)
709                    );
710                }
711            }
712        }
713
714        // Alice adds bob to gossip peers.
715        let members = get_iroh_gossip_peers(alice, alice_webxdc.id)
716            .await
717            .unwrap()
718            .into_iter()
719            .map(|addr| addr.node_id)
720            .collect::<Vec<_>>();
721
722        assert_eq!(
723            members,
724            vec![
725                bob.get_or_try_init_peer_channel()
726                    .await
727                    .unwrap()
728                    .get_node_addr()
729                    .await
730                    .unwrap()
731                    .node_id
732            ]
733        );
734
735        bob.get_or_try_init_peer_channel()
736            .await
737            .unwrap()
738            .send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice 2".as_bytes().to_vec())
739            .await
740            .unwrap();
741
742        loop {
743            let event = alice.evtracker.recv().await.unwrap();
744            if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
745                if data == "bob -> alice 2".as_bytes() {
746                    break;
747                } else {
748                    panic!(
749                        "Unexpected status update: {}",
750                        String::from_utf8_lossy(&data)
751                    );
752                }
753            }
754        }
755
756        // Calling stop_io() closes iroh endpoint as well,
757        // even though I/O was not started in this test.
758        assert!(alice.iroh.read().await.is_some());
759        alice.stop_io().await;
760        assert!(alice.iroh.read().await.is_none());
761    }
762
763    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
764    async fn test_duplicated_out_of_order_advertisement() -> Result<()> {
765        let mut tcm = TestContextManager::new();
766        let alice = &mut tcm.alice().await;
767        let bob = &mut tcm.bob().await;
768
769        let alice_chat = alice.create_chat(bob).await;
770        let mut instance = Message::new(Viewtype::File);
771        instance.set_file_from_bytes(
772            alice,
773            "minimal.xdc",
774            include_bytes!("../test-data/webxdc/minimal.xdc"),
775            None,
776        )?;
777
778        send_msg(alice, alice_chat.id, &mut instance).await?;
779        let alice_webxdc = alice.get_last_msg().await;
780        assert_eq!(alice_webxdc.get_viewtype(), Viewtype::Webxdc);
781
782        let webxdc = alice.pop_sent_msg().await;
783        // Imagine that at this point Alice learns about Bob's new transport...
784        send_webxdc_realtime_advertisement(alice, alice_webxdc.id).await?;
785        let advertisement = alice.pop_sent_msg().await;
786
787        // Bob receives an out-of-order advertisement from his new transport.
788        receive_imf(bob, advertisement.payload().as_bytes(), false).await?;
789
790        let bob_webxdc = bob.recv_msg(&webxdc).await;
791        assert_eq!(bob_webxdc.get_viewtype(), Viewtype::Webxdc);
792
793        bob_webxdc.chat_id.accept(bob).await?;
794
795        bob.recv_msg_trash(&advertisement).await;
796        loop {
797            let event = bob.evtracker.recv().await.unwrap();
798            if let EventType::WebxdcRealtimeAdvertisementReceived { msg_id } = event.typ {
799                assert!(msg_id == bob_webxdc.id);
800                break;
801            }
802        }
803        let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
804            .await?
805            .into_iter()
806            .map(|addr| addr.node_id)
807            .collect::<Vec<_>>();
808        assert_eq!(
809            members,
810            vec![
811                alice
812                    .get_or_try_init_peer_channel()
813                    .await
814                    .unwrap()
815                    .get_node_addr()
816                    .await
817                    .unwrap()
818                    .node_id
819            ]
820        );
821        Ok(())
822    }
823
824    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
825    async fn test_can_reconnect() {
826        let mut tcm = TestContextManager::new();
827        let alice = &mut tcm.alice().await;
828        let bob = &mut tcm.bob().await;
829
830        assert!(
831            alice
832                .get_config_bool(Config::WebxdcRealtimeEnabled)
833                .await
834                .unwrap()
835        );
836        // Alice sends webxdc to bob
837        let alice_chat = alice.create_chat(bob).await;
838        let mut instance = Message::new(Viewtype::File);
839        instance
840            .set_file_from_bytes(
841                alice,
842                "minimal.xdc",
843                include_bytes!("../test-data/webxdc/minimal.xdc"),
844                None,
845            )
846            .unwrap();
847
848        send_msg(alice, alice_chat.id, &mut instance).await.unwrap();
849        let alice_webxdc = alice.get_last_msg().await;
850        assert_eq!(alice_webxdc.get_viewtype(), Viewtype::Webxdc);
851
852        let webxdc = alice.pop_sent_msg().await;
853        let bob_webxdc = bob.recv_msg(&webxdc).await;
854        assert_eq!(bob_webxdc.get_viewtype(), Viewtype::Webxdc);
855
856        bob_webxdc.chat_id.accept(bob).await.unwrap();
857
858        // Alice advertises herself.
859        send_webxdc_realtime_advertisement(alice, alice_webxdc.id)
860            .await
861            .unwrap();
862
863        bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
864
865        // Bob adds alice to gossip peers.
866        let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
867            .await
868            .unwrap()
869            .into_iter()
870            .map(|addr| addr.node_id)
871            .collect::<Vec<_>>();
872
873        assert_eq!(
874            members,
875            vec![
876                alice
877                    .get_or_try_init_peer_channel()
878                    .await
879                    .unwrap()
880                    .get_node_addr()
881                    .await
882                    .unwrap()
883                    .node_id
884            ]
885        );
886
887        bob.get_or_try_init_peer_channel()
888            .await
889            .unwrap()
890            .join_and_subscribe_gossip(bob, bob_webxdc.id)
891            .await
892            .unwrap()
893            .unwrap()
894            .await
895            .unwrap();
896
897        // Alice sends ephemeral message
898        alice
899            .get_or_try_init_peer_channel()
900            .await
901            .unwrap()
902            .send_webxdc_realtime_data(alice, alice_webxdc.id, "alice -> bob".as_bytes().to_vec())
903            .await
904            .unwrap();
905
906        loop {
907            let event = bob.evtracker.recv().await.unwrap();
908            if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
909                if data == "alice -> bob".as_bytes() {
910                    break;
911                } else {
912                    panic!(
913                        "Unexpected status update: {}",
914                        String::from_utf8_lossy(&data)
915                    );
916                }
917            }
918        }
919
920        let bob_topic = get_iroh_topic_for_msg(bob, bob_webxdc.id)
921            .await
922            .unwrap()
923            .unwrap();
924        let bob_sequence_number = bob
925            .iroh
926            .read()
927            .await
928            .as_ref()
929            .unwrap()
930            .sequence_numbers
931            .lock()
932            .get(&bob_topic)
933            .copied();
934        leave_webxdc_realtime(bob, bob_webxdc.id).await.unwrap();
935        let bob_sequence_number_after = bob
936            .iroh
937            .read()
938            .await
939            .as_ref()
940            .unwrap()
941            .sequence_numbers
942            .lock()
943            .get(&bob_topic)
944            .copied();
945        // Check that sequence number is persisted when leaving the channel.
946        assert_eq!(bob_sequence_number, bob_sequence_number_after);
947
948        bob.get_or_try_init_peer_channel()
949            .await
950            .unwrap()
951            .join_and_subscribe_gossip(bob, bob_webxdc.id)
952            .await
953            .unwrap()
954            .unwrap()
955            .await
956            .unwrap();
957
958        bob.get_or_try_init_peer_channel()
959            .await
960            .unwrap()
961            .send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
962            .await
963            .unwrap();
964
965        loop {
966            let event = alice.evtracker.recv().await.unwrap();
967            if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
968                if data == "bob -> alice".as_bytes() {
969                    break;
970                } else {
971                    panic!(
972                        "Unexpected status update: {}",
973                        String::from_utf8_lossy(&data)
974                    );
975                }
976            }
977        }
978
979        // channel is only used to remember if an advertisement has been sent
980        // bob for example does not change the channels because he never sends an
981        // advertisement
982        assert_eq!(
983            alice
984                .iroh
985                .read()
986                .await
987                .as_ref()
988                .unwrap()
989                .iroh_channels
990                .read()
991                .await
992                .len(),
993            1
994        );
995        leave_webxdc_realtime(alice, alice_webxdc.id).await.unwrap();
996        let topic = get_iroh_topic_for_msg(alice, alice_webxdc.id)
997            .await
998            .unwrap()
999            .unwrap();
1000        assert!(
1001            alice
1002                .iroh
1003                .read()
1004                .await
1005                .as_ref()
1006                .unwrap()
1007                .iroh_channels
1008                .read()
1009                .await
1010                .get(&topic)
1011                .is_none()
1012        );
1013    }
1014
1015    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1016    async fn test_parallel_connect() {
1017        let mut tcm = TestContextManager::new();
1018        let alice = &mut tcm.alice().await;
1019        let bob = &mut tcm.bob().await;
1020
1021        let chat = alice.create_chat(bob).await.id;
1022
1023        let mut instance = Message::new(Viewtype::File);
1024        instance
1025            .set_file_from_bytes(
1026                alice,
1027                "minimal.xdc",
1028                include_bytes!("../test-data/webxdc/minimal.xdc"),
1029                None,
1030            )
1031            .unwrap();
1032        connect_alice_bob(alice, chat, &mut instance, bob).await
1033    }
1034
1035    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1036    async fn test_webxdc_resend() {
1037        let mut tcm = TestContextManager::new();
1038        let alice = &mut tcm.alice().await;
1039        let bob = &mut tcm.bob().await;
1040        let group = chat::create_group(alice, "group chat").await.unwrap();
1041
1042        // Alice sends webxdc to bob
1043        let mut instance = Message::new(Viewtype::File);
1044        instance
1045            .set_file_from_bytes(
1046                alice,
1047                "minimal.xdc",
1048                include_bytes!("../test-data/webxdc/minimal.xdc"),
1049                None,
1050            )
1051            .unwrap();
1052
1053        add_contact_to_chat(alice, group, alice.add_or_lookup_contact_id(bob).await)
1054            .await
1055            .unwrap();
1056
1057        connect_alice_bob(alice, group, &mut instance, bob).await;
1058
1059        // fiona joins late
1060        let fiona = &mut tcm.fiona().await;
1061
1062        add_contact_to_chat(alice, group, alice.add_or_lookup_contact_id(fiona).await)
1063            .await
1064            .unwrap();
1065
1066        resend_msgs(alice, &[instance.id]).await.unwrap();
1067        let msg = alice.pop_sent_msg().await;
1068        let fiona_instance = fiona.recv_msg(&msg).await;
1069        fiona_instance.chat_id.accept(fiona).await.unwrap();
1070        assert!(fiona.ctx.iroh.read().await.is_none());
1071
1072        let fiona_connect_future = send_webxdc_realtime_advertisement(fiona, fiona_instance.id)
1073            .await
1074            .unwrap()
1075            .unwrap();
1076        let fiona_advert = fiona.pop_sent_msg().await;
1077        alice.recv_msg_trash(&fiona_advert).await;
1078
1079        fiona_connect_future.await.unwrap();
1080
1081        let realtime_send_loop = async {
1082            // Keep sending in a loop because right after joining
1083            // Fiona may miss messages.
1084            loop {
1085                send_webxdc_realtime_data(alice, instance.id, b"alice -> bob & fiona".into())
1086                    .await
1087                    .unwrap();
1088                tokio::time::sleep(std::time::Duration::from_secs(1)).await;
1089            }
1090        };
1091
1092        let realtime_receive_loop = async {
1093            loop {
1094                let event = fiona.evtracker.recv().await.unwrap();
1095                if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
1096                    if data == b"alice -> bob & fiona" {
1097                        break;
1098                    } else {
1099                        panic!(
1100                            "Unexpected status update: {}",
1101                            String::from_utf8_lossy(&data)
1102                        );
1103                    }
1104                }
1105            }
1106        };
1107        tokio::select!(
1108            _ = realtime_send_loop => {
1109                panic!("Send loop should never finish");
1110            },
1111            _ = realtime_receive_loop => {
1112                return;
1113            }
1114        );
1115    }
1116
1117    async fn connect_alice_bob(
1118        alice: &mut TestContext,
1119        alice_chat_id: ChatId,
1120        instance: &mut Message,
1121        bob: &mut TestContext,
1122    ) {
1123        send_msg(alice, alice_chat_id, instance).await.unwrap();
1124        let alice_webxdc = alice.get_last_msg().await;
1125
1126        let webxdc = alice.pop_sent_msg().await;
1127        let bob_webxdc = bob.recv_msg(&webxdc).await;
1128        assert_eq!(bob_webxdc.get_viewtype(), Viewtype::Webxdc);
1129
1130        bob_webxdc.chat_id.accept(bob).await.unwrap();
1131
1132        eprintln!("Sending advertisements");
1133        // Alice advertises herself.
1134        let alice_advertisement_future = send_webxdc_realtime_advertisement(alice, alice_webxdc.id)
1135            .await
1136            .unwrap()
1137            .unwrap();
1138        let alice_advertisement = alice.pop_sent_msg().await;
1139
1140        let bob_advertisement_future = send_webxdc_realtime_advertisement(bob, bob_webxdc.id)
1141            .await
1142            .unwrap()
1143            .unwrap();
1144        let bob_advertisement = bob.pop_sent_msg().await;
1145
1146        eprintln!("Receiving advertisements");
1147        bob.recv_msg_trash(&alice_advertisement).await;
1148        alice.recv_msg_trash(&bob_advertisement).await;
1149
1150        eprintln!("Alice and Bob wait for connection");
1151        alice_advertisement_future.await.unwrap();
1152        bob_advertisement_future.await.unwrap();
1153
1154        // Alice sends ephemeral message
1155        eprintln!("Sending ephemeral message");
1156        send_webxdc_realtime_data(alice, alice_webxdc.id, b"alice -> bob".into())
1157            .await
1158            .unwrap();
1159
1160        eprintln!("Waiting for ephemeral message");
1161        loop {
1162            let event = bob.evtracker.recv().await.unwrap();
1163            if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
1164                if data == b"alice -> bob" {
1165                    break;
1166                } else {
1167                    panic!(
1168                        "Unexpected status update: {}",
1169                        String::from_utf8_lossy(&data)
1170                    );
1171                }
1172            }
1173        }
1174    }
1175
1176    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1177    async fn test_peer_channels_disabled() {
1178        let mut tcm = TestContextManager::new();
1179        let alice = &mut tcm.alice().await;
1180
1181        alice
1182            .set_config_bool(Config::WebxdcRealtimeEnabled, false)
1183            .await
1184            .unwrap();
1185
1186        // creates iroh endpoint as side effect
1187        send_webxdc_realtime_advertisement(alice, MsgId::new(1))
1188            .await
1189            .unwrap();
1190
1191        assert!(alice.ctx.iroh.read().await.is_none());
1192
1193        // creates iroh endpoint as side effect
1194        send_webxdc_realtime_data(alice, MsgId::new(1), vec![])
1195            .await
1196            .unwrap();
1197
1198        assert!(alice.ctx.iroh.read().await.is_none());
1199
1200        leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap();
1201
1202        assert!(alice.ctx.iroh.read().await.is_none());
1203
1204        // This internal function should return error
1205        // if accidentally called with the setting disabled.
1206        assert!(alice.ctx.get_or_try_init_peer_channel().await.is_err());
1207    }
1208
1209    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1210    async fn test_leave_webxdc_realtime_uninitialized() {
1211        let mut tcm = TestContextManager::new();
1212        let alice = &mut tcm.alice().await;
1213
1214        alice
1215            .set_config_bool(Config::WebxdcRealtimeEnabled, true)
1216            .await
1217            .unwrap();
1218
1219        assert!(alice.ctx.iroh.read().await.is_none());
1220        leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap();
1221        assert!(alice.ctx.iroh.read().await.is_none());
1222    }
1223}