xmtp_common/http.rs
1//! Construction of the HTTP clients libxmtp uses for plain HTTP(S) endpoints.
2//!
3//! gRPC traffic does not go through here — it is configured in `xmtp_api_grpc`.
4
5/// Build a [`reqwest::Client`] for talking to XMTP HTTP endpoints.
6///
7/// Prefer this over `reqwest::Client::new()` anywhere in the workspace: on Android a
8/// default-built client aborts the process the first time it opens a TLS connection.
9/// reqwest 0.13 reaches for `rustls-platform-verifier` whenever no explicit roots are
10/// configured, and that verifier calls into the JVM on Android. It panics with
11/// `Expect rustls-platform-verifier to be initialized` unless the *host application*
12/// initializes it over JNI with a `Context` and also ships the crate's Kotlin component.
13/// libxmtp is consumed as a plain `.so` through uniffi, so that initialization never
14/// happens, and an HTTP request could take the whole app down on its first TLS connection.
15///
16/// So on Android we hand reqwest a rustls config built from the bundled webpki roots and
17/// the platform verifier is never constructed. This mirrors `xmtp_api_grpc`, which
18/// already forces webpki roots for the gRPC channel on Android and iOS.
19///
20/// Other platforms keep reqwest's default behaviour: the platform verifier works there
21/// without any initialization, and it honours user- and enterprise-installed CAs.
22pub fn client() -> Result<reqwest::Client, reqwest::Error> {
23 client_builder().build()
24}
25
26/// The same configuration as [`client`], for callers that need to set timeouts or other
27/// options before building. Note that on Android the TLS setup is already fixed here, so
28/// reqwest's own TLS options (extra roots, `danger_accept_invalid_certs`, ...) are ignored.
29// The one place in the workspace allowed to construct a reqwest client directly; `.clippy.toml`
30// disallows it everywhere else so the Android TLS setup below cannot be bypassed.
31#[allow(clippy::disallowed_methods)]
32pub fn client_builder() -> reqwest::ClientBuilder {
33 let builder = reqwest::Client::builder();
34 #[cfg(target_os = "android")]
35 let builder = builder.use_preconfigured_tls(bundled_roots_tls_config());
36 builder
37}
38
39/// A rustls config that trusts the webpki (Mozilla) root store compiled into the binary,
40/// with no dependency on the platform trust store or on any runtime initialization.
41///
42/// Compiled outside Android as well so the test below can exercise it in CI.
43#[cfg(all(not(target_family = "wasm"), any(target_os = "android", test)))]
44fn bundled_roots_tls_config() -> rustls::ClientConfig {
45 // `ClientConfig::builder` resolves the process-default crypto provider. Installing it
46 // first means that lookup always finds one instead of falling back to rustls'
47 // crate-feature path, which panics when zero or several provider features are enabled.
48 xmtp_cryptography::install_crypto_provider();
49
50 let roots = rustls::RootCertStore {
51 roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
52 };
53 let mut config = rustls::ClientConfig::builder()
54 .with_root_certificates(roots)
55 .with_no_client_auth();
56
57 // reqwest applies its ALPN preferences only to configs it builds itself; a
58 // preconfigured config is used verbatim. The workspace builds reqwest without its
59 // `http2` feature, so http/1.1 is the only protocol the connector can speak — offering
60 // `h2` here would let a server negotiate a protocol the client cannot follow.
61 config.alpn_protocols = vec![b"http/1.1".to_vec()];
62 config
63}
64
65#[cfg(all(test, not(target_family = "wasm")))]
66mod tests {
67 use super::*;
68
69 /// `use_preconfigured_tls` takes an `Any`: when the rustls version we build the config
70 /// with does not match the one reqwest was compiled against, reqwest silently records an
71 /// "unknown" TLS backend and only errors at `build()`. That would be an Android-only
72 /// failure, so pin it down here instead.
73 #[xmtp_common::test(unwrap_try = true)]
74 #[allow(clippy::disallowed_methods)]
75 fn bundled_roots_config_is_accepted_by_reqwest() {
76 let config = bundled_roots_tls_config();
77 assert!(!config.alpn_protocols.is_empty());
78
79 reqwest::Client::builder()
80 .use_preconfigured_tls(config)
81 .build()
82 .expect("reqwest did not recognize our preconfigured rustls config");
83 }
84}