saluki_io/net/client/http/
client.rs1#[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 metrics::Counter;
21use pin_project::pin_project;
22use rustls::ClientConfig;
23use saluki_error::GenericError;
24use saluki_metrics::MetricsBuilder;
25use saluki_tls::{ensure_client_config_fips_compliant, ClientTLSConfigBuilder, TlsMinimumVersion};
26use stringtheory::MetaString;
27use tower::{timeout::TimeoutLayer, util::BoxCloneService, BoxError, Service, ServiceBuilder, ServiceExt as _};
28
29use super::{
30 conn::{check_connection_state, HttpProtocol, HttpsCapableConnectorBuilder},
31 telemetry::HttpTransactionErrorTelemetry,
32 EndpointTelemetryLayer,
33};
34
35pub type ClientBody = BoxBody<Bytes, Box<dyn std::error::Error + Send + Sync>>;
40
41#[pin_project]
42struct ClientBodyAdapter<B> {
43 #[pin]
44 inner: B,
45 size_hint: SizeHint,
46}
47
48impl<B> Body for ClientBodyAdapter<B>
49where
50 B: Body,
51 B::Data: Buf,
52 B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
53{
54 type Data = Bytes;
55 type Error = Box<dyn std::error::Error + Send + Sync>;
56
57 fn poll_frame(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
58 self.project().inner.poll_frame(cx).map(|maybe_frame| {
59 maybe_frame.map(|result| {
60 result
61 .map(|frame| frame.map_data(|mut data| data.copy_to_bytes(data.remaining())))
62 .map_err(Into::into)
63 })
64 })
65 }
66
67 fn size_hint(&self) -> SizeHint {
68 self.size_hint
69 }
70}
71
72#[derive(Clone)]
74pub struct HttpClient {
75 inner: BoxCloneService<Request<ClientBody>, Response<Incoming>, BoxError>,
76}
77
78impl HttpClient {
79 pub fn builder() -> HttpClientBuilder {
81 HttpClientBuilder::default()
82 }
83
84 pub async fn send<B>(&mut self, req: Request<B>) -> Result<Response<Incoming>, GenericError>
93 where
94 B: Body + Send + Sync + 'static,
95 B::Data: Buf + Send,
96 B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
97 {
98 let mut req = req.map(into_client_body);
99 let captured_conn = capture_connection(&mut req);
100 let result = self
101 .inner
102 .ready()
103 .await
104 .map_err(GenericError::from_boxed)?
105 .call(req)
106 .await;
107
108 check_connection_state(captured_conn);
109
110 result.map_err(GenericError::from_boxed)
111 }
112}
113
114impl Service<Request<ClientBody>> for HttpClient {
115 type Response = Response<Incoming>;
116 type Error = BoxError;
117 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
118
119 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
120 self.inner.poll_ready(cx)
121 }
122
123 fn call(&mut self, mut req: Request<ClientBody>) -> Self::Future {
124 let captured_conn = capture_connection(&mut req);
125 let fut = self.inner.call(req);
126
127 Box::pin(async move {
128 let result = fut.await;
129
130 check_connection_state(captured_conn);
131
132 result
133 })
134 }
135}
136
137pub fn into_client_body<B>(body: B) -> ClientBody
142where
143 B: Body + Send + Sync + 'static,
144 B::Data: Buf + Send,
145 B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
146{
147 let size_hint = body.size_hint();
148 BoxBody::new(ClientBodyAdapter { inner: body, size_hint })
149}
150
151pub struct HttpClientBuilder {
166 connector_builder: HttpsCapableConnectorBuilder,
167 hyper_builder: Builder,
168 tls_builder: ClientTLSConfigBuilder,
169 client_tls_config: Option<ClientConfig>,
170 request_timeout: Option<Duration>,
171 endpoint_telemetry: Option<EndpointTelemetryLayer>,
172 proxies: Option<Vec<Proxy>>,
173}
174
175impl HttpClientBuilder {
176 pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
180 self.connector_builder = self.connector_builder.with_connect_timeout(timeout);
181 self
182 }
183
184 pub fn with_tls_handshake_timeout(mut self, timeout: Duration) -> Self {
191 self.connector_builder = self.connector_builder.with_tls_handshake_timeout(timeout);
192 self
193 }
194
195 pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
202 self.request_timeout = Some(timeout);
203 self
204 }
205
206 pub fn without_request_timeout(mut self) -> Self {
211 self.request_timeout = None;
212 self
213 }
214
215 pub fn with_http_protocol(mut self, protocol: HttpProtocol) -> Self {
219 self.connector_builder = self.connector_builder.with_http_protocol(protocol);
220 self
221 }
222
223 pub fn with_connection_age_limit<L>(mut self, limit: L) -> Self
230 where
231 L: Into<Option<Duration>>,
232 {
233 self.connector_builder = self.connector_builder.with_connection_age_limit(limit);
234 self
235 }
236
237 pub fn with_max_idle_conns_per_host(mut self, max: usize) -> Self {
241 self.hyper_builder.pool_max_idle_per_host(max);
242 self
243 }
244
245 pub fn with_idle_conn_timeout(mut self, timeout: Duration) -> Self {
252 self.hyper_builder.pool_idle_timeout(timeout);
253 self
254 }
255
256 pub fn with_proxies(mut self, proxies: Vec<Proxy>) -> Self {
260 self.proxies = Some(proxies);
261 self
262 }
263
264 pub fn with_endpoint_telemetry<F>(mut self, metrics_builder: MetricsBuilder, endpoint_name_fn: Option<F>) -> Self
268 where
269 F: Fn(&Uri) -> Option<MetaString> + Send + Sync + 'static,
270 {
271 let error_telemetry = HttpTransactionErrorTelemetry::from_builder(&metrics_builder);
272 self.connector_builder = self.connector_builder.with_error_telemetry(error_telemetry.clone());
273
274 let mut layer = EndpointTelemetryLayer::default()
275 .with_metrics_builder(metrics_builder)
276 .with_error_telemetry(error_telemetry);
277
278 if let Some(endpoint_name_fn) = endpoint_name_fn {
279 layer = layer.with_endpoint_name_fn(endpoint_name_fn);
280 }
281
282 self.endpoint_telemetry = Some(layer);
283 self
284 }
285
286 #[cfg(unix)]
294 pub fn with_unix_socket_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
295 self.connector_builder = self.connector_builder.with_unix_socket_path(path);
296 self
297 }
298
299 pub fn with_tls_config<F>(mut self, f: F) -> Self
304 where
305 F: FnOnce(ClientTLSConfigBuilder) -> ClientTLSConfigBuilder,
306 {
307 self.tls_builder = f(self.tls_builder);
308 self
309 }
310
311 pub fn with_client_tls_config(mut self, config: ClientConfig) -> Self {
324 self.client_tls_config = Some(config);
325 self
326 }
327
328 pub fn with_min_tls_version(mut self, version: TlsMinimumVersion) -> Self {
336 self.tls_builder = self.tls_builder.with_min_tls_version(version);
337 self
338 }
339
340 pub fn with_hyper_config<F>(mut self, f: F) -> Self
345 where
346 F: FnOnce(&mut Builder),
347 {
348 f(&mut self.hyper_builder);
349 self
350 }
351
352 pub fn with_bytes_sent_counter(mut self, counter: Counter) -> Self {
359 self.connector_builder = self.connector_builder.with_bytes_sent_counter(counter);
360 self
361 }
362
363 pub fn build(self) -> Result<HttpClient, GenericError> {
370 let tls_config = match self.client_tls_config {
371 Some(mut config) => {
372 ensure_client_config_fips_compliant(&config)?;
373 config.alpn_protocols.clear();
374 config
375 }
376 None => self.tls_builder.build()?,
377 };
378 let connector = self.connector_builder.build(tls_config)?;
379 let tls_handshake_timeout = connector.tls_handshake_timeout();
380 let mut proxy_connector = hyper_http_proxy::ProxyConnector::new(connector)?;
384 let proxy_tls_handshake_timeout = (!tls_handshake_timeout.is_zero()).then_some(tls_handshake_timeout);
387 proxy_connector.set_tls_handshake_timeout(proxy_tls_handshake_timeout);
388 if let Some(proxies) = &self.proxies {
389 for proxy in proxies {
390 proxy_connector.add_proxy(proxy.to_owned());
391 }
392 }
393 let client = self.hyper_builder.build(proxy_connector);
394
395 let inner = ServiceBuilder::new()
396 .option_layer(self.request_timeout.map(TimeoutLayer::new))
397 .option_layer(self.endpoint_telemetry)
398 .service(client.map_err(BoxError::from))
399 .boxed_clone();
400
401 Ok(HttpClient { inner })
402 }
403}
404
405impl Default for HttpClientBuilder {
406 fn default() -> Self {
407 let mut hyper_builder = Builder::new(TokioExecutor::new());
408 hyper_builder
409 .pool_timer(TokioTimer::new())
410 .pool_max_idle_per_host(5)
411 .pool_idle_timeout(Duration::from_secs(45));
412
413 Self {
414 connector_builder: HttpsCapableConnectorBuilder::default(),
415 hyper_builder,
416 tls_builder: ClientTLSConfigBuilder::new(),
417 client_tls_config: None,
418 request_timeout: Some(Duration::from_secs(20)),
419 endpoint_telemetry: None,
420 proxies: None,
421 }
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use std::{sync::Arc, time::Duration};
428
429 use http_body::Body as _;
430 use http_body_util::{Empty, Full};
431 use rustls::{server::WebPkiClientVerifier, ClientConfig, RootCertStore, ServerConfig};
432 use saluki_tls::test_util::SelfSignedCert;
433 use tokio::{
434 io::{AsyncReadExt as _, AsyncWriteExt as _},
435 net::TcpListener,
436 time::timeout,
437 };
438 use tokio_rustls::TlsAcceptor;
439
440 use super::*;
441
442 #[test]
443 fn into_client_body_preserves_exact_size_hint() {
444 let body = Full::new(Bytes::from_static(b"hello"));
445 let converted = into_client_body(body);
446
447 assert_eq!(Some(5), converted.size_hint().exact());
448 }
449
450 fn initialize_crypto_provider() {
451 let _ = saluki_tls::initialize_default_crypto_provider();
452 assert!(
453 rustls::crypto::CryptoProvider::get_default().is_some(),
454 "default crypto provider should be installed"
455 );
456 }
457
458 #[tokio::test]
459 async fn complete_tls_config_preserves_mtls_and_overrides_option_settings() {
460 initialize_crypto_provider();
461 let (server_config, client_config) = mutual_tls_configs();
462 let builder = HttpClient::builder()
463 .with_client_tls_config(client_config)
464 .with_tls_config(|builder| builder.with_root_cert_store(RootCertStore::empty()));
465
466 let _ = send_request_to_tls_server(builder, server_config)
467 .await
468 .expect("complete config should verify the server and present the client identity");
469 }
470
471 #[tokio::test]
472 async fn complete_tls_config_alpn_is_normalized_for_http1() {
473 initialize_crypto_provider();
474 let (mut server_config, mut client_config) = mutual_tls_configs();
475 server_config.alpn_protocols = vec![b"custom".to_vec(), b"http/1.1".to_vec()];
476 client_config.alpn_protocols = vec![b"custom".to_vec()];
477 let builder = HttpClient::builder().with_client_tls_config(client_config);
478
479 let negotiated_alpn = send_request_to_tls_server(builder, server_config)
480 .await
481 .expect("client should normalize ALPN and complete an HTTP/1.1 request");
482
483 assert_eq!(negotiated_alpn, None);
484 }
485
486 #[tokio::test]
487 async fn tls_handshake_timeout_fires_against_a_stalled_server() {
488 initialize_crypto_provider();
489 let server_cert = SelfSignedCert::localhost();
490
491 let listener = TcpListener::bind("127.0.0.1:0").await.expect("should bind listener");
492 let port = listener.local_addr().expect("should have local address").port();
493 let server_task = tokio::spawn(async move {
494 let (stream, _) = listener.accept().await.expect("server should accept a connection");
495 tokio::time::sleep(Duration::from_secs(30)).await;
498 drop(stream);
499 });
500
501 let mut client = HttpClient::builder()
502 .with_tls_config(|builder| builder.with_root_cert_store(root_store(&server_cert)))
503 .with_tls_handshake_timeout(Duration::from_millis(200))
504 .with_http_protocol(HttpProtocol::Http1)
505 .build()
506 .expect("client should build");
507 let request = Request::get(format!("https://localhost:{port}/"))
508 .body(Empty::<Bytes>::new())
509 .expect("request should build");
510
511 let error = timeout(Duration::from_secs(5), client.send(request))
512 .await
513 .expect("request should not hit the outer test timeout")
514 .expect_err("handshake should time out before completing");
515 assert!(
516 format!("{error:#}").contains("TLS handshake timed out"),
517 "expected a TLS handshake timeout, got: {error:#}"
518 );
519
520 server_task.abort();
521 }
522
523 #[tokio::test]
524 async fn tls_handshake_timeout_fires_against_a_stalled_proxy_tunnel() {
525 initialize_crypto_provider();
526
527 let listener = TcpListener::bind("127.0.0.1:0").await.expect("should bind listener");
528 let proxy_addr = listener.local_addr().expect("should have local address");
529 let proxy_task = tokio::spawn(async move {
530 let (mut stream, _) = listener.accept().await.expect("proxy should accept a connection");
531 let mut request = [0; 4096];
532 let bytes_read = stream
533 .read(&mut request)
534 .await
535 .expect("proxy should read the CONNECT request");
536 assert!(bytes_read > 0, "proxy should receive a CONNECT request");
537 stream
538 .write_all(b"HTTP/1.1 200 OK\r\n\r\n")
539 .await
540 .expect("proxy should write the CONNECT response");
541 tokio::time::sleep(Duration::from_secs(30)).await;
544 drop(stream);
545 });
546
547 let proxy_uri: Uri = format!("http://{proxy_addr}").parse().expect("proxy URI should parse");
548 let mut client = HttpClient::builder()
549 .with_proxies(vec![Proxy::new(hyper_http_proxy::Intercept::All, proxy_uri)])
550 .with_tls_config(|builder| builder.with_root_cert_store(RootCertStore::empty()))
551 .with_tls_handshake_timeout(Duration::from_millis(200))
552 .with_http_protocol(HttpProtocol::Http1)
553 .build()
554 .expect("client should build");
555 let request = Request::get("https://example.invalid/")
556 .body(Empty::<Bytes>::new())
557 .expect("request should build");
558
559 let error = timeout(Duration::from_secs(5), client.send(request))
560 .await
561 .expect("request should not hit the outer test timeout")
562 .expect_err("handshake should time out before completing");
563 assert!(
564 format!("{error:#}").contains("TLS handshake timed out"),
565 "expected a TLS handshake timeout, got: {error:#}"
566 );
567
568 proxy_task.abort();
569 }
570
571 #[tokio::test]
572 async fn zero_tls_handshake_timeout_disables_the_proxy_tunnel_deadline() {
573 initialize_crypto_provider();
574
575 let listener = TcpListener::bind("127.0.0.1:0").await.expect("should bind listener");
576 let proxy_addr = listener.local_addr().expect("should have local address");
577 let proxy_task = tokio::spawn(async move {
578 let (mut stream, _) = listener.accept().await.expect("proxy should accept a connection");
579 let mut request = [0; 4096];
580 let bytes_read = stream
581 .read(&mut request)
582 .await
583 .expect("proxy should read the CONNECT request");
584 assert!(bytes_read > 0, "proxy should receive a CONNECT request");
585 stream
586 .write_all(b"HTTP/1.1 200 OK\r\n\r\n")
587 .await
588 .expect("proxy should write the CONNECT response");
589 tokio::time::sleep(Duration::from_secs(30)).await;
592 drop(stream);
593 });
594
595 let proxy_uri: Uri = format!("http://{proxy_addr}").parse().expect("proxy URI should parse");
596 let mut client = HttpClient::builder()
597 .with_proxies(vec![Proxy::new(hyper_http_proxy::Intercept::All, proxy_uri)])
598 .with_tls_config(|builder| builder.with_root_cert_store(RootCertStore::empty()))
599 .with_tls_handshake_timeout(Duration::ZERO)
600 .with_http_protocol(HttpProtocol::Http1)
601 .build()
602 .expect("client should build");
603 let request = Request::get("https://example.invalid/")
604 .body(Empty::<Bytes>::new())
605 .expect("request should build");
606
607 let result = timeout(Duration::from_millis(300), client.send(request)).await;
610 assert!(
611 result.is_err(),
612 "a disabled timeout should not fail the proxy tunnel's TLS handshake, got: {result:?}"
613 );
614
615 proxy_task.abort();
616 }
617
618 fn mutual_tls_configs() -> (ServerConfig, ClientConfig) {
619 let server_cert = SelfSignedCert::localhost();
620 let client_cert = SelfSignedCert::new(["saluki-client"]);
621 let client_verifier = WebPkiClientVerifier::builder(Arc::new(root_store(&client_cert)))
622 .build()
623 .expect("client certificate verifier should build");
624 let server_config = ServerConfig::builder()
625 .with_client_cert_verifier(client_verifier)
626 .with_single_cert(server_cert.cert_chain(), server_cert.private_key())
627 .expect("server TLS config should build");
628 let client_config = ClientConfig::builder()
629 .with_root_certificates(root_store(&server_cert))
630 .with_client_auth_cert(client_cert.cert_chain(), client_cert.private_key())
631 .expect("client TLS config should build");
632
633 (server_config, client_config)
634 }
635
636 fn root_store(cert: &SelfSignedCert) -> RootCertStore {
637 let mut root_store = RootCertStore::empty();
638 root_store
639 .add(cert.cert_chain().pop().expect("certificate chain should not be empty"))
640 .expect("self-signed certificate should be a valid trust anchor");
641 root_store
642 }
643
644 async fn send_request_to_tls_server(
645 builder: HttpClientBuilder, server_config: ServerConfig,
646 ) -> Result<Option<Vec<u8>>, GenericError> {
647 let listener = TcpListener::bind("127.0.0.1:0").await?;
648 let port = listener.local_addr()?.port();
649 let server_task = tokio::spawn(async move {
650 let (stream, _) = listener.accept().await.expect("server should accept a connection");
651 let mut stream = TlsAcceptor::from(Arc::new(server_config))
652 .accept(stream)
653 .await
654 .expect("TLS handshake should succeed");
655 let negotiated_alpn = stream.get_ref().1.alpn_protocol().map(ToOwned::to_owned);
656 let mut request = [0; 4096];
657 let bytes_read = stream.read(&mut request).await.expect("server should read the request");
658 assert!(bytes_read > 0, "server should receive an HTTP request");
659 stream
660 .write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\nconnection: close\r\n\r\n")
661 .await
662 .expect("server should write the response");
663 negotiated_alpn
664 });
665
666 let mut client = builder.with_http_protocol(HttpProtocol::Http1).build()?;
667 let request = Request::get(format!("https://localhost:{port}/"))
668 .body(Empty::<Bytes>::new())
669 .expect("request should build");
670 let response = timeout(Duration::from_secs(5), client.send(request))
671 .await
672 .map_err(|_| GenericError::msg("HTTP request timed out"))??;
673 assert_eq!(response.status(), http::StatusCode::OK);
674 Ok(timeout(Duration::from_secs(5), server_task)
675 .await
676 .expect("TLS server should finish")
677 .expect("TLS server task should not panic"))
678 }
679}