Skip to main content

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}