libdd_common/connector/
mod.rs1#[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#[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 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 #[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 #[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(); 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 pub(super) fn build_tls_config() -> anyhow::Result<ClientConfig> {
167 use rustls_platform_verifier::BuilderVerifierExt;
168
169 ensure_crypto_provider_initialized(); 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 #[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 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 #[cfg(target_os = "linux")]
248 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 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 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 assert!(
300 status.is_success() || status.is_redirection(),
301 "unexpected status code: {status}"
302 );
303 }
304}