Skip to main content

fedimint_connectors/
tor.rs

1#[cfg(all(feature = "tor", not(target_family = "wasm")))]
2use std::fmt;
3use std::sync::Arc;
4
5use arti_client::{TorAddr, TorClient, TorClientConfig};
6use async_trait::async_trait;
7use base64::Engine as _;
8use fedimint_core::util::{FmtCompact as _, SafeUrl};
9use jsonrpsee_ws_client::{HeaderMap, HeaderValue, WsClientBuilder};
10use tokio_rustls::TlsConnector;
11use tokio_rustls::rustls::{ClientConfig as TlsClientConfig, RootCertStore};
12use tracing::debug;
13
14use super::{Connector, DynGuaridianConnection};
15use crate::error::ConnectorError;
16use crate::{Connectivity, DynGatewayConnection, IGuardianConnection as _, ServerError};
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() -> Result<Self, ConnectorError> {
31        use tracing::debug;
32
33        let tor_config = TorClientConfig::default();
34        let tor_client = TorClient::create_bootstrapped(tor_config)
35            .await
36            .map_err(|err| ConnectorError::Tor(Box::new(err)))?
37            .isolated_client();
38
39        debug!("Successfully created and bootstrapped the `TorClient`, for given `TorConfig`.");
40
41        Ok(Self { tor_client })
42    }
43}
44
45#[async_trait]
46impl Connector for TorConnector {
47    #[allow(clippy::too_many_lines)]
48    async fn connect_guardian(
49        &self,
50        url: &SafeUrl,
51        api_secret: Option<&str>,
52    ) -> super::ServerResult<DynGuaridianConnection> {
53        let addr = (
54            url.host_str()
55                .ok_or_else(|| ServerError::InvalidEndpoint("Expected host str".into()))?,
56            url.port_or_known_default()
57                .ok_or_else(|| ServerError::InvalidEndpoint("Expected port number".into()))?,
58        );
59        let tor_addr = TorAddr::from(addr).map_err(|e| {
60            ServerError::InvalidEndpoint(
61                format!("Invalid endpoint addr: {addr:?}: {}", e.fmt_compact()).into(),
62            )
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| ServerError::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| ServerError::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(ServerError::InvalidEndpoint(
109                    format!("Unsupported scheme: {unexpected_scheme}").into(),
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| ServerError::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(|| ServerError::InvalidEndpoint("Invalid host str".into()))?;
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| ServerError::InvalidEndpoint(e.into()))?;
167
168                let anonymized_tls_stream = tls_connector
169                    .connect(server_name, anonymized_stream)
170                    .await
171                    .map_err(|e| ServerError::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| ServerError::Connection(e.into()))?;
177
178                Ok(client.into_dyn())
179            }
180        }
181    }
182
183    async fn connect_gateway(
184        &self,
185        _url: &SafeUrl,
186    ) -> Result<DynGatewayConnection, ConnectorError> {
187        Err(ConnectorError::GatewayUnsupported)
188    }
189
190    fn connectivity(&self, _url: &SafeUrl) -> Connectivity {
191        Connectivity::Tor
192    }
193}