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