Skip to main content

bitwarden_ipc/
ipc_client.rs

1use std::sync::{Arc, Mutex};
2
3use bitwarden_threading::cancellation_token::CancellationToken;
4use serde::de::DeserializeOwned;
5use thiserror::Error;
6use tokio::select;
7
8use crate::{
9    broadcast::{BroadcastGroup, EndpointSendResult},
10    constants::CHANNEL_BUFFER_CAPACITY,
11    error::{
12        AlreadyRunningError, BroadcastError, ErrorKind, IpcErrorKind, ReceiveError, SendError,
13        SubscribeError, TypedReceiveError,
14    },
15    fragmentation::{
16        FragmentationError, Fragments, MAX_PAYLOAD_SIZE, REASSEMBLY_IDLE_TIMEOUT,
17        REASSEMBLY_TIMEOUT, Reassembler,
18    },
19    ipc_client_trait::IpcClient,
20    message::{IncomingMessage, OutgoingMessage, PayloadTypeName, TypedIncomingMessage},
21    rpc::{
22        error::RpcError,
23        exec::{
24            handler::{ErasedRpcHandler, RpcRequestInfo},
25            handler_registry::RpcHandlerRegistry,
26        },
27        request_message::{RPC_REQUEST_PAYLOAD_TYPE_NAME, RpcRequestPayload},
28        response_message::OutgoingRpcResponseMessage,
29    },
30    serde_utils,
31    traits::{BroadcastResolution, CommunicationBackend, CryptoProvider, SessionRepository},
32};
33
34/// A subscription to receive messages over IPC.
35/// The subcription will start buffering messages after its creation and return them
36/// when receive() is called. Messages received before the subscription was created will not be
37/// returned.
38pub struct IpcClientSubscription {
39    pub(crate) receiver: tokio::sync::broadcast::Receiver<IncomingMessage>,
40    pub(crate) topic: Option<String>,
41}
42
43/// A subscription to receive messages over IPC.
44/// The subcription will start buffering messages after its creation and return them
45/// when receive() is called. Messages received before the subscription was created will not be
46/// returned.
47pub struct IpcClientTypedSubscription<Payload: DeserializeOwned + PayloadTypeName>(
48    IpcClientSubscription,
49    std::marker::PhantomData<Payload>,
50);
51
52/// Internal shared state for the IPC client.
53struct IpcClientInner<Crypto, Com, Ses>
54where
55    Crypto: CryptoProvider<Com, Ses>,
56    Com: CommunicationBackend,
57    Ses: SessionRepository<Crypto::Session>,
58{
59    crypto: Crypto,
60    communication: Com,
61    sessions: Ses,
62
63    handlers: RpcHandlerRegistry,
64    reassembler: Reassembler,
65    incoming: Mutex<Option<tokio::sync::broadcast::WeakSender<IncomingMessage>>>,
66    cancellation_token: Mutex<Option<CancellationToken>>,
67}
68
69/// An IPC client that handles communication between different components and clients.
70/// It uses a crypto provider to encrypt and decrypt messages, a communication backend to send and
71/// receive messages, and a session repository to persist sessions.
72///
73/// This is the concrete implementation of the [`IpcClient`] trait.
74pub struct IpcClientImpl<Crypto, Com, Ses>
75where
76    Crypto: CryptoProvider<Com, Ses>,
77    Com: CommunicationBackend,
78    Ses: SessionRepository<Crypto::Session>,
79{
80    inner: Arc<IpcClientInner<Crypto, Com, Ses>>,
81}
82
83impl<Crypto, Com, Ses> Clone for IpcClientImpl<Crypto, Com, Ses>
84where
85    Crypto: CryptoProvider<Com, Ses>,
86    Com: CommunicationBackend,
87    Ses: SessionRepository<Crypto::Session>,
88{
89    fn clone(&self) -> Self {
90        Self {
91            inner: self.inner.clone(),
92        }
93    }
94}
95
96impl<Crypto, Com, Ses> IpcClientImpl<Crypto, Com, Ses>
97where
98    Crypto: CryptoProvider<Com, Ses>,
99    Com: CommunicationBackend,
100    Ses: SessionRepository<Crypto::Session>,
101{
102    /// Create a new IPC client with the provided crypto provider, communication backend, and
103    /// session repository.
104    pub fn new(crypto: Crypto, communication: Com, sessions: Ses) -> Self {
105        Self {
106            inner: Arc::new(IpcClientInner {
107                crypto,
108                communication,
109                sessions,
110
111                handlers: RpcHandlerRegistry::new(),
112                reassembler: Reassembler::new(REASSEMBLY_TIMEOUT, REASSEMBLY_IDLE_TIMEOUT),
113                incoming: Mutex::new(None),
114                cancellation_token: Mutex::new(None),
115            }),
116        }
117    }
118}
119
120#[async_trait::async_trait]
121impl<Crypto, Com, Ses> crate::ipc_client_trait::IpcClient for IpcClientImpl<Crypto, Com, Ses>
122where
123    Crypto: CryptoProvider<Com, Ses>,
124    Com: CommunicationBackend,
125    Ses: SessionRepository<Crypto::Session>,
126{
127    async fn start(
128        &self,
129        cancellation_token: Option<CancellationToken>,
130    ) -> Result<(), AlreadyRunningError> {
131        if self.is_running() {
132            return Err(AlreadyRunningError);
133        }
134
135        let cancellation_token = cancellation_token.unwrap_or_default();
136        self.inner
137            .cancellation_token
138            .lock()
139            .expect("Failed to lock cancellation token mutex")
140            .replace(cancellation_token.clone());
141
142        let com_receiver = self.inner.communication.subscribe().await;
143        let (client_tx, _) = tokio::sync::broadcast::channel(CHANNEL_BUFFER_CAPACITY);
144
145        self.inner
146            .incoming
147            .lock()
148            .expect("Failed to lock incoming mutex")
149            .replace(client_tx.downgrade());
150
151        let inner = self.inner.clone();
152        let future = async move {
153            loop {
154                select! {
155                    _ = cancellation_token.cancelled() => {
156                        tracing::debug!("Cancellation signal received, stopping IPC client");
157                        break;
158                    }
159                    received = inner.crypto.receive(&com_receiver, &inner.communication, &inner.sessions) => {
160                        match received {
161                            Ok(message) => {
162                                let Some(message) = inner.reassembler.process(message) else {
163                                    continue;
164                                };
165                                if message.topic.as_deref() == Some(RPC_REQUEST_PAYLOAD_TYPE_NAME) {
166                                    handle_rpc_request(&inner, message);
167                                } else if client_tx.send(message).is_err() {
168                                    tracing::debug!("No subscriptions for incoming message, dropping it");
169                                }
170                            }
171                            Err(error) if matches!(error.kind(), ErrorKind::Fatal) => {
172                                tracing::error!(?error, "Fatal error receiving message, stopping IPC client");
173                                break;
174                            }
175                            Err(error) => {
176                                tracing::warn!(?error, "Recoverable error receiving message, continuing");
177                            }
178                        }
179                    }
180                }
181            }
182            tracing::debug!("IPC client shutting down");
183            stop_inner(&inner);
184        };
185
186        bitwarden_threading::spawn(future);
187
188        Ok(())
189    }
190
191    fn is_running(&self) -> bool {
192        let has_incoming = self
193            .inner
194            .incoming
195            .lock()
196            .expect("Failed to lock incoming mutex")
197            .as_ref()
198            .is_some_and(|sender| sender.strong_count() > 0);
199        let has_cancellation_token = self
200            .inner
201            .cancellation_token
202            .lock()
203            .expect("Failed to lock cancellation token mutex")
204            .is_some();
205        has_incoming && has_cancellation_token
206    }
207
208    async fn send(&self, message: OutgoingMessage) -> Result<(), SendError> {
209        send_message(&self.inner, message).await.result
210    }
211
212    async fn broadcast(
213        &self,
214        group: BroadcastGroup,
215        payload: Vec<u8>,
216        topic: Option<String>,
217    ) -> Result<Vec<EndpointSendResult>, BroadcastError> {
218        let endpoints = match self.inner.communication.resolve(&group).await {
219            BroadcastResolution::Endpoints(endpoints) => endpoints,
220            BroadcastResolution::Delegate(_) => return Err(BroadcastError::DelegationUnsupported),
221            BroadcastResolution::Unsupported => return Err(BroadcastError::Unsupported),
222        };
223
224        // Validate the whole resolution before sending anything, so a backend that names an
225        // endpoint outside the group cannot cause a partial delivery to it.
226        if let Some(endpoint) = endpoints.iter().find(|endpoint| !group.contains(endpoint)) {
227            return Err(BroadcastError::OutOfScope(endpoint.clone()));
228        }
229
230        // Endpoints are sent to one at a time. The crypto provider may serialize sends anyway (the
231        // Noise provider holds one lock per client for the whole send, handshake included), in
232        // which case concurrent sends would not deliver any sooner. A fatal error stops the client,
233        // so the remaining endpoints are not attempted and are absent from the results.
234        let mut results = Vec::with_capacity(endpoints.len());
235        for endpoint in endpoints {
236            let message = OutgoingMessage {
237                payload: payload.clone(),
238                destination: endpoint.clone(),
239                topic: topic.clone(),
240            };
241            let outcome = send_message(&self.inner, message).await;
242            results.push(EndpointSendResult {
243                endpoint,
244                result: outcome.result,
245            });
246            if outcome.fatal {
247                break;
248            }
249        }
250
251        Ok(results)
252    }
253
254    async fn subscribe(
255        &self,
256        topic: Option<String>,
257    ) -> Result<IpcClientSubscription, SubscribeError> {
258        Ok(IpcClientSubscription {
259            receiver: self
260                .inner
261                .incoming
262                .lock()
263                .expect("Failed to lock incoming mutex")
264                .as_ref()
265                .and_then(|sender| sender.upgrade())
266                .ok_or(SubscribeError::NotStarted)?
267                .subscribe(),
268            topic,
269        })
270    }
271
272    async fn register_rpc_handler_erased(&self, name: &str, handler: Box<dyn ErasedRpcHandler>) {
273        self.inner
274            .handlers
275            .register_erased(name.to_owned(), handler)
276            .await;
277    }
278}
279
280/// The outcome of [`send_message`].
281struct SendOutcome {
282    result: Result<(), SendError>,
283    /// Whether the send failed with a fatal error, which has stopped the client.
284    fatal: bool,
285}
286
287/// Send a message through the crypto provider, split into as many fragments as the provider's
288/// payload limit requires. Fragments are sent in order and sending stops at the first failed
289/// fragment. A fatal error stops the client.
290async fn send_message<Crypto, Com, Ses>(
291    inner: &IpcClientInner<Crypto, Com, Ses>,
292    message: OutgoingMessage,
293) -> SendOutcome
294where
295    Crypto: CryptoProvider<Com, Ses>,
296    Com: CommunicationBackend,
297    Ses: SessionRepository<Crypto::Session>,
298{
299    let fragments = match Fragments::split(message, inner.crypto.max_payload_len()) {
300        Ok(fragments) => fragments,
301        Err(e) => {
302            return SendOutcome {
303                result: Err(SendError::Other(e.to_string())),
304                fatal: false,
305            };
306        }
307    };
308
309    for fragment in fragments {
310        let outcome = send_fragment(inner, fragment).await;
311        if outcome.result.is_err() {
312            return outcome;
313        }
314    }
315
316    SendOutcome {
317        result: Ok(()),
318        fatal: false,
319    }
320}
321
322/// Send a single fragment through the crypto provider, stopping the client on a fatal error.
323async fn send_fragment<Crypto, Com, Ses>(
324    inner: &IpcClientInner<Crypto, Com, Ses>,
325    message: OutgoingMessage,
326) -> SendOutcome
327where
328    Crypto: CryptoProvider<Com, Ses>,
329    Com: CommunicationBackend,
330    Ses: SessionRepository<Crypto::Session>,
331{
332    let result = inner
333        .crypto
334        .send(&inner.communication, &inner.sessions, message)
335        .await;
336
337    if let Err(ref error) = result {
338        match error.kind() {
339            ErrorKind::Fatal => {
340                tracing::error!(?error, "Fatal error sending message, stopping IPC client");
341                stop_inner(inner);
342            }
343            // An unreachable destination is an expected condition and not logged
344            ErrorKind::Unreachable => {}
345            // Every other recoverable send failure is still surfaced.
346            ErrorKind::Other => {
347                tracing::debug!(
348                    ?error,
349                    "Recoverable error sending message, IPC client will continue running"
350                );
351            }
352        }
353    }
354
355    SendOutcome {
356        fatal: matches!(&result, Err(e) if e.kind() == ErrorKind::Fatal),
357        result: result.map_err(|e| match e.kind() {
358            ErrorKind::Unreachable => SendError::Unreachable,
359            _ => SendError::Other(format!("{e:?}")),
360        }),
361    }
362}
363
364fn stop_inner<Crypto, Com, Ses>(inner: &IpcClientInner<Crypto, Com, Ses>)
365where
366    Crypto: CryptoProvider<Com, Ses>,
367    Com: CommunicationBackend,
368    Ses: SessionRepository<Crypto::Session>,
369{
370    let mut cancellation_token = inner
371        .cancellation_token
372        .lock()
373        .expect("Failed to lock cancellation token mutex");
374
375    if let Some(cancellation_token) = cancellation_token.take() {
376        cancellation_token.cancel();
377    }
378}
379
380fn handle_rpc_request<Crypto, Com, Ses>(
381    inner: &Arc<IpcClientInner<Crypto, Com, Ses>>,
382    incoming_message: IncomingMessage,
383) where
384    Crypto: CryptoProvider<Com, Ses>,
385    Com: CommunicationBackend,
386    Ses: SessionRepository<Crypto::Session>,
387{
388    let inner = inner.clone();
389    let future = async move {
390        #[derive(Debug, Error)]
391        enum HandleError {
392            #[error("Failed to deserialize request message: {0}")]
393            Deserialize(String),
394
395            #[error("Failed to serialize response message: {0}")]
396            Serialize(String),
397        }
398
399        async fn handle(
400            incoming_message: IncomingMessage,
401            handlers: &RpcHandlerRegistry,
402        ) -> Result<OutgoingMessage, HandleError> {
403            let IncomingMessage {
404                payload, source, ..
405            } = incoming_message;
406            let request = RpcRequestPayload::from_slice(payload).map_err(
407                |e: serde_utils::DeserializeError| HandleError::Deserialize(e.to_string()),
408            )?;
409
410            let info = RpcRequestInfo {
411                source: source.clone(),
412            };
413            let response = handlers.handle(&request, info).await;
414
415            let response_message = OutgoingRpcResponseMessage {
416                request_id: request.request_id(),
417                request_type: request.request_type(),
418                result: response,
419            };
420
421            // Publish the response on the dedicated topic the requester is subscribed to.
422            let mut payload = serde_utils::to_vec(&response_message)
423                .map_err(|e: serde_utils::SerializeError| HandleError::Serialize(e.to_string()))?;
424            if payload.len() > MAX_PAYLOAD_SIZE {
425                // Report the error, or the requester waits for a response that never arrives.
426                let error = FragmentationError::PayloadTooLarge(payload.len()).to_string();
427                let error_message = OutgoingRpcResponseMessage {
428                    result: Err(RpcError::ResponseSerialization(error)),
429                    ..response_message
430                };
431                payload = serde_utils::to_vec(&error_message).map_err(
432                    |e: serde_utils::SerializeError| HandleError::Serialize(e.to_string()),
433                )?;
434            }
435
436            Ok(OutgoingMessage {
437                payload,
438                destination: source.into(),
439                topic: Some(request.response_topic().to_owned()),
440            })
441        }
442
443        match handle(incoming_message, &inner.handlers).await {
444            Ok(outgoing_message) => {
445                // Send through the client, so the response is fragmented like any other message.
446                match (IpcClientImpl { inner }).send(outgoing_message).await {
447                    // A requester that went away is an expected condition and not logged.
448                    Ok(()) | Err(SendError::Unreachable) => {}
449                    Err(error) => tracing::error!(%error, "Failed to send response message"),
450                }
451            }
452            Err(error) => {
453                tracing::error!(%error, "Error handling RPC request");
454            }
455        }
456    };
457
458    bitwarden_threading::spawn(future);
459}
460
461impl IpcClientSubscription {
462    /// Receive a message, optionally filtering by topic.
463    /// Setting the cancellation_token to `None` will wait indefinitely.
464    pub async fn receive(
465        &mut self,
466        cancellation_token: Option<CancellationToken>,
467    ) -> Result<IncomingMessage, ReceiveError> {
468        let cancellation_token = cancellation_token.unwrap_or_default();
469
470        loop {
471            select! {
472                _ = cancellation_token.cancelled() => {
473                    return Err(ReceiveError::Cancelled)
474                }
475                result = self.receiver.recv() => {
476                    let received = result?;
477                    if self.topic.is_none() || received.topic == self.topic {
478                        return Ok::<IncomingMessage, ReceiveError>(received);
479                    }
480                }
481            }
482        }
483    }
484}
485
486impl<Payload> IpcClientTypedSubscription<Payload>
487where
488    Payload: DeserializeOwned + PayloadTypeName,
489{
490    pub(crate) fn new(subscription: IpcClientSubscription) -> Self {
491        Self(subscription, std::marker::PhantomData)
492    }
493
494    /// Receive a message.
495    /// Setting the cancellation_token to `None` will wait indefinitely.
496    pub async fn receive(
497        &mut self,
498        cancellation_token: Option<CancellationToken>,
499    ) -> Result<TypedIncomingMessage<Payload>, TypedReceiveError> {
500        let received = self.0.receive(cancellation_token).await?;
501        received
502            .try_into()
503            .map_err(|e: serde_utils::DeserializeError| TypedReceiveError::Typing(e.to_string()))
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use std::{collections::HashMap, time::Duration};
510
511    use bitwarden_threading::time::sleep;
512    use serde::{Deserialize, Serialize};
513
514    use super::*;
515    use crate::{
516        IpcClientExt,
517        endpoint::{Endpoint, HostId, Source},
518        ipc_client_trait::IpcClient,
519        message::PayloadTypeName,
520        rpc::{
521            request::RpcRequest,
522            request_message::{RPC_REQUEST_PAYLOAD_TYPE_NAME, RpcRequestMessage},
523            response_message::IncomingRpcResponseMessage,
524        },
525        traits::{InMemorySessionRepository, NoEncryptionCryptoProvider, TestCommunicationBackend},
526    };
527
528    /// Error type for [`TestCryptoProvider`] that carries an explicit fatal/recoverable
529    /// classification so tests can exercise both control-flow paths.
530    #[derive(Debug, Clone)]
531    struct TestCryptoError {
532        // Read only through the derived `Debug` impl (the outer `SendError` wraps it via
533        // `{e:?}`), which dead-code analysis does not count as a use.
534        #[allow(dead_code)]
535        message: String,
536        fatal: bool,
537    }
538
539    impl IpcErrorKind for TestCryptoError {
540        fn kind(&self) -> ErrorKind {
541            if self.fatal {
542                ErrorKind::Fatal
543            } else {
544                ErrorKind::Other
545            }
546        }
547    }
548
549    struct TestCryptoProvider {
550        /// Simulate a send result. Set to `None` wait indefinitely
551        send_result: Option<Result<(), TestCryptoError>>,
552        /// Simulate a receive result. Set to `None` wait indefinitely
553        receive_result: Option<Result<IncomingMessage, TestCryptoError>>,
554    }
555
556    type TestSessionRepository = InMemorySessionRepository<String>;
557    impl CryptoProvider<TestCommunicationBackend, TestSessionRepository> for TestCryptoProvider {
558        type Session = String;
559        type SendError = TestCryptoError;
560        type ReceiveError = TestCryptoError;
561
562        fn max_payload_len(&self) -> usize {
563            usize::MAX
564        }
565
566        async fn receive(
567            &self,
568            _receiver: &<TestCommunicationBackend as CommunicationBackend>::Receiver,
569            _communication: &TestCommunicationBackend,
570            _sessions: &TestSessionRepository,
571        ) -> Result<IncomingMessage, Self::ReceiveError> {
572            match &self.receive_result {
573                Some(result) => {
574                    // Yield (and throttle) so a recoverable error that makes the processing loop
575                    // `continue` doesn't busy-spin and starve the single-threaded test runtime.
576                    // Real backends await their underlying transport here, which has the same
577                    // effect.
578                    sleep(Duration::from_millis(5)).await;
579                    result.clone()
580                }
581                None => {
582                    // Simulate waiting for a message but never returning
583                    sleep(Duration::from_secs(600)).await;
584                    Err(TestCryptoError {
585                        message: "Simulated timeout".to_string(),
586                        fatal: true,
587                    })
588                }
589            }
590        }
591
592        async fn send(
593            &self,
594            _communication: &TestCommunicationBackend,
595            _sessions: &TestSessionRepository,
596            _message: OutgoingMessage,
597        ) -> Result<(), Self::SendError> {
598            match &self.send_result {
599                Some(result) => result.clone(),
600                None => {
601                    // Simulate waiting for a message to be send but never returning
602                    sleep(Duration::from_secs(600)).await;
603                    Err(TestCryptoError {
604                        message: "Simulated timeout".to_string(),
605                        fatal: true,
606                    })
607                }
608            }
609        }
610    }
611
612    #[tokio::test]
613    async fn returns_send_error_when_crypto_provider_returns_error() {
614        let message = OutgoingMessage {
615            payload: vec![],
616            destination: Endpoint::BrowserBackground { id: HostId::Own },
617            topic: None,
618        };
619        let crypto_provider = TestCryptoProvider {
620            send_result: Some(Err(TestCryptoError {
621                message: "Crypto error".to_string(),
622                fatal: false,
623            })),
624            receive_result: Some(Err(TestCryptoError {
625                message: "Should not have be called".to_string(),
626                fatal: false,
627            })),
628        };
629        let communication_provider = TestCommunicationBackend::new();
630        let session_map = TestSessionRepository::new(HashMap::new());
631        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
632        let _ = client.start(None).await;
633
634        let error = client.send(message).await.unwrap_err();
635
636        assert!(error.to_string().contains("Crypto error"));
637    }
638
639    #[tokio::test]
640    async fn communication_provider_has_outgoing_message_when_sending_through_ipc_client() {
641        let message = OutgoingMessage {
642            payload: vec![],
643            destination: Endpoint::BrowserBackground { id: HostId::Own },
644            topic: None,
645        };
646        let crypto_provider = NoEncryptionCryptoProvider;
647        let communication_provider = TestCommunicationBackend::new();
648        let session_map = InMemorySessionRepository::new(HashMap::new());
649        let client =
650            IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
651        let _ = client.start(None).await;
652
653        client.send(message.clone()).await.unwrap();
654
655        let outgoing_messages = communication_provider.outgoing().await;
656        assert_eq!(outgoing_messages, vec![message]);
657    }
658
659    #[tokio::test]
660    async fn returns_received_message_when_received_from_backend() {
661        let message = IncomingMessage {
662            payload: vec![],
663            source: Source::Web {
664                tab_id: 9001,
665                document_id: "doc-1".to_string(),
666                origin: "https://example.com".to_string(),
667            },
668            destination: Endpoint::BrowserBackground { id: HostId::Own },
669            topic: None,
670        };
671        let crypto_provider = NoEncryptionCryptoProvider;
672        let communication_provider = TestCommunicationBackend::new();
673        let session_map = InMemorySessionRepository::new(HashMap::new());
674        let client =
675            IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
676        let _ = client.start(None).await;
677
678        let mut subscription = client
679            .subscribe(None)
680            .await
681            .expect("Subscribing should not fail");
682        communication_provider.push_incoming(message.clone());
683        let received_message = subscription.receive(None).await.unwrap();
684
685        assert_eq!(received_message, message);
686    }
687
688    #[tokio::test]
689    async fn channel_keeps_no_message_every_subscription_has_received() {
690        let message = IncomingMessage {
691            payload: vec![1, 2, 3],
692            source: Source::DesktopMain,
693            destination: Endpoint::BrowserBackground { id: HostId::Own },
694            topic: Some("topic".to_owned()),
695        };
696        let communication_provider = TestCommunicationBackend::new();
697        let client = IpcClientImpl::new(
698            NoEncryptionCryptoProvider,
699            communication_provider.clone(),
700            InMemorySessionRepository::new(HashMap::new()),
701        );
702        let _ = client.start(None).await;
703        let queued = || {
704            client
705                .inner
706                .incoming
707                .lock()
708                .unwrap()
709                .as_ref()
710                .and_then(|sender| sender.upgrade())
711                .expect("Client should be running")
712                .len()
713        };
714
715        // Without subscriptions, the message is dropped and the client keeps running.
716        communication_provider.push_incoming(message.clone());
717        sleep(Duration::from_millis(50)).await;
718        assert_eq!(queued(), 0);
719        assert!(client.is_running());
720
721        let mut subscription = client.subscribe(None).await.unwrap();
722        communication_provider.push_incoming(message.clone());
723        assert_eq!(subscription.receive(None).await.unwrap(), message);
724        assert_eq!(queued(), 0);
725    }
726
727    #[tokio::test]
728    async fn skips_non_matching_topics_and_returns_first_matching_message() {
729        let non_matching_message = IncomingMessage {
730            payload: vec![],
731            source: Source::Web {
732                tab_id: 9001,
733                document_id: "doc-1".to_string(),
734                origin: "https://example.com".to_string(),
735            },
736            destination: Endpoint::BrowserBackground { id: HostId::Own },
737            topic: Some("non_matching_topic".to_owned()),
738        };
739        let matching_message = IncomingMessage {
740            payload: vec![109],
741            source: Source::Web {
742                tab_id: 9001,
743                document_id: "doc-1".to_string(),
744                origin: "https://example.com".to_string(),
745            },
746            destination: Endpoint::BrowserBackground { id: HostId::Own },
747            topic: Some("matching_topic".to_owned()),
748        };
749
750        let crypto_provider = NoEncryptionCryptoProvider;
751        let communication_provider = TestCommunicationBackend::new();
752        let session_map = InMemorySessionRepository::new(HashMap::new());
753        let client =
754            IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
755        let _ = client.start(None).await;
756        let mut subscription = client
757            .subscribe(Some("matching_topic".to_owned()))
758            .await
759            .expect("Subscribing should not fail");
760        communication_provider.push_incoming(non_matching_message.clone());
761        communication_provider.push_incoming(non_matching_message.clone());
762        communication_provider.push_incoming(matching_message.clone());
763
764        let received_message: IncomingMessage = subscription.receive(None).await.unwrap();
765
766        assert_eq!(received_message, matching_message);
767    }
768
769    #[tokio::test]
770    async fn skips_unrelated_messages_and_returns_typed_message() {
771        #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
772        struct TestPayload {
773            some_data: String,
774        }
775
776        impl PayloadTypeName for TestPayload {
777            const PAYLOAD_TYPE_NAME: &str = "TestPayload";
778        }
779
780        let unrelated = IncomingMessage {
781            payload: vec![],
782            source: Source::Web {
783                tab_id: 9001,
784                document_id: "doc-1".to_string(),
785                origin: "https://example.com".to_string(),
786            },
787            destination: Endpoint::BrowserBackground { id: HostId::Own },
788            topic: None,
789        };
790        let typed_message = crate::message::TypedIncomingMessage {
791            payload: TestPayload {
792                some_data: "Hello, world!".to_string(),
793            },
794            source: Source::Web {
795                tab_id: 9001,
796                document_id: "doc-1".to_string(),
797                origin: "https://example.com".to_string(),
798            },
799            destination: Endpoint::BrowserBackground { id: HostId::Own },
800        };
801
802        let crypto_provider = NoEncryptionCryptoProvider;
803        let communication_provider = TestCommunicationBackend::new();
804        let session_map = InMemorySessionRepository::new(HashMap::new());
805        let client =
806            IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
807        let _ = client.start(None).await;
808        let mut subscription = client
809            .subscribe_typed::<TestPayload>()
810            .await
811            .expect("Subscribing should not fail");
812        communication_provider.push_incoming(unrelated.clone());
813        communication_provider.push_incoming(unrelated.clone());
814        communication_provider.push_incoming(
815            typed_message
816                .clone()
817                .try_into()
818                .expect("Serialization should not fail"),
819        );
820
821        let received_message = subscription.receive(None).await.unwrap();
822
823        assert_eq!(received_message, typed_message);
824    }
825
826    #[tokio::test]
827    async fn returns_error_if_related_message_was_not_deserializable() {
828        #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
829        struct TestPayload {
830            some_data: String,
831        }
832
833        impl PayloadTypeName for TestPayload {
834            const PAYLOAD_TYPE_NAME: &str = "TestPayload";
835        }
836
837        let non_deserializable_message = IncomingMessage {
838            payload: vec![],
839            source: Source::Web {
840                tab_id: 9001,
841                document_id: "doc-1".to_string(),
842                origin: "https://example.com".to_string(),
843            },
844            destination: Endpoint::BrowserBackground { id: HostId::Own },
845            topic: Some("TestPayload".to_owned()),
846        };
847
848        let crypto_provider = NoEncryptionCryptoProvider;
849        let communication_provider = TestCommunicationBackend::new();
850        let session_map = InMemorySessionRepository::new(HashMap::new());
851        let client =
852            IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
853        let _ = client.start(None).await;
854        let mut subscription = client
855            .subscribe_typed::<TestPayload>()
856            .await
857            .expect("Subscribing should not fail");
858        communication_provider.push_incoming(non_deserializable_message.clone());
859
860        let result = subscription.receive(None).await;
861        assert!(matches!(result, Err(TypedReceiveError::Typing(_))));
862    }
863
864    #[tokio::test]
865    async fn ipc_client_stops_if_crypto_returns_fatal_send_error() {
866        let message = OutgoingMessage {
867            payload: vec![],
868            destination: Endpoint::BrowserBackground { id: HostId::Own },
869            topic: None,
870        };
871        let crypto_provider = TestCryptoProvider {
872            send_result: Some(Err(TestCryptoError {
873                message: "Crypto error".to_string(),
874                fatal: true,
875            })),
876            receive_result: None,
877        };
878        let communication_provider = TestCommunicationBackend::new();
879        let session_map = TestSessionRepository::new(HashMap::new());
880        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
881        let _ = client.start(None).await;
882
883        let error = client.send(message).await.unwrap_err();
884        let is_running = client.is_running();
885
886        assert!(error.to_string().contains("Crypto error"));
887        assert!(!is_running);
888    }
889
890    #[tokio::test]
891    async fn ipc_client_keeps_running_if_crypto_returns_recoverable_send_error() {
892        let message = OutgoingMessage {
893            payload: vec![],
894            destination: Endpoint::BrowserBackground { id: HostId::Own },
895            topic: None,
896        };
897        let crypto_provider = TestCryptoProvider {
898            // A recoverable send error (e.g. a handshake timeout because the peer is down) must
899            // not tear down the shared client.
900            send_result: Some(Err(TestCryptoError {
901                message: "Crypto error".to_string(),
902                fatal: false,
903            })),
904            // Block forever on receive so the loop stays alive while we inspect it.
905            receive_result: None,
906        };
907        let communication_provider = TestCommunicationBackend::new();
908        let session_map = TestSessionRepository::new(HashMap::new());
909        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
910        let _ = client.start(None).await;
911
912        let error = client.send(message).await.unwrap_err();
913        let is_running = client.is_running();
914
915        // The error is still surfaced to the caller...
916        assert!(error.to_string().contains("Crypto error"));
917        // ...but the client keeps running so future sends/requests can succeed.
918        assert!(is_running);
919    }
920
921    #[tokio::test]
922    async fn ipc_client_stops_if_crypto_returns_fatal_receive_error() {
923        let crypto_provider = TestCryptoProvider {
924            send_result: None,
925            receive_result: Some(Err(TestCryptoError {
926                message: "Crypto error".to_string(),
927                fatal: true,
928            })),
929        };
930        let communication_provider = TestCommunicationBackend::new();
931        let session_map = TestSessionRepository::new(HashMap::new());
932        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
933        let cancellation_token = CancellationToken::new();
934        let _ = client.start(Some(cancellation_token.clone())).await;
935
936        // Give the client some time to process the error
937        tokio::time::sleep(Duration::from_millis(100)).await;
938        let is_running = client.is_running();
939
940        assert!(!is_running);
941        assert!(cancellation_token.is_cancelled());
942    }
943
944    #[tokio::test]
945    async fn ipc_client_keeps_running_if_crypto_returns_recoverable_receive_error() {
946        let crypto_provider = TestCryptoProvider {
947            send_result: None,
948            // A recoverable receive error must not stop the processing loop.
949            receive_result: Some(Err(TestCryptoError {
950                message: "Crypto error".to_string(),
951                fatal: false,
952            })),
953        };
954        let communication_provider = TestCommunicationBackend::new();
955        let session_map = TestSessionRepository::new(HashMap::new());
956        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
957        let cancellation_token = CancellationToken::new();
958        let _ = client.start(Some(cancellation_token.clone())).await;
959
960        // Give the client time to hit the recoverable receive error (repeatedly).
961        tokio::time::sleep(Duration::from_millis(100)).await;
962        let is_running = client.is_running();
963
964        assert!(is_running);
965        assert!(!cancellation_token.is_cancelled());
966    }
967
968    #[tokio::test]
969    async fn ipc_client_is_not_running_if_cancellation_token_is_cancelled() {
970        let crypto_provider = TestCryptoProvider {
971            send_result: None,
972            receive_result: None,
973        };
974        let communication_provider = TestCommunicationBackend::new();
975        let session_map = TestSessionRepository::new(HashMap::new());
976        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
977        let cancellation_token = CancellationToken::new();
978        let _ = client.start(Some(cancellation_token.clone())).await;
979
980        // Give the client some time to process
981        tokio::time::sleep(Duration::from_millis(100)).await;
982
983        // Cancel the token and give the client some time to process the cancellation
984        cancellation_token.cancel();
985        tokio::time::sleep(Duration::from_millis(100)).await;
986        let is_running = client.is_running();
987
988        assert!(!is_running);
989    }
990
991    #[tokio::test]
992    async fn ipc_client_is_running_if_no_errors_are_encountered() {
993        let crypto_provider = TestCryptoProvider {
994            send_result: None,
995            receive_result: None,
996        };
997        let communication_provider = TestCommunicationBackend::new();
998        let session_map = TestSessionRepository::new(HashMap::new());
999        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
1000        let cancellation_token = CancellationToken::new();
1001        let _ = client.start(Some(cancellation_token.clone())).await;
1002
1003        // Give the client some time to process
1004        tokio::time::sleep(Duration::from_millis(100)).await;
1005        let is_running = client.is_running();
1006
1007        assert!(is_running);
1008        assert!(!cancellation_token.is_cancelled());
1009    }
1010
1011    #[tokio::test]
1012    async fn ipc_client_is_not_running_if_not_started() {
1013        let crypto_provider = TestCryptoProvider {
1014            send_result: None,
1015            receive_result: None,
1016        };
1017        let communication_provider = TestCommunicationBackend::new();
1018        let session_map = TestSessionRepository::new(HashMap::new());
1019        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
1020
1021        // Give the client some time to process
1022        tokio::time::sleep(Duration::from_millis(100)).await;
1023        let is_running = client.is_running();
1024
1025        assert!(!is_running);
1026    }
1027
1028    #[tokio::test]
1029    async fn ipc_client_start_returns_error_if_already_running() {
1030        let crypto_provider = TestCryptoProvider {
1031            send_result: None,
1032            receive_result: None,
1033        };
1034        let communication_provider = TestCommunicationBackend::new();
1035        let session_map = TestSessionRepository::new(HashMap::new());
1036        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
1037        let cancellation_token = CancellationToken::new();
1038        let first_result = client.start(Some(cancellation_token.clone())).await;
1039        assert_eq!(first_result, Ok(()));
1040
1041        // Give the client some time to process
1042        tokio::time::sleep(Duration::from_millis(100)).await;
1043        assert!(client.is_running());
1044
1045        let second_result = client.start(Some(cancellation_token.clone())).await;
1046        assert_eq!(second_result, Err(AlreadyRunningError));
1047    }
1048
1049    mod request {
1050        use super::*;
1051        use crate::{RpcHandler, RpcRequestInfo};
1052
1053        #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
1054        struct TestRequest {
1055            a: i32,
1056            b: i32,
1057        }
1058
1059        #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
1060        struct TestResponse {
1061            result: i32,
1062        }
1063
1064        impl RpcRequest for TestRequest {
1065            type Response = TestResponse;
1066
1067            const NAME: &str = "TestRequest";
1068        }
1069
1070        struct TestHandler;
1071
1072        impl RpcHandler for TestHandler {
1073            type Request = TestRequest;
1074
1075            async fn handle(&self, request: Self::Request, _info: RpcRequestInfo) -> TestResponse {
1076                TestResponse {
1077                    result: request.a + request.b,
1078                }
1079            }
1080        }
1081
1082        /// A request whose handler responds with the source it was given, so tests can observe
1083        /// what the IPC client passes to handlers.
1084        #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
1085        struct EchoSourceRequest;
1086
1087        impl RpcRequest for EchoSourceRequest {
1088            type Response = Source;
1089
1090            const NAME: &str = "EchoSourceRequest";
1091        }
1092
1093        struct EchoSourceHandler;
1094
1095        impl RpcHandler for EchoSourceHandler {
1096            type Request = EchoSourceRequest;
1097
1098            async fn handle(&self, _request: Self::Request, info: RpcRequestInfo) -> Source {
1099                info.source
1100            }
1101        }
1102
1103        /// A second request type whose response is a unit enum, which serde serializes to a bare
1104        /// JSON string. Deserializing it as [`TestResponse`] fails, which is what makes it a
1105        /// faithful stand-in for the real `GetBiometricsStatus` / `UnlockBiometrics` pair.
1106        #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
1107        struct OtherRequest;
1108
1109        #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
1110        enum OtherResponse {
1111            Available,
1112        }
1113
1114        impl RpcRequest for OtherRequest {
1115            type Response = OtherResponse;
1116
1117            const NAME: &str = "OtherRequest";
1118        }
1119
1120        #[tokio::test]
1121        async fn request_sends_message_and_returns_response() {
1122            let crypto_provider = NoEncryptionCryptoProvider;
1123            let communication_provider = TestCommunicationBackend::new();
1124            let session_map = InMemorySessionRepository::default();
1125            let client =
1126                IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
1127            let _ = client.start(None).await;
1128            let request = TestRequest { a: 1, b: 2 };
1129            let response = TestResponse { result: 3 };
1130
1131            // Send the request
1132            let request_clone = request.clone();
1133            let client_clone = client.clone();
1134            let result_handle = tokio::spawn(async move {
1135                client_clone
1136                    .request::<TestRequest>(
1137                        request_clone,
1138                        Endpoint::BrowserBackground { id: HostId::Own },
1139                        None,
1140                    )
1141                    .await
1142            });
1143            tokio::time::sleep(Duration::from_millis(100)).await;
1144
1145            // Read and verify the outgoing message
1146            let outgoing_messages = communication_provider.outgoing().await;
1147            let outgoing_request: RpcRequestMessage<TestRequest> =
1148                serde_utils::from_slice(&outgoing_messages[0].payload)
1149                    .expect("Deserialization should not fail");
1150            assert_eq!(outgoing_request.request_type, "TestRequest");
1151            assert_eq!(outgoing_request.request, request);
1152
1153            // Simulate receiving a response
1154            let simulated_response = IncomingRpcResponseMessage {
1155                result: Ok(response),
1156                request_id: outgoing_request.request_id.clone(),
1157                request_type: outgoing_request.request_type.clone(),
1158            };
1159            let simulated_response = IncomingMessage {
1160                payload: serde_utils::to_vec(&simulated_response)
1161                    .expect("Serialization should not fail"),
1162                source: Source::BrowserBackground { id: HostId::Own },
1163                destination: Endpoint::Web {
1164                    tab_id: 9001,
1165                    document_id: "doc-1".to_string(),
1166                },
1167                // The requester subscribes to the dedicated topic it minted, so the response must
1168                // be published there.
1169                topic: Some(outgoing_request.response_topic.clone()),
1170            };
1171            communication_provider.push_incoming(simulated_response);
1172
1173            // Wait for the response
1174            let result = result_handle.await.unwrap();
1175            assert_eq!(result.unwrap().result, 3);
1176        }
1177
1178        /// Two requests in flight at once must each receive their own response. Each request mints
1179        /// its own response topic, so `TestRequest` and `OtherRequest` never see each other's
1180        /// responses even though their response types differ (`OtherResponse::Available` is a bare
1181        /// JSON string that would not deserialize into `TestResponse`).
1182        #[tokio::test]
1183        async fn concurrent_request_of_a_different_type_does_not_disturb_a_pending_request() {
1184            let crypto_provider = NoEncryptionCryptoProvider;
1185            let communication_provider = TestCommunicationBackend::new();
1186            let session_map = InMemorySessionRepository::default();
1187            let client =
1188                IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
1189            let _ = client.start(None).await;
1190
1191            // Put both requests in flight at the same time.
1192            let test_handle = {
1193                let client = client.clone();
1194                tokio::spawn(async move {
1195                    client
1196                        .request::<TestRequest>(
1197                            TestRequest { a: 1, b: 2 },
1198                            Endpoint::BrowserBackground { id: HostId::Own },
1199                            None,
1200                        )
1201                        .await
1202                })
1203            };
1204            let other_handle = {
1205                let client = client.clone();
1206                tokio::spawn(async move {
1207                    client
1208                        .request::<OtherRequest>(
1209                            OtherRequest,
1210                            Endpoint::BrowserBackground { id: HostId::Own },
1211                            None,
1212                        )
1213                        .await
1214                })
1215            };
1216            tokio::time::sleep(Duration::from_millis(100)).await;
1217
1218            // Recover both requests from the wire, keyed by type, so we can answer each on the
1219            // dedicated response topic it minted.
1220            let outgoing = communication_provider.outgoing().await;
1221            let requests: HashMap<String, RpcRequestMessage<serde_json::Value>> = outgoing
1222                .iter()
1223                .map(|message| {
1224                    let partial: RpcRequestMessage<serde_json::Value> =
1225                        serde_utils::from_slice(&message.payload)
1226                            .expect("Deserialization should not fail");
1227                    (partial.request_type.clone(), partial)
1228                })
1229                .collect();
1230
1231            let respond = |topic: String, payload: Vec<u8>| {
1232                communication_provider.push_incoming(IncomingMessage {
1233                    payload,
1234                    source: Source::BrowserBackground { id: HostId::Own },
1235                    destination: Endpoint::Web {
1236                        tab_id: 9001,
1237                        document_id: "doc-1".to_string(),
1238                    },
1239                    topic: Some(topic),
1240                });
1241            };
1242
1243            // Answer `OtherRequest` first, while `TestRequest` is still waiting. Each response goes
1244            // to its own dedicated topic, so the two can never be confused for one another.
1245            respond(
1246                requests["OtherRequest"].response_topic.clone(),
1247                serde_utils::to_vec(&IncomingRpcResponseMessage {
1248                    result: Ok(OtherResponse::Available),
1249                    request_id: requests["OtherRequest"].request_id.clone(),
1250                    request_type: "OtherRequest".to_string(),
1251                })
1252                .expect("Serialization should not fail"),
1253            );
1254
1255            respond(
1256                requests["TestRequest"].response_topic.clone(),
1257                serde_utils::to_vec(&IncomingRpcResponseMessage {
1258                    result: Ok(TestResponse { result: 3 }),
1259                    request_id: requests["TestRequest"].request_id.clone(),
1260                    request_type: "TestRequest".to_string(),
1261                })
1262                .expect("Serialization should not fail"),
1263            );
1264
1265            assert_eq!(
1266                other_handle
1267                    .await
1268                    .unwrap()
1269                    .expect("OtherRequest should succeed"),
1270                OtherResponse::Available
1271            );
1272            assert_eq!(
1273                test_handle
1274                    .await
1275                    .unwrap()
1276                    .expect("TestRequest should succeed"),
1277                TestResponse { result: 3 }
1278            );
1279        }
1280
1281        /// A response that reaches this request's dedicated topic but fails to deserialize into the
1282        /// expected response type is a genuine error for *this* request: because the topic is
1283        /// dedicated, there is no other request it could belong to, so the failure is surfaced
1284        /// immediately.
1285        #[tokio::test]
1286        async fn malformed_response_surfaces_error_instead_of_hanging() {
1287            let crypto_provider = NoEncryptionCryptoProvider;
1288            let communication_provider = TestCommunicationBackend::new();
1289            let session_map = InMemorySessionRepository::default();
1290            let client =
1291                IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
1292            let _ = client.start(None).await;
1293
1294            let result_handle = {
1295                let client = client.clone();
1296                tokio::spawn(async move {
1297                    client
1298                        .request::<TestRequest>(
1299                            TestRequest { a: 1, b: 2 },
1300                            Endpoint::BrowserBackground { id: HostId::Own },
1301                            None,
1302                        )
1303                        .await
1304                })
1305            };
1306            tokio::time::sleep(Duration::from_millis(100)).await;
1307
1308            // Recover the dedicated response topic and answer it with a body that cannot be
1309            // deserialized into `TestResponse` (`OtherResponse::Available` serializes to a bare
1310            // string).
1311            let outgoing = communication_provider.outgoing().await;
1312            let request: RpcRequestMessage<TestRequest> =
1313                serde_utils::from_slice(&outgoing[0].payload)
1314                    .expect("Deserialization should not fail");
1315            let malformed = IncomingRpcResponseMessage {
1316                result: Ok(OtherResponse::Available),
1317                request_id: request.request_id.clone(),
1318                request_type: request.request_type.clone(),
1319            };
1320            communication_provider.push_incoming(IncomingMessage {
1321                payload: serde_utils::to_vec(&malformed).expect("Serialization should not fail"),
1322                source: Source::BrowserBackground { id: HostId::Own },
1323                destination: Endpoint::Web {
1324                    tab_id: 9001,
1325                    document_id: "doc-1".to_string(),
1326                },
1327                topic: Some(request.response_topic.clone()),
1328            });
1329
1330            let result = tokio::time::timeout(Duration::from_secs(5), result_handle)
1331                .await
1332                .expect("request must not hang on a malformed response")
1333                .unwrap();
1334            assert!(matches!(
1335                result,
1336                Err(crate::error::RequestError::Rpc(
1337                    crate::rpc::error::RpcError::ResponseDeserialization(_)
1338                ))
1339            ));
1340        }
1341
1342        /// Builds a `TestRequest` response on the request's dedicated topic, as sent by `source`.
1343        fn test_response_from(
1344            request: &RpcRequestMessage<TestRequest>,
1345            result: i32,
1346            source: Source,
1347        ) -> IncomingMessage {
1348            let response = IncomingRpcResponseMessage {
1349                result: Ok(TestResponse { result }),
1350                request_id: request.request_id.clone(),
1351                request_type: request.request_type.clone(),
1352            };
1353            IncomingMessage {
1354                payload: serde_utils::to_vec(&response).expect("Serialization should not fail"),
1355                source,
1356                destination: Endpoint::DesktopRenderer,
1357                topic: Some(request.response_topic.clone()),
1358            }
1359        }
1360
1361        /// A message on the response topic from a peer other than the destination must not be
1362        /// accepted as the response, and must not stop the request from receiving the real one.
1363        #[tokio::test]
1364        async fn response_from_other_source_is_ignored_and_destination_response_is_returned() {
1365            let communication_provider = TestCommunicationBackend::new();
1366            let client = IpcClientImpl::new(
1367                NoEncryptionCryptoProvider,
1368                communication_provider.clone(),
1369                InMemorySessionRepository::default(),
1370            );
1371            let _ = client.start(None).await;
1372
1373            let result_handle = {
1374                let client = client.clone();
1375                tokio::spawn(async move {
1376                    client
1377                        .request::<TestRequest>(
1378                            TestRequest { a: 1, b: 2 },
1379                            Endpoint::DesktopMain,
1380                            None,
1381                        )
1382                        .await
1383                })
1384            };
1385            tokio::time::sleep(Duration::from_millis(100)).await;
1386
1387            let outgoing = communication_provider.outgoing().await;
1388            let request: RpcRequestMessage<TestRequest> =
1389                serde_utils::from_slice(&outgoing[0].payload)
1390                    .expect("Deserialization should not fail");
1391
1392            communication_provider.push_incoming(test_response_from(
1393                &request,
1394                999,
1395                Source::BrowserBackground { id: HostId::Id(42) },
1396            ));
1397            communication_provider.push_incoming(test_response_from(
1398                &request,
1399                3,
1400                Source::DesktopMain,
1401            ));
1402
1403            let result = tokio::time::timeout(Duration::from_secs(5), result_handle)
1404                .await
1405                .expect("request must complete once the destination responds")
1406                .unwrap();
1407            assert_eq!(result, Ok(TestResponse { result: 3 }));
1408        }
1409
1410        /// When only a peer other than the destination answers, the request keeps waiting and ends
1411        /// through its cancellation token, never returning the other peer's result.
1412        #[tokio::test]
1413        async fn response_only_from_other_source_waits_until_cancelled() {
1414            let communication_provider = TestCommunicationBackend::new();
1415            let client = IpcClientImpl::new(
1416                NoEncryptionCryptoProvider,
1417                communication_provider.clone(),
1418                InMemorySessionRepository::default(),
1419            );
1420            let _ = client.start(None).await;
1421
1422            let cancellation_token = CancellationToken::new();
1423            let result_handle = {
1424                let client = client.clone();
1425                let cancellation_token = cancellation_token.clone();
1426                tokio::spawn(async move {
1427                    client
1428                        .request::<TestRequest>(
1429                            TestRequest { a: 1, b: 2 },
1430                            Endpoint::DesktopMain,
1431                            Some(cancellation_token),
1432                        )
1433                        .await
1434                })
1435            };
1436            tokio::time::sleep(Duration::from_millis(100)).await;
1437
1438            let outgoing = communication_provider.outgoing().await;
1439            let request: RpcRequestMessage<TestRequest> =
1440                serde_utils::from_slice(&outgoing[0].payload)
1441                    .expect("Deserialization should not fail");
1442
1443            communication_provider.push_incoming(test_response_from(
1444                &request,
1445                999,
1446                Source::DesktopRenderer,
1447            ));
1448            tokio::time::sleep(Duration::from_millis(100)).await;
1449            assert!(
1450                !result_handle.is_finished(),
1451                "request must not complete on a response from another source"
1452            );
1453
1454            cancellation_token.cancel();
1455            let result = tokio::time::timeout(Duration::from_secs(5), result_handle)
1456                .await
1457                .expect("request must end once cancelled")
1458                .unwrap();
1459            assert!(matches!(
1460                result,
1461                Err(crate::error::RequestError::Receive(
1462                    TypedReceiveError::Cancelled
1463                ))
1464            ));
1465        }
1466
1467        /// A web source carries an `origin` that its endpoint does not, so the response is matched
1468        /// against the destination by endpoint identity (tab and document) alone.
1469        #[tokio::test]
1470        async fn response_from_web_destination_is_accepted_regardless_of_origin() {
1471            let communication_provider = TestCommunicationBackend::new();
1472            let client = IpcClientImpl::new(
1473                NoEncryptionCryptoProvider,
1474                communication_provider.clone(),
1475                InMemorySessionRepository::default(),
1476            );
1477            let _ = client.start(None).await;
1478
1479            let result_handle = {
1480                let client = client.clone();
1481                tokio::spawn(async move {
1482                    client
1483                        .request::<TestRequest>(
1484                            TestRequest { a: 1, b: 2 },
1485                            Endpoint::Web {
1486                                tab_id: 9001,
1487                                document_id: "doc-1".to_string(),
1488                            },
1489                            None,
1490                        )
1491                        .await
1492                })
1493            };
1494            tokio::time::sleep(Duration::from_millis(100)).await;
1495
1496            let outgoing = communication_provider.outgoing().await;
1497            let request: RpcRequestMessage<TestRequest> =
1498                serde_utils::from_slice(&outgoing[0].payload)
1499                    .expect("Deserialization should not fail");
1500
1501            // Same tab, different document: a page the request was not sent to.
1502            communication_provider.push_incoming(test_response_from(
1503                &request,
1504                999,
1505                Source::Web {
1506                    tab_id: 9001,
1507                    document_id: "doc-2".to_string(),
1508                    origin: "https://vault.bitwarden.com".to_string(),
1509                },
1510            ));
1511            communication_provider.push_incoming(test_response_from(
1512                &request,
1513                3,
1514                Source::Web {
1515                    tab_id: 9001,
1516                    document_id: "doc-1".to_string(),
1517                    origin: "https://vault.bitwarden.com".to_string(),
1518                },
1519            ));
1520
1521            let result = tokio::time::timeout(Duration::from_secs(5), result_handle)
1522                .await
1523                .expect("request must complete once the destination responds")
1524                .unwrap();
1525            assert_eq!(result, Ok(TestResponse { result: 3 }));
1526        }
1527
1528        #[tokio::test]
1529        async fn incoming_rpc_message_handles_request_and_returns_response() {
1530            let crypto_provider = NoEncryptionCryptoProvider;
1531            let communication_provider = TestCommunicationBackend::new();
1532            let session_map = InMemorySessionRepository::default();
1533            let client =
1534                IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
1535            let _ = client.start(None).await;
1536            let request_id = uuid::Uuid::new_v4().to_string();
1537            let request = TestRequest { a: 1, b: 2 };
1538            let response = TestResponse { result: 3 };
1539
1540            // Register the handler
1541            client.register_rpc_handler(TestHandler).await;
1542
1543            // Simulate receiving a request
1544            let response_topic = format!("RpcResponseMessage:{request_id}");
1545            let simulated_request = RpcRequestMessage {
1546                request,
1547                request_id: request_id.clone(),
1548                request_type: "TestRequest".to_string(),
1549                response_topic: response_topic.clone(),
1550            };
1551            let simulated_request_message = IncomingMessage {
1552                payload: serde_utils::to_vec(&simulated_request)
1553                    .expect("Serialization should not fail"),
1554                source: Source::Web {
1555                    tab_id: 9001,
1556                    document_id: "doc-1".to_string(),
1557                    origin: "https://example.com".to_string(),
1558                },
1559                destination: Endpoint::BrowserBackground { id: HostId::Own },
1560                topic: Some(RPC_REQUEST_PAYLOAD_TYPE_NAME.to_owned()),
1561            };
1562            communication_provider.push_incoming(simulated_request_message);
1563
1564            // Give the client some time to process the request
1565            tokio::time::sleep(Duration::from_millis(100)).await;
1566
1567            // Read and verify the outgoing message
1568            let outgoing_messages = communication_provider.outgoing().await;
1569            let outgoing_response: IncomingRpcResponseMessage<TestResponse> =
1570                serde_utils::from_slice(&outgoing_messages[0].payload)
1571                    .expect("Deserialization should not fail");
1572
1573            // The response must be published on the dedicated topic carried by the request.
1574            assert_eq!(outgoing_messages[0].topic, Some(response_topic));
1575            assert_eq!(outgoing_response.request_type, "TestRequest");
1576            assert_eq!(outgoing_response.result, Ok(response));
1577        }
1578
1579        #[tokio::test]
1580        async fn ipc_client_stops_if_rpc_response_send_returns_fatal_error() {
1581            let request = RpcRequestMessage {
1582                request: TestRequest { a: 1, b: 2 },
1583                request_id: "request".to_string(),
1584                request_type: "TestRequest".to_string(),
1585                response_topic: "RpcResponseMessage:request".to_string(),
1586            };
1587            let crypto_provider = TestCryptoProvider {
1588                send_result: Some(Err(TestCryptoError {
1589                    message: "Crypto error".to_string(),
1590                    fatal: true,
1591                })),
1592                receive_result: Some(Ok(IncomingMessage {
1593                    payload: serde_utils::to_vec(&request).expect("Serialization should not fail"),
1594                    source: Source::Web {
1595                        tab_id: 9001,
1596                        document_id: "doc-1".to_string(),
1597                        origin: "https://example.com".to_string(),
1598                    },
1599                    destination: Endpoint::BrowserBackground { id: HostId::Own },
1600                    topic: Some(RPC_REQUEST_PAYLOAD_TYPE_NAME.to_owned()),
1601                })),
1602            };
1603            let client = IpcClientImpl::new(
1604                crypto_provider,
1605                TestCommunicationBackend::new(),
1606                TestSessionRepository::new(HashMap::new()),
1607            );
1608            client.register_rpc_handler(TestHandler).await;
1609            let _ = client.start(None).await;
1610
1611            // Give the client some time to receive the request and fail to send the response
1612            tokio::time::sleep(Duration::from_millis(100)).await;
1613
1614            assert!(!client.is_running());
1615        }
1616
1617        #[tokio::test]
1618        async fn incoming_rpc_message_passes_message_source_to_handler() {
1619            let crypto_provider = NoEncryptionCryptoProvider;
1620            let communication_provider = TestCommunicationBackend::new();
1621            let session_map = InMemorySessionRepository::default();
1622            let client =
1623                IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
1624            let _ = client.start(None).await;
1625            client.register_rpc_handler(EchoSourceHandler).await;
1626
1627            let source = Source::Web {
1628                tab_id: 9001,
1629                document_id: "doc-1".to_string(),
1630                origin: "https://example.com".to_string(),
1631            };
1632            let request_id = uuid::Uuid::new_v4().to_string();
1633            let simulated_request = RpcRequestMessage {
1634                request: EchoSourceRequest,
1635                request_id: request_id.clone(),
1636                request_type: EchoSourceRequest::NAME.to_string(),
1637                response_topic: format!("RpcResponseMessage:{request_id}"),
1638            };
1639            communication_provider.push_incoming(IncomingMessage {
1640                payload: serde_utils::to_vec(&simulated_request)
1641                    .expect("Serialization should not fail"),
1642                source: source.clone(),
1643                destination: Endpoint::BrowserBackground { id: HostId::Own },
1644                topic: Some(RPC_REQUEST_PAYLOAD_TYPE_NAME.to_owned()),
1645            });
1646
1647            // Give the client some time to process the request
1648            tokio::time::sleep(Duration::from_millis(100)).await;
1649
1650            let outgoing_messages = communication_provider.outgoing().await;
1651            let outgoing_response: IncomingRpcResponseMessage<Source> =
1652                serde_utils::from_slice(&outgoing_messages[0].payload)
1653                    .expect("Deserialization should not fail");
1654
1655            assert_eq!(outgoing_response.result, Ok(source));
1656        }
1657    }
1658
1659    mod fragmentation {
1660        use super::*;
1661        use crate::{
1662            RequestError, RpcHandler, RpcRequestInfo,
1663            crypto_provider::noise::crypto_provider::NoiseCryptoProvider,
1664            fragmentation::{FRAGMENT_TOPIC, MAX_PAYLOAD_SIZE, test_support::*},
1665            traits::TestTwoWayCommunicationBackend,
1666        };
1667
1668        /// Larger than several Noise messages, and not a multiple of the fragment size.
1669        const LARGE_LEN: usize = 4 * 65535 + 123;
1670
1671        fn noise_client(
1672            backend: TestTwoWayCommunicationBackend,
1673        ) -> IpcClientImpl<
1674            NoiseCryptoProvider,
1675            TestTwoWayCommunicationBackend,
1676            InMemorySessionRepository<crate::NoiseCryptoProviderState>,
1677        > {
1678            IpcClientImpl::new(
1679                NoiseCryptoProvider::new(),
1680                backend,
1681                InMemorySessionRepository::new(HashMap::new()),
1682            )
1683        }
1684
1685        #[tokio::test]
1686        async fn large_payload_round_trips_through_noise_once_on_its_topic() {
1687            let (backend_1, backend_2) = TestTwoWayCommunicationBackend::new();
1688            let client_1 = noise_client(backend_1);
1689            let client_2 = noise_client(backend_2);
1690            client_1.start(None).await.unwrap();
1691            client_2.start(None).await.unwrap();
1692            let mut all_messages = client_2.subscribe(None).await.unwrap();
1693            let mut on_topic = client_2.subscribe(Some("bulk".to_owned())).await.unwrap();
1694            let mut on_fragment_topic = client_2
1695                .subscribe(Some(format!("{FRAGMENT_TOPIC}/bulk")))
1696                .await
1697                .unwrap();
1698
1699            let payload = large_payload(LARGE_LEN, 0);
1700            let message = OutgoingMessage {
1701                payload: payload.clone(),
1702                destination: Endpoint::DesktopMain,
1703                topic: Some("bulk".to_owned()),
1704            };
1705            client_1.send(message).await.unwrap();
1706
1707            let received = all_messages.receive(None).await.unwrap();
1708            assert_eq!(received.topic.as_deref(), Some("bulk"));
1709            assert_eq!(received.payload, payload);
1710
1711            // Neither the fragments nor a second copy reach subscribers.
1712            let next = bitwarden_threading::time::timeout(
1713                Duration::from_millis(200),
1714                all_messages.receive(None),
1715            )
1716            .await;
1717            assert!(next.is_err(), "unexpected extra message: {next:?}");
1718
1719            // Topic filters see the reassembled message, never its fragments.
1720            assert_eq!(on_topic.receive(None).await.unwrap().payload, payload);
1721            let fragment = bitwarden_threading::time::timeout(
1722                Duration::from_millis(10),
1723                on_fragment_topic.receive(None),
1724            )
1725            .await;
1726            assert!(
1727                fragment.is_err(),
1728                "fragment reached a subscriber: {fragment:?}"
1729            );
1730        }
1731
1732        #[derive(Debug, Clone, Serialize, Deserialize)]
1733        struct EchoRequest {
1734            data: String,
1735        }
1736
1737        impl RpcRequest for EchoRequest {
1738            type Response = String;
1739
1740            const NAME: &str = "EchoRequest";
1741        }
1742
1743        struct EchoHandler;
1744
1745        impl RpcHandler for EchoHandler {
1746            type Request = EchoRequest;
1747
1748            async fn handle(&self, request: EchoRequest, _info: RpcRequestInfo) -> String {
1749                request.data.repeat(2)
1750            }
1751        }
1752
1753        #[tokio::test]
1754        async fn rpc_with_response_larger_than_one_noise_message_succeeds() {
1755            let (backend_1, backend_2) = TestTwoWayCommunicationBackend::new();
1756            let client_1 = noise_client(backend_1);
1757            let client_2 = noise_client(backend_2);
1758            client_1.start(None).await.unwrap();
1759            client_2.start(None).await.unwrap();
1760            client_2.register_rpc_handler(EchoHandler).await;
1761
1762            // The request itself also exceeds one Noise message.
1763            let data = "a".repeat(LARGE_LEN / 2);
1764            let response = client_1
1765                .request(
1766                    EchoRequest { data: data.clone() },
1767                    Endpoint::DesktopMain,
1768                    None,
1769                )
1770                .await
1771                .unwrap();
1772
1773            assert_eq!(response, data.repeat(2));
1774        }
1775
1776        #[tokio::test]
1777        async fn rpc_response_over_the_maximum_returns_an_error_to_the_requester() {
1778            let (backend_1, backend_2) = TestTwoWayCommunicationBackend::new();
1779            let client_1 = noise_client(backend_1);
1780            let client_2 = noise_client(backend_2);
1781            client_1.start(None).await.unwrap();
1782            client_2.start(None).await.unwrap();
1783            client_2.register_rpc_handler(EchoHandler).await;
1784
1785            // The request fits, but the doubled response does not.
1786            let data = "a".repeat(MAX_PAYLOAD_SIZE / 2 + 1);
1787            let result = bitwarden_threading::time::timeout(
1788                Duration::from_secs(10),
1789                client_1.request(EchoRequest { data }, Endpoint::DesktopMain, None),
1790            )
1791            .await
1792            .expect("Requester should receive an error response");
1793
1794            assert!(
1795                matches!(
1796                    result,
1797                    Err(RequestError::Rpc(RpcError::ResponseSerialization(_)))
1798                ),
1799                "{result:?}"
1800            );
1801        }
1802
1803        #[tokio::test]
1804        async fn client_keeps_receiving_after_invalid_fragments() {
1805            let backend = TestCommunicationBackend::new();
1806            let client = IpcClientImpl::new(
1807                NoEncryptionCryptoProvider,
1808                backend.clone(),
1809                InMemorySessionRepository::default(),
1810            );
1811            client.start(None).await.unwrap();
1812            let mut subscription = client.subscribe(None).await.unwrap();
1813            let source = web_source(1);
1814
1815            backend.push_incoming(incoming(
1816                outgoing(vec![1, 2, 3], FRAGMENT_TOPIC),
1817                source.clone(),
1818            ));
1819            backend.push_incoming(crafted(source.clone(), 1, 5, 1024));
1820            backend.push_incoming(crafted(source.clone(), 2, 0, MAX_PAYLOAD_SIZE + 1));
1821            backend.push_incoming(crafted(source.clone(), 3, 0, 1024));
1822            backend.push_incoming(crafted(source.clone(), 3, 0, 1024));
1823
1824            let payload = large_payload(FRAGMENT_LEN * 2 + 1, 0);
1825            for fragment in split_all(outgoing(payload.clone(), "bulk"), MAX_LEN) {
1826                backend.push_incoming(incoming(fragment, source.clone()));
1827            }
1828
1829            let received = subscription.receive(None).await.unwrap();
1830            assert_eq!(received.topic.as_deref(), Some("bulk"));
1831            assert_eq!(received.payload, payload);
1832            assert!(client.is_running());
1833        }
1834
1835        #[tokio::test]
1836        async fn send_rejects_reserved_fragment_topic() {
1837            let client = IpcClientImpl::new(
1838                NoEncryptionCryptoProvider,
1839                TestCommunicationBackend::new(),
1840                InMemorySessionRepository::default(),
1841            );
1842            let result = client.send(outgoing(vec![0; 16], FRAGMENT_TOPIC)).await;
1843            assert!(matches!(result, Err(SendError::Other(_))));
1844        }
1845
1846        /// Records sent messages of up to [`MAX_LEN`] bytes and fails the send at index `fail_at`.
1847        struct FailingCryptoProvider {
1848            fail_at: usize,
1849            fatal: bool,
1850            sent: Arc<Mutex<Vec<OutgoingMessage>>>,
1851        }
1852
1853        impl CryptoProvider<TestCommunicationBackend, TestSessionRepository> for FailingCryptoProvider {
1854            type Session = String;
1855            type SendError = TestCryptoError;
1856            type ReceiveError = TestCryptoError;
1857
1858            fn max_payload_len(&self) -> usize {
1859                MAX_LEN
1860            }
1861
1862            async fn send(
1863                &self,
1864                _communication: &TestCommunicationBackend,
1865                _sessions: &TestSessionRepository,
1866                message: OutgoingMessage,
1867            ) -> Result<(), TestCryptoError> {
1868                let mut sent = self.sent.lock().unwrap();
1869                if sent.len() == self.fail_at {
1870                    return Err(TestCryptoError {
1871                        message: "Send failed".to_owned(),
1872                        fatal: self.fatal,
1873                    });
1874                }
1875                sent.push(message);
1876                Ok(())
1877            }
1878
1879            async fn receive(
1880                &self,
1881                _receiver: &<TestCommunicationBackend as CommunicationBackend>::Receiver,
1882                _communication: &TestCommunicationBackend,
1883                _sessions: &TestSessionRepository,
1884            ) -> Result<IncomingMessage, TestCryptoError> {
1885                std::future::pending().await
1886            }
1887        }
1888
1889        #[tokio::test]
1890        async fn failed_fragment_send_stops_the_message_and_fatal_errors_stop_the_client() {
1891            for fatal in [false, true] {
1892                let sent = Arc::new(Mutex::new(Vec::new()));
1893                let client = IpcClientImpl::new(
1894                    FailingCryptoProvider {
1895                        fail_at: 1,
1896                        fatal,
1897                        sent: sent.clone(),
1898                    },
1899                    TestCommunicationBackend::new(),
1900                    TestSessionRepository::default(),
1901                );
1902                client.start(None).await.unwrap();
1903
1904                let message = outgoing(large_payload(FRAGMENT_LEN * 3, 0), "bulk");
1905                assert!(client.send(message).await.is_err());
1906
1907                // Fragments after the failed one are not sent.
1908                assert_eq!(sent.lock().unwrap().len(), 1);
1909                assert_eq!(client.is_running(), !fatal);
1910            }
1911        }
1912    }
1913}