1use citadel_proto::prelude::*;
39
40use citadel_io::ServerMode;
41use citadel_proto::kernel::KernelExecutorArguments;
42use citadel_proto::macros::{ContextRequirements, LocalContextRequirements};
43use citadel_types::crypto::{HeaderObfuscatorSettings, PreSharedKey};
44use futures::Future;
45use std::fmt::{Debug, Formatter};
46use std::marker::PhantomData;
47use std::pin::Pin;
48use std::task::{Context, Poll};
49
50pub struct NodeBuilder<R: Ratchet = StackedRatchet, T: PlatformOps = DefaultTransport> {
52 hypernode_type: Option<NodeType>,
53 underlying_protocol: Option<ServerMode<T>>,
54 backend_type: Option<BackendType>,
55 server_argon_settings: Option<ArgonDefaultServerSettings>,
56 #[cfg(feature = "google-services")]
57 services: Option<ServicesConfig>,
58 server_misc_settings: Option<ServerMiscSettings>,
59 client_tls_config: Option<T::ClientConfig>,
60 kernel_executor_settings: Option<KernelExecutorSettings>,
61 stun_servers: Option<Vec<String>>,
62 turn_servers: Option<Vec<TurnServerConfig>>,
63 local_only_server_settings: Option<ServerOnlySessionInitSettings>,
64 websocket_listener: Option<citadel_io::WebSocketListen>,
65 injected_listener: Option<T::Listener>,
66 #[cfg(target_family = "wasm")]
67 serverless_config: Option<ServerlessConfig>,
68 _ratchet: PhantomData<R>,
69 _transport: PhantomData<T>,
70}
71
72pub type DefaultNodeBuilder = NodeBuilder<StackedRatchet, DefaultTransport>;
74pub type LightweightNodeBuilder = NodeBuilder<MonoRatchet, DefaultTransport>;
76
77impl<R: Ratchet, T: PlatformOps> Default for NodeBuilder<R, T> {
78 fn default() -> Self {
79 Self {
80 hypernode_type: None,
81 underlying_protocol: None,
82 backend_type: None,
83 server_argon_settings: None,
84 #[cfg(feature = "google-services")]
85 services: None,
86 server_misc_settings: None,
87 client_tls_config: None,
88 kernel_executor_settings: None,
89 stun_servers: None,
90 turn_servers: None,
91 local_only_server_settings: None,
92 websocket_listener: None,
93 injected_listener: None,
94 #[cfg(target_family = "wasm")]
95 serverless_config: None,
96 _ratchet: Default::default(),
97 _transport: Default::default(),
98 }
99 }
100}
101
102pub struct NodeFuture<'a, K> {
104 inner: Pin<Box<dyn FutureContextRequirements<'a, Result<K, NetworkError>>>>,
105 _pd: PhantomData<fn() -> K>,
106}
107
108#[cfg(feature = "multi-threaded")]
109trait FutureContextRequirements<'a, Output>:
110 Future<Output = Output> + Send + LocalContextRequirements<'a>
111{
112}
113#[cfg(feature = "multi-threaded")]
114impl<'a, T: Future<Output = Output> + Send + LocalContextRequirements<'a>, Output>
115 FutureContextRequirements<'a, Output> for T
116{
117}
118
119#[cfg(not(feature = "multi-threaded"))]
120trait FutureContextRequirements<'a, Output>:
121 Future<Output = Output> + LocalContextRequirements<'a>
122{
123}
124#[cfg(not(feature = "multi-threaded"))]
125impl<'a, T: Future<Output = Output> + LocalContextRequirements<'a>, Output>
126 crate::builder::node_builder::FutureContextRequirements<'a, Output> for T
127{
128}
129
130impl<K> Debug for NodeFuture<'_, K> {
131 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
132 write!(f, "NodeFuture")
133 }
134}
135
136impl<K> Future for NodeFuture<'_, K> {
137 type Output = Result<K, NetworkError>;
138
139 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
140 self.inner.as_mut().poll(cx)
141 }
142}
143
144impl<R: Ratchet + ContextRequirements, T: PlatformOps> NodeBuilder<R, T> {
145 pub fn build<'a, 'b: 'a, K: NetKernel<R> + 'b>(
147 &'a mut self,
148 kernel: K,
149 ) -> anyhow::Result<NodeFuture<'b, K>> {
150 self.check()?;
151 let hypernode_type = self.hypernode_type.take().unwrap_or_default();
152 let backend_type = self.backend_type.take().unwrap_or_default();
153 let server_argon_settings = self.server_argon_settings.take();
154 #[cfg(feature = "google-services")]
155 let server_services_cfg = self.services.take();
156 #[cfg(not(feature = "google-services"))]
157 let server_services_cfg = None;
158 let server_misc_settings = self.server_misc_settings.take();
159 let client_config = self.client_tls_config.take();
160 let kernel_executor_settings = self.kernel_executor_settings.take().unwrap_or_default();
161 let stun_servers = self.stun_servers.take();
162 let turn_servers = self.turn_servers.take();
163 let underlying_proto = self.underlying_protocol.take();
164 let server_only_session_init_settings = self.local_only_server_settings.take();
165 let websocket_listener = self.websocket_listener.take();
166 let injected_listener = self.injected_listener.take();
167 #[cfg(target_family = "wasm")]
168 let serverless_config = self.serverless_config.take();
169
170 Ok(NodeFuture {
171 _pd: Default::default(),
172 inner: Box::pin(async move {
173 let underlying_proto = match underlying_proto {
174 Some(proto) => proto,
175 None => T::default_server_config().await.map_err(|err| {
176 citadel_io::error!(
177 citadel_io::ErrorCode::NodeDefaultServerConfigFailed,
178 err.to_string()
179 )
180 })?,
181 };
182
183 T::config_warnings(&underlying_proto);
184
185 #[cfg(target_family = "wasm")]
188 let (pre_built_listener, client_config, hypernode_type) = if let Some(sl_config) =
189 serverless_config
190 {
191 let conn = establish_serverless_connection(
192 sl_config.signaling.as_ref(),
193 &sl_config.room_token,
194 &sl_config.ice_servers,
195 sl_config.poll_interval_ms,
196 sl_config.timeout_ms,
197 )
198 .await
199 .map_err(|e: std::io::Error| NetworkError::generic(e.to_string()))?;
200
201 T::setup_serverless_transport(conn.stream, conn.is_server_role, client_config)
202 } else {
203 (injected_listener, client_config, hypernode_type)
204 };
205
206 #[cfg(not(target_family = "wasm"))]
207 let pre_built_listener = injected_listener;
208
209 log::trace!(target: "citadel", "[NodeBuilder] Checking Tokio runtime ...");
210 let rt = citadel_io::try_current_runtime().map_err(NetworkError::generic)?;
211 log::trace!(target: "citadel", "[NodeBuilder] Creating account manager ...");
212 let account_manager = AccountManager::new(
213 backend_type,
214 server_argon_settings,
215 server_services_cfg,
216 server_misc_settings,
217 )
218 .await?;
219
220 let args: KernelExecutorArguments<_, _, T> = KernelExecutorArguments {
221 rt,
222 hypernode_type,
223 account_manager,
224 kernel,
225 underlying_proto,
226 client_config,
227 kernel_executor_settings,
228 stun_servers,
229 turn_servers,
230 server_only_session_init_settings,
231 websocket_listener,
232 pre_built_listener,
233 };
234
235 log::trace!(target: "citadel", "[NodeBuilder] Creating KernelExecutor ...");
236 let kernel_executor = KernelExecutor::<_, R>::new(args).await?;
237 log::trace!(target: "citadel", "[NodeBuilder] Executing kernel");
238 kernel_executor.execute().await
239 }),
240 })
241 }
242
243 pub fn with_node_type(&mut self, node_type: NodeType) -> &mut Self {
251 self.hypernode_type = Some(node_type);
252 self
253 }
254
255 pub fn with_backend(&mut self, backend_type: BackendType) -> &mut Self {
259 self.backend_type = Some(backend_type);
260 self
261 }
262
263 pub fn with_kernel_executor_settings(
265 &mut self,
266 kernel_executor_settings: KernelExecutorSettings,
267 ) -> &mut Self {
268 self.kernel_executor_settings = Some(kernel_executor_settings);
269 self
270 }
271
272 pub fn with_server_argon_settings(
274 &mut self,
275 settings: ArgonDefaultServerSettings,
276 ) -> &mut Self {
277 self.server_argon_settings = Some(settings);
278 self
279 }
280
281 #[cfg(feature = "google-services")]
283 pub fn with_google_services_json_path<V: Into<String>>(&mut self, path: V) -> &mut Self {
284 let cfg = self.get_or_create_services();
285 cfg.google_services_json_path = Some(path.into());
286 self
287 }
288
289 pub fn with_server_misc_settings(&mut self, misc_settings: ServerMiscSettings) -> &mut Self {
291 self.server_misc_settings = Some(misc_settings);
292 self
293 }
294
295 #[cfg(feature = "google-services")]
298 pub fn with_google_realtime_database_config<V: Into<String>, W: Into<String>>(
299 &mut self,
300 url: V,
301 api_key: W,
302 ) -> &mut Self {
303 let cfg = self.get_or_create_services();
304 cfg.google_rtdb = Some(RtdbConfig {
305 url: url.into(),
306 api_key: api_key.into(),
307 });
308 self
309 }
310
311 pub fn with_underlying_protocol(&mut self, proto: ServerMode<T>) -> &mut Self {
314 self.underlying_protocol = Some(proto);
315 self
316 }
317
318 pub fn with_client_config(&mut self, config: T::ClientConfig) -> &mut Self {
320 self.client_tls_config = Some(config);
321 self
322 }
323
324 #[cfg(feature = "google-services")]
325 fn get_or_create_services(&mut self) -> &mut ServicesConfig {
326 if self.services.is_some() {
327 self.services.as_mut().unwrap()
328 } else {
329 let cfg = ServicesConfig::default();
330 self.services = Some(cfg);
331 self.services.as_mut().unwrap()
332 }
333 }
334
335 pub fn with_stun_servers<V: Into<String>, S: Into<Vec<V>>>(&mut self, servers: S) -> &mut Self {
337 self.stun_servers = Some(servers.into().into_iter().map(|t| t.into()).collect());
338 self
339 }
340
341 pub fn with_turn_servers<S: Into<Vec<TurnServerConfig>>>(&mut self, servers: S) -> &mut Self {
347 self.turn_servers = Some(servers.into());
348 self
349 }
350
351 pub fn with_websocket_listener(&mut self, addr: std::net::SocketAddr) -> &mut Self {
359 self.websocket_listener = Some(citadel_io::WebSocketListen::Addr(addr));
360 self
361 }
362
363 pub fn with_bound_websocket_listener(&mut self, listener: std::net::TcpListener) -> &mut Self {
367 self.websocket_listener = Some(citadel_io::WebSocketListen::Bound(listener));
368 self
369 }
370
371 pub fn with_injected_listener(&mut self, listener: T::Listener) -> &mut Self {
378 self.injected_listener = Some(listener);
379 self
380 }
381
382 #[cfg(target_family = "wasm")]
389 pub fn with_no_central_server(&mut self, config: ServerlessConfig) -> &mut Self {
390 self.serverless_config = Some(config);
391 self
392 }
393
394 pub fn with_server_password<V: Into<PreSharedKey>>(&mut self, password: V) -> &mut Self {
399 let mut server_only_settings = self.local_only_server_settings.clone().unwrap_or_default();
400 server_only_settings.declared_pre_shared_key = Some(password.into());
401 self.local_only_server_settings = Some(server_only_settings);
402 self
403 }
404
405 pub fn with_server_declared_header_obfuscation<V: Into<HeaderObfuscatorSettings>>(
407 &mut self,
408 header_obfuscator_settings: V,
409 ) -> &mut Self {
410 let mut server_only_settings = self.local_only_server_settings.clone().unwrap_or_default();
411 server_only_settings.declared_header_obfuscation_setting =
412 header_obfuscator_settings.into();
413 self.local_only_server_settings = Some(server_only_settings);
414 self
415 }
416
417 fn check(&self) -> anyhow::Result<()> {
418 if self.injected_listener.is_some()
419 && !matches!(self.hypernode_type, Some(NodeType::Server(_)))
420 {
421 return Err(anyhow::Error::msg(
422 "An injected listener requires NodeType::Server",
423 ));
424 }
425
426 #[cfg(feature = "google-services")]
427 if let Some(svc) = self.services.as_ref() {
428 if svc.google_rtdb.is_some() && svc.google_services_json_path.is_none() {
429 return Err(anyhow::Error::msg(
430 "Google realtime database is enabled, yet, a services path is not provided",
431 ));
432 }
433 }
434
435 if let Some(stun_servers) = self.stun_servers.as_ref() {
436 if stun_servers.len() != 3 {
437 return Err(anyhow::Error::msg(
438 "There must be exactly 3 specified STUN servers",
439 ));
440 }
441 }
442
443 Ok(())
444 }
445}
446
447#[cfg(not(target_family = "wasm"))]
449impl<R: Ratchet + ContextRequirements> NodeBuilder<R, NativeIO> {
450 fn set_client_rustls_config(
454 &mut self,
455 config: std::sync::Arc<citadel_proto::re_imports::RustlsClientConfig>,
456 ) {
457 let require_cert_verification = self
458 .client_tls_config
459 .as_ref()
460 .map(|cfg| cfg.require_cert_verification)
461 .unwrap_or(false);
462 self.client_tls_config = Some(citadel_proto::re_imports::NativeClientConfig {
463 config,
464 require_cert_verification,
465 });
466 }
467
468 pub async fn with_native_certs(&mut self) -> anyhow::Result<&mut Self> {
472 let certs = citadel_proto::re_imports::load_native_certs_async().await?;
473 let cfg = citadel_proto::re_imports::cert_vec_to_secure_client_config(&certs)?;
474 self.set_client_rustls_config(std::sync::Arc::new(cfg));
475 Ok(self)
476 }
477
478 pub fn with_insecure_skip_cert_verification(&mut self) -> &mut Self {
481 self.client_tls_config = Some(citadel_proto::re_imports::NativeClientConfig::new(
483 std::sync::Arc::new(citadel_proto::re_imports::insecure::rustls_client_config()),
484 ));
485 self
486 }
487
488 pub async fn with_require_cert_verification(&mut self) -> anyhow::Result<&mut Self> {
494 if self.client_tls_config.is_none() {
495 let _ = self.with_native_certs().await?;
496 }
497 if let Some(cfg) = self.client_tls_config.as_mut() {
498 cfg.require_cert_verification = true;
499 }
500 Ok(self)
501 }
502
503 pub fn with_custom_certs<V: AsRef<[u8]>>(
506 &mut self,
507 custom_certs: &[V],
508 ) -> anyhow::Result<&mut Self> {
509 let cfg = citadel_proto::re_imports::create_rustls_client_config(custom_certs)?;
510 self.set_client_rustls_config(std::sync::Arc::new(cfg));
511 Ok(self)
512 }
513
514 #[cfg(feature = "std")]
516 pub async fn with_pem_file<P: AsRef<std::path::Path>>(
517 &mut self,
518 path: P,
519 ) -> anyhow::Result<&mut Self> {
520 use citadel_wire::exports::{Certificate, PemObject};
521 let mut der = std::io::Cursor::new(citadel_io::tokio::fs::read(path).await?);
522 let certs: Vec<Certificate<'static>> =
523 Certificate::pem_reader_iter(&mut der).collect::<Result<Vec<_>, _>>()?;
524 let cfg = citadel_proto::re_imports::create_rustls_client_config(&certs)?;
525 self.set_client_rustls_config(std::sync::Arc::new(cfg));
526 Ok(self)
527 }
528}
529
530#[cfg(all(test, not(target_family = "wasm")))]
531mod tests {
532 use crate::builder::node_builder::DefaultNodeBuilder;
533 use crate::prefabs::server::empty::EmptyKernel;
534 use crate::prelude::{BackendType, NodeType};
535 use citadel_io::tokio;
536 use citadel_proto::prelude::{
537 KernelExecutorSettings, NativeIO, NativeP2PConfig, NativeSecureConfig, ServerMode,
538 };
539 use rstest::rstest;
540 use std::str::FromStr;
541
542 #[test]
543 #[cfg(feature = "google-services")]
544 fn okay_config() {
545 let _ = DefaultNodeBuilder::default()
546 .with_google_realtime_database_config("123", "456")
547 .with_google_services_json_path("abc")
548 .build(EmptyKernel::default())
549 .unwrap();
550 }
551
552 #[test]
553 #[cfg(feature = "google-services")]
554 fn bad_config() {
555 assert!(DefaultNodeBuilder::default()
556 .with_google_realtime_database_config("123", "456")
557 .build(EmptyKernel::default())
558 .is_err());
559 }
560
561 #[test]
562 fn bad_config2() {
563 assert!(DefaultNodeBuilder::default()
564 .with_stun_servers(["dummy1", "dummy2"])
565 .build(EmptyKernel::default())
566 .is_err());
567 }
568
569 #[rstest]
570 #[tokio::test]
571 #[timeout(std::time::Duration::from_secs(60))]
572 #[allow(clippy::let_underscore_must_use)]
573 async fn test_options(
574 #[values(ServerMode::P2P(NativeP2PConfig::self_signed()), ServerMode::OrderedReliableSecure(NativeSecureConfig::self_signed().unwrap())
575 )]
576 underlying_protocol: ServerMode<NativeIO>,
577 #[values(NodeType::Peer, NodeType::Server(std::net::SocketAddr::from_str("127.0.0.1:9999").unwrap()
578 ))]
579 node_type: NodeType,
580 #[values(KernelExecutorSettings::default(), KernelExecutorSettings::default().with_max_concurrency(2)
581 )]
582 kernel_settings: KernelExecutorSettings,
583 #[values(BackendType::InMemory, BackendType::new("file:/hello_world/path/").unwrap())]
584 backend_type: BackendType,
585 ) {
586 let mut builder = DefaultNodeBuilder::default();
587 let _ = builder
588 .with_underlying_protocol(underlying_protocol.clone())
589 .with_backend(backend_type.clone())
590 .with_node_type(node_type)
591 .with_kernel_executor_settings(kernel_settings.clone())
592 .with_insecure_skip_cert_verification()
593 .with_stun_servers(["dummy1", "dummy1", "dummy3"])
594 .with_native_certs()
595 .await
596 .unwrap();
597
598 assert!(builder.underlying_protocol.is_some());
599 assert_eq!(backend_type, builder.backend_type.clone().unwrap());
600 assert_eq!(node_type, builder.hypernode_type.unwrap());
601 assert_eq!(
602 kernel_settings,
603 builder.kernel_executor_settings.clone().unwrap()
604 );
605
606 drop(builder.build(EmptyKernel::default()).unwrap());
607 }
608}