Skip to main content

libdd_common/connector/
mod.rs

1// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4#[cfg(feature = "http-client")]
5use futures::future::BoxFuture;
6#[cfg(feature = "http-client")]
7use futures::{future, FutureExt};
8#[cfg(feature = "http-client")]
9use hyper_util::client::legacy::connect;
10
11#[cfg(feature = "http-client")]
12use core::future::Future;
13#[cfg(feature = "http-client")]
14use core::pin::Pin;
15#[cfg(feature = "http-client")]
16use core::task::{Context, Poll};
17#[cfg(feature = "http-client")]
18use std::sync::LazyLock;
19
20#[cfg(unix)]
21pub mod uds;
22
23pub mod named_pipe;
24
25pub mod errors;
26
27#[cfg(feature = "http-client")]
28mod conn_stream;
29#[cfg(feature = "http-client")]
30use conn_stream::{ConnStream, ConnStreamError};
31
32#[cfg(feature = "hyper-proxy")]
33mod proxy;
34
35#[cfg(feature = "http-client")]
36#[derive(Clone)]
37// `proxy::HttpProxyConnector` is crate internal, and the field anyway not pub.
38#[allow(private_interfaces)]
39pub enum Connector {
40    Http(connect::HttpConnector),
41    #[cfg(feature = "tls-core")]
42    Https(hyper_rustls::HttpsConnector<connect::HttpConnector>),
43    #[cfg(feature = "hyper-proxy")]
44    Proxy(Box<proxy::HttpProxyConnector>),
45}
46
47#[cfg(feature = "http-client")]
48static DEFAULT_CONNECTOR: LazyLock<Connector> = LazyLock::new(Connector::new);
49
50#[cfg(feature = "http-client")]
51impl Default for Connector {
52    fn default() -> Self {
53        DEFAULT_CONNECTOR.clone()
54    }
55}
56
57#[cfg(feature = "http-client")]
58impl Connector {
59    /// Make sure this function is not called frequently. Fetching the root certificates is an
60    /// expensive operation. Access the globally cached connector via Connector::default().
61    fn new() -> Self {
62        #[cfg(feature = "hyper-proxy")]
63        {
64            Connector::Proxy(Box::new(proxy::HttpProxyConnector::new(
65                Self::new_no_proxy(),
66            )))
67        }
68        #[cfg(not(feature = "hyper-proxy"))]
69        {
70            Self::new_no_proxy()
71        }
72    }
73
74    pub(super) fn new_no_proxy() -> Self {
75        #[cfg(feature = "tls-core")]
76        {
77            match https::build_https_connector() {
78                Ok(connector) => Connector::Https(connector),
79                Err(_) => Connector::Http(connect::HttpConnector::new()),
80            }
81        }
82        #[cfg(not(feature = "tls-core"))]
83        {
84            Connector::Http(connect::HttpConnector::new())
85        }
86    }
87
88    fn build_conn_stream(
89        &mut self,
90        uri: hyper::Uri,
91        require_tls: bool,
92    ) -> BoxFuture<'static, Result<ConnStream, ConnStreamError>> {
93        match self {
94            Self::Http(c) => {
95                if require_tls {
96                    future::err::<ConnStream, ConnStreamError>(
97                        errors::Error::CannotEstablishTlsConnection.into(),
98                    )
99                    .boxed()
100                } else {
101                    ConnStream::from_http_connector_with_uri(c, uri).boxed()
102                }
103            }
104            #[cfg(feature = "tls-core")]
105            Self::Https(c) => {
106                ConnStream::from_https_connector_with_uri(c, uri, require_tls).boxed()
107            }
108            #[cfg(feature = "hyper-proxy")]
109            Self::Proxy(p) => p.build_conn_stream(uri, require_tls),
110        }
111    }
112}
113
114#[cfg(feature = "tls-core")]
115mod https {
116    #[cfg(feature = "use_webpki_roots")]
117    use hyper_rustls::ConfigBuilderExt;
118
119    use rustls::ClientConfig;
120
121    /// Ensures the rustls default CryptoProvider is installed (ring for non-FIPS).
122    /// In FIPS mode, the caller must install the FIPS provider before any TLS use.
123    #[cfg(feature = "https")]
124    fn ensure_crypto_provider_initialized() {
125        use std::sync::Once;
126
127        static INIT_CRYPTO_PROVIDER: Once = Once::new();
128
129        INIT_CRYPTO_PROVIDER.call_once(|| {
130            let _ = rustls::crypto::ring::default_provider().install_default();
131        });
132    }
133
134    /// In FIPS mode, the caller must install the FIPS-compliant crypto provider
135    /// (e.g., aws-lc-rs FIPS) before any TLS connections are established.
136    #[cfg(not(feature = "https"))]
137    fn ensure_crypto_provider_initialized() {}
138
139    #[cfg(any(feature = "https", feature = "fips"))]
140    fn require_crypto_provider() -> anyhow::Result<()> {
141        Ok(())
142    }
143
144    #[cfg(not(any(feature = "https", feature = "fips")))]
145    fn require_crypto_provider() -> anyhow::Result<()> {
146        if rustls::crypto::CryptoProvider::get_default().is_none() {
147            anyhow::bail!("no rustls CryptoProvider installed");
148        }
149        Ok(())
150    }
151
152    #[cfg(feature = "use_webpki_roots")]
153    pub(super) fn build_tls_config() -> anyhow::Result<ClientConfig> {
154        ensure_crypto_provider_initialized(); // One-time initialization of a crypto provider if needed
155        require_crypto_provider()?;
156
157        Ok(ClientConfig::builder()
158            .with_webpki_roots()
159            .with_no_client_auth())
160    }
161
162    #[cfg(not(feature = "use_webpki_roots"))]
163    /// Builds the client TLS config using the system trust roots.
164    /// `SSL_CERT_FILE` and `SSL_CERT_DIR` variable are only supported on linux, see
165    /// `rustls_platform_verifier` doc for details.
166    pub(super) fn build_tls_config() -> anyhow::Result<ClientConfig> {
167        use rustls_platform_verifier::BuilderVerifierExt;
168
169        ensure_crypto_provider_initialized(); // One-time initialization of a crypto provider if needed
170        require_crypto_provider()?;
171
172        Ok(ClientConfig::builder()
173            .with_platform_verifier()?
174            .with_no_client_auth())
175    }
176
177    pub(super) fn build_https_connector() -> anyhow::Result<
178        hyper_rustls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>,
179    > {
180        Ok(hyper_rustls::HttpsConnectorBuilder::new()
181            .with_tls_config(build_tls_config()?)
182            .https_or_http()
183            .enable_http1()
184            .build())
185    }
186}
187
188#[cfg(feature = "http-client")]
189impl tower_service::Service<hyper::Uri> for Connector {
190    type Response = ConnStream;
191    type Error = ConnStreamError;
192
193    // This lint gets lifted in this place in a newer version, see:
194    // https://github.com/rust-lang/rust-clippy/pull/8030
195    #[allow(clippy::type_complexity)]
196    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
197
198    fn call(&mut self, uri: hyper::Uri) -> Self::Future {
199        match uri.scheme_str() {
200            Some("unix") => conn_stream::ConnStream::from_uds_uri(uri).boxed(),
201            Some("windows") => conn_stream::ConnStream::from_named_pipe_uri(uri).boxed(),
202            Some("https") => self.build_conn_stream(uri, true),
203            _ => self.build_conn_stream(uri, false),
204        }
205    }
206
207    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
208        match self {
209            Connector::Http(c) => c.poll_ready(cx).map_err(|e| e.into()),
210            #[cfg(feature = "tls-core")]
211            Connector::Https(c) => c.poll_ready(cx),
212            #[cfg(feature = "hyper-proxy")]
213            Connector::Proxy(p) => p.poll_ready(cx),
214        }
215    }
216}
217
218#[cfg(all(test, feature = "http-client"))]
219mod tests {
220    use crate::http_common;
221    #[cfg(any(feature = "use_webpki_roots", target_os = "linux"))]
222    use {super::*, std::env};
223    #[cfg(feature = "tls-core")]
224    use {crate::http_common::Body, hyper::Request};
225
226    #[test]
227    #[cfg_attr(miri, ignore)]
228    #[cfg(not(feature = "use_webpki_roots"))]
229    /// Verify that the Connector type implements the correct bound Connect + Clone
230    /// to be able to use the hyper::Client
231    fn test_hyper_client_from_connector() {
232        let _ = http_common::new_default_client();
233    }
234
235    #[test]
236    #[cfg_attr(miri, ignore)]
237    #[cfg(feature = "use_webpki_roots")]
238    fn test_hyper_client_from_connector_with_webpki_roots() {
239        let _ = http_common::new_default_client();
240    }
241
242    #[test]
243    #[cfg_attr(miri, ignore)]
244    #[cfg(not(feature = "use_webpki_roots"))]
245    // Only Linux eagerly loads roots at connector construction; macOS/Windows verify lazily
246    // during the TLS handshake, so SSL_CERT_FILE/SSL_CERT_DIR cannot be exercised there.
247    #[cfg(target_os = "linux")]
248    /// Verify that Connector falls back to Http when native root certificates
249    /// are not available and webpki roots are not enabled.
250    fn test_missing_root_certificates_only_allow_http_connections() {
251        const ENV_SSL_CERT_FILE: &str = "SSL_CERT_FILE";
252        const ENV_SSL_CERT_DIR: &str = "SSL_CERT_DIR";
253        let old_value = env::var(ENV_SSL_CERT_FILE).unwrap_or_default();
254        let old_dir_value = env::var(ENV_SSL_CERT_DIR).unwrap_or_default();
255
256        env::set_var(ENV_SSL_CERT_FILE, "this/folder/does/not/exist");
257        env::set_var(ENV_SSL_CERT_DIR, "this/folder/does/not/exist");
258        let connector = Connector::new_no_proxy();
259
260        assert!(matches!(connector, Connector::Http(_)));
261
262        env::set_var(ENV_SSL_CERT_FILE, old_value);
263        env::set_var(ENV_SSL_CERT_DIR, old_dir_value);
264    }
265
266    #[test]
267    #[cfg_attr(miri, ignore)]
268    #[cfg(feature = "use_webpki_roots")]
269    #[cfg(feature = "tls-core")]
270    /// Verify that Connector builds an Https connector using webpki certificates
271    /// even when native root certificates are not available.
272    fn test_missing_root_certificates_use_webpki_certificates() {
273        const ENV_SSL_CERT_FILE: &str = "SSL_CERT_FILE";
274        let old_value = env::var(ENV_SSL_CERT_FILE).unwrap_or_default();
275
276        env::set_var(ENV_SSL_CERT_FILE, "this/folder/does/not/exist");
277        let connector = Connector::new_no_proxy();
278        assert!(matches!(connector, Connector::Https(_)));
279
280        env::set_var(ENV_SSL_CERT_FILE, old_value);
281    }
282
283    #[tokio::test]
284    #[cfg_attr(miri, ignore)]
285    #[cfg(feature = "tls-core")]
286    /// Verify that a HTTPS GET request succeeds using
287    /// the default Connector (native platform TLS verifier or webpki roots).
288    async fn test_https_request_succeeds() {
289        let client = http_common::new_default_client();
290        let request = Request::get("https://www.datadoghq.com")
291            .body(Body::empty())
292            .expect("failed to build request");
293        let response = client
294            .request(request)
295            .await
296            .expect("HTTPS request to datadoghq.com failed");
297        let status = response.status();
298        // Accept any successful (2xx) or redirect (3xx) response.
299        assert!(
300            status.is_success() || status.is_redirection(),
301            "unexpected status code: {status}"
302        );
303    }
304}