Skip to main content

iota_sdk_graphql_client/
tls.rs

1// Copyright (c) 2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Client construction for the constructors that build their own
5//! [`reqwest::Client`].
6//!
7//! The crypto provider and the trust anchors are both chosen by feature.
8//! `tls-ring` and `tls-aws-lc` are what turn on `reqwest`'s own TLS, and they
9//! turn it on with `rustls-no-provider` so that neither choice is made on our
10//! behalf. Without either of them `reqwest` is built with no TLS at all, so an
11//! HTTP-only build needs no provider and reaches a plain-HTTP endpoint as-is.
12
13/// A [`reqwest::ClientBuilder`] carrying this crate's user agent and trust
14/// anchors, and a connect timeout of
15/// [`DEFAULT_CONNECT_TIMEOUT`](crate::client::DEFAULT_CONNECT_TIMEOUT).
16///
17/// With the default features the bundled Mozilla roots are merged into the
18/// platform trust store rather than replacing it, so corporate CAs and
19/// user-installed certificates keep working while the bundled set acts as a
20/// floor. That floor matters because reqwest builds its certificate verifier
21/// eagerly, before it knows whether a request will use TLS at all: on Linux the
22/// platform verifier refuses to build when the system store is empty, which
23/// otherwise breaks plain-HTTP use on images without `ca-certificates`. A build
24/// with no provider feature has no verifier to build and so is unaffected.
25///
26/// Note that merging is a union, not a fallback. A CA the platform has
27/// deliberately distrusted is still accepted if the bundled set carries it.
28/// Build with only `tls-webpki-roots`, or only `tls-native-roots`, to get one
29/// set alone.
30///
31/// Android is the exception: reqwest cannot merge roots there and rejects the
32/// attempt, and its platform verifier aborts the process unless the hosting
33/// application performs a JNI handshake the SDK cannot do on its behalf. The
34/// bundled roots are used on their own instead, whenever they are available.
35#[cfg(not(target_arch = "wasm32"))]
36pub(crate) fn default_http_client_builder() -> reqwest::ClientBuilder {
37    install_default_crypto_provider();
38    apply_roots(
39        reqwest::Client::builder()
40            .user_agent(crate::client::USER_AGENT)
41            .connect_timeout(crate::client::DEFAULT_CONNECT_TIMEOUT),
42    )
43}
44
45/// A [`reqwest::ClientBuilder`] carrying this crate's user agent.
46///
47/// On wasm32 the browser owns certificate verification, so there are no trust
48/// anchors and no crypto provider to configure.
49#[cfg(target_arch = "wasm32")]
50pub(crate) fn default_http_client_builder() -> reqwest::ClientBuilder {
51    reqwest::Client::builder().user_agent(crate::client::USER_AGENT)
52}
53
54/// Select this crate's rustls crypto provider for the process, if nothing has
55/// chosen one already.
56///
57/// A provider feature builds `reqwest` with `rustls-no-provider`, so a provider
58/// has to be installed before any client is built. The first caller wins, so an
59/// application that has already chosen one keeps it.
60///
61/// Does nothing when neither `tls-ring` nor `tls-aws-lc` is enabled.
62#[cfg(not(target_arch = "wasm32"))]
63fn install_default_crypto_provider() {
64    // `install_default` returns `Err` when a provider is already installed,
65    // which is the application's prerogative rather than our problem.
66    #[cfg(feature = "tls-ring")]
67    let _ = rustls::crypto::ring::default_provider().install_default();
68
69    #[cfg(all(feature = "tls-aws-lc", not(feature = "tls-ring")))]
70    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
71}
72
73/// Trust the bundled roots, merged into the platform store where that is
74/// supported. Mirrors the platforms on which reqwest routes extra roots to
75/// `rustls_platform_verifier::Verifier::new_with_extra_roots`; anywhere else it
76/// returns a builder error, so the bundled set is used alone.
77///
78/// Needs a provider feature as well as `tls-webpki-roots`:
79/// `reqwest::Certificate` does not exist without reqwest's TLS.
80#[cfg(all(
81    not(target_arch = "wasm32"),
82    feature = "tls-webpki-roots",
83    any(feature = "tls-ring", feature = "tls-aws-lc")
84))]
85fn apply_roots(builder: reqwest::ClientBuilder) -> reqwest::ClientBuilder {
86    let roots = webpki_root_certs::TLS_SERVER_ROOT_CERTS
87        .iter()
88        .filter_map(|cert| reqwest::Certificate::from_der(cert).ok());
89
90    #[cfg(all(
91        feature = "tls-native-roots",
92        any(all(unix, not(target_os = "android")), target_os = "windows")
93    ))]
94    {
95        builder.tls_certs_merge(roots)
96    }
97    #[cfg(not(all(
98        feature = "tls-native-roots",
99        any(all(unix, not(target_os = "android")), target_os = "windows")
100    )))]
101    {
102        builder.tls_certs_only(roots)
103    }
104}
105
106/// Without `tls-webpki-roots` there is nothing to add, so verification is left
107/// to reqwest and the platform trust store. Also taken when no provider
108/// feature is enabled, where there is no TLS to configure and the bundled roots
109/// are compiled in but unused.
110#[cfg(all(
111    not(target_arch = "wasm32"),
112    not(all(
113        feature = "tls-webpki-roots",
114        any(feature = "tls-ring", feature = "tls-aws-lc")
115    ))
116))]
117fn apply_roots(builder: reqwest::ClientBuilder) -> reqwest::ClientBuilder {
118    builder
119}
120
121/// The scheme of `server` when this build cannot reach it, or `None` when it
122/// can.
123///
124/// Without a provider feature `reqwest` is built with no TLS, so an `https` or
125/// `wss` URL fails at request time with a transport error that names neither
126/// TLS nor the feature that would enable it. Reporting it from the constructor
127/// puts the cause where the caller can act on it.
128///
129/// A URL that does not parse is left for the caller's own parse to report.
130#[cfg(all(
131    not(target_arch = "wasm32"),
132    not(any(feature = "tls-ring", feature = "tls-aws-lc"))
133))]
134pub(crate) fn unsupported_scheme(server: &str) -> Option<String> {
135    let scheme = reqwest::Url::parse(server).ok()?.scheme().to_owned();
136    matches!(scheme.as_str(), "https" | "wss").then_some(scheme)
137}
138
139/// Every scheme this crate accepts is reachable once TLS is compiled in, and on
140/// wasm32 the browser provides it.
141#[cfg(any(target_arch = "wasm32", feature = "tls-ring", feature = "tls-aws-lc"))]
142pub(crate) fn unsupported_scheme(_server: &str) -> Option<String> {
143    None
144}
145
146#[cfg(all(test, not(target_arch = "wasm32")))]
147mod tests {
148    use super::unsupported_scheme;
149
150    #[test]
151    fn plain_schemes_need_no_tls() {
152        assert_eq!(unsupported_scheme("http://127.0.0.1:9125"), None);
153        assert_eq!(unsupported_scheme("ws://127.0.0.1:9125"), None);
154    }
155
156    #[test]
157    fn unparsable_url_is_left_to_the_caller() {
158        assert_eq!(unsupported_scheme("not a url"), None);
159    }
160
161    #[cfg(all(
162        not(target_arch = "wasm32"),
163        not(any(feature = "tls-ring", feature = "tls-aws-lc"))
164    ))]
165    #[test]
166    fn tls_schemes_are_rejected_without_a_provider() {
167        assert_eq!(
168            unsupported_scheme("https://graphql.testnet.iota.cafe").as_deref(),
169            Some("https")
170        );
171        assert_eq!(
172            unsupported_scheme("wss://graphql.testnet.iota.cafe").as_deref(),
173            Some("wss")
174        );
175    }
176
177    #[cfg(any(target_arch = "wasm32", feature = "tls-ring", feature = "tls-aws-lc"))]
178    #[test]
179    fn tls_schemes_are_accepted_with_a_provider() {
180        assert_eq!(
181            unsupported_scheme("https://graphql.testnet.iota.cafe"),
182            None
183        );
184        assert_eq!(unsupported_scheme("wss://graphql.testnet.iota.cafe"), None);
185    }
186}