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 std::future::Future;
9use std::pin::Pin;
10use std::sync::LazyLock;
11use std::task::{Context, Poll};
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    pub(super) fn build_https_connector() -> anyhow::Result<
127        hyper_rustls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>,
128    > {
129        ensure_crypto_provider_initialized(); // One-time initialization of a crypto provider if needed
130
131        let certs = load_root_certs()?;
132        let client_config = ClientConfig::builder()
133            .with_root_certificates(certs)
134            .with_no_client_auth();
135        Ok(hyper_rustls::HttpsConnectorBuilder::new()
136            .with_tls_config(client_config)
137            .https_or_http()
138            .enable_http1()
139            .build())
140    }
141
142    #[cfg(not(feature = "use_webpki_roots"))]
143    fn load_root_certs() -> anyhow::Result<rustls::RootCertStore> {
144        use super::errors;
145
146        let mut roots = rustls::RootCertStore::empty();
147
148        let cert_result = rustls_native_certs::load_native_certs();
149        if cert_result.certs.is_empty() {
150            if let Some(err) = cert_result.errors.into_iter().next() {
151                return Err(err.into());
152            }
153        }
154        // TODO(paullgdfc): log errors even if there are valid certs, instead of ignoring them
155
156        for cert in cert_result.certs {
157            //TODO: log when invalid cert is loaded
158            roots.add(cert).ok();
159        }
160        if roots.is_empty() {
161            return Err(errors::Error::NoValidCertifacteRootsFound.into());
162        }
163        Ok(roots)
164    }
165}
166
167impl tower_service::Service<hyper::Uri> for Connector {
168    type Response = ConnStream;
169    type Error = ConnStreamError;
170
171    // This lint gets lifted in this place in a newer version, see:
172    // https://github.com/rust-lang/rust-clippy/pull/8030
173    #[allow(clippy::type_complexity)]
174    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
175
176    fn call(&mut self, uri: hyper::Uri) -> Self::Future {
177        match uri.scheme_str() {
178            Some("unix") => conn_stream::ConnStream::from_uds_uri(uri).boxed(),
179            Some("windows") => conn_stream::ConnStream::from_named_pipe_uri(uri).boxed(),
180            Some("https") => self.build_conn_stream(uri, true),
181            _ => self.build_conn_stream(uri, false),
182        }
183    }
184
185    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
186        match self {
187            Connector::Http(c) => c.poll_ready(cx).map_err(|e| e.into()),
188            #[cfg(feature = "tls-core")]
189            Connector::Https(c) => c.poll_ready(cx),
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use crate::http_common;
197    use std::env;
198
199    use super::*;
200
201    #[test]
202    #[cfg_attr(miri, ignore)]
203    #[cfg(not(feature = "use_webpki_roots"))]
204    /// Verify that the Connector type implements the correct bound Connect + Clone
205    /// to be able to use the hyper::Client
206    fn test_hyper_client_from_connector() {
207        let _ = http_common::new_default_client();
208    }
209
210    #[test]
211    #[cfg_attr(miri, ignore)]
212    #[cfg(feature = "use_webpki_roots")]
213    fn test_hyper_client_from_connector_with_webpki_roots() {
214        let _ = http_common::new_default_client();
215    }
216
217    #[test]
218    #[cfg_attr(miri, ignore)]
219    #[cfg(not(feature = "use_webpki_roots"))]
220    /// Verify that Connector falls back to Http when native root certificates
221    /// are not available and webpki roots are not enabled.
222    fn test_missing_root_certificates_only_allow_http_connections() {
223        const ENV_SSL_CERT_FILE: &str = "SSL_CERT_FILE";
224        const ENV_SSL_CERT_DIR: &str = "SSL_CERT_DIR";
225        let old_value = env::var(ENV_SSL_CERT_FILE).unwrap_or_default();
226        let old_dir_value = env::var(ENV_SSL_CERT_DIR).unwrap_or_default();
227
228        env::set_var(ENV_SSL_CERT_FILE, "this/folder/does/not/exist");
229        env::set_var(ENV_SSL_CERT_DIR, "this/folder/does/not/exist");
230        let connector = Connector::new();
231
232        assert!(matches!(connector, Connector::Http(_)));
233
234        env::set_var(ENV_SSL_CERT_FILE, old_value);
235        env::set_var(ENV_SSL_CERT_DIR, old_dir_value);
236    }
237
238    #[test]
239    #[cfg_attr(miri, ignore)]
240    #[cfg(feature = "use_webpki_roots")]
241    #[cfg(feature = "tls-core")]
242    /// Verify that Connector builds an Https connector using webpki certificates
243    /// even when native root certificates are not available.
244    fn test_missing_root_certificates_use_webpki_certificates() {
245        const ENV_SSL_CERT_FILE: &str = "SSL_CERT_FILE";
246        let old_value = env::var(ENV_SSL_CERT_FILE).unwrap_or_default();
247
248        env::set_var(ENV_SSL_CERT_FILE, "this/folder/does/not/exist");
249        let connector = Connector::new();
250        assert!(matches!(connector, Connector::Https(_)));
251
252        env::set_var(ENV_SSL_CERT_FILE, old_value);
253    }
254}