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