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 constants::CHANNEL_BUFFER_CAPACITY,
10 error::{
11 AlreadyRunningError, ErrorKind, IpcErrorKind, ReceiveError, SendError, SubscribeError,
12 TypedReceiveError,
13 },
14 message::{IncomingMessage, OutgoingMessage, PayloadTypeName, TypedIncomingMessage},
15 rpc::{
16 exec::{handler::ErasedRpcHandler, handler_registry::RpcHandlerRegistry},
17 request_message::{RPC_REQUEST_PAYLOAD_TYPE_NAME, RpcRequestPayload},
18 response_message::OutgoingRpcResponseMessage,
19 },
20 serde_utils,
21 traits::{CommunicationBackend, CryptoProvider, SessionRepository},
22};
23
24pub struct IpcClientSubscription {
29 pub(crate) receiver: tokio::sync::broadcast::Receiver<IncomingMessage>,
30 pub(crate) topic: Option<String>,
31}
32
33pub struct IpcClientTypedSubscription<Payload: DeserializeOwned + PayloadTypeName>(
38 IpcClientSubscription,
39 std::marker::PhantomData<Payload>,
40);
41
42struct IpcClientInner<Crypto, Com, Ses>
44where
45 Crypto: CryptoProvider<Com, Ses>,
46 Com: CommunicationBackend,
47 Ses: SessionRepository<Crypto::Session>,
48{
49 crypto: Crypto,
50 communication: Com,
51 sessions: Ses,
52
53 handlers: RpcHandlerRegistry,
54 incoming: Mutex<Option<tokio::sync::broadcast::Receiver<IncomingMessage>>>,
55 cancellation_token: Mutex<Option<CancellationToken>>,
56}
57
58pub struct IpcClientImpl<Crypto, Com, Ses>
64where
65 Crypto: CryptoProvider<Com, Ses>,
66 Com: CommunicationBackend,
67 Ses: SessionRepository<Crypto::Session>,
68{
69 inner: Arc<IpcClientInner<Crypto, Com, Ses>>,
70}
71
72impl<Crypto, Com, Ses> Clone for IpcClientImpl<Crypto, Com, Ses>
73where
74 Crypto: CryptoProvider<Com, Ses>,
75 Com: CommunicationBackend,
76 Ses: SessionRepository<Crypto::Session>,
77{
78 fn clone(&self) -> Self {
79 Self {
80 inner: self.inner.clone(),
81 }
82 }
83}
84
85impl<Crypto, Com, Ses> IpcClientImpl<Crypto, Com, Ses>
86where
87 Crypto: CryptoProvider<Com, Ses>,
88 Com: CommunicationBackend,
89 Ses: SessionRepository<Crypto::Session>,
90{
91 pub fn new(crypto: Crypto, communication: Com, sessions: Ses) -> Self {
94 Self {
95 inner: Arc::new(IpcClientInner {
96 crypto,
97 communication,
98 sessions,
99
100 handlers: RpcHandlerRegistry::new(),
101 incoming: Mutex::new(None),
102 cancellation_token: Mutex::new(None),
103 }),
104 }
105 }
106}
107
108#[async_trait::async_trait]
109impl<Crypto, Com, Ses> crate::ipc_client_trait::IpcClient for IpcClientImpl<Crypto, Com, Ses>
110where
111 Crypto: CryptoProvider<Com, Ses>,
112 Com: CommunicationBackend,
113 Ses: SessionRepository<Crypto::Session>,
114{
115 async fn start(
116 &self,
117 cancellation_token: Option<CancellationToken>,
118 ) -> Result<(), AlreadyRunningError> {
119 if self.is_running() {
120 return Err(AlreadyRunningError);
121 }
122
123 let cancellation_token = cancellation_token.unwrap_or_default();
124 self.inner
125 .cancellation_token
126 .lock()
127 .expect("Failed to lock cancellation token mutex")
128 .replace(cancellation_token.clone());
129
130 let com_receiver = self.inner.communication.subscribe().await;
131 let (client_tx, client_rx) = tokio::sync::broadcast::channel(CHANNEL_BUFFER_CAPACITY);
132
133 self.inner
134 .incoming
135 .lock()
136 .expect("Failed to lock incoming mutex")
137 .replace(client_rx);
138
139 let inner = self.inner.clone();
140 let future = async move {
141 loop {
142 let rpc_topic = RPC_REQUEST_PAYLOAD_TYPE_NAME.to_owned();
143 select! {
144 _ = cancellation_token.cancelled() => {
145 tracing::debug!("Cancellation signal received, stopping IPC client");
146 break;
147 }
148 received = inner.crypto.receive(&com_receiver, &inner.communication, &inner.sessions) => {
149 match received {
150 Ok(message) if message.topic == Some(rpc_topic) => {
151 handle_rpc_request(&inner, message)
152 }
153 Ok(message) => {
154 if client_tx.send(message).is_err() {
155 tracing::error!("Failed to save incoming message");
156 break;
157 };
158 }
159 Err(error) if matches!(error.kind(), ErrorKind::Fatal) => {
160 tracing::error!(?error, "Fatal error receiving message, stopping IPC client");
161 break;
162 }
163 Err(error) => {
164 tracing::warn!(?error, "Recoverable error receiving message, continuing");
165 }
166 }
167 }
168 }
169 }
170 tracing::debug!("IPC client shutting down");
171 stop_inner(&inner);
172 };
173
174 #[cfg(not(target_arch = "wasm32"))]
175 tokio::spawn(future);
176
177 #[cfg(target_arch = "wasm32")]
178 wasm_bindgen_futures::spawn_local(future);
179
180 Ok(())
181 }
182
183 fn is_running(&self) -> bool {
184 let has_incoming = self
185 .inner
186 .incoming
187 .lock()
188 .expect("Failed to lock incoming mutex")
189 .as_ref()
190 .map(|receiver| !receiver.is_closed())
191 .unwrap_or(false);
192 let has_cancellation_token = self
193 .inner
194 .cancellation_token
195 .lock()
196 .expect("Failed to lock cancellation token mutex")
197 .is_some();
198 has_incoming && has_cancellation_token
199 }
200
201 async fn send(&self, message: OutgoingMessage) -> Result<(), SendError> {
202 let result = self
203 .inner
204 .crypto
205 .send(&self.inner.communication, &self.inner.sessions, message)
206 .await;
207
208 if let Err(ref error) = result {
209 match error.kind() {
210 ErrorKind::Fatal => {
211 tracing::error!(?error, "Fatal error sending message, stopping IPC client");
212 stop_inner(&self.inner);
213 }
214 ErrorKind::Unreachable => {}
216 ErrorKind::Other => {
218 tracing::debug!(
219 ?error,
220 "Recoverable error sending message, IPC client will continue running"
221 );
222 }
223 }
224 }
225
226 result.map_err(|e| match e.kind() {
227 ErrorKind::Unreachable => SendError::Unreachable,
228 _ => SendError::Other(format!("{e:?}")),
229 })
230 }
231
232 async fn subscribe(
233 &self,
234 topic: Option<String>,
235 ) -> Result<IpcClientSubscription, SubscribeError> {
236 Ok(IpcClientSubscription {
237 receiver: self
238 .inner
239 .incoming
240 .lock()
241 .expect("Failed to lock incoming mutex")
242 .as_ref()
243 .ok_or(SubscribeError::NotStarted)?
244 .resubscribe(),
245 topic,
246 })
247 }
248
249 async fn register_rpc_handler_erased(&self, name: &str, handler: Box<dyn ErasedRpcHandler>) {
250 self.inner
251 .handlers
252 .register_erased(name.to_owned(), handler)
253 .await;
254 }
255}
256
257fn stop_inner<Crypto, Com, Ses>(inner: &IpcClientInner<Crypto, Com, Ses>)
258where
259 Crypto: CryptoProvider<Com, Ses>,
260 Com: CommunicationBackend,
261 Ses: SessionRepository<Crypto::Session>,
262{
263 let mut cancellation_token = inner
264 .cancellation_token
265 .lock()
266 .expect("Failed to lock cancellation token mutex");
267
268 if let Some(cancellation_token) = cancellation_token.take() {
269 cancellation_token.cancel();
270 }
271}
272
273fn handle_rpc_request<Crypto, Com, Ses>(
274 inner: &Arc<IpcClientInner<Crypto, Com, Ses>>,
275 incoming_message: IncomingMessage,
276) where
277 Crypto: CryptoProvider<Com, Ses>,
278 Com: CommunicationBackend,
279 Ses: SessionRepository<Crypto::Session>,
280{
281 let inner = inner.clone();
282 let future = async move {
283 #[derive(Debug, Error)]
284 enum HandleError {
285 #[error("Failed to deserialize request message: {0}")]
286 Deserialize(String),
287
288 #[error("Failed to serialize response message: {0}")]
289 Serialize(String),
290 }
291
292 async fn handle(
293 incoming_message: IncomingMessage,
294 handlers: &RpcHandlerRegistry,
295 ) -> Result<OutgoingMessage, HandleError> {
296 let request = RpcRequestPayload::from_slice(incoming_message.payload.clone()).map_err(
297 |e: serde_utils::DeserializeError| HandleError::Deserialize(e.to_string()),
298 )?;
299
300 let response = handlers.handle(&request).await;
301
302 let response_message = OutgoingRpcResponseMessage {
303 request_id: request.request_id(),
304 request_type: request.request_type(),
305 result: response,
306 };
307
308 let payload = serde_utils::to_vec(&response_message)
310 .map_err(|e: serde_utils::SerializeError| HandleError::Serialize(e.to_string()))?;
311
312 Ok(OutgoingMessage {
313 payload,
314 destination: incoming_message.source.into(),
315 topic: Some(request.response_topic().to_owned()),
316 })
317 }
318
319 match handle(incoming_message, &inner.handlers).await {
320 Ok(outgoing_message) => {
321 let result = inner
324 .crypto
325 .send(&inner.communication, &inner.sessions, outgoing_message)
326 .await;
327 if result.is_err() {
328 tracing::error!("Failed to send response message");
329 }
330 }
331 Err(error) => {
332 tracing::error!(%error, "Error handling RPC request");
333 }
334 }
335 };
336
337 #[cfg(not(target_arch = "wasm32"))]
338 tokio::spawn(future);
339
340 #[cfg(target_arch = "wasm32")]
341 wasm_bindgen_futures::spawn_local(future);
342}
343
344impl IpcClientSubscription {
345 pub async fn receive(
348 &mut self,
349 cancellation_token: Option<CancellationToken>,
350 ) -> Result<IncomingMessage, ReceiveError> {
351 let cancellation_token = cancellation_token.unwrap_or_default();
352
353 loop {
354 select! {
355 _ = cancellation_token.cancelled() => {
356 return Err(ReceiveError::Cancelled)
357 }
358 result = self.receiver.recv() => {
359 let received = result?;
360 if self.topic.is_none() || received.topic == self.topic {
361 return Ok::<IncomingMessage, ReceiveError>(received);
362 }
363 }
364 }
365 }
366 }
367}
368
369impl<Payload> IpcClientTypedSubscription<Payload>
370where
371 Payload: DeserializeOwned + PayloadTypeName,
372{
373 pub(crate) fn new(subscription: IpcClientSubscription) -> Self {
374 Self(subscription, std::marker::PhantomData)
375 }
376
377 pub async fn receive(
380 &mut self,
381 cancellation_token: Option<CancellationToken>,
382 ) -> Result<TypedIncomingMessage<Payload>, TypedReceiveError> {
383 let received = self.0.receive(cancellation_token).await?;
384 received
385 .try_into()
386 .map_err(|e: serde_utils::DeserializeError| TypedReceiveError::Typing(e.to_string()))
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use std::{collections::HashMap, time::Duration};
393
394 use bitwarden_threading::time::sleep;
395 use serde::{Deserialize, Serialize};
396
397 use super::*;
398 use crate::{
399 IpcClientExt,
400 endpoint::{Endpoint, HostId, Source},
401 ipc_client_trait::IpcClient,
402 message::PayloadTypeName,
403 rpc::{
404 request::RpcRequest,
405 request_message::{RPC_REQUEST_PAYLOAD_TYPE_NAME, RpcRequestMessage},
406 response_message::IncomingRpcResponseMessage,
407 },
408 traits::{InMemorySessionRepository, NoEncryptionCryptoProvider, TestCommunicationBackend},
409 };
410
411 #[derive(Debug, Clone)]
414 struct TestCryptoError {
415 #[allow(dead_code)]
418 message: String,
419 fatal: bool,
420 }
421
422 impl IpcErrorKind for TestCryptoError {
423 fn kind(&self) -> ErrorKind {
424 if self.fatal {
425 ErrorKind::Fatal
426 } else {
427 ErrorKind::Other
428 }
429 }
430 }
431
432 struct TestCryptoProvider {
433 send_result: Option<Result<(), TestCryptoError>>,
435 receive_result: Option<Result<IncomingMessage, TestCryptoError>>,
437 }
438
439 type TestSessionRepository = InMemorySessionRepository<String>;
440 impl CryptoProvider<TestCommunicationBackend, TestSessionRepository> for TestCryptoProvider {
441 type Session = String;
442 type SendError = TestCryptoError;
443 type ReceiveError = TestCryptoError;
444
445 async fn receive(
446 &self,
447 _receiver: &<TestCommunicationBackend as CommunicationBackend>::Receiver,
448 _communication: &TestCommunicationBackend,
449 _sessions: &TestSessionRepository,
450 ) -> Result<IncomingMessage, Self::ReceiveError> {
451 match &self.receive_result {
452 Some(result) => {
453 sleep(Duration::from_millis(5)).await;
458 result.clone()
459 }
460 None => {
461 sleep(Duration::from_secs(600)).await;
463 Err(TestCryptoError {
464 message: "Simulated timeout".to_string(),
465 fatal: true,
466 })
467 }
468 }
469 }
470
471 async fn send(
472 &self,
473 _communication: &TestCommunicationBackend,
474 _sessions: &TestSessionRepository,
475 _message: OutgoingMessage,
476 ) -> Result<(), Self::SendError> {
477 match &self.send_result {
478 Some(result) => result.clone(),
479 None => {
480 sleep(Duration::from_secs(600)).await;
482 Err(TestCryptoError {
483 message: "Simulated timeout".to_string(),
484 fatal: true,
485 })
486 }
487 }
488 }
489 }
490
491 #[tokio::test]
492 async fn returns_send_error_when_crypto_provider_returns_error() {
493 let message = OutgoingMessage {
494 payload: vec![],
495 destination: Endpoint::BrowserBackground { id: HostId::Own },
496 topic: None,
497 };
498 let crypto_provider = TestCryptoProvider {
499 send_result: Some(Err(TestCryptoError {
500 message: "Crypto error".to_string(),
501 fatal: false,
502 })),
503 receive_result: Some(Err(TestCryptoError {
504 message: "Should not have be called".to_string(),
505 fatal: false,
506 })),
507 };
508 let communication_provider = TestCommunicationBackend::new();
509 let session_map = TestSessionRepository::new(HashMap::new());
510 let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
511 let _ = client.start(None).await;
512
513 let error = client.send(message).await.unwrap_err();
514
515 assert!(error.to_string().contains("Crypto error"));
516 }
517
518 #[tokio::test]
519 async fn communication_provider_has_outgoing_message_when_sending_through_ipc_client() {
520 let message = OutgoingMessage {
521 payload: vec![],
522 destination: Endpoint::BrowserBackground { id: HostId::Own },
523 topic: None,
524 };
525 let crypto_provider = NoEncryptionCryptoProvider;
526 let communication_provider = TestCommunicationBackend::new();
527 let session_map = InMemorySessionRepository::new(HashMap::new());
528 let client =
529 IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
530 let _ = client.start(None).await;
531
532 client.send(message.clone()).await.unwrap();
533
534 let outgoing_messages = communication_provider.outgoing().await;
535 assert_eq!(outgoing_messages, vec![message]);
536 }
537
538 #[tokio::test]
539 async fn returns_received_message_when_received_from_backend() {
540 let message = IncomingMessage {
541 payload: vec![],
542 source: Source::Web {
543 tab_id: 9001,
544 document_id: "doc-1".to_string(),
545 origin: "https://example.com".to_string(),
546 },
547 destination: Endpoint::BrowserBackground { id: HostId::Own },
548 topic: None,
549 };
550 let crypto_provider = NoEncryptionCryptoProvider;
551 let communication_provider = TestCommunicationBackend::new();
552 let session_map = InMemorySessionRepository::new(HashMap::new());
553 let client =
554 IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
555 let _ = client.start(None).await;
556
557 let mut subscription = client
558 .subscribe(None)
559 .await
560 .expect("Subscribing should not fail");
561 communication_provider.push_incoming(message.clone());
562 let received_message = subscription.receive(None).await.unwrap();
563
564 assert_eq!(received_message, message);
565 }
566
567 #[tokio::test]
568 async fn skips_non_matching_topics_and_returns_first_matching_message() {
569 let non_matching_message = IncomingMessage {
570 payload: vec![],
571 source: Source::Web {
572 tab_id: 9001,
573 document_id: "doc-1".to_string(),
574 origin: "https://example.com".to_string(),
575 },
576 destination: Endpoint::BrowserBackground { id: HostId::Own },
577 topic: Some("non_matching_topic".to_owned()),
578 };
579 let matching_message = IncomingMessage {
580 payload: vec![109],
581 source: Source::Web {
582 tab_id: 9001,
583 document_id: "doc-1".to_string(),
584 origin: "https://example.com".to_string(),
585 },
586 destination: Endpoint::BrowserBackground { id: HostId::Own },
587 topic: Some("matching_topic".to_owned()),
588 };
589
590 let crypto_provider = NoEncryptionCryptoProvider;
591 let communication_provider = TestCommunicationBackend::new();
592 let session_map = InMemorySessionRepository::new(HashMap::new());
593 let client =
594 IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
595 let _ = client.start(None).await;
596 let mut subscription = client
597 .subscribe(Some("matching_topic".to_owned()))
598 .await
599 .expect("Subscribing should not fail");
600 communication_provider.push_incoming(non_matching_message.clone());
601 communication_provider.push_incoming(non_matching_message.clone());
602 communication_provider.push_incoming(matching_message.clone());
603
604 let received_message: IncomingMessage = subscription.receive(None).await.unwrap();
605
606 assert_eq!(received_message, matching_message);
607 }
608
609 #[tokio::test]
610 async fn skips_unrelated_messages_and_returns_typed_message() {
611 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
612 struct TestPayload {
613 some_data: String,
614 }
615
616 impl PayloadTypeName for TestPayload {
617 const PAYLOAD_TYPE_NAME: &str = "TestPayload";
618 }
619
620 let unrelated = IncomingMessage {
621 payload: vec![],
622 source: Source::Web {
623 tab_id: 9001,
624 document_id: "doc-1".to_string(),
625 origin: "https://example.com".to_string(),
626 },
627 destination: Endpoint::BrowserBackground { id: HostId::Own },
628 topic: None,
629 };
630 let typed_message = crate::message::TypedIncomingMessage {
631 payload: TestPayload {
632 some_data: "Hello, world!".to_string(),
633 },
634 source: Source::Web {
635 tab_id: 9001,
636 document_id: "doc-1".to_string(),
637 origin: "https://example.com".to_string(),
638 },
639 destination: Endpoint::BrowserBackground { id: HostId::Own },
640 };
641
642 let crypto_provider = NoEncryptionCryptoProvider;
643 let communication_provider = TestCommunicationBackend::new();
644 let session_map = InMemorySessionRepository::new(HashMap::new());
645 let client =
646 IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
647 let _ = client.start(None).await;
648 let mut subscription = client
649 .subscribe_typed::<TestPayload>()
650 .await
651 .expect("Subscribing should not fail");
652 communication_provider.push_incoming(unrelated.clone());
653 communication_provider.push_incoming(unrelated.clone());
654 communication_provider.push_incoming(
655 typed_message
656 .clone()
657 .try_into()
658 .expect("Serialization should not fail"),
659 );
660
661 let received_message = subscription.receive(None).await.unwrap();
662
663 assert_eq!(received_message, typed_message);
664 }
665
666 #[tokio::test]
667 async fn returns_error_if_related_message_was_not_deserializable() {
668 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
669 struct TestPayload {
670 some_data: String,
671 }
672
673 impl PayloadTypeName for TestPayload {
674 const PAYLOAD_TYPE_NAME: &str = "TestPayload";
675 }
676
677 let non_deserializable_message = IncomingMessage {
678 payload: vec![],
679 source: Source::Web {
680 tab_id: 9001,
681 document_id: "doc-1".to_string(),
682 origin: "https://example.com".to_string(),
683 },
684 destination: Endpoint::BrowserBackground { id: HostId::Own },
685 topic: Some("TestPayload".to_owned()),
686 };
687
688 let crypto_provider = NoEncryptionCryptoProvider;
689 let communication_provider = TestCommunicationBackend::new();
690 let session_map = InMemorySessionRepository::new(HashMap::new());
691 let client =
692 IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
693 let _ = client.start(None).await;
694 let mut subscription = client
695 .subscribe_typed::<TestPayload>()
696 .await
697 .expect("Subscribing should not fail");
698 communication_provider.push_incoming(non_deserializable_message.clone());
699
700 let result = subscription.receive(None).await;
701 assert!(matches!(result, Err(TypedReceiveError::Typing(_))));
702 }
703
704 #[tokio::test]
705 async fn ipc_client_stops_if_crypto_returns_fatal_send_error() {
706 let message = OutgoingMessage {
707 payload: vec![],
708 destination: Endpoint::BrowserBackground { id: HostId::Own },
709 topic: None,
710 };
711 let crypto_provider = TestCryptoProvider {
712 send_result: Some(Err(TestCryptoError {
713 message: "Crypto error".to_string(),
714 fatal: true,
715 })),
716 receive_result: None,
717 };
718 let communication_provider = TestCommunicationBackend::new();
719 let session_map = TestSessionRepository::new(HashMap::new());
720 let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
721 let _ = client.start(None).await;
722
723 let error = client.send(message).await.unwrap_err();
724 let is_running = client.is_running();
725
726 assert!(error.to_string().contains("Crypto error"));
727 assert!(!is_running);
728 }
729
730 #[tokio::test]
731 async fn ipc_client_keeps_running_if_crypto_returns_recoverable_send_error() {
732 let message = OutgoingMessage {
733 payload: vec![],
734 destination: Endpoint::BrowserBackground { id: HostId::Own },
735 topic: None,
736 };
737 let crypto_provider = TestCryptoProvider {
738 send_result: Some(Err(TestCryptoError {
741 message: "Crypto error".to_string(),
742 fatal: false,
743 })),
744 receive_result: None,
746 };
747 let communication_provider = TestCommunicationBackend::new();
748 let session_map = TestSessionRepository::new(HashMap::new());
749 let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
750 let _ = client.start(None).await;
751
752 let error = client.send(message).await.unwrap_err();
753 let is_running = client.is_running();
754
755 assert!(error.to_string().contains("Crypto error"));
757 assert!(is_running);
759 }
760
761 #[tokio::test]
762 async fn ipc_client_stops_if_crypto_returns_fatal_receive_error() {
763 let crypto_provider = TestCryptoProvider {
764 send_result: None,
765 receive_result: Some(Err(TestCryptoError {
766 message: "Crypto error".to_string(),
767 fatal: true,
768 })),
769 };
770 let communication_provider = TestCommunicationBackend::new();
771 let session_map = TestSessionRepository::new(HashMap::new());
772 let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
773 let cancellation_token = CancellationToken::new();
774 let _ = client.start(Some(cancellation_token.clone())).await;
775
776 tokio::time::sleep(Duration::from_millis(100)).await;
778 let is_running = client.is_running();
779
780 assert!(!is_running);
781 assert!(cancellation_token.is_cancelled());
782 }
783
784 #[tokio::test]
785 async fn ipc_client_keeps_running_if_crypto_returns_recoverable_receive_error() {
786 let crypto_provider = TestCryptoProvider {
787 send_result: None,
788 receive_result: Some(Err(TestCryptoError {
790 message: "Crypto error".to_string(),
791 fatal: false,
792 })),
793 };
794 let communication_provider = TestCommunicationBackend::new();
795 let session_map = TestSessionRepository::new(HashMap::new());
796 let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
797 let cancellation_token = CancellationToken::new();
798 let _ = client.start(Some(cancellation_token.clone())).await;
799
800 tokio::time::sleep(Duration::from_millis(100)).await;
802 let is_running = client.is_running();
803
804 assert!(is_running);
805 assert!(!cancellation_token.is_cancelled());
806 }
807
808 #[tokio::test]
809 async fn ipc_client_is_not_running_if_cancellation_token_is_cancelled() {
810 let crypto_provider = TestCryptoProvider {
811 send_result: None,
812 receive_result: None,
813 };
814 let communication_provider = TestCommunicationBackend::new();
815 let session_map = TestSessionRepository::new(HashMap::new());
816 let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
817 let cancellation_token = CancellationToken::new();
818 let _ = client.start(Some(cancellation_token.clone())).await;
819
820 tokio::time::sleep(Duration::from_millis(100)).await;
822
823 cancellation_token.cancel();
825 tokio::time::sleep(Duration::from_millis(100)).await;
826 let is_running = client.is_running();
827
828 assert!(!is_running);
829 }
830
831 #[tokio::test]
832 async fn ipc_client_is_running_if_no_errors_are_encountered() {
833 let crypto_provider = TestCryptoProvider {
834 send_result: None,
835 receive_result: None,
836 };
837 let communication_provider = TestCommunicationBackend::new();
838 let session_map = TestSessionRepository::new(HashMap::new());
839 let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
840 let cancellation_token = CancellationToken::new();
841 let _ = client.start(Some(cancellation_token.clone())).await;
842
843 tokio::time::sleep(Duration::from_millis(100)).await;
845 let is_running = client.is_running();
846
847 assert!(is_running);
848 assert!(!cancellation_token.is_cancelled());
849 }
850
851 #[tokio::test]
852 async fn ipc_client_is_not_running_if_not_started() {
853 let crypto_provider = TestCryptoProvider {
854 send_result: None,
855 receive_result: None,
856 };
857 let communication_provider = TestCommunicationBackend::new();
858 let session_map = TestSessionRepository::new(HashMap::new());
859 let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
860
861 tokio::time::sleep(Duration::from_millis(100)).await;
863 let is_running = client.is_running();
864
865 assert!(!is_running);
866 }
867
868 #[tokio::test]
869 async fn ipc_client_start_returns_error_if_already_running() {
870 let crypto_provider = TestCryptoProvider {
871 send_result: None,
872 receive_result: None,
873 };
874 let communication_provider = TestCommunicationBackend::new();
875 let session_map = TestSessionRepository::new(HashMap::new());
876 let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
877 let cancellation_token = CancellationToken::new();
878 let first_result = client.start(Some(cancellation_token.clone())).await;
879 assert_eq!(first_result, Ok(()));
880
881 tokio::time::sleep(Duration::from_millis(100)).await;
883 assert!(client.is_running());
884
885 let second_result = client.start(Some(cancellation_token.clone())).await;
886 assert_eq!(second_result, Err(AlreadyRunningError));
887 }
888
889 mod request {
890 use super::*;
891 use crate::RpcHandler;
892
893 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
894 struct TestRequest {
895 a: i32,
896 b: i32,
897 }
898
899 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
900 struct TestResponse {
901 result: i32,
902 }
903
904 impl RpcRequest for TestRequest {
905 type Response = TestResponse;
906
907 const NAME: &str = "TestRequest";
908 }
909
910 struct TestHandler;
911
912 impl RpcHandler for TestHandler {
913 type Request = TestRequest;
914
915 async fn handle(&self, request: Self::Request) -> TestResponse {
916 TestResponse {
917 result: request.a + request.b,
918 }
919 }
920 }
921
922 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
926 struct OtherRequest;
927
928 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
929 enum OtherResponse {
930 Available,
931 }
932
933 impl RpcRequest for OtherRequest {
934 type Response = OtherResponse;
935
936 const NAME: &str = "OtherRequest";
937 }
938
939 #[tokio::test]
940 async fn request_sends_message_and_returns_response() {
941 let crypto_provider = NoEncryptionCryptoProvider;
942 let communication_provider = TestCommunicationBackend::new();
943 let session_map = InMemorySessionRepository::default();
944 let client =
945 IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
946 let _ = client.start(None).await;
947 let request = TestRequest { a: 1, b: 2 };
948 let response = TestResponse { result: 3 };
949
950 let request_clone = request.clone();
952 let client_clone = client.clone();
953 let result_handle = tokio::spawn(async move {
954 client_clone
955 .request::<TestRequest>(
956 request_clone,
957 Endpoint::BrowserBackground { id: HostId::Own },
958 None,
959 )
960 .await
961 });
962 tokio::time::sleep(Duration::from_millis(100)).await;
963
964 let outgoing_messages = communication_provider.outgoing().await;
966 let outgoing_request: RpcRequestMessage<TestRequest> =
967 serde_utils::from_slice(&outgoing_messages[0].payload)
968 .expect("Deserialization should not fail");
969 assert_eq!(outgoing_request.request_type, "TestRequest");
970 assert_eq!(outgoing_request.request, request);
971
972 let simulated_response = IncomingRpcResponseMessage {
974 result: Ok(response),
975 request_id: outgoing_request.request_id.clone(),
976 request_type: outgoing_request.request_type.clone(),
977 };
978 let simulated_response = IncomingMessage {
979 payload: serde_utils::to_vec(&simulated_response)
980 .expect("Serialization should not fail"),
981 source: Source::BrowserBackground { id: HostId::Own },
982 destination: Endpoint::Web {
983 tab_id: 9001,
984 document_id: "doc-1".to_string(),
985 },
986 topic: Some(outgoing_request.response_topic.clone()),
989 };
990 communication_provider.push_incoming(simulated_response);
991
992 let result = result_handle.await.unwrap();
994 assert_eq!(result.unwrap().result, 3);
995 }
996
997 #[tokio::test]
1002 async fn concurrent_request_of_a_different_type_does_not_disturb_a_pending_request() {
1003 let crypto_provider = NoEncryptionCryptoProvider;
1004 let communication_provider = TestCommunicationBackend::new();
1005 let session_map = InMemorySessionRepository::default();
1006 let client =
1007 IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
1008 let _ = client.start(None).await;
1009
1010 let test_handle = {
1012 let client = client.clone();
1013 tokio::spawn(async move {
1014 client
1015 .request::<TestRequest>(
1016 TestRequest { a: 1, b: 2 },
1017 Endpoint::BrowserBackground { id: HostId::Own },
1018 None,
1019 )
1020 .await
1021 })
1022 };
1023 let other_handle = {
1024 let client = client.clone();
1025 tokio::spawn(async move {
1026 client
1027 .request::<OtherRequest>(
1028 OtherRequest,
1029 Endpoint::BrowserBackground { id: HostId::Own },
1030 None,
1031 )
1032 .await
1033 })
1034 };
1035 tokio::time::sleep(Duration::from_millis(100)).await;
1036
1037 let outgoing = communication_provider.outgoing().await;
1040 let requests: HashMap<String, RpcRequestMessage<serde_json::Value>> = outgoing
1041 .iter()
1042 .map(|message| {
1043 let partial: RpcRequestMessage<serde_json::Value> =
1044 serde_utils::from_slice(&message.payload)
1045 .expect("Deserialization should not fail");
1046 (partial.request_type.clone(), partial)
1047 })
1048 .collect();
1049
1050 let respond = |topic: String, payload: Vec<u8>| {
1051 communication_provider.push_incoming(IncomingMessage {
1052 payload,
1053 source: Source::BrowserBackground { id: HostId::Own },
1054 destination: Endpoint::Web {
1055 tab_id: 9001,
1056 document_id: "doc-1".to_string(),
1057 },
1058 topic: Some(topic),
1059 });
1060 };
1061
1062 respond(
1065 requests["OtherRequest"].response_topic.clone(),
1066 serde_utils::to_vec(&IncomingRpcResponseMessage {
1067 result: Ok(OtherResponse::Available),
1068 request_id: requests["OtherRequest"].request_id.clone(),
1069 request_type: "OtherRequest".to_string(),
1070 })
1071 .expect("Serialization should not fail"),
1072 );
1073
1074 respond(
1075 requests["TestRequest"].response_topic.clone(),
1076 serde_utils::to_vec(&IncomingRpcResponseMessage {
1077 result: Ok(TestResponse { result: 3 }),
1078 request_id: requests["TestRequest"].request_id.clone(),
1079 request_type: "TestRequest".to_string(),
1080 })
1081 .expect("Serialization should not fail"),
1082 );
1083
1084 assert_eq!(
1085 other_handle
1086 .await
1087 .unwrap()
1088 .expect("OtherRequest should succeed"),
1089 OtherResponse::Available
1090 );
1091 assert_eq!(
1092 test_handle
1093 .await
1094 .unwrap()
1095 .expect("TestRequest should succeed"),
1096 TestResponse { result: 3 }
1097 );
1098 }
1099
1100 #[tokio::test]
1105 async fn malformed_response_surfaces_error_instead_of_hanging() {
1106 let crypto_provider = NoEncryptionCryptoProvider;
1107 let communication_provider = TestCommunicationBackend::new();
1108 let session_map = InMemorySessionRepository::default();
1109 let client =
1110 IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
1111 let _ = client.start(None).await;
1112
1113 let result_handle = {
1114 let client = client.clone();
1115 tokio::spawn(async move {
1116 client
1117 .request::<TestRequest>(
1118 TestRequest { a: 1, b: 2 },
1119 Endpoint::BrowserBackground { id: HostId::Own },
1120 None,
1121 )
1122 .await
1123 })
1124 };
1125 tokio::time::sleep(Duration::from_millis(100)).await;
1126
1127 let outgoing = communication_provider.outgoing().await;
1131 let request: RpcRequestMessage<TestRequest> =
1132 serde_utils::from_slice(&outgoing[0].payload)
1133 .expect("Deserialization should not fail");
1134 let malformed = IncomingRpcResponseMessage {
1135 result: Ok(OtherResponse::Available),
1136 request_id: request.request_id.clone(),
1137 request_type: request.request_type.clone(),
1138 };
1139 communication_provider.push_incoming(IncomingMessage {
1140 payload: serde_utils::to_vec(&malformed).expect("Serialization should not fail"),
1141 source: Source::BrowserBackground { id: HostId::Own },
1142 destination: Endpoint::Web {
1143 tab_id: 9001,
1144 document_id: "doc-1".to_string(),
1145 },
1146 topic: Some(request.response_topic.clone()),
1147 });
1148
1149 let result = tokio::time::timeout(Duration::from_secs(5), result_handle)
1150 .await
1151 .expect("request must not hang on a malformed response")
1152 .unwrap();
1153 assert!(matches!(
1154 result,
1155 Err(crate::error::RequestError::Rpc(
1156 crate::rpc::error::RpcError::ResponseDeserialization(_)
1157 ))
1158 ));
1159 }
1160
1161 #[tokio::test]
1162 async fn incoming_rpc_message_handles_request_and_returns_response() {
1163 let crypto_provider = NoEncryptionCryptoProvider;
1164 let communication_provider = TestCommunicationBackend::new();
1165 let session_map = InMemorySessionRepository::default();
1166 let client =
1167 IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
1168 let _ = client.start(None).await;
1169 let request_id = uuid::Uuid::new_v4().to_string();
1170 let request = TestRequest { a: 1, b: 2 };
1171 let response = TestResponse { result: 3 };
1172
1173 client.register_rpc_handler(TestHandler).await;
1175
1176 let response_topic = format!("RpcResponseMessage:{request_id}");
1178 let simulated_request = RpcRequestMessage {
1179 request,
1180 request_id: request_id.clone(),
1181 request_type: "TestRequest".to_string(),
1182 response_topic: response_topic.clone(),
1183 };
1184 let simulated_request_message = IncomingMessage {
1185 payload: serde_utils::to_vec(&simulated_request)
1186 .expect("Serialization should not fail"),
1187 source: Source::Web {
1188 tab_id: 9001,
1189 document_id: "doc-1".to_string(),
1190 origin: "https://example.com".to_string(),
1191 },
1192 destination: Endpoint::BrowserBackground { id: HostId::Own },
1193 topic: Some(RPC_REQUEST_PAYLOAD_TYPE_NAME.to_owned()),
1194 };
1195 communication_provider.push_incoming(simulated_request_message);
1196
1197 tokio::time::sleep(Duration::from_millis(100)).await;
1199
1200 let outgoing_messages = communication_provider.outgoing().await;
1202 let outgoing_response: IncomingRpcResponseMessage<TestResponse> =
1203 serde_utils::from_slice(&outgoing_messages[0].payload)
1204 .expect("Deserialization should not fail");
1205
1206 assert_eq!(outgoing_messages[0].topic, Some(response_topic));
1208 assert_eq!(outgoing_response.request_type, "TestRequest");
1209 assert_eq!(outgoing_response.result, Ok(response));
1210 }
1211 }
1212}