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