libdd_common/connector/
mod.rs1use 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 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 #[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 #[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(); 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<
130 hyper_rustls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>,
131 > {
132 use rustls_platform_verifier::BuilderVerifierExt;
133
134 ensure_crypto_provider_initialized(); 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 #[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 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 #[cfg(target_os = "linux")]
205 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 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 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 assert!(
257 status.is_success() || status.is_redirection(),
258 "unexpected status code: {status}"
259 );
260 }
261}