saluki_io/net/client/http/
client.rs

1#[cfg(unix)]
2use std::path::PathBuf;
3use std::{
4    future::Future,
5    pin::Pin,
6    task::{Context, Poll},
7    time::Duration,
8};
9
10use bytes::{Buf, Bytes};
11use http::{Request, Response, Uri};
12use http_body::{Body, Frame, SizeHint};
13use http_body_util::combinators::BoxBody;
14use hyper::body::Incoming;
15use hyper_http_proxy::Proxy;
16use hyper_util::{
17    client::legacy::{connect::capture_connection, Builder},
18    rt::{TokioExecutor, TokioTimer},
19};
20use pin_project::pin_project;
21use rustls::ClientConfig;
22use saluki_error::GenericError;
23use saluki_metrics::MetricsBuilder;
24use saluki_tls::{ensure_client_config_fips_compliant, ClientTLSConfigBuilder, TlsMinimumVersion};
25use stringtheory::MetaString;
26use tower::{timeout::TimeoutLayer, util::BoxCloneService, BoxError, Service, ServiceBuilder, ServiceExt as _};
27
28use super::{
29    conn::{check_connection_state, HttpProtocol, HttpsCapableConnectorBuilder},
30    telemetry::HttpTransactionErrorTelemetry,
31    EndpointTelemetryLayer,
32};
33
34/// The type-erased body type used internally by [`HttpClient`].
35///
36/// All request bodies are converted to this type before being sent over the wire, which ensures a single
37/// monomorphization of the underlying HTTP/2 and TLS stacks regardless of the caller's body type.
38pub type ClientBody = BoxBody<Bytes, Box<dyn std::error::Error + Send + Sync>>;
39
40#[pin_project]
41struct ClientBodyAdapter<B> {
42    #[pin]
43    inner: B,
44    size_hint: SizeHint,
45}
46
47impl<B> Body for ClientBodyAdapter<B>
48where
49    B: Body,
50    B::Data: Buf,
51    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
52{
53    type Data = Bytes;
54    type Error = Box<dyn std::error::Error + Send + Sync>;
55
56    fn poll_frame(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
57        self.project().inner.poll_frame(cx).map(|maybe_frame| {
58            maybe_frame.map(|result| {
59                result
60                    .map(|frame| frame.map_data(|mut data| data.copy_to_bytes(data.remaining())))
61                    .map_err(Into::into)
62            })
63        })
64    }
65
66    fn size_hint(&self) -> SizeHint {
67        self.size_hint
68    }
69}
70
71/// An HTTP client.
72#[derive(Clone)]
73pub struct HttpClient {
74    inner: BoxCloneService<Request<ClientBody>, Response<Incoming>, BoxError>,
75}
76
77impl HttpClient {
78    /// Creates a new builder for configuring an HTTP client.
79    pub fn builder() -> HttpClientBuilder {
80        HttpClientBuilder::default()
81    }
82
83    /// Sends a request to the server, and waits for a response.
84    ///
85    /// The request body is type-erased internally, so callers can use any body type that implements
86    /// [`Body`] with `Data` types that implement [`Buf`].
87    ///
88    /// # Errors
89    ///
90    /// If there was an error sending the request, an error will be returned.
91    pub async fn send<B>(&mut self, req: Request<B>) -> Result<Response<Incoming>, GenericError>
92    where
93        B: Body + Send + Sync + 'static,
94        B::Data: Buf + Send,
95        B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
96    {
97        let mut req = req.map(into_client_body);
98        let captured_conn = capture_connection(&mut req);
99        let result = self
100            .inner
101            .ready()
102            .await
103            .map_err(GenericError::from_boxed)?
104            .call(req)
105            .await;
106
107        check_connection_state(captured_conn);
108
109        result.map_err(GenericError::from_boxed)
110    }
111}
112
113impl Service<Request<ClientBody>> for HttpClient {
114    type Response = Response<Incoming>;
115    type Error = BoxError;
116    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
117
118    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
119        self.inner.poll_ready(cx)
120    }
121
122    fn call(&mut self, mut req: Request<ClientBody>) -> Self::Future {
123        let captured_conn = capture_connection(&mut req);
124        let fut = self.inner.call(req);
125
126        Box::pin(async move {
127            let result = fut.await;
128
129            check_connection_state(captured_conn);
130
131            result
132        })
133    }
134}
135
136/// Converts an arbitrary body into the type-erased [`ClientBody`].
137///
138/// This uses `Buf::copy_to_bytes` for the data conversion, which is zero-copy when the underlying
139/// data is already `Bytes`.
140pub fn into_client_body<B>(body: B) -> ClientBody
141where
142    B: Body + Send + Sync + 'static,
143    B::Data: Buf + Send,
144    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
145{
146    let size_hint = body.size_hint();
147    BoxBody::new(ClientBodyAdapter { inner: body, size_hint })
148}
149
150/// An HTTP client builder.
151///
152/// Provides an ergonomic builder API for configuring an HTTP client.
153///
154/// # Defaults
155///
156/// A number of sensible defaults are provided:
157///
158/// - support for both HTTP and HTTPS (uses platform's root certificates for server certificate validation)
159/// - support for both HTTP/1.1 and HTTP/2 (automatically negotiated via ALPN)
160/// - non-infinite timeouts for various stages of the request lifecycle (30 second connect timeout, 60 second per-request timeout)
161/// - connection pool for reusing connections (45 second idle connection timeout, and a maximum of 5 idle connections
162///   per host)
163/// - support for FIPS-compliant cryptography if the `fips` feature is enabled in the `saluki-tls` crate
164pub struct HttpClientBuilder {
165    connector_builder: HttpsCapableConnectorBuilder,
166    hyper_builder: Builder,
167    tls_builder: ClientTLSConfigBuilder,
168    client_tls_config: Option<ClientConfig>,
169    request_timeout: Option<Duration>,
170    endpoint_telemetry: Option<EndpointTelemetryLayer>,
171    proxies: Option<Vec<Proxy>>,
172}
173
174impl HttpClientBuilder {
175    /// Sets the timeout when connecting to the remote host.
176    ///
177    /// Defaults to 30 seconds.
178    pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
179        self.connector_builder = self.connector_builder.with_connect_timeout(timeout);
180        self
181    }
182
183    /// Sets the timeout for completing the TLS handshake after a connection is established.
184    ///
185    /// This bounds only the TLS handshake step, distinct from the connect timeout, which bounds the underlying TCP
186    /// (or other transport) connection setup that precedes it.
187    ///
188    /// Defaults to 10 seconds.
189    pub fn with_tls_handshake_timeout(mut self, timeout: Duration) -> Self {
190        self.connector_builder = self.connector_builder.with_tls_handshake_timeout(timeout);
191        self
192    }
193
194    /// Sets the per-request timeout.
195    ///
196    /// The request timeout applies to each individual request made to the remote host, including each request made when
197    /// retrying a failed request.
198    ///
199    /// Defaults to 20 seconds.
200    pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
201        self.request_timeout = Some(timeout);
202        self
203    }
204
205    /// Allow requests to run indefinitely.
206    ///
207    /// This means there will be no overall timeout for the request, but the request still may be subject to other
208    /// configuration settings, such as the connect timeout or retry policy.
209    pub fn without_request_timeout(mut self) -> Self {
210        self.request_timeout = None;
211        self
212    }
213
214    /// Sets the HTTP protocol selection for client connections.
215    ///
216    /// Defaults to [`HttpProtocol::Auto`], which automatically negotiates HTTP/2 with HTTP/1.1 fallback.
217    pub fn with_http_protocol(mut self, protocol: HttpProtocol) -> Self {
218        self.connector_builder = self.connector_builder.with_http_protocol(protocol);
219        self
220    }
221
222    /// Sets the maximum age of a connection before it's closed.
223    ///
224    /// This is distinct from the maximum idle time: if any connection's age exceeds `limit`, it will be closed rather
225    /// than being reused and added to the idle connection pool.
226    ///
227    /// Defaults to no limit.
228    pub fn with_connection_age_limit<L>(mut self, limit: L) -> Self
229    where
230        L: Into<Option<Duration>>,
231    {
232        self.connector_builder = self.connector_builder.with_connection_age_limit(limit);
233        self
234    }
235
236    /// Sets the maximum number of idle connections per host.
237    ///
238    /// Defaults to 5.
239    pub fn with_max_idle_conns_per_host(mut self, max: usize) -> Self {
240        self.hyper_builder.pool_max_idle_per_host(max);
241        self
242    }
243
244    /// Sets the idle connection timeout.
245    ///
246    /// Once a connection has been idle in the pool for longer than this duration, it will be closed and removed from
247    /// the pool.
248    ///
249    /// Defaults to 45 seconds.
250    pub fn with_idle_conn_timeout(mut self, timeout: Duration) -> Self {
251        self.hyper_builder.pool_idle_timeout(timeout);
252        self
253    }
254
255    /// Sets the proxies to be used for outgoing requests.
256    ///
257    /// Defaults to no proxies. (i.e requests will be sent directly without using a proxy).
258    pub fn with_proxies(mut self, proxies: Vec<Proxy>) -> Self {
259        self.proxies = Some(proxies);
260        self
261    }
262
263    /// Enables per-endpoint telemetry for HTTP transactions.
264    ///
265    /// See [`EndpointTelemetryLayer`] for more information.
266    pub fn with_endpoint_telemetry<F>(mut self, metrics_builder: MetricsBuilder, endpoint_name_fn: Option<F>) -> Self
267    where
268        F: Fn(&Uri) -> Option<MetaString> + Send + Sync + 'static,
269    {
270        let error_telemetry = HttpTransactionErrorTelemetry::from_builder(&metrics_builder);
271        self.connector_builder = self.connector_builder.with_error_telemetry(error_telemetry.clone());
272
273        let mut layer = EndpointTelemetryLayer::default()
274            .with_metrics_builder(metrics_builder)
275            .with_error_telemetry(error_telemetry);
276
277        if let Some(endpoint_name_fn) = endpoint_name_fn {
278            layer = layer.with_endpoint_name_fn(endpoint_name_fn);
279        }
280
281        self.endpoint_telemetry = Some(layer);
282        self
283    }
284
285    /// Sets a Unix domain socket path to route all connections through.
286    ///
287    /// When set, the client will connect to this Unix socket instead of performing DNS resolution
288    /// and TCP connection. The URI host is ignored—all requests are sent through the configured
289    /// socket.
290    ///
291    /// Defaults to unset (TCP connections via DNS).
292    #[cfg(unix)]
293    pub fn with_unix_socket_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
294        self.connector_builder = self.connector_builder.with_unix_socket_path(path);
295        self
296    }
297
298    /// Sets the TLS configuration.
299    ///
300    /// A TLS configuration builder is provided to allow for more advanced configuration of the TLS connection.
301    /// [`Self::with_client_tls_config`] overrides these settings regardless of call order.
302    pub fn with_tls_config<F>(mut self, f: F) -> Self
303    where
304        F: FnOnce(ClientTLSConfigBuilder) -> ClientTLSConfigBuilder,
305    {
306        self.tls_builder = f(self.tls_builder);
307        self
308    }
309
310    /// Sets a complete Rustls client TLS configuration.
311    ///
312    /// This configuration takes precedence over all option-based settings made through [`Self::with_tls_config`] or
313    /// [`Self::with_min_tls_version`], regardless of call order. The configuration is otherwise preserved, including
314    /// its certificate verifier, client identity, enabled TLS protocol versions, and other security settings.
315    ///
316    /// Any ALPN protocols already present in `config` are discarded before the configuration is passed to
317    /// `hyper-rustls`. [`Self::with_http_protocol`] remains the sole source of HTTP protocol selection:
318    /// [`HttpProtocol::Auto`] advertises HTTP/2 with HTTP/1.1 fallback, while [`HttpProtocol::Http1`] enables only
319    /// HTTP/1.1 and does not advertise ALPN.
320    ///
321    /// The supplied configuration is validated for FIPS compliance during [`Self::build`].
322    pub fn with_client_tls_config(mut self, config: ClientConfig) -> Self {
323        self.client_tls_config = Some(config);
324        self
325    }
326
327    /// Sets the minimum TLS protocol version for HTTPS connections.
328    ///
329    /// Defaults to TLS 1.2.
330    ///
331    /// This updates the same TLS builder configured by [`Self::with_tls_config`], so call order matters when both
332    /// methods change the minimum TLS version. [`Self::with_client_tls_config`] overrides this setting regardless of
333    /// call order.
334    pub fn with_min_tls_version(mut self, version: TlsMinimumVersion) -> Self {
335        self.tls_builder = self.tls_builder.with_min_tls_version(version);
336        self
337    }
338
339    /// Sets the underlying Hyper client configuration.
340    ///
341    /// This is provided to allow for more advanced configuration of the Hyper client itself, and should generally be
342    /// used sparingly.
343    pub fn with_hyper_config<F>(mut self, f: F) -> Self
344    where
345        F: FnOnce(&mut Builder),
346    {
347        f(&mut self.hyper_builder);
348        self
349    }
350
351    /// Builds the `HttpClient`.
352    ///
353    /// # Errors
354    ///
355    /// If there was an error building the TLS configuration for the client, or if a supplied complete TLS
356    /// configuration fails FIPS validation in a FIPS build, an error will be returned.
357    pub fn build(self) -> Result<HttpClient, GenericError> {
358        let tls_config = match self.client_tls_config {
359            Some(mut config) => {
360                ensure_client_config_fips_compliant(&config)?;
361                config.alpn_protocols.clear();
362                config
363            }
364            None => self.tls_builder.build()?,
365        };
366        let connector = self.connector_builder.build(tls_config)?;
367        let tls_handshake_timeout = connector.tls_handshake_timeout();
368        // TODO(fips): Look into updating `hyper-http-proxy` to use the provided connector for establishing the
369        // connection to the proxy itself, even when the proxy is at an HTTPS URL, to ensure our desired TLS stack is
370        // being used.
371        let mut proxy_connector = hyper_http_proxy::ProxyConnector::new(connector)?;
372        // A zero timeout means the handshake deadline is disabled, matching `HttpsCapableConnector`'s own
373        // handling of `Duration::ZERO` for direct connections.
374        let proxy_tls_handshake_timeout = (!tls_handshake_timeout.is_zero()).then_some(tls_handshake_timeout);
375        proxy_connector.set_tls_handshake_timeout(proxy_tls_handshake_timeout);
376        if let Some(proxies) = &self.proxies {
377            for proxy in proxies {
378                proxy_connector.add_proxy(proxy.to_owned());
379            }
380        }
381        let client = self.hyper_builder.build(proxy_connector);
382
383        let inner = ServiceBuilder::new()
384            .option_layer(self.request_timeout.map(TimeoutLayer::new))
385            .option_layer(self.endpoint_telemetry)
386            .service(client.map_err(BoxError::from))
387            .boxed_clone();
388
389        Ok(HttpClient { inner })
390    }
391}
392
393impl Default for HttpClientBuilder {
394    fn default() -> Self {
395        let mut hyper_builder = Builder::new(TokioExecutor::new());
396        hyper_builder
397            .pool_timer(TokioTimer::new())
398            .pool_max_idle_per_host(5)
399            .pool_idle_timeout(Duration::from_secs(45));
400
401        Self {
402            connector_builder: HttpsCapableConnectorBuilder::default(),
403            hyper_builder,
404            tls_builder: ClientTLSConfigBuilder::new(),
405            client_tls_config: None,
406            request_timeout: Some(Duration::from_secs(20)),
407            endpoint_telemetry: None,
408            proxies: None,
409        }
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use std::{sync::Arc, time::Duration};
416
417    use http_body::Body as _;
418    use http_body_util::{Empty, Full};
419    use rustls::{server::WebPkiClientVerifier, ClientConfig, RootCertStore, ServerConfig};
420    use saluki_tls::test_util::SelfSignedCert;
421    use tokio::{
422        io::{AsyncReadExt as _, AsyncWriteExt as _},
423        net::TcpListener,
424        time::timeout,
425    };
426    use tokio_rustls::TlsAcceptor;
427
428    use super::*;
429
430    #[test]
431    fn into_client_body_preserves_exact_size_hint() {
432        let body = Full::new(Bytes::from_static(b"hello"));
433        let converted = into_client_body(body);
434
435        assert_eq!(Some(5), converted.size_hint().exact());
436    }
437
438    fn initialize_crypto_provider() {
439        let _ = saluki_tls::initialize_default_crypto_provider();
440        assert!(
441            rustls::crypto::CryptoProvider::get_default().is_some(),
442            "default crypto provider should be installed"
443        );
444    }
445
446    #[tokio::test]
447    async fn complete_tls_config_preserves_mtls_and_overrides_option_settings() {
448        initialize_crypto_provider();
449        let (server_config, client_config) = mutual_tls_configs();
450        let builder = HttpClient::builder()
451            .with_client_tls_config(client_config)
452            .with_tls_config(|builder| builder.with_root_cert_store(RootCertStore::empty()));
453
454        let _ = send_request_to_tls_server(builder, server_config)
455            .await
456            .expect("complete config should verify the server and present the client identity");
457    }
458
459    #[tokio::test]
460    async fn complete_tls_config_alpn_is_normalized_for_http1() {
461        initialize_crypto_provider();
462        let (mut server_config, mut client_config) = mutual_tls_configs();
463        server_config.alpn_protocols = vec![b"custom".to_vec(), b"http/1.1".to_vec()];
464        client_config.alpn_protocols = vec![b"custom".to_vec()];
465        let builder = HttpClient::builder().with_client_tls_config(client_config);
466
467        let negotiated_alpn = send_request_to_tls_server(builder, server_config)
468            .await
469            .expect("client should normalize ALPN and complete an HTTP/1.1 request");
470
471        assert_eq!(negotiated_alpn, None);
472    }
473
474    #[tokio::test]
475    async fn tls_handshake_timeout_fires_against_a_stalled_server() {
476        initialize_crypto_provider();
477        let server_cert = SelfSignedCert::localhost();
478
479        let listener = TcpListener::bind("127.0.0.1:0").await.expect("should bind listener");
480        let port = listener.local_addr().expect("should have local address").port();
481        let server_task = tokio::spawn(async move {
482            let (stream, _) = listener.accept().await.expect("server should accept a connection");
483            // Accept the TCP connection but never complete the TLS handshake, so the client's handshake
484            // deadline is the only thing that can end the connection attempt.
485            tokio::time::sleep(Duration::from_secs(30)).await;
486            drop(stream);
487        });
488
489        let mut client = HttpClient::builder()
490            .with_tls_config(|builder| builder.with_root_cert_store(root_store(&server_cert)))
491            .with_tls_handshake_timeout(Duration::from_millis(200))
492            .with_http_protocol(HttpProtocol::Http1)
493            .build()
494            .expect("client should build");
495        let request = Request::get(format!("https://localhost:{port}/"))
496            .body(Empty::<Bytes>::new())
497            .expect("request should build");
498
499        let error = timeout(Duration::from_secs(5), client.send(request))
500            .await
501            .expect("request should not hit the outer test timeout")
502            .expect_err("handshake should time out before completing");
503        assert!(
504            format!("{error:#}").contains("TLS handshake timed out"),
505            "expected a TLS handshake timeout, got: {error:#}"
506        );
507
508        server_task.abort();
509    }
510
511    #[tokio::test]
512    async fn tls_handshake_timeout_fires_against_a_stalled_proxy_tunnel() {
513        initialize_crypto_provider();
514
515        let listener = TcpListener::bind("127.0.0.1:0").await.expect("should bind listener");
516        let proxy_addr = listener.local_addr().expect("should have local address");
517        let proxy_task = tokio::spawn(async move {
518            let (mut stream, _) = listener.accept().await.expect("proxy should accept a connection");
519            let mut request = [0; 4096];
520            let bytes_read = stream
521                .read(&mut request)
522                .await
523                .expect("proxy should read the CONNECT request");
524            assert!(bytes_read > 0, "proxy should receive a CONNECT request");
525            stream
526                .write_all(b"HTTP/1.1 200 OK\r\n\r\n")
527                .await
528                .expect("proxy should write the CONNECT response");
529            // Complete the CONNECT tunnel but never speak TLS, so the client's handshake deadline is
530            // the only thing that can end the connection attempt.
531            tokio::time::sleep(Duration::from_secs(30)).await;
532            drop(stream);
533        });
534
535        let proxy_uri: Uri = format!("http://{proxy_addr}").parse().expect("proxy URI should parse");
536        let mut client = HttpClient::builder()
537            .with_proxies(vec![Proxy::new(hyper_http_proxy::Intercept::All, proxy_uri)])
538            .with_tls_config(|builder| builder.with_root_cert_store(RootCertStore::empty()))
539            .with_tls_handshake_timeout(Duration::from_millis(200))
540            .with_http_protocol(HttpProtocol::Http1)
541            .build()
542            .expect("client should build");
543        let request = Request::get("https://example.invalid/")
544            .body(Empty::<Bytes>::new())
545            .expect("request should build");
546
547        let error = timeout(Duration::from_secs(5), client.send(request))
548            .await
549            .expect("request should not hit the outer test timeout")
550            .expect_err("handshake should time out before completing");
551        assert!(
552            format!("{error:#}").contains("TLS handshake timed out"),
553            "expected a TLS handshake timeout, got: {error:#}"
554        );
555
556        proxy_task.abort();
557    }
558
559    #[tokio::test]
560    async fn zero_tls_handshake_timeout_disables_the_proxy_tunnel_deadline() {
561        initialize_crypto_provider();
562
563        let listener = TcpListener::bind("127.0.0.1:0").await.expect("should bind listener");
564        let proxy_addr = listener.local_addr().expect("should have local address");
565        let proxy_task = tokio::spawn(async move {
566            let (mut stream, _) = listener.accept().await.expect("proxy should accept a connection");
567            let mut request = [0; 4096];
568            let bytes_read = stream
569                .read(&mut request)
570                .await
571                .expect("proxy should read the CONNECT request");
572            assert!(bytes_read > 0, "proxy should receive a CONNECT request");
573            stream
574                .write_all(b"HTTP/1.1 200 OK\r\n\r\n")
575                .await
576                .expect("proxy should write the CONNECT response");
577            // Complete the CONNECT tunnel but never speak TLS, so the request only fails if some
578            // deadline (mistakenly) bounds the handshake.
579            tokio::time::sleep(Duration::from_secs(30)).await;
580            drop(stream);
581        });
582
583        let proxy_uri: Uri = format!("http://{proxy_addr}").parse().expect("proxy URI should parse");
584        let mut client = HttpClient::builder()
585            .with_proxies(vec![Proxy::new(hyper_http_proxy::Intercept::All, proxy_uri)])
586            .with_tls_config(|builder| builder.with_root_cert_store(RootCertStore::empty()))
587            .with_tls_handshake_timeout(Duration::ZERO)
588            .with_http_protocol(HttpProtocol::Http1)
589            .build()
590            .expect("client should build");
591        let request = Request::get("https://example.invalid/")
592            .body(Empty::<Bytes>::new())
593            .expect("request should build");
594
595        // A zero handshake timeout means "disabled", so the request should still be pending well past
596        // the point where a mistakenly active zero-length deadline would have already failed it.
597        let result = timeout(Duration::from_millis(300), client.send(request)).await;
598        assert!(
599            result.is_err(),
600            "a disabled timeout should not fail the proxy tunnel's TLS handshake, got: {result:?}"
601        );
602
603        proxy_task.abort();
604    }
605
606    fn mutual_tls_configs() -> (ServerConfig, ClientConfig) {
607        let server_cert = SelfSignedCert::localhost();
608        let client_cert = SelfSignedCert::new(["saluki-client"]);
609        let client_verifier = WebPkiClientVerifier::builder(Arc::new(root_store(&client_cert)))
610            .build()
611            .expect("client certificate verifier should build");
612        let server_config = ServerConfig::builder()
613            .with_client_cert_verifier(client_verifier)
614            .with_single_cert(server_cert.cert_chain(), server_cert.private_key())
615            .expect("server TLS config should build");
616        let client_config = ClientConfig::builder()
617            .with_root_certificates(root_store(&server_cert))
618            .with_client_auth_cert(client_cert.cert_chain(), client_cert.private_key())
619            .expect("client TLS config should build");
620
621        (server_config, client_config)
622    }
623
624    fn root_store(cert: &SelfSignedCert) -> RootCertStore {
625        let mut root_store = RootCertStore::empty();
626        root_store
627            .add(cert.cert_chain().pop().expect("certificate chain should not be empty"))
628            .expect("self-signed certificate should be a valid trust anchor");
629        root_store
630    }
631
632    async fn send_request_to_tls_server(
633        builder: HttpClientBuilder, server_config: ServerConfig,
634    ) -> Result<Option<Vec<u8>>, GenericError> {
635        let listener = TcpListener::bind("127.0.0.1:0").await?;
636        let port = listener.local_addr()?.port();
637        let server_task = tokio::spawn(async move {
638            let (stream, _) = listener.accept().await.expect("server should accept a connection");
639            let mut stream = TlsAcceptor::from(Arc::new(server_config))
640                .accept(stream)
641                .await
642                .expect("TLS handshake should succeed");
643            let negotiated_alpn = stream.get_ref().1.alpn_protocol().map(ToOwned::to_owned);
644            let mut request = [0; 4096];
645            let bytes_read = stream.read(&mut request).await.expect("server should read the request");
646            assert!(bytes_read > 0, "server should receive an HTTP request");
647            stream
648                .write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\nconnection: close\r\n\r\n")
649                .await
650                .expect("server should write the response");
651            negotiated_alpn
652        });
653
654        let mut client = builder.with_http_protocol(HttpProtocol::Http1).build()?;
655        let request = Request::get(format!("https://localhost:{port}/"))
656            .body(Empty::<Bytes>::new())
657            .expect("request should build");
658        let response = timeout(Duration::from_secs(5), client.send(request))
659            .await
660            .map_err(|_| GenericError::msg("HTTP request timed out"))??;
661        assert_eq!(response.status(), http::StatusCode::OK);
662        Ok(timeout(Duration::from_secs(5), server_task)
663            .await
664            .expect("TLS server should finish")
665            .expect("TLS server task should not panic"))
666    }
667}