Skip to main content

citadel_sdk/
remote_ext.rs

1//! Remote Protocol Extensions
2//!
3//! This module extends the core NodeRemote functionality with high-level operations
4//! for managing connections, file transfers, and peer interactions in the Citadel
5//! Protocol network.
6//!
7//! # Features
8//! - User registration and authentication
9//! - Connection management
10//! - File transfer operations
11//! - Virtual filesystem support
12//! - Peer discovery and management
13//! - Group communication
14//! - Security settings configuration
15//!
16//! # Example
17//! ```rust
18//! use citadel_sdk::prelude::*;
19//!
20//! async fn example<R: Ratchet>(remote: NodeRemote<R>) -> Result<(), NetworkError> {
21//!     // Register a new user
22//!     let reg = remote.register_with_defaults(
23//!         "127.0.0.1:25021",
24//!         "John Doe",
25//!         "john.doe",
26//!         "password123"
27//!     ).await?;
28//!
29//!     // Connect to a peer
30//!     let auth = AuthenticationRequest::credentialed("john.doe", "password123");
31//!     let conn = remote.connect_with_defaults(auth).await?;
32//!
33//!     // Send a file to a peer
34//!     remote.find_target("john.doe", "peer.name")
35//!         .await?
36//!         .send_file("/path/to/file.txt")
37//!         .await?;
38//!
39//!     Ok(())
40//! }
41//! ```
42//!
43//! # Important Notes
44//! - All operations are asynchronous
45//! - Connections are automatically managed
46//! - File transfers support chunking
47//! - Virtual filesystem is encrypted
48//! - Peer connections require mutual registration
49//!
50//! # Related Components
51//! - [`NodeRemote`]: Core remote interface
52//! - [`ClientServerRemote`]: Client-server communication
53//! - [`PeerRemote`]: Peer-to-peer communication
54//! - [`CitadelClientServerConnection`]: Connection management
55//! - [`RegisterSuccess`]: Registration handling
56//!
57//! [`NodeRemote`]: crate::prelude::NodeRemote
58//! [`ClientServerRemote`]: crate::prelude::ClientServerRemote
59//! [`PeerRemote`]: crate::prelude::PeerRemote
60//! [`CitadelClientServerConnection`]: crate::prelude::CitadelClientServerConnection
61//! [`RegisterSuccess`]: crate::prelude::RegisterSuccess
62
63use crate::prefabs::ClientServerRemote;
64use crate::prelude::results::{PeerConnectSuccess, PeerRegisterStatus};
65use crate::prelude::*;
66use crate::remote_ext::remote_specialization::PeerRemote;
67use crate::remote_ext::results::LocalGroupPeerFullInfo;
68use std::ops::{Deref, DerefMut};
69
70use futures::StreamExt;
71use std::path::PathBuf;
72use std::time::Duration;
73
74pub(crate) mod user_ids {
75    use crate::prelude::*;
76    use std::ops::Deref;
77
78    #[derive(Debug)]
79    /// A reference to a user identifier
80    pub struct SymmetricIdentifierHandleRef<'a, R: Ratchet> {
81        pub(crate) user: VirtualTargetType,
82        pub(crate) remote: &'a NodeRemote<R>,
83        pub(crate) target_username: Option<String>,
84    }
85
86    impl<R: Ratchet> SymmetricIdentifierHandleRef<'_, R> {
87        pub fn into_owned(self) -> SymmetricIdentifierHandle<R> {
88            SymmetricIdentifierHandle {
89                user: self.user,
90                remote: self.remote.clone(),
91                target_username: self.target_username,
92            }
93        }
94    }
95
96    #[derive(Clone, Debug)]
97    /// A convenience structure for executing commands that depend on a specific registered user
98    pub struct SymmetricIdentifierHandle<R: Ratchet> {
99        user: VirtualTargetType,
100        remote: NodeRemote<R>,
101        target_username: Option<String>,
102    }
103
104    pub trait TargetLockedRemote<R: Ratchet>: Send + Sync {
105        fn user(&self) -> &VirtualTargetType;
106        fn remote(&self) -> &NodeRemote<R>;
107        fn target_username(&self) -> Option<&str>;
108        fn user_mut(&mut self) -> &mut VirtualTargetType;
109        fn session_security_settings(&self) -> Option<&SessionSecuritySettings>;
110    }
111
112    impl<R: Ratchet> TargetLockedRemote<R> for SymmetricIdentifierHandleRef<'_, R> {
113        fn user(&self) -> &VirtualTargetType {
114            &self.user
115        }
116        fn remote(&self) -> &NodeRemote<R> {
117            self.remote
118        }
119        fn target_username(&self) -> Option<&str> {
120            self.target_username.as_deref()
121        }
122        fn user_mut(&mut self) -> &mut VirtualTargetType {
123            &mut self.user
124        }
125
126        fn session_security_settings(&self) -> Option<&SessionSecuritySettings> {
127            None
128        }
129    }
130
131    impl<R: Ratchet> TargetLockedRemote<R> for SymmetricIdentifierHandle<R> {
132        fn user(&self) -> &VirtualTargetType {
133            &self.user
134        }
135        fn remote(&self) -> &NodeRemote<R> {
136            &self.remote
137        }
138        fn target_username(&self) -> Option<&str> {
139            self.target_username.as_deref()
140        }
141        fn user_mut(&mut self) -> &mut VirtualTargetType {
142            &mut self.user
143        }
144
145        fn session_security_settings(&self) -> Option<&SessionSecuritySettings> {
146            None
147        }
148    }
149
150    impl<R: Ratchet> From<SymmetricIdentifierHandleRef<'_, R>> for SymmetricIdentifierHandle<R> {
151        fn from(this: SymmetricIdentifierHandleRef<'_, R>) -> Self {
152            this.into_owned()
153        }
154    }
155
156    impl<R: Ratchet> Deref for SymmetricIdentifierHandle<R> {
157        type Target = NodeRemote<R>;
158
159        fn deref(&self) -> &Self::Target {
160            &self.remote
161        }
162    }
163
164    impl<R: Ratchet> Deref for SymmetricIdentifierHandleRef<'_, R> {
165        type Target = NodeRemote<R>;
166
167        fn deref(&self) -> &Self::Target {
168            self.remote
169        }
170    }
171}
172
173/// Contains the elements required to communicate with the adjacent node
174pub struct CitadelClientServerConnection<R: Ratchet> {
175    /// An interface to send ordered, reliable, and encrypted messages
176    pub(crate) channel: Option<PeerChannel<R>>,
177    pub remote: ClientServerRemote<R>,
178    /// Only available if UdpMode was enabled at the beginning of a session
179    pub udp_channel_rx: Option<citadel_io::tokio::sync::oneshot::Receiver<UdpChannel<R>>>,
180    /// Contains the Google auth minted at the central server (if the central server enabled it), as well as any other services enabled by the central server
181    pub services: ServicesObject,
182    pub cid: u64,
183    pub session_security_settings: SessionSecuritySettings,
184}
185
186impl<R: Ratchet> CitadelClientServerConnection<R> {
187    /// Splits the channel into a send and receive half. This will render
188    /// the other fields of this connection object innaccessible
189    ///
190    /// # Panics
191    ///  - If the channel has already been taken
192    pub fn split(self) -> (PeerChannelSendHalf<R>, PeerChannelRecvHalf<R>) {
193        self.channel.expect("Channel already taken").split()
194    }
195
196    pub fn take_channel(&mut self) -> Option<PeerChannel<R>> {
197        self.channel.take()
198    }
199}
200
201impl<R: Ratchet> Deref for CitadelClientServerConnection<R> {
202    type Target = ClientServerRemote<R>;
203
204    fn deref(&self) -> &Self::Target {
205        &self.remote
206    }
207}
208
209impl<R: Ratchet> DerefMut for CitadelClientServerConnection<R> {
210    fn deref_mut(&mut self) -> &mut Self::Target {
211        &mut self.remote
212    }
213}
214
215/// Contains the elements entailed by a successful registration
216pub struct RegisterSuccess {
217    pub cid: u64,
218    /// The recovery codes of a post-quantum registration, to show the user once (see
219    /// `RegisterOkay::recovery_codes`). Empty for a legacy registration.
220    pub recovery_codes: Vec<String>,
221}
222
223/// Waits for the outcome of a registration request.
224async fn await_registration<R: Ratchet, Rem: Remote<R>>(
225    remote: &Rem,
226    register_request: NodeRequest,
227) -> Result<RegisterSuccess, NetworkError> {
228    let mut subscription = remote.send_callback_subscription(register_request).await?;
229    while let Some(status) = subscription.next().await {
230        match status.into_result()? {
231            NodeResult::RegisterOkay(RegisterOkay {
232                cid,
233                recovery_codes,
234                ..
235            }) => {
236                return Ok(RegisterSuccess {
237                    cid,
238                    recovery_codes,
239                });
240            }
241            NodeResult::RegisterFailure(err) => {
242                return Err(citadel_io::error!(
243                    citadel_io::ErrorCode::RemoteRegisterFailure,
244                    err.error_message
245                ));
246            }
247            NodeResult::Disconnect(err) => {
248                return Err(citadel_io::error!(
249                    citadel_io::ErrorCode::RemoteDisconnected,
250                    err.message
251                ));
252            }
253            evt => {
254                log::warn!(target: "citadel", "Invalid NodeResult for Register request received: {evt:?}");
255            }
256        }
257    }
258
259    Err(citadel_io::error!(
260        citadel_io::ErrorCode::RemoteKernelStreamDied,
261        "register"
262    ))
263}
264
265/// The legacy credentials (for a server below 0.12) and the password a post-quantum registration
266/// uses, trimmed the same way the legacy credentials trim it.
267async fn registration_credentials<
268    P: Into<String> + Send,
269    V: Into<String> + Send,
270    K: Into<SecBuffer>,
271>(
272    full_name: P,
273    username: V,
274    proposed_password: K,
275) -> Result<(ProposedCredentials, SecBuffer), NetworkError> {
276    let password: SecBuffer = proposed_password.into();
277    let trimmed = ProposedCredentials::registration_password(&password);
278    let creds = ProposedCredentials::new_register(full_name, username, password).await?;
279    Ok((creds, trimmed))
280}
281
282#[async_trait]
283/// Endows the [NodeRemote] with additional functions
284pub trait ProtocolRemoteExt<R: Ratchet>: Remote<R> {
285    /// Registers with custom settings
286    /// Returns a ticket which is used to uniquely identify the request in the protocol
287    async fn register<
288        T: std::net::ToSocketAddrs + Send,
289        P: Into<String> + Send,
290        V: Into<String> + Send,
291        K: Into<SecBuffer> + Send,
292    >(
293        &self,
294        addr: T,
295        full_name: P,
296        username: V,
297        proposed_password: K,
298        default_security_settings: SessionSecuritySettings,
299        server_password: Option<PreSharedKey>,
300    ) -> Result<RegisterSuccess, NetworkError> {
301        self.register_admitted(
302            addr,
303            full_name,
304            username,
305            proposed_password,
306            default_security_settings,
307            server_password,
308            None,
309        )
310        .await
311    }
312
313    /// As [`register`](Self::register), carrying an admission token (a Turnstile response) for a
314    /// server that asks fresh registrations for one. A refusal is a `PqSignInAdmissionRequired`
315    /// or `PqSignInAdmissionFailed` error.
316    #[allow(clippy::too_many_arguments)]
317    async fn register_admitted<
318        T: std::net::ToSocketAddrs + Send,
319        P: Into<String> + Send,
320        V: Into<String> + Send,
321        K: Into<SecBuffer> + Send,
322    >(
323        &self,
324        addr: T,
325        full_name: P,
326        username: V,
327        proposed_password: K,
328        default_security_settings: SessionSecuritySettings,
329        server_password: Option<PreSharedKey>,
330        admission: Option<String>,
331    ) -> Result<RegisterSuccess, NetworkError> {
332        let (creds, password) =
333            registration_credentials(full_name, username, proposed_password).await?;
334        let register_request = NodeRequest::RegisterToHypernode(RegisterToHypernode {
335            remote_addr: addr.to_socket_addrs()?.next().ok_or(citadel_io::error!(
336                citadel_io::ErrorCode::RemoteInvalidSocketAddr
337            ))?,
338            proposed_credentials: creds,
339            static_security_settings: default_security_settings,
340            session_password: server_password.unwrap_or_default(),
341            endpoint: None,
342            password: Some(password),
343            admission,
344        });
345
346        await_registration(self, register_request).await
347    }
348
349    /// Registers with a server reached by a WebSocket URL (`wss://org.example.net/`, or
350    /// `ws://host:port/path`) rather than at an address of its own: a server behind an HTTP edge,
351    /// which is told apart from the edge's other servers by hostname and path. The account
352    /// remembers the URL, so a later [`connect`](Self::connect) with credentials dials it too.
353    #[cfg(not(target_family = "wasm"))]
354    async fn register_to_endpoint<
355        P: Into<String> + Send,
356        V: Into<String> + Send,
357        K: Into<SecBuffer> + Send,
358    >(
359        &self,
360        endpoint: citadel_io::WebSocketEndpoint,
361        full_name: P,
362        username: V,
363        proposed_password: K,
364        default_security_settings: SessionSecuritySettings,
365        server_password: Option<PreSharedKey>,
366    ) -> Result<RegisterSuccess, NetworkError> {
367        self.register_to_endpoint_admitted(
368            endpoint,
369            full_name,
370            username,
371            proposed_password,
372            default_security_settings,
373            server_password,
374            None,
375        )
376        .await
377    }
378
379    /// As [`register_to_endpoint`](Self::register_to_endpoint), carrying an admission token.
380    #[cfg(not(target_family = "wasm"))]
381    #[allow(clippy::too_many_arguments)]
382    async fn register_to_endpoint_admitted<
383        P: Into<String> + Send,
384        V: Into<String> + Send,
385        K: Into<SecBuffer> + Send,
386    >(
387        &self,
388        endpoint: citadel_io::WebSocketEndpoint,
389        full_name: P,
390        username: V,
391        proposed_password: K,
392        default_security_settings: SessionSecuritySettings,
393        server_password: Option<PreSharedKey>,
394        admission: Option<String>,
395    ) -> Result<RegisterSuccess, NetworkError> {
396        let remote_addr = endpoint.resolve().await?;
397        let (creds, password) =
398            registration_credentials(full_name, username, proposed_password).await?;
399        let register_request = NodeRequest::RegisterToHypernode(RegisterToHypernode {
400            remote_addr,
401            proposed_credentials: creds,
402            static_security_settings: default_security_settings,
403            session_password: server_password.unwrap_or_default(),
404            endpoint: Some(endpoint),
405            password: Some(password),
406            admission,
407        });
408
409        await_registration(self, register_request).await
410    }
411
412    /// The WebSocket URL the local account `cid` registered to (see
413    /// [`register_to_endpoint`](Self::register_to_endpoint)), or `None` when its server is the
414    /// address in its connection info. Behind an HTTP edge that address is one of the edge's,
415    /// shared with every other server there, so it does not identify the server; this does.
416    async fn server_endpoint(
417        &self,
418        cid: u64,
419    ) -> Result<Option<citadel_io::WebSocketEndpoint>, NetworkError> {
420        citadel_proto::prelude::load_server_endpoint(self.account_manager(), cid).await
421    }
422
423    /// Registers using the default settings. The default uses No Google FCM keys and the default session security settings
424    /// Returns a ticket which is used to uniquely identify the request in the protocol
425    async fn register_with_defaults<
426        T: std::net::ToSocketAddrs + Send,
427        P: Into<String> + Send,
428        V: Into<String> + Send,
429        K: Into<SecBuffer> + Send,
430    >(
431        &self,
432        addr: T,
433        full_name: P,
434        username: V,
435        proposed_password: K,
436    ) -> Result<RegisterSuccess, NetworkError> {
437        self.register(
438            addr,
439            full_name,
440            username,
441            proposed_password,
442            Default::default(),
443            Default::default(),
444        )
445        .await
446    }
447
448    /// Connects with custom settings
449    /// Returns a ticket which is used to uniquely identify the request in the protocol
450    async fn connect(
451        &self,
452        auth: AuthenticationRequest,
453        connect_mode: ConnectMode,
454        udp_mode: UdpMode,
455        keep_alive_timeout: Option<Duration>,
456        session_security_settings: SessionSecuritySettings,
457        server_password: Option<PreSharedKey>,
458    ) -> Result<CitadelClientServerConnection<R>, NetworkError> {
459        let connect_request = NodeRequest::ConnectToHypernode(ConnectToHypernode {
460            auth_request: auth,
461            connect_mode,
462            udp_mode,
463            keep_alive_timeout: keep_alive_timeout.map(|r| r.as_secs()),
464            session_security_settings,
465            session_password: server_password.unwrap_or_default(),
466        });
467
468        let mut subscription = self.send_callback_subscription(connect_request).await?;
469        let status = subscription.next().await.ok_or(citadel_io::error!(
470            citadel_io::ErrorCode::RemoteKernelStreamDied,
471            "connect"
472        ))?;
473
474        return match status.into_result()? {
475            NodeResult::ConnectSuccess(ConnectSuccess {
476                ticket: _,
477                session_cid: cid,
478                remote_addr: _,
479                is_personal: _,
480                v_conn_type,
481                services,
482                welcome_message: _,
483                channel,
484                udp_rx_opt: udp_channel_rx,
485                session_security_settings,
486            }) => Ok(CitadelClientServerConnection {
487                remote: ClientServerRemote::new(
488                    v_conn_type,
489                    self.remote_ref().clone(),
490                    session_security_settings,
491                    None,
492                    None,
493                ),
494                channel: Some(*channel),
495                udp_channel_rx,
496                services,
497                cid,
498                session_security_settings,
499            }),
500            NodeResult::ConnectFail(ConnectFail {
501                error_message: err, ..
502            }) => Err(citadel_io::error!(
503                citadel_io::ErrorCode::RemoteConnectFailed,
504                err
505            )),
506            NodeResult::Disconnect(err) => {
507                return Err(citadel_io::error!(
508                    citadel_io::ErrorCode::RemoteDisconnected,
509                    err.message
510                ));
511            }
512            res => Err(citadel_io::error!(
513                citadel_io::ErrorCode::RemoteConnectUnexpectedResponse,
514                citadel_io::Dbg(res)
515            )),
516        };
517    }
518
519    /// Connects with the default settings
520    /// If FCM keys were created during the registration phase, then those keys will be used for the session. If new FCM keys need to be used, consider using [`Self::connect`]
521    ///
522    /// Never displaces a session the server already holds for the account
523    /// (`ConnectMode::Standard { force_login: false }`). A client reconnecting after its link
524    /// dropped should use [`Self::connect`] with `force_login: true`: see [`ConnectMode`].
525    async fn connect_with_defaults(
526        &self,
527        auth: AuthenticationRequest,
528    ) -> Result<CitadelClientServerConnection<R>, NetworkError> {
529        self.connect(
530            auth,
531            ConnectMode::Standard { force_login: false },
532            Default::default(),
533            None,
534            Default::default(),
535            Default::default(),
536        )
537        .await
538    }
539
540    /// Creates a valid target identifier used to make protocol requests. Raw user IDs or usernames can be used
541    /// ```
542    /// use citadel_sdk::prelude::*;
543    /// # use citadel_sdk::prefabs::client::single_connection::SingleClientServerConnectionKernel;
544    ///
545    /// let server_connection_settings = DefaultServerConnectionSettingsBuilder::credentialed_login("127.0.0.1:25021", "john.doe", "password").build().unwrap();
546    ///
547    /// # SingleClientServerConnectionKernel::new(server_connection_settings, |conn| async move {
548    /// conn.find_target("my_account", "my_peer").await?.send_file("/path/to/file.pdf").await
549    /// // or: conn.find_target(1234, "my_peer").await? [...]
550    /// # });
551    /// ```
552    async fn find_target<T: Into<UserIdentifier> + Send, P: Into<UserIdentifier> + Send>(
553        &self,
554        local_user: T,
555        peer: P,
556    ) -> Result<SymmetricIdentifierHandleRef<'_, R>, NetworkError> {
557        let account_manager = self.account_manager();
558        account_manager
559            .find_target_information(local_user, peer)
560            .await?
561            .map(move |(cid, peer)| {
562                if peer.parent_icid != 0 {
563                    SymmetricIdentifierHandleRef {
564                        user: VirtualTargetType::ExternalGroupPeer {
565                            session_cid: cid,
566                            interserver_cid: peer.parent_icid,
567                            peer_cid: peer.cid,
568                        },
569                        remote: self.remote_ref(),
570                        target_username: None,
571                    }
572                } else {
573                    SymmetricIdentifierHandleRef {
574                        user: VirtualTargetType::LocalGroupPeer {
575                            session_cid: cid,
576                            peer_cid: peer.cid,
577                        },
578                        remote: self.remote_ref(),
579                        target_username: None,
580                    }
581                }
582            })
583            .ok_or_else(|| citadel_io::error!(citadel_io::ErrorCode::RemoteTargetPairNotFound))
584    }
585
586    /// Creates a proposed target from the valid local user to an unregistered peer in the network. Used when creating registration requests for peers.
587    /// Currently only supports LocalGroup <-> LocalGroup peer connections
588    async fn propose_target<T: Into<UserIdentifier> + Send, P: Into<UserIdentifier> + Send>(
589        &self,
590        local_user: T,
591        peer: P,
592    ) -> Result<SymmetricIdentifierHandleRef<'_, R>, NetworkError> {
593        let local_cid = self.get_session_cid(local_user).await?;
594        match peer.into() {
595            UserIdentifier::ID(peer_cid) => Ok(SymmetricIdentifierHandleRef {
596                user: VirtualTargetType::LocalGroupPeer {
597                    session_cid: local_cid,
598                    peer_cid,
599                },
600                remote: self.remote_ref(),
601                target_username: None,
602            }),
603            UserIdentifier::Username(uname) => {
604                let peer_cid = self
605                    .remote_ref()
606                    .account_manager()
607                    .find_target_information(local_cid, uname.clone())
608                    .await?
609                    .map(|r| r.1.cid)
610                    .unwrap_or(0);
611                Ok(SymmetricIdentifierHandleRef {
612                    user: VirtualTargetType::LocalGroupPeer {
613                        session_cid: local_cid,
614                        peer_cid,
615                    },
616                    remote: self.remote_ref(),
617                    target_username: Some(uname),
618                })
619            }
620        }
621    }
622
623    /// Returns a list of local group peers on the network for local_user. May or may not be registered to the user. To get a list of registered users to local_user, run [`Self::get_local_group_mutual_peers`]
624    /// - limit: if None, all peers are obtained. If Some, at most the specified number of peers will be obtained
625    async fn get_local_group_peers<T: Into<UserIdentifier> + Send>(
626        &self,
627        local_user: T,
628        limit: Option<usize>,
629    ) -> Result<Vec<LocalGroupPeerFullInfo>, NetworkError> {
630        let local_cid = self.get_session_cid(local_user).await?;
631        let command = NodeRequest::PeerCommand(PeerCommand {
632            session_cid: local_cid,
633            command: PeerSignal::GetRegisteredPeers {
634                peer_conn_type: ClientConnectionType::Server {
635                    session_cid: local_cid,
636                },
637                response: None,
638                limit: limit.map(|r| r as i32),
639            },
640        });
641
642        let mut stream = self.send_callback_subscription(command).await?;
643
644        while let Some(status) = stream.next().await {
645            if let NodeResult::PeerEvent(PeerEvent {
646                event:
647                    PeerSignal::GetRegisteredPeers {
648                        peer_conn_type: _,
649                        response: Some(PeerResponse::RegisteredCids(peer_info, is_onlines)),
650                        limit: _,
651                    },
652                ..
653            }) = status.into_result()?
654            {
655                return Ok(peer_info
656                    .into_iter()
657                    .zip(is_onlines)
658                    .filter_map(|(peer_info, is_online)| {
659                        peer_info.map(|info| LocalGroupPeerFullInfo {
660                            cid: info.cid,
661                            username: Some(info.username),
662                            full_name: Some(info.full_name),
663                            is_online,
664                        })
665                    })
666                    .collect());
667            }
668        }
669
670        Err(citadel_io::error!(
671            citadel_io::ErrorCode::RemoteKernelStreamDied,
672            "get_local_group_peers"
673        ))
674    }
675
676    /// Returns a list of mutually-registered peers with the local_user
677    async fn get_local_group_mutual_peers<T: Into<UserIdentifier> + Send>(
678        &self,
679        local_user: T,
680    ) -> Result<Vec<LocalGroupPeerFullInfo>, NetworkError> {
681        let local_cid = self.get_session_cid(local_user).await?;
682        let command = NodeRequest::PeerCommand(PeerCommand {
683            session_cid: local_cid,
684            command: PeerSignal::GetMutuals {
685                v_conn_type: ClientConnectionType::Server {
686                    session_cid: local_cid,
687                },
688                response: None,
689            },
690        });
691
692        let mut stream = self.send_callback_subscription(command).await?;
693
694        while let Some(status) = stream.next().await {
695            if let NodeResult::PeerEvent(PeerEvent {
696                event:
697                    PeerSignal::GetMutuals {
698                        v_conn_type: _,
699                        response: Some(PeerResponse::RegisteredCids(peer_info, is_onlines)),
700                    },
701                ..
702            }) = status.into_result()?
703            {
704                return Ok(peer_info
705                    .into_iter()
706                    .zip(is_onlines)
707                    .filter_map(|(peer_info, is_online)| {
708                        peer_info.map(|info| LocalGroupPeerFullInfo {
709                            cid: info.cid,
710                            username: Some(info.username),
711                            full_name: Some(info.full_name),
712                            is_online,
713                        })
714                    })
715                    .collect());
716            }
717        }
718
719        Err(citadel_io::error!(
720            citadel_io::ErrorCode::RemoteSessionStreamDied
721        ))
722    }
723
724    /// Round-trips an authenticated probe to the server of session `cid` now, rather than on the
725    /// keep-alive schedule (15 minutes, 45 to time out): `Ok(rtt)`, `Timeout` when no answer came
726    /// within `timeout`, or `Error` when it could not be sent (no such connected session, or a
727    /// server below protocol 0.12.1, which cannot answer one). Over a WebSocket the probe also
728    /// sends a WebSocket ping.
729    async fn probe_server(&self, cid: u64, timeout: Duration) -> ServerProbeOutcome {
730        crate::server_probe::probe_server(self, cid, timeout).await
731    }
732
733    /// Ends session `cid` here and now, without asking the server: its transport is dropped, the
734    /// session is forgotten and its CID freed, so the next login is not refused with "Session for
735    /// CID .. already exists". For a link that died silently (after `probe_server` timed out):
736    /// the server's copy is replaced by that login's resume token. Resolves once the session has
737    /// dropped; `Err` when this node holds no session for `cid`.
738    async fn abandon_session(&self, cid: u64) -> Result<(), NetworkError> {
739        crate::server_probe::abandon_session(self, cid).await
740    }
741
742    /// Returns all the active sessions in the protocol, including all P2P connections hierarchically placed as children to C2S
743    /// connections
744    async fn sessions(&self) -> Result<ActiveSessions, NetworkError> {
745        match self
746            .send_callback_subscription(NodeRequest::GetActiveSessions)
747            .await
748        {
749            Ok(mut stream) => {
750                while let Some(result) = stream.next().await {
751                    match result.into_result()? {
752                        NodeResult::SessionList(res) => return Ok(res.sessions),
753                        res => {
754                            citadel_logging::warn!("Received unexpected result: {res:?}");
755                        }
756                    }
757                }
758
759                citadel_logging::warn!("Failed to receive response from SDK (stream died)");
760                return Err(citadel_io::error!(
761                    citadel_io::ErrorCode::RemoteKernelStreamDied,
762                    "get_active_sessions"
763                ));
764            }
765            Err(e) => {
766                citadel_logging::warn!(target: "citadel", "Failed to query SDK sessions: {e}");
767            }
768        }
769
770        Err(citadel_io::error!(
771            citadel_io::ErrorCode::RemoteQuerySessionsFailed
772        ))
773    }
774
775    /// Moves this node's live client-role QUIC endpoints to fresh sockets on the current local
776    /// address, so Direct and TURN QUIC paths (and a C2S QUIC connection) migrate without a new
777    /// handshake. Call it on a local network change. Local only, so not version-gated; see
778    /// [`NodeRemote::rebind_local`].
779    fn rebind_local(&self) -> Result<RebindReport, NetworkError> {
780        self.remote_ref().rebind_local()
781    }
782
783    #[doc(hidden)]
784    fn remote_ref(&self) -> &NodeRemote<R>;
785
786    #[doc(hidden)]
787    async fn get_session_cid<T: Into<UserIdentifier> + Send>(
788        &self,
789        local_user: T,
790    ) -> Result<u64, NetworkError> {
791        let account_manager = self.account_manager();
792        Ok(account_manager
793            .find_local_user_information(local_user)
794            .await?
795            .ok_or(citadel_io::error!(
796                citadel_io::ErrorCode::RemoteUserDoesNotExist
797            ))?)
798    }
799}
800
801impl<R: Ratchet> ProtocolRemoteExt<R> for NodeRemote<R> {
802    fn remote_ref(&self) -> &NodeRemote<R> {
803        self
804    }
805}
806
807impl<R: Ratchet> ProtocolRemoteExt<R> for ClientServerRemote<R> {
808    fn remote_ref(&self) -> &NodeRemote<R> {
809        &self.inner
810    }
811}
812
813#[async_trait]
814/// Some functions require that a target exists
815pub trait ProtocolRemoteTargetExt<R: Ratchet>: TargetLockedRemote<R> {
816    /// Sends a file with a custom size. The smaller the chunks, the higher the degree of scrambling, but the higher the performance cost. A chunk size of zero will use the default
817    async fn send_file_with_custom_opts<T: ObjectSource>(
818        &self,
819        source: T,
820        chunk_size: usize,
821        transfer_type: TransferType,
822    ) -> Result<(), NetworkError> {
823        let chunk_size = if chunk_size == 0 {
824            None
825        } else {
826            Some(chunk_size)
827        };
828        let session_cid = self.user().get_session_cid();
829        let user = *self.user();
830        let remote = self.remote();
831
832        let mut stream = remote
833            .send_callback_subscription(NodeRequest::SendObject(SendObject {
834                source: Box::new(source),
835                chunk_size,
836                session_cid,
837                v_conn_type: user,
838                transfer_type,
839            }))
840            .await?;
841
842        while let Some(event) = stream.next().await {
843            match event.into_result()? {
844                NodeResult::ObjectTransferHandle(ObjectTransferHandle { mut handle, .. }) => {
845                    return handle.transfer_file().await.map_err(|err| {
846                        citadel_io::error!(
847                            citadel_io::ErrorCode::RemoteFileTransferFailed,
848                            err.into_string()
849                        )
850                    });
851                }
852
853                NodeResult::PeerEvent(PeerEvent {
854                    event: PeerSignal::SignalReceived { .. },
855                    ..
856                }) => {}
857
858                res => {
859                    log::warn!(target: "citadel", "Invalid NodeResult for FileTransfer request received: {res:?}")
860                }
861            }
862        }
863
864        Err(citadel_io::error!(
865            citadel_io::ErrorCode::RemoteFileTransferStreamDied
866        ))
867    }
868
869    /// Sends a file to the provided target using the default chunking size
870    async fn send_file<T: ObjectSource>(&self, source: T) -> Result<(), NetworkError> {
871        self.send_file_with_custom_opts(source, 0, TransferType::FileTransfer)
872            .await
873    }
874
875    /// Sends a file to the provided target using custom chunking size with local encryption.
876    /// Only this local node may decrypt the information send to the adjacent node.
877    async fn remote_encrypted_virtual_filesystem_push_custom_chunking<
878        T: ObjectSource,
879        P: Into<PathBuf> + Send,
880    >(
881        &self,
882        source: T,
883        virtual_directory: P,
884        chunk_size: usize,
885        security_level: SecurityLevel,
886    ) -> Result<(), NetworkError> {
887        self.can_use_revfs()?;
888        let mut virtual_path = virtual_directory.into();
889        virtual_path = prepare_virtual_path(virtual_path);
890        validate_virtual_path(&virtual_path).map_err(|err| {
891            citadel_io::error!(
892                citadel_io::ErrorCode::RemoteRevfsInvalidVirtualPath,
893                err.into_string()
894            )
895        })?;
896        let tx_type = TransferType::RemoteEncryptedVirtualFilesystem {
897            virtual_path,
898            security_level,
899        };
900        self.send_file_with_custom_opts(source, chunk_size, tx_type)
901            .await
902    }
903
904    /// Sends a file to the provided target using the default chunking size with local encryption.
905    /// Only this local node may decrypt the information send to the adjacent node.
906    async fn remote_encrypted_virtual_filesystem_push<T: ObjectSource, P: Into<PathBuf> + Send>(
907        &self,
908        source: T,
909        virtual_directory: P,
910        security_level: SecurityLevel,
911    ) -> Result<(), NetworkError> {
912        self.remote_encrypted_virtual_filesystem_push_custom_chunking(
913            source,
914            virtual_directory,
915            0,
916            security_level,
917        )
918        .await
919    }
920
921    /// Pulls a virtual file from the RE-VFS. If `delete_on_pull` is true, then, the virtual file
922    /// will be taken from the RE-VFS
923    async fn remote_encrypted_virtual_filesystem_pull<P: Into<PathBuf> + Send>(
924        &self,
925        virtual_directory: P,
926        transfer_security_level: SecurityLevel,
927        delete_on_pull: bool,
928    ) -> Result<PathBuf, NetworkError> {
929        self.can_use_revfs()?;
930        let request = NodeRequest::PullObject(PullObject {
931            v_conn: *self.user(),
932            virtual_dir: virtual_directory.into(),
933            delete_on_pull,
934            transfer_security_level,
935        });
936
937        let mut stream = self.remote().send_callback_subscription(request).await?;
938
939        while let Some(event) = stream.next().await {
940            match event.into_result()? {
941                NodeResult::ObjectTransferHandle(ObjectTransferHandle { mut handle, .. }) => {
942                    return handle.receive_file().await.map_err(|err| {
943                        citadel_io::error!(
944                            citadel_io::ErrorCode::RemoteFileTransferFailed,
945                            err.into_string()
946                        )
947                    });
948                }
949
950                NodeResult::PeerEvent(PeerEvent {
951                    event: PeerSignal::SignalReceived { .. },
952                    ..
953                }) => {}
954
955                // The server's refusal (no such file, or it cannot send it) carries its reason.
956                NodeResult::ReVFS(ReVFSResult {
957                    error_message: Some(error),
958                    ..
959                }) => {
960                    return Err(citadel_io::error!(
961                        citadel_io::ErrorCode::RemoteFileTransferFailed,
962                        error
963                    ));
964                }
965
966                NodeResult::ReVFS(ReVFSResult {
967                    error_message: None,
968                    ..
969                }) => {}
970
971                res => {
972                    log::error!(target: "citadel", "Invalid NodeResult for REVFS FileTransfer request received: {res:?}");
973                    return Err(citadel_io::error!(
974                        citadel_io::ErrorCode::RemoteRevfsInvalidResponse
975                    ));
976                }
977            }
978        }
979
980        Err(citadel_io::error!(
981            citadel_io::ErrorCode::RemoteRevfsFileTransferStreamDied
982        ))
983    }
984
985    /// Deletes the file from the RE-VFS. If the contents are desired on delete,
986    /// consider calling `Self::remote_encrypted_virtual_filesystem_pull` with the delete
987    /// parameter set to true
988    async fn remote_encrypted_virtual_filesystem_delete<P: Into<PathBuf> + Send>(
989        &self,
990        virtual_directory: P,
991    ) -> Result<(), NetworkError> {
992        self.can_use_revfs()?;
993        let request = NodeRequest::DeleteObject(DeleteObject {
994            v_conn: *self.user(),
995            virtual_dir: virtual_directory.into(),
996            security_level: Default::default(),
997        });
998
999        let mut stream = self.remote().send_callback_subscription(request).await?;
1000        while let Some(event) = stream.next().await {
1001            match event.into_result()? {
1002                NodeResult::ReVFS(result) => {
1003                    return if let Some(error) = result.error_message {
1004                        Err(citadel_io::error!(
1005                            citadel_io::ErrorCode::RemoteFileTransferFailed,
1006                            error
1007                        ))
1008                    } else {
1009                        Ok(())
1010                    }
1011                }
1012
1013                evt => {
1014                    log::error!(target: "citadel", "Invalid NodeResult for REVFS Delete request received: {evt:?}");
1015                }
1016            }
1017        }
1018
1019        Err(citadel_io::error!(
1020            citadel_io::ErrorCode::RemoteRevfsDeleteStreamDied
1021        ))
1022    }
1023
1024    /// Connects to the peer with custom settings
1025    async fn connect_to_peer_custom(
1026        &self,
1027        session_security_settings: SessionSecuritySettings,
1028        udp_mode: UdpMode,
1029        peer_session_password: Option<PreSharedKey>,
1030    ) -> Result<PeerConnectSuccess<R>, NetworkError> {
1031        use std::time::Duration;
1032
1033        // Bounds the wait for the peer to accept and for the key exchange to finish: the
1034        // channel is delivered as soon as it works over the server relay, so NAT traversal is no
1035        // longer inside this wait (it runs in the background; see `PeerChannel::ensure_direct`).
1036        // What remains is the peer's answer — it has to be online and accept — plus two relayed
1037        // key-exchange round trips; this prevents an indefinite hang when neither arrives.
1038        const P2P_CONNECT_TIMEOUT: Duration = Duration::from_secs(60);
1039
1040        let session_cid = self.user().get_session_cid();
1041        let peer_target = self.try_as_peer_connection().await?;
1042
1043        let mut stream = self
1044            .remote()
1045            .send_callback_subscription(NodeRequest::PeerCommand(PeerCommand {
1046                session_cid,
1047                command: PeerSignal::PostConnect {
1048                    peer_conn_type: peer_target,
1049                    ticket_opt: None,
1050                    invitee_response: None,
1051                    session_security_settings,
1052                    udp_mode,
1053                    session_password: peer_session_password,
1054                },
1055            }))
1056            .await?;
1057
1058        let connect_task = async {
1059            while let Some(status) = stream.next().await {
1060                match status.into_result()? {
1061                    NodeResult::PeerChannelCreated(PeerChannelCreated {
1062                        ticket: _,
1063                        channel,
1064                        udp_rx_opt,
1065                    }) => {
1066                        let username = self.target_username().map(ToString::to_string);
1067                        let remote = PeerRemote {
1068                            inner: self.remote().clone(),
1069                            peer: peer_target.as_virtual_connection(),
1070                            username,
1071                            session_security_settings,
1072                        };
1073
1074                        return Ok(PeerConnectSuccess {
1075                            remote,
1076                            channel: *channel,
1077                            udp_channel_rx: udp_rx_opt,
1078                            incoming_object_transfer_handles: None,
1079                        });
1080                    }
1081
1082                    NodeResult::PeerEvent(PeerEvent {
1083                        event:
1084                            PeerSignal::PostConnect {
1085                                invitee_response, ..
1086                            },
1087                        ..
1088                    }) => match invitee_response {
1089                        Some(PeerResponse::Timeout) => {
1090                            return Err(citadel_io::error!(
1091                                citadel_io::ErrorCode::RemotePeerNoResponse
1092                            ))
1093                        }
1094                        Some(PeerResponse::Decline) => {
1095                            return Err(citadel_io::error!(
1096                                citadel_io::ErrorCode::RemotePeerDeclined
1097                            ))
1098                        }
1099                        _ => {}
1100                    },
1101
1102                    // A routing failure for this ticket (e.g. the server could not deliver our
1103                    // accept or request). Fail fast with the reason instead of waiting out the
1104                    // full 60s watchdog — the caller can retry immediately.
1105                    NodeResult::PeerEvent(PeerEvent {
1106                        event: PeerSignal::SignalError { error, .. },
1107                        ..
1108                    }) => {
1109                        return Err(citadel_io::error!(
1110                            citadel_io::ErrorCode::RemoteP2pSignalRoutingFailed,
1111                            error
1112                        ))
1113                    }
1114
1115                    _ => {}
1116                }
1117            }
1118
1119            Err(citadel_io::error!(
1120                citadel_io::ErrorCode::RemoteKernelStreamDied,
1121                "connect_to_peer_custom"
1122            ))
1123        };
1124
1125        match citadel_io::time::timeout(P2P_CONNECT_TIMEOUT, connect_task).await {
1126            Ok(result) => result,
1127            Err(_elapsed) => Err(citadel_io::error!(
1128                citadel_io::ErrorCode::RemoteP2pConnectTimeout,
1129                P2P_CONNECT_TIMEOUT.as_secs()
1130            )),
1131        }
1132    }
1133
1134    /// Supplies (`Some`) or withdraws (`None`) the TURN relay configuration for the next P2P
1135    /// attempt with this peer. Both peers must supply one for the relay to be used (the
1136    /// accepting peer before it accepts); the attempt consumes it. When direct P2P is impossible
1137    /// or fails, the peers then connect through the relay, and
1138    /// [`PeerChannel::p2p_path`](citadel_proto::prelude::PeerChannel::p2p_path) reports
1139    /// [`P2pPath::Turn`].
1140    async fn set_turn_config(
1141        &self,
1142        config: Option<citadel_proto::prelude::TurnRelayConfig>,
1143    ) -> Result<(), NetworkError> {
1144        let session_cid = self.user().get_session_cid();
1145        let peer_cid = self
1146            .try_as_peer_connection()
1147            .await?
1148            .get_original_target_cid();
1149        self.remote()
1150            .send(NodeRequest::SetPeerTurnConfig(SetPeerTurnConfig {
1151                session_cid,
1152                peer_cid,
1153                config,
1154            }))
1155            .await
1156            .map(|_| ())
1157    }
1158
1159    /// [`Self::connect_to_peer_custom`] with a TURN relay configuration for this attempt.
1160    async fn connect_to_peer_with_turn(
1161        &self,
1162        session_security_settings: SessionSecuritySettings,
1163        udp_mode: UdpMode,
1164        peer_session_password: Option<PreSharedKey>,
1165        turn: citadel_proto::prelude::TurnRelayConfig,
1166    ) -> Result<PeerConnectSuccess<R>, NetworkError> {
1167        self.set_turn_config(Some(turn)).await?;
1168        self.connect_to_peer_custom(session_security_settings, udp_mode, peer_session_password)
1169            .await
1170    }
1171
1172    /// Connects to the target peer with default settings
1173    async fn connect_to_peer(&self) -> Result<PeerConnectSuccess<R>, NetworkError> {
1174        self.connect_to_peer_custom(Default::default(), Default::default(), Default::default())
1175            .await
1176    }
1177
1178    /// Posts a registration request to a peer
1179    async fn register_to_peer(&self) -> Result<PeerRegisterStatus, NetworkError> {
1180        let session_cid = self.user().get_session_cid();
1181        let peer_target = self.try_as_peer_connection().await?;
1182        // TODO: Get rid of this step. Should be handled by the protocol
1183        let local_username = self
1184            .remote()
1185            .account_manager()
1186            .get_username_by_cid(session_cid)
1187            .await?
1188            .ok_or_else(|| citadel_io::error!(citadel_io::ErrorCode::RemoteLocalUsernameMissing))?;
1189        let peer_username_opt = self.target_username().map(ToString::to_string);
1190
1191        let mut stream = self
1192            .remote()
1193            .send_callback_subscription(NodeRequest::PeerCommand(PeerCommand {
1194                session_cid,
1195                command: PeerSignal::PostRegister {
1196                    peer_conn_type: peer_target,
1197                    inviter_username: local_username,
1198                    invitee_username: peer_username_opt,
1199                    ticket_opt: None,
1200                    invitee_response: None,
1201                },
1202            }))
1203            .await?;
1204
1205        while let Some(status) = stream.next().await {
1206            if let NodeResult::PeerEvent(PeerEvent {
1207                event:
1208                    PeerSignal::PostRegister {
1209                        peer_conn_type: _,
1210                        inviter_username: _,
1211                        invitee_username: _,
1212                        ticket_opt: _,
1213                        invitee_response: Some(resp),
1214                    },
1215                ..
1216            }) = status.into_result()?
1217            {
1218                match resp {
1219                    PeerResponse::Accept(..) => return Ok(PeerRegisterStatus::Accepted),
1220                    PeerResponse::Decline => return Ok(PeerRegisterStatus::Declined),
1221                    PeerResponse::Timeout => return Ok(PeerRegisterStatus::Failed { reason: Some("Timeout on register request. Peer did not accept in time. Try again later".to_string()) }),
1222                    _ => {}
1223                }
1224            }
1225        }
1226
1227        Err(citadel_io::error!(
1228            citadel_io::ErrorCode::RemoteKernelStreamDied,
1229            format!("register_to_peer: {:?}", stream.callback_key())
1230        ))
1231    }
1232
1233    /// Deregisters the currently locked target. If the target is a client to server
1234    /// connection, deregisters from the server. If the target is a p2p connection,
1235    /// deregisters the p2p
1236    async fn deregister(&self) -> Result<(), NetworkError> {
1237        if let Ok(peer_conn) = self.try_as_peer_connection().await {
1238            let peer_request = PeerSignal::Deregister {
1239                peer_conn_type: peer_conn,
1240            };
1241            let session_cid = self.user().get_session_cid();
1242            let request = NodeRequest::PeerCommand(PeerCommand {
1243                session_cid,
1244                command: peer_request,
1245            });
1246
1247            let mut subscription = self.remote().send_callback_subscription(request).await?;
1248            while let Some(result) = subscription.next().await {
1249                if let NodeResult::PeerEvent(PeerEvent {
1250                    event: PeerSignal::DeregistrationSuccess { .. },
1251                    ..
1252                }) = result.into_result()?
1253                {
1254                    return Ok(());
1255                }
1256            }
1257        } else {
1258            // c2s conn
1259            let cid = self.user().get_session_cid();
1260            let request = NodeRequest::DeregisterFromHypernode(DeregisterFromHypernode {
1261                session_cid: cid,
1262                v_conn_type: *self.user(),
1263            });
1264            let mut subscription = self.remote().send_callback_subscription(request).await?;
1265            while let Some(result) = subscription.next().await {
1266                match result.into_result()? {
1267                    NodeResult::DeRegistration(DeRegistration {
1268                        session_cid: _,
1269                        ticket_opt: _,
1270                        success: true,
1271                    }) => return Ok(()),
1272                    NodeResult::DeRegistration(DeRegistration {
1273                        session_cid: _,
1274                        ticket_opt: _,
1275                        success: false,
1276                    }) => {
1277                        return Err(citadel_io::error!(
1278                            citadel_io::ErrorCode::RemoteDeregisterFailed
1279                        ))
1280                    }
1281
1282                    _ => {}
1283                }
1284            }
1285        }
1286
1287        Err(citadel_io::error!(
1288            citadel_io::ErrorCode::RemoteDeregisterEndedUnexpectedly
1289        ))
1290    }
1291
1292    async fn disconnect(&self) -> Result<(), NetworkError> {
1293        if let Ok(peer_conn) = self.try_as_peer_connection().await {
1294            if let PeerConnectionType::LocalGroupPeer {
1295                session_cid,
1296                peer_cid: _,
1297            } = peer_conn
1298            {
1299                let request = NodeRequest::PeerCommand(PeerCommand {
1300                    session_cid,
1301                    command: PeerSignal::Disconnect {
1302                        peer_conn_type: peer_conn,
1303                        disconnect_response: None,
1304                        disconnect_token: None,
1305                    },
1306                });
1307
1308                let mut subscription = self.remote().send_callback_subscription(request).await?;
1309
1310                while let Some(event) = subscription.next().await {
1311                    if let NodeResult::PeerEvent(PeerEvent {
1312                        event:
1313                            PeerSignal::Disconnect {
1314                                peer_conn_type: _,
1315                                disconnect_response: Some(_),
1316                                ..
1317                            },
1318                        ..
1319                    }) = event.into_result()?
1320                    {
1321                        return Ok(());
1322                    }
1323                }
1324
1325                Err(citadel_io::error!(
1326                    citadel_io::ErrorCode::RemoteDisconnectEventMissing
1327                ))
1328            } else {
1329                Err(citadel_io::error!(
1330                    citadel_io::ErrorCode::RemoteExternalGroupPeerUnsupported
1331                ))
1332            }
1333        } else {
1334            //c2s conn
1335            let cid = self.user().get_session_cid();
1336            let request =
1337                NodeRequest::DisconnectFromHypernode(DisconnectFromHypernode { session_cid: cid });
1338
1339            let mut subscription = self.remote().send_callback_subscription(request).await?;
1340            // Bounded, because this wait is otherwise the end of the line.
1341            //
1342            // The stream closing is handled below, but a subscription that
1343            // stays OPEN and simply never carries the matching Disconnect
1344            // parks here for ever. That is the reconnection suite's 240s
1345            // timeout: with the phase markers reaching CI, both peers finish
1346            // phase one and the disconnecting side never logs the line after
1347            // this call, while the other blocks on the barrier it never
1348            // reaches. The only await between those two markers is this one.
1349            //
1350            // The session is being torn down either way, so an unconfirmed
1351            // disconnect is reported rather than waited on: a caller can retry
1352            // or proceed, and neither is possible from inside an indefinite
1353            // await.
1354            const DISCONNECT_CONFIRMATION_TIMEOUT: Duration = Duration::from_secs(30);
1355            let deadline = citadel_io::time::Instant::now() + DISCONNECT_CONFIRMATION_TIMEOUT;
1356            // What the subscription DID carry, for the occasion when it does not
1357            // carry a Disconnect.
1358            //
1359            // `RemoteDisconnectEventMissing` says only that nothing matched in
1360            // thirty seconds, which cannot distinguish the two things it might
1361            // mean. A NON-ZERO count says the subscription was alive and other
1362            // events reached it, so the Disconnect was never emitted or was
1363            // emitted under a key this subscription does not hold. ZERO says the
1364            // subscription heard nothing at all, which points at the session
1365            // rather than at the routing. Those want different fixes, and the
1366            // error tells them apart not at all.
1367            //
1368            // This is the same instrument that localised the UDP one-shot race:
1369            // an intermittent CI failure that no local run reproduces is only
1370            // ever going to be solved by what it says about itself when it next
1371            // happens.
1372            let mut carried = 0usize;
1373            loop {
1374                let remaining =
1375                    deadline.saturating_duration_since(citadel_io::time::Instant::now());
1376                if remaining.is_zero() {
1377                    log::warn!(target: "citadel", "[dc-wait] no Disconnect for cid {cid} within {DISCONNECT_CONFIRMATION_TIMEOUT:?}; the subscription carried {carried} other event(s)");
1378                    return Err(citadel_io::error!(
1379                        citadel_io::ErrorCode::RemoteDisconnectEventMissing
1380                    ));
1381                }
1382                match citadel_io::time::timeout(remaining, subscription.next()).await {
1383                    Ok(Some(event)) => {
1384                        carried += 1;
1385                        if let NodeResult::Disconnect(Disconnect {
1386                            success, message, ..
1387                        }) = event.into_result()?
1388                        {
1389                            return if success {
1390                                Ok(())
1391                            } else {
1392                                Err(citadel_io::error!(
1393                                    citadel_io::ErrorCode::RemoteDisconnected,
1394                                    message
1395                                ))
1396                            };
1397                        }
1398                    }
1399                    // The stream ended without ever carrying the event.
1400                    Ok(None) => {
1401                        log::warn!(target: "citadel", "[dc-wait] the subscription for cid {cid} closed without a Disconnect; it carried {carried} other event(s)");
1402                        break;
1403                    }
1404                    Err(_elapsed) => {
1405                        log::warn!(target: "citadel", "[dc-wait] timed out waiting for a Disconnect for cid {cid}; the subscription carried {carried} other event(s)");
1406                        return Err(citadel_io::error!(
1407                            citadel_io::ErrorCode::RemoteDisconnectEventMissing
1408                        ));
1409                    }
1410                }
1411            }
1412
1413            Err(citadel_io::error!(
1414                citadel_io::ErrorCode::RemoteDisconnectEventMissing
1415            ))
1416        }
1417    }
1418
1419    async fn create_group(
1420        &self,
1421        initial_users_to_invite: Option<Vec<UserIdentifier>>,
1422    ) -> Result<GroupChannel, NetworkError> {
1423        self.create_group_with_options(initial_users_to_invite, MessageGroupOptions::default())
1424            .await
1425    }
1426
1427    /// Create a group with explicit [`MessageGroupOptions`] — e.g. a zero-trust
1428    /// [`GroupHierarchyMode::CommandHierarchy`](citadel_types::proto::GroupHierarchyMode) where a
1429    /// superior can read its subordinates' messages. `options.hierarchy` carries the initial rank
1430    /// assignment (member cid → command path); it is consumed owner-locally and never reaches the relay.
1431    async fn create_group_with_options(
1432        &self,
1433        initial_users_to_invite: Option<Vec<UserIdentifier>>,
1434        options: MessageGroupOptions,
1435    ) -> Result<GroupChannel, NetworkError> {
1436        let session_cid = self.user().get_session_cid();
1437
1438        let mut initial_users = vec![];
1439        // NOTE: default is PRIVATE mode, meaning all users in group must be registered to the owner.
1440        // Initial users are UserIdentifiers resolved to cids below.
1441        if let Some(initial_users_to_invite) = initial_users_to_invite {
1442            for user in initial_users_to_invite {
1443                initial_users.push(
1444                    self.remote()
1445                        .account_manager()
1446                        .find_target_information(session_cid, user.clone())
1447                        .await?
1448                        .ok_or_else(|| {
1449                            citadel_io::error!(
1450                                citadel_io::ErrorCode::RemoteGroupAccountNotFound,
1451                                citadel_io::Dbg(user),
1452                                citadel_io::Dbg(session_cid)
1453                            )
1454                        })
1455                        .map(|r| r.1.cid)?,
1456                )
1457            }
1458        }
1459
1460        let group_request = GroupBroadcast::Create {
1461            initial_invitees: initial_users,
1462            options,
1463        };
1464        let request = NodeRequest::GroupBroadcastCommand(GroupBroadcastCommand {
1465            session_cid,
1466            command: group_request,
1467        });
1468        let mut subscription = self.remote().send_callback_subscription(request).await?;
1469
1470        // The loop lives in `group_create_wait` so its refusal paths can be
1471        // tested without a running node: the only way to make a real server
1472        // refuse a Create is to give the owner 256 groups first.
1473        //
1474        // It matched GroupChannelCreated and nothing else, and — alone among its
1475        // neighbours — never called into_result(). So SignalError,
1476        // OutboundRequestRejected and InternalServerError were discarded, and so
1477        // was the server's own answer to a Create it could not perform:
1478        // CreateResponse { key: None }, delivered on this very ticket. Every one
1479        // of those left the caller parked on a stream that would never speak
1480        // again. The broadcast prefab handles that same event as
1481        // BroadcastCreateGroupFailed — the guard existed, in the prefab only.
1482        match crate::group_create_wait::await_group_creation(&mut subscription).await? {
1483            crate::group_create_wait::GroupCreation::Created(channel) => Ok(channel),
1484            crate::group_create_wait::GroupCreation::Refused => Err(citadel_io::error!(
1485                citadel_io::ErrorCode::Generic,
1486                "The server refused to create the group"
1487            )),
1488            crate::group_create_wait::GroupCreation::Ended => Err(citadel_io::error!(
1489                citadel_io::ErrorCode::RemoteCreateGroupEndedUnexpectedly
1490            )),
1491        }
1492    }
1493
1494    /// Lists all groups that which the current peer owns
1495    async fn list_owned_groups(&self) -> Result<Vec<MessageGroupKey>, NetworkError> {
1496        let session_cid = self.user().get_session_cid();
1497        let cid_to_check_for = match self.try_as_peer_connection().await {
1498            Ok(res) => res.get_original_target_cid(),
1499            _ => session_cid,
1500        };
1501        let group_request = GroupBroadcast::ListGroupsFor {
1502            cid: cid_to_check_for,
1503        };
1504        let request = NodeRequest::GroupBroadcastCommand(GroupBroadcastCommand {
1505            session_cid,
1506            command: group_request,
1507        });
1508
1509        let mut subscription = self.remote().send_callback_subscription(request).await?;
1510
1511        while let Some(evt) = subscription.next().await {
1512            if let NodeResult::GroupEvent(GroupEvent {
1513                session_cid: _,
1514                ticket: _,
1515                event: GroupBroadcast::ListResponse { groups },
1516            }) = evt.into_result()?
1517            {
1518                return Ok(groups);
1519            }
1520        }
1521
1522        Err(citadel_io::error!(
1523            citadel_io::ErrorCode::RemoteListGroupsEndedUnexpectedly
1524        ))
1525    }
1526
1527    /// Lists all active sessions, including the local nat type. For each active session,
1528    /// lists each connection info which includes the remote nat type, connection status,
1529    /// peer id, and latest ratchet version.
1530    async fn list_sessions(&self) -> Result<ActiveSessions, NetworkError> {
1531        let request = NodeRequest::GetActiveSessions;
1532        let mut subscription = self.remote().send_callback_subscription(request).await?;
1533
1534        if let Some(NodeResult::SessionList(result)) = subscription.next().await {
1535            return Ok(result.sessions);
1536        }
1537
1538        Err(citadel_io::error!(
1539            citadel_io::ErrorCode::RemoteListSessionsEndedUnexpectedly
1540        ))
1541    }
1542
1543    /// Begins a re-key, updating the container in the process.
1544    /// Returns the new key matrix version. Does not return the new key version
1545    /// if the rekey fails, or, if a current rekey is already executing
1546    async fn rekey(&self) -> Result<Option<u32>, NetworkError> {
1547        let request = NodeRequest::ReKey(ReKey {
1548            v_conn_type: *self.user(),
1549        });
1550        let mut subscription = self.remote().send_callback_subscription(request).await?;
1551
1552        while let Some(evt) = subscription.next().await {
1553            if let NodeResult::ReKeyResult(result) = evt {
1554                return match result.status {
1555                    ReKeyReturnType::Success { version } => Ok(Some(version)),
1556                    ReKeyReturnType::AlreadyInProgress => Ok(None),
1557                    ReKeyReturnType::Failure { err } => Err(citadel_io::error!(
1558                        citadel_io::ErrorCode::RemoteRekeyFailed,
1559                        err
1560                    )),
1561                };
1562            }
1563        }
1564
1565        Err(citadel_io::error!(
1566            citadel_io::ErrorCode::RemoteRekeyEndedUnexpectedly
1567        ))
1568    }
1569
1570    /// Checks if the locked target is registered
1571    async fn is_peer_registered(&self) -> Result<bool, NetworkError> {
1572        let target = self.try_as_peer_connection().await?;
1573        if let PeerConnectionType::LocalGroupPeer {
1574            session_cid: local_cid,
1575            peer_cid,
1576        } = target
1577        {
1578            let peers = self.remote().get_local_group_peers(local_cid, None).await?;
1579            citadel_logging::info!(target: "citadel", "Checking to see if {target} is registered in {peers:?}");
1580            Ok(peers.iter().any(|p| p.cid == peer_cid))
1581        } else {
1582            Err(citadel_io::error!(
1583                citadel_io::ErrorCode::RemoteExternalGroupPeerUnsupportedYet
1584            ))
1585        }
1586    }
1587
1588    #[doc(hidden)]
1589    async fn try_as_peer_connection(&self) -> Result<PeerConnectionType, NetworkError> {
1590        let verified_return = |user: &VirtualTargetType| {
1591            user.try_as_peer_connection().ok_or(citadel_io::error!(
1592                citadel_io::ErrorCode::RemoteTargetNotPeer
1593            ))
1594        };
1595
1596        if self.user().get_target_cid() == 0 {
1597            // in this case, the user re-used a remote locked to a registration target
1598            // where the username was provided, but the cid was 0 (unknown).
1599            let peer_username = self.target_username().ok_or_else(|| {
1600                citadel_io::error!(citadel_io::ErrorCode::RemoteTargetCidZeroNoUsername)
1601            })?;
1602            let session_cid = self.user().get_session_cid();
1603            let expected_peer_cid = self
1604                .remote()
1605                .account_manager()
1606                .get_persistence_handler()
1607                .get_cid_by_username(peer_username);
1608            // get the peer cid from the account manager (implying the peers are already registered).
1609            // fallback to the mapped cid if the peer is not registered
1610            let peer_cid = self
1611                .remote()
1612                .account_manager()
1613                .find_target_information(session_cid, peer_username)
1614                .await?
1615                .map(|r| r.1.cid)
1616                .unwrap_or(expected_peer_cid);
1617
1618            let mut user = *self.user();
1619            user.set_target_cid(peer_cid);
1620            verified_return(&user)
1621        } else {
1622            verified_return(self.user())
1623        }
1624    }
1625
1626    #[doc(hidden)]
1627    fn can_use_revfs(&self) -> Result<(), NetworkError> {
1628        if let Some(sess) = self.session_security_settings() {
1629            if sess.crypto_params.kem_algorithm == KemAlgorithm::MlKem {
1630                Ok(())
1631            } else {
1632                Err(citadel_io::error!(
1633                    citadel_io::ErrorCode::RemoteRevfsRequiresKyber
1634                ))
1635            }
1636        } else {
1637            Err(citadel_io::error!(
1638                citadel_io::ErrorCode::RemoteRevfsUnsupportedRemote
1639            ))
1640        }
1641    }
1642}
1643
1644impl<T: TargetLockedRemote<R>, R: Ratchet> ProtocolRemoteTargetExt<R> for T {}
1645
1646pub mod results {
1647    use crate::prefabs::client::peer_connection::FileTransferHandleRx;
1648    use crate::prelude::{PeerChannel, UdpChannel};
1649    use crate::remote_ext::remote_specialization::PeerRemote;
1650    use citadel_io::tokio::sync::oneshot::Receiver;
1651    use citadel_proto::prelude::*;
1652    use std::fmt::Debug;
1653
1654    pub struct PeerConnectSuccess<R: Ratchet> {
1655        pub channel: PeerChannel<R>,
1656        pub udp_channel_rx: Option<Receiver<UdpChannel<R>>>,
1657        pub remote: PeerRemote<R>,
1658        /// Receives incoming file/object transfer requests. The handles must be
1659        /// .accepted() before the file/object transfer is allowed to proceed
1660        pub(crate) incoming_object_transfer_handles: Option<FileTransferHandleRx>,
1661    }
1662
1663    impl<R: Ratchet> Debug for PeerConnectSuccess<R> {
1664        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1665            f.debug_struct("PeerConnectSuccess")
1666                .field("channel", &self.channel)
1667                .field("udp_channel_rx", &self.udp_channel_rx)
1668                .finish()
1669        }
1670    }
1671
1672    impl<R: Ratchet> PeerConnectSuccess<R> {
1673        /// Obtains a receiver which yields incoming file/object transfer handles
1674        pub fn get_incoming_file_transfer_handle(
1675            &mut self,
1676        ) -> Result<FileTransferHandleRx, NetworkError> {
1677            self.incoming_object_transfer_handles
1678                .take()
1679                .ok_or(citadel_io::error!(
1680                    citadel_io::ErrorCode::RemoteFunctionAlreadyCalled
1681                ))
1682        }
1683    }
1684
1685    /// The peer's answer to a registration request.
1686    ///
1687    /// `Ok` means the request completed, not that it was accepted — a decline is
1688    /// a successful round trip with the answer "no". This type derived nothing,
1689    /// so a caller could neither compare it nor log it, and all three real
1690    /// callers wrote `Ok(_)` / `let _ =` and carried on as though the peer had
1691    /// said yes. `peer_connection.rs` then issued a PostConnect to a peer that
1692    /// had just declined and waited out a 60s RemoteP2pConnectTimeout, reporting
1693    /// that instead of the decline the SDK had in hand.
1694    ///
1695    /// The derives are the fix for the type; `is_accepted` is the fix for the
1696    /// call sites, which needed something shorter to write than the mistake.
1697    #[derive(Clone, Debug, PartialEq, Eq)]
1698    pub enum PeerRegisterStatus {
1699        Accepted,
1700        Declined,
1701        Failed { reason: Option<String> },
1702    }
1703
1704    impl PeerRegisterStatus {
1705        /// Did the peer say yes?
1706        pub fn is_accepted(&self) -> bool {
1707            matches!(self, PeerRegisterStatus::Accepted)
1708        }
1709
1710        /// The reason this was not an acceptance, or `None` if it was.
1711        pub fn refusal_reason(&self) -> Option<String> {
1712            match self {
1713                PeerRegisterStatus::Accepted => None,
1714                PeerRegisterStatus::Declined => Some("The peer declined the request".to_string()),
1715                PeerRegisterStatus::Failed { reason } => Some(
1716                    reason
1717                        .clone()
1718                        .unwrap_or_else(|| "The registration request failed".to_string()),
1719                ),
1720            }
1721        }
1722    }
1723
1724    #[derive(Clone, Debug)]
1725    pub struct LocalGroupPeer {
1726        pub cid: u64,
1727        pub is_online: bool,
1728    }
1729
1730    #[derive(Clone, Debug)]
1731    pub struct LocalGroupPeerFullInfo {
1732        pub cid: u64,
1733        pub username: Option<String>,
1734        pub full_name: Option<String>,
1735        pub is_online: bool,
1736    }
1737}
1738
1739pub mod remote_specialization {
1740    use crate::prelude::*;
1741    use std::ops::{Deref, DerefMut};
1742
1743    #[derive(Debug, Clone)]
1744    pub struct PeerRemote<R: Ratchet> {
1745        pub(crate) inner: NodeRemote<R>,
1746        pub(crate) peer: VirtualTargetType,
1747        pub(crate) username: Option<String>,
1748        pub(crate) session_security_settings: SessionSecuritySettings,
1749    }
1750
1751    impl<R: Ratchet> Deref for PeerRemote<R> {
1752        type Target = NodeRemote<R>;
1753        fn deref(&self) -> &Self::Target {
1754            &self.inner
1755        }
1756    }
1757
1758    impl<R: Ratchet> DerefMut for PeerRemote<R> {
1759        fn deref_mut(&mut self) -> &mut Self::Target {
1760            &mut self.inner
1761        }
1762    }
1763
1764    impl<R: Ratchet> TargetLockedRemote<R> for PeerRemote<R> {
1765        fn user(&self) -> &VirtualTargetType {
1766            &self.peer
1767        }
1768        fn remote(&self) -> &NodeRemote<R> {
1769            &self.inner
1770        }
1771        fn target_username(&self) -> Option<&str> {
1772            self.username.as_deref()
1773        }
1774        fn user_mut(&mut self) -> &mut VirtualTargetType {
1775            &mut self.peer
1776        }
1777
1778        fn session_security_settings(&self) -> Option<&SessionSecuritySettings> {
1779            Some(&self.session_security_settings)
1780        }
1781    }
1782}
1783
1784#[cfg(all(test, not(target_family = "wasm")))]
1785mod tests {
1786    use crate::prefabs::client::single_connection::SingleClientServerConnectionKernel;
1787    use crate::prefabs::client::DefaultServerConnectionSettingsBuilder;
1788    use crate::prelude::*;
1789    use citadel_io::tokio;
1790    use rstest::rstest;
1791    use std::net::SocketAddr;
1792    use std::sync::atomic::{AtomicBool, Ordering};
1793    use std::sync::Arc;
1794    use uuid::Uuid;
1795
1796    pub struct ReceiverFileTransferKernel<R: Ratchet>(
1797        pub Option<NodeRemote<R>>,
1798        pub Arc<AtomicBool>,
1799    );
1800
1801    #[async_trait]
1802    impl<R: Ratchet> NetKernel<R> for ReceiverFileTransferKernel<R> {
1803        fn load_remote(&mut self, node_remote: NodeRemote<R>) -> Result<(), NetworkError> {
1804            self.0 = Some(node_remote);
1805            Ok(())
1806        }
1807
1808        async fn on_start(&self) -> Result<(), NetworkError> {
1809            Ok(())
1810        }
1811
1812        async fn on_node_event_received(&self, message: NodeResult<R>) -> Result<(), NetworkError> {
1813            log::trace!(target: "citadel", "SERVER received {:?}", message);
1814            if let NodeResult::ObjectTransferHandle(ObjectTransferHandle { mut handle, .. }) =
1815                message.into_result()?
1816            {
1817                let mut path = None;
1818                // accept the transfer
1819                handle
1820                    .accept()
1821                    .map_err(|err| NetworkError::msg(err.into_string()))?;
1822
1823                use citadel_types::proto::ObjectTransferStatus;
1824                use futures::StreamExt;
1825                while let Some(status) = handle.next().await {
1826                    match status {
1827                        ObjectTransferStatus::ReceptionComplete => {
1828                            log::trace!(target: "citadel", "Server has finished receiving the file!");
1829                            let cmp = include_bytes!("../../resources/TheBridge.pdf");
1830                            let streamed_data = citadel_io::tokio::fs::read(path.clone().unwrap())
1831                                .await
1832                                .unwrap();
1833                            assert_eq!(
1834                                cmp,
1835                                streamed_data.as_slice(),
1836                                "Original data and streamed data does not match"
1837                            );
1838
1839                            self.1.store(true, Ordering::Relaxed);
1840                            self.0.clone().unwrap().shutdown().await?;
1841                        }
1842
1843                        ObjectTransferStatus::ReceptionBeginning(file_path, vfm) => {
1844                            path = Some(file_path);
1845                            assert_eq!(vfm.name, "TheBridge.pdf")
1846                        }
1847
1848                        _ => {}
1849                    }
1850                }
1851            }
1852
1853            Ok(())
1854        }
1855
1856        async fn on_stop(&mut self) -> Result<(), NetworkError> {
1857            Ok(())
1858        }
1859    }
1860
1861    pub fn server_info<'a, R: Ratchet>(
1862        switch: Arc<AtomicBool>,
1863    ) -> (NodeFuture<'a, ReceiverFileTransferKernel<R>>, SocketAddr) {
1864        crate::test_common::server_test_node(ReceiverFileTransferKernel(None, switch), |_| {})
1865    }
1866
1867    #[rstest]
1868    #[case(
1869        EncryptionAlgorithm::AES_GCM_256,
1870        KemAlgorithm::MlKem,
1871        SigAlgorithm::None
1872    )]
1873    #[case(
1874        EncryptionAlgorithm::MlKemHybrid,
1875        KemAlgorithm::MlKem,
1876        SigAlgorithm::MlDsa65
1877    )]
1878    #[timeout(std::time::Duration::from_secs(90))]
1879    #[tokio::test]
1880    async fn test_c2s_file_transfer(
1881        #[case] enx: EncryptionAlgorithm,
1882        #[case] kem: KemAlgorithm,
1883        #[case] sig: SigAlgorithm,
1884    ) {
1885        citadel_logging::setup_log();
1886        let client_success = &AtomicBool::new(false);
1887        let server_success = &Arc::new(AtomicBool::new(false));
1888        let (server, server_addr) = server_info::<StackedRatchet>(server_success.clone());
1889        let uuid = Uuid::new_v4();
1890
1891        let session_security_settings = SessionSecuritySettingsBuilder::default()
1892            .with_crypto_params(enx + kem + sig)
1893            .build()
1894            .unwrap();
1895
1896        let server_connection_settings =
1897            DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
1898                .with_session_security_settings(session_security_settings)
1899                .disable_udp()
1900                .build()
1901                .unwrap();
1902
1903        let client_kernel = SingleClientServerConnectionKernel::new(
1904            server_connection_settings,
1905            |connection| async move {
1906                log::trace!(target: "citadel", "***CLIENT LOGIN SUCCESS :: File transfer next ***");
1907                connection
1908                    .send_file_with_custom_opts(
1909                        "../resources/TheBridge.pdf",
1910                        32 * 1024,
1911                        TransferType::FileTransfer,
1912                    )
1913                    .await
1914                    .unwrap();
1915                log::trace!(target: "citadel", "***CLIENT FILE TRANSFER SUCCESS***");
1916                client_success.store(true, Ordering::Relaxed);
1917                connection.shutdown_kernel().await
1918            },
1919        );
1920
1921        let client = DefaultNodeBuilder::default().build(client_kernel).unwrap();
1922
1923        let joined = futures::future::try_join(server, client);
1924
1925        let _ = joined.await.unwrap();
1926
1927        assert!(client_success.load(Ordering::Relaxed));
1928        assert!(server_success.load(Ordering::Relaxed));
1929    }
1930}