fedimint_api_client/api/
tor.rs

1#[cfg(all(feature = "tor", not(target_family = "wasm")))]
2use std::fmt;
3use std::sync::Arc;
4
5use anyhow::anyhow;
6use arti_client::{TorAddr, TorClient, TorClientConfig};
7use async_trait::async_trait;
8use base64::Engine as _;
9use fedimint_core::util::SafeUrl;
10use jsonrpsee_ws_client::{HeaderMap, HeaderValue, WsClientBuilder};
11use tokio_rustls::TlsConnector;
12use tokio_rustls::rustls::{ClientConfig as TlsClientConfig, RootCertStore};
13use tracing::debug;
14
15use super::{Connector, DynGuaridianConnection};
16use crate::api::{IGuardianConnection as _, PeerError};
17
18#[derive(Clone)]
19pub struct TorConnector {
20    tor_client: TorClient<tor_rtcompat::PreferredRuntime>,
21}
22
23impl fmt::Debug for TorConnector {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.debug_struct("TorEndpoint").finish_non_exhaustive()
26    }
27}
28
29impl TorConnector {
30    pub async fn bootstrap() -> anyhow::Result<Self> {
31        use tracing::debug;
32
33        use crate::api::PeerError;
34
35        let tor_config = TorClientConfig::default();
36        let tor_client = TorClient::create_bootstrapped(tor_config)
37            .await
38            .map_err(|err| PeerError::InternalClientError(err.into()))?
39            .isolated_client();
40
41        debug!("Successfully created and bootstrapped the `TorClient`, for given `TorConfig`.");
42
43        Ok(Self { tor_client })
44    }
45}
46
47#[async_trait]
48impl Connector for TorConnector {
49    #[allow(clippy::too_many_lines)]
50    async fn connect_guardian(
51        &self,
52        url: &SafeUrl,
53        api_secret: Option<&str>,
54    ) -> super::PeerResult<DynGuaridianConnection> {
55        let addr = (
56            url.host_str()
57                .ok_or_else(|| PeerError::InvalidEndpoint(anyhow!("Expected host str")))?,
58            url.port_or_known_default()
59                .ok_or_else(|| PeerError::InvalidEndpoint(anyhow!("Expected port number")))?,
60        );
61        let tor_addr = TorAddr::from(addr).map_err(|e| {
62            PeerError::InvalidEndpoint(anyhow!("Invalid endpoint addr: {addr:?}: {e:#}"))
63        })?;
64
65        let tor_addr_clone = tor_addr.clone();
66
67        debug!(
68            ?tor_addr,
69            ?addr,
70            "Successfully created `TorAddr` for given address (i.e. host and port)"
71        );
72
73        // TODO: It can be updated to use `is_onion_address()` implementation,
74        // once https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/2214 lands.
75        let anonymized_stream = if url.is_onion_address() {
76            let mut stream_prefs = arti_client::StreamPrefs::default();
77            stream_prefs.connect_to_onion_services(arti_client::config::BoolOrAuto::Explicit(true));
78
79            let anonymized_stream = self
80                .tor_client
81                .connect_with_prefs(tor_addr, &stream_prefs)
82                .await
83                .map_err(|e| PeerError::Connection(e.into()))?;
84
85            debug!(
86                ?tor_addr_clone,
87                "Successfully connected to onion address `TorAddr`, and established an anonymized `DataStream`"
88            );
89            anonymized_stream
90        } else {
91            let anonymized_stream = self
92                .tor_client
93                .connect(tor_addr)
94                .await
95                .map_err(|e| PeerError::Connection(e.into()))?;
96
97            debug!(
98                ?tor_addr_clone,
99                "Successfully connected to `Hostname`or `Ip` `TorAddr`, and established an anonymized `DataStream`"
100            );
101            anonymized_stream
102        };
103
104        let is_tls = match url.scheme() {
105            "wss" => true,
106            "ws" => false,
107            unexpected_scheme => {
108                return Err(PeerError::InvalidEndpoint(anyhow!(
109                    "Unsupported scheme: {unexpected_scheme}"
110                )));
111            }
112        };
113
114        let tls_connector = if is_tls {
115            let webpki_roots = webpki_roots::TLS_SERVER_ROOTS.iter().cloned();
116            let mut root_certs = RootCertStore::empty();
117            root_certs.extend(webpki_roots);
118
119            let tls_config = TlsClientConfig::builder()
120                .with_root_certificates(root_certs)
121                .with_no_client_auth();
122            let tls_connector = TlsConnector::from(Arc::new(tls_config));
123            Some(tls_connector)
124        } else {
125            None
126        };
127
128        let mut ws_client_builder =
129            WsClientBuilder::default().max_concurrent_requests(u16::MAX as usize);
130
131        if let Some(api_secret) = api_secret {
132            // on native platforms, jsonrpsee-client ignores `user:pass@...` in the Url,
133            // but we can set up the headers manually
134
135            let mut headers = HeaderMap::new();
136
137            let auth =
138                base64::engine::general_purpose::STANDARD.encode(format!("fedimint:{api_secret}"));
139
140            headers.insert(
141                "Authorization",
142                HeaderValue::from_str(&format!("Basic {auth}")).expect("Can't fail"),
143            );
144
145            ws_client_builder = ws_client_builder.set_headers(headers);
146        }
147
148        match tls_connector {
149            None => {
150                let client = ws_client_builder
151                    .build_with_stream(url.as_str(), anonymized_stream)
152                    .await
153                    .map_err(|e| PeerError::Connection(e.into()))?;
154
155                Ok(client.into_dyn())
156            }
157            Some(tls_connector) => {
158                let host = url
159                    .host_str()
160                    .map(ToOwned::to_owned)
161                    .ok_or_else(|| PeerError::InvalidEndpoint(anyhow!("Invalid host str")))?;
162
163                // FIXME: (@leonardo) Is this leaking any data ? Should investigate it further
164                // if it's really needed.
165                let server_name = rustls_pki_types::ServerName::try_from(host)
166                    .map_err(|e| PeerError::InvalidEndpoint(e.into()))?;
167
168                let anonymized_tls_stream = tls_connector
169                    .connect(server_name, anonymized_stream)
170                    .await
171                    .map_err(|e| PeerError::Connection(e.into()))?;
172
173                let client = ws_client_builder
174                    .build_with_stream(url.as_str(), anonymized_tls_stream)
175                    .await
176                    .map_err(|e| PeerError::Connection(e.into()))?;
177
178                Ok(client.into_dyn())
179            }
180        }
181    }
182}