Skip to main content

citadel_sdk/prefabs/client/
mod.rs

1//! Client-Side Network Components
2//!
3//! This module provides pre-built client-side networking components for the Citadel Protocol.
4//! It includes implementations for various connection patterns including single server
5//! connections, peer-to-peer networking, and broadcast capabilities.
6//!
7//! # Features
8//! - Connection builders with flexible configuration
9//! - Multiple authentication methods (Transient, Credentials)
10//! - UDP support for NAT traversal
11//! - Session security customization
12//! - Pre-shared key authentication
13//! - Connection lifecycle management
14//!
15//! # Example
16//! ```rust
17//! use citadel_sdk::prelude::*;
18//! use std::net::SocketAddr;
19//! use std::str::FromStr;
20//!
21//! # fn main() -> Result<(), NetworkError> {
22//! // Create transient connection settings
23//! let settings = DefaultServerConnectionSettingsBuilder::transient("127.0.0.1:25021")
24//!     .with_udp_mode(UdpMode::Enabled)
25//!     .build()?;
26//! # Ok(())
27//! # }
28//! ```
29//!
30//! # Important Notes
31//! - Connection settings must be built before use
32//! - UDP mode affects NAT traversal capabilities
33//! - Pre-shared keys must match server configuration
34//! - Transient connections do not persist data
35//!
36//! # Related Components
37//! - [`broadcast`]: Group communication support
38//! - [`peer_connection`]: Peer-to-peer networking
39//! - [`single_connection`]: Single server connections
40//!
41//! [`broadcast`]: crate::prefabs::client::broadcast
42//! [`peer_connection`]: crate::prefabs::client::peer_connection
43//! [`single_connection`]: crate::prefabs::client::single_connection
44
45use crate::prefabs::client::single_connection::SingleClientServerConnectionKernel;
46use crate::prelude::*;
47use std::marker::PhantomData;
48use std::net::{SocketAddr, ToSocketAddrs};
49use uuid::Uuid;
50
51/// A kernel that assists in creating and/or connecting to a group
52pub mod broadcast;
53/// How a broadcast joiner waits for its owner's group
54pub(crate) mod broadcast_join;
55/// A kernel that assists in allowing multiple possible peer-to-peer connections
56pub mod peer_connection;
57/// Internal kernel for serverless browser-to-browser connections
58#[cfg(target_family = "wasm")]
59pub(crate) mod serverless;
60/// A kernel that only makes a single client-to-server connection
61pub mod single_connection;
62
63#[async_trait]
64pub trait PrefabFunctions<'a, Arg: Send + 'a, R: Ratchet>: Sized + 'a {
65    type UserLevelInputFunction: Send + 'a;
66    /// Shared between the kernel and the on_c2s_channel_received function
67    type SharedBundle: Send + 'a;
68
69    fn get_shared_bundle(&self) -> Self::SharedBundle;
70
71    async fn on_c2s_channel_received(
72        connect_success: CitadelClientServerConnection<R>,
73        arg: Arg,
74        fx: Self::UserLevelInputFunction,
75        shared: Self::SharedBundle,
76    ) -> Result<(), NetworkError>;
77
78    fn construct(kernel: Box<dyn NetKernel<R> + 'a>) -> Self;
79
80    /// Creates a new connection with a central server entailed by the user information
81    fn new(
82        server_connection_settings: ServerConnectionSettings<R>,
83        arg: Arg,
84        on_channel_received: Self::UserLevelInputFunction,
85    ) -> Self {
86        let (tx, rx) = citadel_io::tokio::sync::oneshot::channel();
87        let server_conn_kernel = SingleClientServerConnectionKernel::<_, _, R>::new(
88            server_connection_settings,
89            |connect_success| {
90                on_channel_received_fn::<_, Self, R>(connect_success, rx, arg, on_channel_received)
91            },
92        );
93
94        let this = Self::construct(Box::new(server_conn_kernel));
95        assert!(tx.send(this.get_shared_bundle()).is_ok());
96        this
97    }
98}
99
100async fn on_channel_received_fn<'a, Arg: Send + 'a, T: PrefabFunctions<'a, Arg, R>, R: Ratchet>(
101    connect_success: CitadelClientServerConnection<R>,
102    rx_bundle: citadel_io::tokio::sync::oneshot::Receiver<T::SharedBundle>,
103    arg: Arg,
104    on_channel_received: T::UserLevelInputFunction,
105) -> Result<(), NetworkError> {
106    let shared = rx_bundle
107        .await
108        .map_err(|err| NetworkError::generic(err.to_string()))?;
109    T::on_c2s_channel_received(connect_success, arg, on_channel_received, shared).await
110}
111
112/// Used to instantiate a client to server connection
113pub struct ServerConnectionSettingsBuilder<R: Ratchet, T: ToSocketAddrs> {
114    password: Option<SecBuffer>,
115    username: Option<String>,
116    name: Option<String>,
117    psk: Option<PreSharedKey>,
118    address: Option<T>,
119    udp_mode: Option<UdpMode>,
120    session_security_settings: Option<SessionSecuritySettings>,
121    transient_uuid: Option<Uuid>,
122    is_connect: bool,
123    _ratchet: PhantomData<R>,
124}
125
126pub type DefaultServerConnectionSettingsBuilder<T> =
127    ServerConnectionSettingsBuilder<StackedRatchet, T>;
128
129impl<R: Ratchet, T: ToSocketAddrs> ServerConnectionSettingsBuilder<R, T> {
130    fn base(addr: T) -> Self {
131        Self {
132            password: None,
133            username: None,
134            udp_mode: None,
135            session_security_settings: None,
136            name: None,
137            psk: None,
138            transient_uuid: None,
139            address: Some(addr),
140            is_connect: false,
141            _ratchet: PhantomData,
142        }
143    }
144
145    /// Creates a new connection to a central server that does not persist client metadata and account information
146    /// after the connection is dropped to the server. This is ideal for applications that do not require
147    /// persistence.
148    pub fn transient(addr: T) -> Self {
149        Self::transient_with_id(addr, Uuid::new_v4())
150    }
151
152    /// See docs for `transient`. This function allows you to specify a custom UUID for the transient connection.
153    pub fn transient_with_id(addr: T, id: impl Into<Uuid>) -> Self {
154        Self {
155            transient_uuid: Some(id.into()),
156            ..Self::base(addr)
157        }
158    }
159
160    /// Creates a new connection to a central server that uses a username and password for authentication. This should be used directly when
161    /// constructing a registration request. If you are logging in, use the `credentialed_login` function instead.
162    pub fn credentialed_registration<U: Into<String>, N: Into<String>, P: Into<SecBuffer>>(
163        addr: T,
164        username: U,
165        alias: N,
166        password: P,
167    ) -> Self {
168        Self {
169            password: Some(password.into()),
170            username: Some(username.into()),
171            name: Some(alias.into()),
172            ..Self::base(addr)
173        }
174    }
175
176    /// Creates a new connection to a central server that uses a username and password for authentication. This should be used for the login process
177    pub fn credentialed_login<U: Into<String>, P: Into<SecBuffer>>(
178        addr: T,
179        username: U,
180        password: P,
181    ) -> Self {
182        Self {
183            password: Some(password.into()),
184            username: Some(username.into()),
185            is_connect: true,
186            ..Self::base(addr)
187        }
188    }
189
190    /// Adds a pre-shared key to the client-to-server connection. If the server expects a PSK, this is necessary.
191    pub fn with_session_password<V: Into<PreSharedKey>>(mut self, psk: V) -> Self {
192        self.psk = Some(psk.into());
193        self
194    }
195
196    /// Sets the UDP mode for the client-to-server connection
197    pub fn with_udp_mode(mut self, mode: UdpMode) -> Self {
198        self.udp_mode = Some(mode);
199        self
200    }
201
202    /// Disables the UDP mode for the client-to-server connection. The default setting is Disabled
203    pub fn disable_udp(self) -> Self {
204        self.with_udp_mode(UdpMode::Disabled)
205    }
206
207    pub fn enable_udp(self) -> Self {
208        self.with_udp_mode(UdpMode::Enabled)
209    }
210
211    /// Adds a session security settings to the client-to-server connection. This is necessary for the server to know how to handle the connection.
212    pub fn with_session_security_settings<V: Into<SessionSecuritySettings>>(
213        mut self,
214        settings: V,
215    ) -> Self {
216        self.session_security_settings = Some(settings.into());
217        self
218    }
219
220    /// Builds the client-to-server connection settings
221    pub fn build(self) -> Result<ServerConnectionSettings<R>, NetworkError> {
222        let server_addr = if let Some(addr) = self.address {
223            let addr = addr
224                .to_socket_addrs()
225                .map_err(|err| NetworkError::generic(err.to_string()))?
226                .next()
227                .ok_or(citadel_io::error!(citadel_io::ErrorCode::BuilderNoAddress))?;
228            Some(addr)
229        } else {
230            None
231        };
232
233        if let Some(uuid) = self.transient_uuid {
234            Ok(ServerConnectionSettings::<R>::Transient {
235                server_addr: server_addr
236                    .ok_or(citadel_io::error!(citadel_io::ErrorCode::BuilderNoAddress))?,
237                uuid,
238                udp_mode: self.udp_mode.unwrap_or_default(),
239                session_security_settings: self.session_security_settings.unwrap_or_default(),
240                pre_shared_key: self.psk,
241                _ratchet: PhantomData,
242            })
243        } else if self.is_connect {
244            Ok(ServerConnectionSettings::<R>::CredentialedConnect {
245                username: self
246                    .username
247                    .ok_or(citadel_io::error!(citadel_io::ErrorCode::BuilderNoUsername))?,
248                password: self
249                    .password
250                    .ok_or(citadel_io::error!(citadel_io::ErrorCode::BuilderNoPassword))?,
251                udp_mode: self.udp_mode.unwrap_or_default(),
252                session_security_settings: self.session_security_settings.unwrap_or_default(),
253                pre_shared_key: self.psk,
254                _ratchet: PhantomData,
255            })
256        } else {
257            Ok(ServerConnectionSettings::<R>::CredentialedRegister {
258                address: server_addr
259                    .ok_or(citadel_io::error!(citadel_io::ErrorCode::BuilderNoAddress))?,
260                username: self
261                    .username
262                    .ok_or(citadel_io::error!(citadel_io::ErrorCode::BuilderNoUsername))?,
263                alias: self
264                    .name
265                    .ok_or(citadel_io::error!(citadel_io::ErrorCode::BuilderNoAlias))?,
266                password: self
267                    .password
268                    .ok_or(citadel_io::error!(citadel_io::ErrorCode::BuilderNoPassword))?,
269                pre_shared_key: self.psk,
270                udp_mode: self.udp_mode.unwrap_or_default(),
271                session_security_settings: self.session_security_settings.unwrap_or_default(),
272                _ratchet: PhantomData,
273            })
274        }
275    }
276}
277
278/// The settings for a client-to-server connection
279pub enum ServerConnectionSettings<R: Ratchet> {
280    Transient {
281        server_addr: SocketAddr,
282        uuid: Uuid,
283        udp_mode: UdpMode,
284        session_security_settings: SessionSecuritySettings,
285        pre_shared_key: Option<PreSharedKey>,
286        _ratchet: PhantomData<R>,
287    },
288    CredentialedConnect {
289        username: String,
290        password: SecBuffer,
291        udp_mode: UdpMode,
292        session_security_settings: SessionSecuritySettings,
293        pre_shared_key: Option<PreSharedKey>,
294        _ratchet: PhantomData<R>,
295    },
296    CredentialedRegister {
297        address: SocketAddr,
298        username: String,
299        alias: String,
300        password: SecBuffer,
301        pre_shared_key: Option<PreSharedKey>,
302        udp_mode: UdpMode,
303        session_security_settings: SessionSecuritySettings,
304        _ratchet: PhantomData<R>,
305    },
306}
307
308impl<R: Ratchet> ServerConnectionSettings<R> {
309    pub(crate) fn udp_mode(&self) -> UdpMode {
310        match self {
311            Self::Transient { udp_mode, .. } => *udp_mode,
312            Self::CredentialedRegister { udp_mode, .. } => *udp_mode,
313            Self::CredentialedConnect { udp_mode, .. } => *udp_mode,
314        }
315    }
316
317    pub(crate) fn session_security_settings(&self) -> SessionSecuritySettings {
318        match self {
319            Self::Transient {
320                session_security_settings,
321                ..
322            } => *session_security_settings,
323            Self::CredentialedRegister {
324                session_security_settings,
325                ..
326            } => *session_security_settings,
327            Self::CredentialedConnect {
328                session_security_settings,
329                ..
330            } => *session_security_settings,
331        }
332    }
333
334    pub(crate) fn pre_shared_key(&self) -> Option<&PreSharedKey> {
335        match self {
336            Self::Transient { pre_shared_key, .. } => pre_shared_key.as_ref(),
337            Self::CredentialedRegister { pre_shared_key, .. } => pre_shared_key.as_ref(),
338            Self::CredentialedConnect { pre_shared_key, .. } => pre_shared_key.as_ref(),
339        }
340    }
341}