1pub mod error;
2pub mod http;
3pub mod iroh;
4pub mod metrics;
5#[cfg(all(feature = "tor", not(target_family = "wasm")))]
6pub mod tor;
7pub mod ws;
8
9use std::collections::{BTreeMap, BTreeSet, HashMap};
10use std::fmt::{self, Debug};
11use std::pin::Pin;
12use std::sync::Arc;
13use std::time::Duration;
14
15use anyhow::{anyhow, bail};
16use async_trait::async_trait;
17use fedimint_core::envs::{FM_WS_API_CONNECT_OVERRIDES_ENV, parse_kv_list_from_env};
18use fedimint_core::module::{ApiMethod, ApiRequestErased};
19use fedimint_core::util::backoff_util::{FibonacciBackoff, custom_backoff};
20use fedimint_core::util::{FmtCompact, FmtCompactAnyhow, SafeUrl};
21use fedimint_core::{apply, async_trait_maybe_send};
22use fedimint_logging::{LOG_CLIENT_NET_API, LOG_NET};
23use fedimint_metrics::HistogramExt as _;
24use reqwest::Method;
25use serde_json::Value;
26use tokio::sync::{OnceCell, SetOnce, broadcast, watch};
27use tracing::trace;
28
29use crate::error::ServerError;
30use crate::metrics::{CONNECTION_ATTEMPTS_TOTAL, CONNECTION_DURATION_SECONDS};
31use crate::ws::WebsocketConnector;
32
33pub type ServerResult<T> = Result<T, ServerError>;
34
35type ConnectorInitFn = Arc<
37 dyn Fn() -> Pin<Box<dyn Future<Output = anyhow::Result<DynConnector>> + Send>> + Send + Sync,
38>;
39
40#[derive(Debug, Clone)]
45#[allow(clippy::struct_excessive_bools)] pub struct ConnectorRegistryBuilder {
47 connection_overrides: BTreeMap<SafeUrl, SafeUrl>,
52
53 iroh_enable: bool,
55 iroh_dns: Option<SafeUrl>,
57 iroh_next: bool,
59 iroh_pkarr_dht: bool,
61
62 ws_enable: bool,
64 ws_force_tor: bool,
65
66 http_enable: bool,
68}
69
70impl ConnectorRegistryBuilder {
71 #[allow(clippy::unused_async)] pub async fn bind(self) -> anyhow::Result<ConnectorRegistry> {
73 let mut connectors_lazy: BTreeMap<String, (ConnectorInitFn, OnceCell<DynConnector>)> =
75 BTreeMap::new();
76
77 let builder_ws = self.clone();
79 let ws_connector_init = Arc::new(move || {
80 let builder = builder_ws.clone();
81 Box::pin(async move { builder.build_ws_connector().await })
82 as Pin<Box<dyn Future<Output = anyhow::Result<DynConnector>> + Send>>
83 });
84 connectors_lazy.insert("ws".into(), (ws_connector_init.clone(), OnceCell::new()));
85 connectors_lazy.insert("wss".into(), (ws_connector_init.clone(), OnceCell::new()));
86
87 let builder_iroh = self.clone();
89 connectors_lazy.insert(
90 "iroh".into(),
91 (
92 Arc::new(move || {
93 let builder = builder_iroh.clone();
94 Box::pin(async move { builder.build_iroh_connector().await })
95 as Pin<Box<dyn Future<Output = anyhow::Result<DynConnector>> + Send>>
96 }),
97 OnceCell::new(),
98 ),
99 );
100
101 let builder_http = self.clone();
102 let http_connector_init = Arc::new(move || {
103 let builder = builder_http.clone();
104 Box::pin(async move { builder.build_http_connector() })
105 as Pin<Box<dyn Future<Output = anyhow::Result<DynConnector>> + Send>>
106 });
107
108 connectors_lazy.insert(
109 "http".into(),
110 (http_connector_init.clone(), OnceCell::new()),
111 );
112 connectors_lazy.insert(
113 "https".into(),
114 (http_connector_init.clone(), OnceCell::new()),
115 );
116
117 Ok(ConnectorRegistry {
118 inner: ConnectorRegistryInner {
119 connectors_lazy,
120 connection_overrides: self.connection_overrides,
121 initialized: SetOnce::new(),
122 }
123 .into(),
124 })
125 }
126
127 pub async fn build_iroh_connector(&self) -> anyhow::Result<DynConnector> {
128 if !self.iroh_enable {
129 bail!("Iroh connector not enabled");
130 }
131 Ok(Arc::new(
132 iroh::IrohConnector::new(self.iroh_dns.clone(), self.iroh_pkarr_dht, self.iroh_next)
133 .await?,
134 ) as DynConnector)
135 }
136
137 pub async fn build_ws_connector(&self) -> anyhow::Result<DynConnector> {
138 if !self.ws_enable {
139 bail!("Websocket connector not enabled");
140 }
141
142 match self.ws_force_tor {
143 #[cfg(all(feature = "tor", not(target_family = "wasm")))]
144 true => {
145 use crate::tor::TorConnector;
146
147 Ok(Arc::new(TorConnector::bootstrap().await?) as DynConnector)
148 }
149
150 false => Ok(Arc::new(WebsocketConnector::new()) as DynConnector),
151 #[allow(unreachable_patterns)]
152 _ => bail!("Tor requested, but not support not compiled in"),
153 }
154 }
155
156 pub fn build_http_connector(&self) -> anyhow::Result<DynConnector> {
157 if !self.http_enable {
158 bail!("Http connector not enabled");
159 }
160
161 Ok(Arc::new(crate::http::HttpConnector::default()) as DynConnector)
162 }
163
164 pub fn iroh_pkarr_dht(self, enable: bool) -> Self {
165 Self {
166 iroh_pkarr_dht: enable,
167 ..self
168 }
169 }
170
171 pub fn iroh_next(self, enable: bool) -> Self {
172 Self {
173 iroh_next: enable,
174 ..self
175 }
176 }
177
178 pub fn ws_force_tor(self, enable: bool) -> Self {
179 Self {
180 ws_force_tor: enable,
181 ..self
182 }
183 }
184
185 pub fn http(self, enable: bool) -> Self {
186 Self {
187 http_enable: enable,
188 ..self
189 }
190 }
191
192 pub fn set_iroh_dns(self, url: SafeUrl) -> Self {
193 Self {
194 iroh_dns: Some(url),
195 ..self
196 }
197 }
198
199 pub fn with_env_var_overrides(mut self) -> anyhow::Result<Self> {
201 for (k, v) in parse_kv_list_from_env::<_, SafeUrl>(FM_WS_API_CONNECT_OVERRIDES_ENV)? {
203 self = self.with_connection_override(k, v);
204 }
205
206 Ok(Self { ..self })
207 }
208
209 pub fn with_connection_override(
210 mut self,
211 original_url: SafeUrl,
212 replacement_url: SafeUrl,
213 ) -> Self {
214 self.connection_overrides
215 .insert(original_url, replacement_url);
216 self
217 }
218}
219
220struct ConnectorRegistryInner {
222 connectors_lazy: BTreeMap<String, (ConnectorInitFn, OnceCell<DynConnector>)>,
224 connection_overrides: BTreeMap<SafeUrl, SafeUrl>,
226 initialized: tokio::sync::SetOnce<()>,
231}
232
233#[derive(Clone)]
246pub struct ConnectorRegistry {
247 inner: Arc<ConnectorRegistryInner>,
248}
249
250impl fmt::Debug for ConnectorRegistry {
251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 f.debug_struct("ConnectorRegistry")
253 .field("connectors_lazy", &self.inner.connectors_lazy.len())
254 .field("connection_overrides", &self.inner.connection_overrides)
255 .finish()
256 }
257}
258
259impl ConnectorRegistry {
260 pub fn build_from_client_defaults() -> ConnectorRegistryBuilder {
265 ConnectorRegistryBuilder {
266 iroh_enable: true,
267 iroh_dns: None,
268 iroh_pkarr_dht: false,
269 iroh_next: true,
270 ws_enable: true,
271 ws_force_tor: false,
272 http_enable: true,
273
274 connection_overrides: BTreeMap::default(),
275 }
276 }
277
278 pub fn build_from_server_defaults() -> ConnectorRegistryBuilder {
281 ConnectorRegistryBuilder {
282 iroh_enable: true,
283 iroh_dns: None,
284 iroh_pkarr_dht: true,
285 iroh_next: true,
286 ws_enable: true,
287 ws_force_tor: false,
288 http_enable: false,
289
290 connection_overrides: BTreeMap::default(),
291 }
292 }
293
294 pub fn build_from_testing_defaults() -> ConnectorRegistryBuilder {
297 ConnectorRegistryBuilder {
298 iroh_enable: true,
299 iroh_dns: None,
300 iroh_pkarr_dht: false,
301 iroh_next: false,
302 ws_enable: true,
303 ws_force_tor: false,
304 http_enable: true,
305
306 connection_overrides: BTreeMap::default(),
307 }
308 }
309
310 pub fn build_from_client_env() -> anyhow::Result<ConnectorRegistryBuilder> {
313 let builder = Self::build_from_client_defaults().with_env_var_overrides()?;
314 Ok(builder)
315 }
316
317 pub fn build_from_server_env() -> anyhow::Result<ConnectorRegistryBuilder> {
320 let builder = Self::build_from_server_defaults().with_env_var_overrides()?;
321 Ok(builder)
322 }
323
324 pub fn build_from_testing_env() -> anyhow::Result<ConnectorRegistryBuilder> {
327 let builder = Self::build_from_testing_defaults().with_env_var_overrides()?;
328 Ok(builder)
329 }
330
331 pub async fn wait_for_initialized_connections(&self) {
333 self.inner.initialized.wait().await;
334 }
335
336 pub async fn connect_guardian(
341 &self,
342 url: &SafeUrl,
343 api_secret: Option<&str>,
344 ) -> ServerResult<DynGuaridianConnection> {
345 trace!(
346 target: LOG_NET,
347 %url,
348 "Connection requested to guardian"
349 );
350 let _ = self.inner.initialized.set(());
351
352 let url = match self.inner.connection_overrides.get(url) {
353 Some(replacement) => {
354 trace!(
355 target: LOG_NET,
356 original_url = %url,
357 replacement_url = %replacement,
358 "Using a connectivity override for connection"
359 );
360
361 replacement
362 }
363 None => url,
364 };
365
366 let scheme = url.scheme().to_string();
367
368 let Some(connector_lazy) = self.inner.connectors_lazy.get(&scheme) else {
369 return Err(ServerError::InvalidEndpoint(anyhow!(
370 "Unsupported scheme: {}; missing endpoint handler",
371 url.scheme()
372 )));
373 };
374
375 let init_fn = connector_lazy.0.clone();
377
378 let timer = CONNECTION_DURATION_SECONDS
379 .with_label_values(&[&scheme])
380 .start_timer_ext();
381
382 let result = connector_lazy
383 .1
384 .get_or_try_init(|| async move { init_fn().await })
385 .await
386 .map_err(|e| {
387 ServerError::Transport(anyhow!(
388 "Connector failed to initialize: {}",
389 e.fmt_compact_anyhow()
390 ))
391 })?
392 .connect_guardian(url, api_secret)
393 .await;
394
395 timer.observe_duration();
396
397 let result_label = if result.is_ok() { "success" } else { "error" }.to_string();
398 CONNECTION_ATTEMPTS_TOTAL
399 .with_label_values(&[&scheme, &result_label])
400 .inc();
401
402 let conn = result.inspect_err(|err| {
403 trace!(
404 target: LOG_NET,
405 %url,
406 err = %err.fmt_compact(),
407 "Connection failed"
408 );
409 })?;
410
411 trace!(
412 target: LOG_NET,
413 %url,
414 "Connection returned"
415 );
416 Ok(conn)
417 }
418
419 pub async fn connect_gateway(&self, url: &SafeUrl) -> anyhow::Result<DynGatewayConnection> {
424 trace!(
425 target: LOG_NET,
426 %url,
427 "Connection requested to gateway"
428 );
429 let _ = self.inner.initialized.set(());
430
431 let url = match self.inner.connection_overrides.get(url) {
432 Some(replacement) => {
433 trace!(
434 target: LOG_NET,
435 original_url = %url,
436 replacement_url = %replacement,
437 "Using a connectivity override for connection"
438 );
439
440 replacement
441 }
442 None => url,
443 };
444
445 let scheme = url.scheme().to_string();
446
447 let Some(connector_lazy) = self.inner.connectors_lazy.get(&scheme) else {
448 return Err(anyhow!(
449 "Unsupported scheme: {}; missing endpoint handler",
450 url.scheme()
451 ));
452 };
453
454 let init_fn = connector_lazy.0.clone();
456
457 let timer = CONNECTION_DURATION_SECONDS
458 .with_label_values(&[&scheme])
459 .start_timer_ext();
460
461 let result = connector_lazy
462 .1
463 .get_or_try_init(|| async move { init_fn().await })
464 .await
465 .map_err(|e| {
466 ServerError::Transport(anyhow!(
467 "Connector failed to initialize: {}",
468 e.fmt_compact_anyhow()
469 ))
470 })?
471 .connect_gateway(url)
472 .await;
473
474 timer.observe_duration();
475
476 let result_label = if result.is_ok() { "success" } else { "error" }.to_string();
477 CONNECTION_ATTEMPTS_TOTAL
478 .with_label_values(&[&scheme, &result_label])
479 .inc();
480
481 result
482 }
483}
484pub type DynConnector = Arc<dyn Connector>;
485
486#[async_trait]
487pub trait Connector: Send + Sync + 'static + Debug {
488 async fn connect_guardian(
489 &self,
490 url: &SafeUrl,
491 api_secret: Option<&str>,
492 ) -> ServerResult<DynGuaridianConnection>;
493
494 async fn connect_gateway(&self, url: &SafeUrl) -> anyhow::Result<DynGatewayConnection>;
495}
496
497#[apply(async_trait_maybe_send!)]
500pub trait IConnection: Debug + Send + Sync + 'static {
501 fn is_connected(&self) -> bool;
502
503 async fn await_disconnection(&self);
504}
505
506pub type DynGuaridianConnection = Arc<dyn IGuardianConnection>;
508
509#[async_trait]
511pub trait IGuardianConnection: IConnection + Debug + Send + Sync + 'static {
512 async fn request(&self, method: ApiMethod, request: ApiRequestErased) -> ServerResult<Value>;
513
514 fn into_dyn(self) -> DynGuaridianConnection
515 where
516 Self: Sized,
517 {
518 Arc::new(self)
519 }
520}
521
522pub type DynGatewayConnection = Arc<dyn IGatewayConnection>;
524
525#[apply(async_trait_maybe_send!)]
527pub trait IGatewayConnection: IConnection + Debug + Send + Sync + 'static {
528 async fn request(
529 &self,
530 password: Option<String>,
531 method: Method,
532 route: &str,
533 payload: Option<Value>,
534 ) -> ServerResult<Value>;
535
536 fn into_dyn(self) -> DynGatewayConnection
537 where
538 Self: Sized,
539 {
540 Arc::new(self)
541 }
542}
543
544#[derive(Debug)]
545pub struct ConnectionPool<T: IConnection + ?Sized> {
546 connectors: ConnectorRegistry,
548
549 active_connections: watch::Sender<BTreeSet<SafeUrl>>,
550
551 #[allow(clippy::type_complexity)]
557 connections: Arc<tokio::sync::Mutex<HashMap<SafeUrl, Arc<ConnectionState<T>>>>>,
558}
559
560impl<T: IConnection + ?Sized> Clone for ConnectionPool<T> {
561 fn clone(&self) -> Self {
562 Self {
563 connectors: self.connectors.clone(),
564 connections: self.connections.clone(),
565 active_connections: self.active_connections.clone(),
566 }
567 }
568}
569
570impl<T: IConnection + ?Sized> ConnectionPool<T> {
571 pub fn new(connectors: ConnectorRegistry) -> Self {
572 Self {
573 connectors,
574 connections: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
575 active_connections: watch::channel(BTreeSet::new()).0,
576 }
577 }
578
579 async fn get_or_init_pool_entry(&self, url: &SafeUrl) -> Arc<ConnectionState<T>> {
580 let mut pool_locked = self.connections.lock().await;
581 pool_locked
582 .entry(url.to_owned())
583 .and_modify(|entry_arc| {
584 if let Some(existing_conn) = entry_arc.connection.get()
590 && !existing_conn.is_connected()
591 {
592 trace!(
593 target: LOG_CLIENT_NET_API,
594 %url,
595 "Existing connection is disconnected, removing from pool"
596 );
597 self.active_connections.send_modify(|v| {
598 v.remove(url);
599 });
600 *entry_arc = Arc::new(ConnectionState::new_reconnecting());
601 }
602 })
603 .or_insert_with(|| Arc::new(ConnectionState::new_initial()))
604 .clone()
605 }
606
607 pub async fn get_or_create_connection<F, Fut>(
608 &self,
609 url: &SafeUrl,
610 api_secret: Option<&str>,
611 create_connection: F,
612 ) -> ServerResult<Arc<T>>
613 where
614 F: Fn(SafeUrl, Option<String>, ConnectorRegistry) -> Fut + Clone + Send + Sync + 'static,
615 Fut: Future<Output = ServerResult<Arc<T>>> + Send + 'static,
616 {
617 let pool_entry_arc = self.get_or_init_pool_entry(url).await;
618
619 let leader_tx = loop {
620 let mut leader_rx = {
621 let mut chan_locked = pool_entry_arc
622 .merge_connection_attempts_chan
623 .lock()
624 .expect("locking error");
625
626 if chan_locked.is_closed() {
627 let (leader_tx, leader_rx) = broadcast::channel(1);
628 *chan_locked = leader_rx;
629 break leader_tx;
632 }
633
634 chan_locked.resubscribe()
636 };
637
638 if let Ok(res) = leader_rx.recv().await {
639 match res {
640 Ok(o) => return Ok(o),
641 Err(err) => {
642 return Err(ServerError::Connection(anyhow::format_err!("{}", err)));
643 }
644 }
645 }
646 };
647
648 let conn = pool_entry_arc
649 .connection
650 .get_or_try_init(|| async {
651 let retry_delay = pool_entry_arc.pre_reconnect_delay();
652 fedimint_core::runtime::sleep(retry_delay).await;
653
654 trace!(target: LOG_CLIENT_NET_API, %url, "Attempting to create a new connection");
655 let res = create_connection(
656 url.clone(),
657 api_secret.map(std::string::ToString::to_string),
658 self.connectors.clone(),
659 )
660 .await;
661
662 let _ = leader_tx.send(
667 res.as_ref()
668 .map(|o| o.clone())
669 .map_err(|err| err.to_string()),
670 );
671
672 let conn = res?;
673
674 self.active_connections.send_modify(|v| {
675 v.insert(url.clone());
676 });
677
678 fedimint_core::runtime::spawn("connection disconnect watch", {
679 let conn = conn.clone();
680 let s = self.clone();
681 let url = url.clone();
682 async move {
683 conn.await_disconnection().await;
685 s.get_or_init_pool_entry(&url).await;
691 }
692 });
693
694 Ok(conn)
695 })
696 .await?;
697
698 trace!(target: LOG_CLIENT_NET_API, %url, "Connection ready");
699 Ok(conn.clone())
700 }
701 pub fn get_active_connection_receiver(&self) -> watch::Receiver<BTreeSet<SafeUrl>> {
703 self.active_connections.subscribe()
704 }
705
706 pub async fn wait_for_initialized_connections(&self) {
707 self.connectors.wait_for_initialized_connections().await
708 }
709}
710
711#[derive(Debug)]
714struct ConnectionStateInner {
715 fresh: bool,
716 backoff: FibonacciBackoff,
717}
718
719#[derive(Debug)]
720pub struct ConnectionState<T: ?Sized> {
721 pub connection: tokio::sync::OnceCell<Arc<T>>,
723
724 merge_connection_attempts_chan:
728 std::sync::Mutex<broadcast::Receiver<std::result::Result<Arc<T>, String>>>,
729
730 inner: std::sync::Mutex<ConnectionStateInner>,
734}
735
736impl<T: ?Sized> ConnectionState<T> {
737 pub fn new_initial() -> Self {
739 Self {
740 connection: OnceCell::new(),
741 inner: std::sync::Mutex::new(ConnectionStateInner {
742 fresh: true,
743 backoff: custom_backoff(
744 Duration::from_millis(5),
746 Duration::from_secs(30),
747 None,
748 ),
749 }),
750 merge_connection_attempts_chan: std::sync::Mutex::new(broadcast::channel(1).1),
751 }
752 }
753
754 pub fn new_reconnecting() -> Self {
757 Self {
758 connection: OnceCell::new(),
759 inner: std::sync::Mutex::new(ConnectionStateInner {
760 fresh: false,
762 backoff: custom_backoff(
763 Duration::from_millis(500),
765 Duration::from_secs(30),
766 None,
767 ),
768 }),
769 merge_connection_attempts_chan: std::sync::Mutex::new(broadcast::channel(1).1),
770 }
771 }
772
773 pub fn pre_reconnect_delay(&self) -> Duration {
776 let mut backoff_locked = self.inner.lock().expect("Locking failed");
777 let fresh = backoff_locked.fresh;
778
779 backoff_locked.fresh = false;
780
781 if fresh {
782 Duration::default()
783 } else {
784 backoff_locked.backoff.next().expect("Keeps retrying")
785 }
786 }
787}