1use std::{fmt, io};
7
8use crate::{
9 Layer, Service,
10 error::BoxError,
11 extensions::ExtensionsRef,
12 http::{Body as ResponseBody, Request, Response, StreamingBody},
13 net::client::EstablishedClientConnection,
14 rt::Executor,
15 service::BoxService,
16 telemetry::tracing,
17};
18
19#[doc(inline)]
20pub use ::rama_http::service::client::blocking::{
21 Body as BlockingBody, Client as BlockingHttpClient, Response as BlockingResponse,
22};
23#[doc(inline)]
24pub use ::rama_http_backend::client::*;
25use rama_core::{
26 error::{ErrorContext, ErrorExt as _, extra::OpaqueError},
27 extensions::Egress,
28 layer::MapErr,
29};
30use rama_http::{
31 layer::{
32 forward_proxy::{HttpForwardProxyLayer, HttpForwardProxyService},
33 map_response_body::MapResponseBody,
34 },
35 proxy::PlaintextHttpProxyMode,
36};
37
38#[cfg(any(feature = "boring", feature = "rustls"))]
39use crate::tls::client::TlsClientConfig;
40
41#[cfg(feature = "boring")]
42use {crate::quic::tls::BoringTlsProvider, std::sync::Arc};
43
44pub mod builder;
45#[doc(inline)]
46pub use builder::EasyHttpConnectorBuilder;
47
48#[cfg(feature = "socks5")]
49mod proxy_connector;
50#[cfg(feature = "socks5")]
51#[cfg_attr(docsrs, doc(cfg(feature = "socks5")))]
52#[doc(inline)]
53pub use proxy_connector::{MaybeProxiedConnection, ProxyConnector, ProxyConnectorLayer};
54
55pub struct EasyHttpWebClient<BodyIn, ConnResponse, L> {
72 connector: BoxService<Request<BodyIn>, ConnResponse, OpaqueError>,
73 forward_proxy_layer: HttpForwardProxyLayer,
74 plaintext_http_proxy_mode: Option<PlaintextHttpProxyMode>,
75 jit_layers: L,
76}
77
78impl<BodyIn, ConnResponse, L> fmt::Debug for EasyHttpWebClient<BodyIn, ConnResponse, L> {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 f.debug_struct("EasyHttpWebClient").finish()
81 }
82}
83
84impl<BodyIn, ConnResponse, L: Clone> Clone for EasyHttpWebClient<BodyIn, ConnResponse, L> {
85 fn clone(&self) -> Self {
86 Self {
87 connector: self.connector.clone(),
88 forward_proxy_layer: self.forward_proxy_layer.clone(),
89 plaintext_http_proxy_mode: self.plaintext_http_proxy_mode,
90 jit_layers: self.jit_layers.clone(),
91 }
92 }
93}
94
95impl EasyHttpWebClient<(), (), ()> {
96 #[must_use]
98 pub fn connector_builder() -> EasyHttpConnectorBuilder {
99 EasyHttpConnectorBuilder::new()
100 }
101
102 pub fn try_blocking() -> io::Result<BlockingHttpWebClient> {
121 BlockingHttpClient::try_new(EasyHttpWebClient::default())
122 }
123}
124
125pub type DefaultHttpWebClient<Body = crate::http::Body> = EasyHttpWebClient<
132 Body,
133 EstablishedClientConnection<
134 rama_http::layer::alt_svc::AltSvc<
135 BindBodyToConn<
136 crate::net::client::pool::MultiplexedConnection<
137 HttpClientService<Body>,
138 HttpConnId,
139 >,
140 >,
141 >,
142 Request<Body>,
143 >,
144 (),
145>;
146
147pub type BlockingHttpWebClient = BlockingHttpClient<DefaultHttpWebClient>;
149
150impl<Body> Default for DefaultHttpWebClient<Body>
151where
152 Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
153{
154 #[inline(always)]
155 fn default() -> Self {
156 Self::default_with_executor(Executor::default())
157 }
158}
159
160impl<Body> DefaultHttpWebClient<Body>
161where
162 Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
163{
164 core::cfg_select! {
165 feature = "boring" => {
166 pub fn default_with_executor(exec: Executor) -> Self {
167 let tls_config = TlsClientConfig::default_http();
168 #[expect(
169 clippy::expect_used,
170 reason = "fixed default H3 limits and the explicit BoringSSL provider are valid"
171 )]
172 let h3 = Http3Connector::builder(exec.clone())
173 .with_tls_config(tls_config.clone())
174 .with_tls_provider(Arc::new(BoringTlsProvider))
175 .build_lazy()
176 .expect("default BoringSSL HTTP/3 configuration is valid");
177
178 EasyHttpConnectorBuilder::new()
179 .with_default_transport_connector()
180 .with_default_dns_connector()
181 .with_tls_proxy_support_using_boringssl()
182 .with_proxy_support()
183 .with_tls_support_using_boringssl(tls_config)
184 .with_default_http_connector(exec)
185 .with_http3_support(h3)
186 .with_default_connection_pool()
187 .build_client()
188 }
189 }
190 feature = "rustls" => {
191 pub fn default_with_executor(exec: Executor) -> Self {
192 let tls_config = TlsClientConfig::default_http();
193 #[cfg(any(feature = "ring", feature = "aws-lc"))]
194 #[expect(
195 clippy::expect_used,
196 reason = "fixed default H3 limits and a compiled-in Rustls crypto provider are valid"
197 )]
198 let h3 = Http3Connector::builder(exec.clone())
199 .with_tls_config(tls_config.clone())
200 .build_lazy()
201 .expect("default Rustls HTTP/3 configuration is valid");
202
203 let builder = EasyHttpConnectorBuilder::new()
204 .with_default_transport_connector()
205 .with_default_dns_connector()
206 .with_tls_proxy_support_using_rustls()
207 .with_proxy_support()
208 .with_tls_support_using_rustls(tls_config)
209 .with_default_http_connector(exec);
210 #[cfg(any(feature = "ring", feature = "aws-lc"))]
211 let builder = builder.with_http3_support(h3);
212
213 builder
214 .with_default_connection_pool()
215 .build_client()
216 }
217 }
218 _ => {
219 pub fn default_with_executor(exec: Executor) -> Self {
220 EasyHttpConnectorBuilder::new()
221 .with_default_transport_connector()
222 .with_default_dns_connector()
223 .without_tls_proxy_support()
224 .with_proxy_support()
225 .without_tls_support()
226 .with_default_http_connector(exec)
227 .with_default_connection_pool()
228 .build_client()
229 }
230 }
231 }
232}
233
234impl<BodyIn, ConnResponse> EasyHttpWebClient<BodyIn, ConnResponse, ()>
235where
236 BodyIn: Send + 'static,
237{
238 #[must_use]
244 pub fn new<S>(connector: S) -> Self
245 where
246 S: Service<Request<BodyIn>, Output = ConnResponse, Error: Into<BoxError>>,
247 {
248 Self {
249 connector: MapErr::into_opaque_error(connector).boxed(),
250 forward_proxy_layer: HttpForwardProxyLayer::new(),
251 plaintext_http_proxy_mode: None,
252 jit_layers: (),
253 }
254 }
255}
256
257impl<BodyIn, ConnResponse, L> EasyHttpWebClient<BodyIn, ConnResponse, L> {
258 pub fn try_into_blocking(self) -> io::Result<BlockingHttpClient<Self>> {
261 BlockingHttpClient::try_new(self)
262 }
263
264 #[must_use]
267 pub fn into_blocking_with_runtime(
268 self,
269 runtime: &crate::rt::blocking::Runtime,
270 ) -> BlockingHttpClient<Self> {
271 BlockingHttpClient::with_runtime(self, runtime)
272 }
273
274 #[must_use]
278 pub fn with_connector<S, BodyInNew, ConnResponseNew>(
279 self,
280 connector: S,
281 ) -> EasyHttpWebClient<BodyInNew, ConnResponseNew, L>
282 where
283 S: Service<Request<BodyInNew>, Output = ConnResponseNew, Error: Into<BoxError>>,
284 BodyInNew: Send + 'static,
285 {
286 EasyHttpWebClient {
287 connector: MapErr::into_opaque_error(connector).boxed(),
288 forward_proxy_layer: self.forward_proxy_layer,
289 plaintext_http_proxy_mode: self.plaintext_http_proxy_mode,
290 jit_layers: self.jit_layers,
291 }
292 }
293
294 pub fn with_jit_layer<T>(self, jit_layers: T) -> EasyHttpWebClient<BodyIn, ConnResponse, T> {
306 EasyHttpWebClient {
307 connector: self.connector,
308 forward_proxy_layer: self.forward_proxy_layer,
309 plaintext_http_proxy_mode: self.plaintext_http_proxy_mode,
310 jit_layers,
311 }
312 }
313
314 crate::utils::macros::generate_set_and_with! {
315 pub fn forward_proxy_auth(mut self, enabled: bool) -> Self {
327 self.forward_proxy_layer.set_proxy_auth(enabled);
328 self
329 }
330 }
331
332 #[must_use]
336 pub fn without_forward_proxy_auth(self) -> Self {
337 self.with_forward_proxy_auth(false)
338 }
339
340 crate::utils::macros::generate_set_and_with! {
341 pub fn tunnel_plaintext_http(mut self, enabled: bool) -> Self {
353 self.plaintext_http_proxy_mode = Some(if enabled {
354 PlaintextHttpProxyMode::Tunnel
355 } else {
356 PlaintextHttpProxyMode::Forward
357 });
358 self
359 }
360 }
361
362 crate::utils::macros::generate_set_and_with! {
363 pub fn isolate_forward_proxy_auth_error(mut self, enabled: bool) -> Self {
370 self.forward_proxy_layer.set_isolate_auth_error(enabled);
371 self
372 }
373 }
374}
375
376impl<Body, ConnectionBody, Connection, IncomingBody, L> Service<Request<Body>>
377 for EasyHttpWebClient<Body, EstablishedClientConnection<Connection, Request<ConnectionBody>>, L>
378where
379 Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
380 Connection: Service<Request<ConnectionBody>, Output = Response<IncomingBody>, Error = BoxError>
381 + ExtensionsRef,
382 IncomingBody: Send + Sync + 'static,
383 ResponseBody: From<IncomingBody>,
384 ConnectionBody:
387 StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
388 L: Layer<
389 HttpForwardProxyService<MapResponseBody<Connection, fn(IncomingBody) -> ResponseBody>>,
390 Service: Service<Request<ConnectionBody>, Output = Response, Error = BoxError>,
391 > + Send
392 + Sync
393 + 'static,
394{
395 type Output = Response;
396 type Error = OpaqueError;
397
398 async fn serve(&self, req: Request<Body>) -> Result<Self::Output, Self::Error> {
399 let uri = req.uri().clone();
400
401 if let Some(mode) = self.plaintext_http_proxy_mode {
402 req.extensions().insert(mode);
403 }
404
405 let EstablishedClientConnection {
406 input: req,
407 conn: http_connection,
408 } = self.connector.serve(req).await.into_opaque_error()?;
409
410 req.extensions()
414 .insert(Egress(http_connection.extensions().clone()));
415
416 let http_connection = MapResponseBody::into_boxed_streaming_body(http_connection);
417 let http_connection = self.forward_proxy_layer.layer(http_connection);
418 let http_connection = self.jit_layers.layer(http_connection);
419
420 tracing::trace!(url.full = %uri, "send http req to connector stack");
422
423 let result = http_connection.serve(req).await;
424
425 match result {
426 Ok(resp) => {
427 tracing::trace!(url.full = %uri, "response received from connector stack");
428 Ok(resp)
429 }
430 Err(err) => Err(err
431 .context("http request failure")
432 .context_field("uri", uri)
433 .into_opaque_error()),
434 }
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use std::{
441 convert::Infallible,
442 sync::{
443 Arc,
444 atomic::{AtomicUsize, Ordering},
445 },
446 time::Duration,
447 };
448
449 use rama_core::extensions::Extensions;
450 use rama_core::{error::BoxErrorExt as _, service::service_fn};
451 use rama_http::{Body, BodyExtractExt, Version};
452 use rama_http_backend::server::HttpServer;
453 use rama_net::{
454 address::ProxyAddress,
455 client::{
456 ConnectRequest, ConnectionError, ConnectionErrorDomain, ConnectionErrorKind,
457 ConnectorService, ConnectorTarget, EstablishedProxyRoute, ProxyRoute,
458 ProxyRouteFailureCache, ProxyRouteFailureCacheConfig, ProxyRouteFailureCacheScope,
459 ProxyRoutes,
460 },
461 test_utils::client::{MockConnectorService, MockSocket},
462 };
463 use rama_utils::octets::kib;
464 use serde::{Deserialize, Serialize};
465 use tokio::time::sleep;
466
467 use super::*;
468
469 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
470 struct Output {
471 conn: usize,
472 resp: usize,
473 }
474
475 #[tokio::test]
476 async fn default_client_request_future_stays_within_stack_budget() {
477 let client = DefaultHttpWebClient::<Body>::default();
478 let request = Request::builder()
479 .uri("https://example.com/")
480 .body(Body::empty())
481 .unwrap();
482 let request = client.serve(request);
483 let inner_size = std::mem::size_of_val(&request);
484 let request = tokio::time::timeout(Duration::from_secs(1), request);
485 let size = std::mem::size_of_val(&request);
486 eprintln!("default client request: {inner_size} bytes; with timeout: {size} bytes");
487 assert!(
488 size <= kib(16),
489 "default client request future is {size} bytes"
490 );
491 }
492
493 #[test]
494 fn default_client_can_be_constructed_and_cloned_without_a_runtime() {
495 let client = DefaultHttpWebClient::<Body>::default();
496 drop(client.clone());
497 drop(client);
498 }
499
500 #[derive(Debug, Clone, Default)]
501 struct EmptyHttpConnection {
502 extensions: Extensions,
503 }
504
505 impl ExtensionsRef for EmptyHttpConnection {
506 fn extensions(&self) -> &Extensions {
507 &self.extensions
508 }
509 }
510
511 impl Service<Request> for EmptyHttpConnection {
512 type Output = Response;
513 type Error = BoxError;
514
515 async fn serve(&self, _request: Request) -> Result<Self::Output, Self::Error> {
516 Ok(Response::new(Body::empty()))
517 }
518 }
519
520 #[derive(Debug, Clone)]
521 struct InspectConnectionRouteLayer(EstablishedProxyRoute);
522
523 impl<S: ExtensionsRef> Layer<S> for InspectConnectionRouteLayer {
524 type Service = S;
525
526 fn layer(&self, inner: S) -> Self::Service {
527 assert_eq!(
528 inner.extensions().get_ref::<EstablishedProxyRoute>(),
529 Some(&self.0),
530 );
531 inner
532 }
533 }
534
535 fn dummy_server<Input: Send + 'static>()
536 -> impl Service<
537 Input,
538 Output = EstablishedClientConnection<MockSocket, Input>,
539 Error = Infallible,
540 > + Clone {
541 let created_connections = Arc::new(AtomicUsize::new(0));
542 MockConnectorService::new(move || {
543 let created_connections = created_connections.clone();
544 let conn = created_connections.fetch_add(1, Ordering::Relaxed);
545
546 let created_response = Arc::new(AtomicUsize::new(0));
548
549 HttpServer::auto(Executor::default()).service(service_fn(move |_req: Request| {
550 let created_response = created_response.clone();
551 let resp = created_response.fetch_add(1, Ordering::Relaxed);
552 async move {
553 sleep(Duration::from_millis(5)).await;
554 let out = Output { conn, resp };
555 let resp = Response::new(Body::from(serde_json::to_vec(&out).unwrap()));
556 Ok::<_, Infallible>(resp)
557 }
558 }))
559 })
560 }
561
562 #[tokio::test]
563 async fn custom_connector_receives_plaintext_http_proxy_mode() {
564 let connector = service_fn(|request: Request| async move {
565 assert_eq!(
566 request.extensions().get_ref::<PlaintextHttpProxyMode>(),
567 Some(&PlaintextHttpProxyMode::Tunnel)
568 );
569
570 let conn = EmptyHttpConnection::default();
571 Ok::<_, Infallible>(EstablishedClientConnection {
572 input: request,
573 conn,
574 })
575 });
576 let client = EasyHttpWebClient::new(connector).with_tunnel_plaintext_http(true);
577 let request = Request::builder()
578 .uri("http://example.com/")
579 .body(Body::empty())
580 .unwrap();
581
582 let response = client.serve(request).await.unwrap();
583 assert_eq!(response.status(), crate::http::StatusCode::OK);
584 }
585
586 #[tokio::test]
587 async fn jit_layer_can_read_established_connection_extensions() {
588 let connector = service_fn(|request: Request| async move {
589 let conn = EmptyHttpConnection::default();
590 conn.extensions().insert(EstablishedProxyRoute::Direct);
591 Ok::<_, Infallible>(EstablishedClientConnection {
592 input: request,
593 conn,
594 })
595 });
596 let client = EasyHttpWebClient::new(connector)
597 .with_jit_layer(InspectConnectionRouteLayer(EstablishedProxyRoute::Direct));
598 let request = Request::builder()
599 .uri("http://example.com/")
600 .extension(ProxyRoute::Direct)
601 .body(Body::empty())
602 .unwrap();
603
604 let response = client.serve(request).await.unwrap();
605 assert_eq!(response.status(), crate::http::StatusCode::OK);
606 }
607
608 #[tokio::test]
609 async fn jit_request_metadata_cannot_change_proxy_credentials_target_or_challenge_isolation() {
610 use rama_core::{
611 bytes::BytesMut,
612 layer::{MapInputLayer, MapOutputLayer},
613 };
614 use rama_http::{HeaderValue, StatusCode, header::PROXY_AUTHORIZATION};
615
616 #[derive(Debug, Clone)]
617 struct InspectProxyConnection {
618 extensions: Extensions,
619 }
620
621 impl ExtensionsRef for InspectProxyConnection {
622 fn extensions(&self) -> &Extensions {
623 &self.extensions
624 }
625 }
626
627 impl Service<Request> for InspectProxyConnection {
628 type Output = Response;
629 type Error = BoxError;
630
631 async fn serve(&self, request: Request) -> Result<Response, BoxError> {
632 let route = self.extensions.get_ref::<EstablishedProxyRoute>();
633 let is_forward = route.is_some_and(EstablishedProxyRoute::is_http_forward);
634 assert_eq!(
635 request
636 .extensions()
637 .egress()
638 .unwrap()
639 .0
640 .get_ref::<EstablishedProxyRoute>(),
641 route,
642 );
643 assert_eq!(
644 request.extensions().get_ref::<ProxyRoute>(),
645 Some(&ProxyRoute::Proxy(
646 "http://wrong:request-secret@requested.example:8080"
647 .parse()
648 .unwrap(),
649 )),
650 "forward policy must preserve the caller's requested route",
651 );
652 let mut target = BytesMut::new();
653 rama_http::proto::h1::head::encode_request_target(
654 request.method(),
655 request.uri(),
656 request.extensions(),
657 &mut target,
658 )
659 .unwrap();
660 if is_forward {
661 assert_eq!(
662 request.headers().get(PROXY_AUTHORIZATION).unwrap(),
663 "Basic dXBzdHJlYW06c2VjcmV0",
664 );
665 assert_eq!(&target[..], b"http://origin.example/resource");
666 } else {
667 assert!(request.headers().get(PROXY_AUTHORIZATION).is_none());
668 assert_eq!(&target[..], b"/resource");
669 }
670 Ok(Response::builder()
671 .status(StatusCode::PROXY_AUTHENTICATION_REQUIRED)
672 .header("proxy-authenticate", "Basic realm=private-upstream")
673 .body(Body::from("private upstream challenge"))
674 .unwrap())
675 }
676 }
677
678 let proxy: ProxyAddress = "http://upstream:secret@proxy.example:8080".parse().unwrap();
679 for isolate in [false, true] {
680 for route in [
681 None,
682 Some(EstablishedProxyRoute::Direct),
683 Some(EstablishedProxyRoute::Tunnel(proxy.clone())),
684 Some(EstablishedProxyRoute::Tunnel(
685 "socks5://proxy.example:1080".parse().unwrap(),
686 )),
687 Some(EstablishedProxyRoute::Forward(proxy.clone())),
688 ] {
689 let is_forward = route
690 .as_ref()
691 .is_some_and(EstablishedProxyRoute::is_http_forward);
692 let connector = service_fn(move |request: Request| {
693 let route = route.clone();
694 async move {
695 let extensions = Extensions::new();
696 if let Some(route) = route {
697 extensions.insert(route);
698 }
699 Ok::<_, Infallible>(EstablishedClientConnection {
700 input: request,
701 conn: InspectProxyConnection { extensions },
702 })
703 }
704 });
705 let stale_route = if is_forward {
706 EstablishedProxyRoute::Direct
707 } else {
708 EstablishedProxyRoute::Forward(proxy.clone())
709 };
710 let observed_responses = Arc::new(AtomicUsize::new(0));
711 let client = EasyHttpWebClient::new(connector)
712 .with_isolate_forward_proxy_auth_error(isolate)
713 .with_jit_layer((
714 MapInputLayer::new(move |mut request: Request| {
715 request.extensions().insert(stale_route.clone());
716 let stale_egress = Extensions::new();
717 stale_egress.insert(stale_route.clone());
718 request.extensions().insert(Egress(stale_egress));
719 request.headers_mut().insert(
720 PROXY_AUTHORIZATION,
721 HeaderValue::from_static("Basic downstream-secret"),
722 );
723 request
724 }),
725 MapOutputLayer::new({
726 let observed_responses = observed_responses.clone();
727 move |response: Response| {
728 observed_responses.fetch_add(1, Ordering::Relaxed);
729 response
730 }
731 }),
732 ));
733 let request = Request::builder()
734 .uri("http://origin.example/resource")
735 .body(Body::empty())
736 .unwrap();
737 request.extensions().insert(ProxyRoute::Proxy(
738 "http://wrong:request-secret@requested.example:8080"
739 .parse()
740 .unwrap(),
741 ));
742 let result = client.serve(request).await;
743 if isolate && is_forward {
744 assert!(result.is_err());
745 assert_eq!(observed_responses.load(Ordering::Relaxed), 0);
746 } else {
747 assert_eq!(
748 result.unwrap().status(),
749 StatusCode::PROXY_AUTHENTICATION_REQUIRED
750 );
751 assert_eq!(observed_responses.load(Ordering::Relaxed), 1);
752 }
753 }
754 }
755 }
756
757 #[test]
758 fn blocking_client_drives_the_composed_http_stack() {
759 let client = EasyHttpWebClient::connector_builder()
760 .with_custom_transport_connector(dummy_server())
761 .without_dns_connector()
762 .without_tls_proxy_support()
763 .without_proxy_support()
764 .without_tls_support()
765 .with_default_http_connector(Executor::default())
766 .without_connection_pool()
767 .build_client()
768 .try_into_blocking()
769 .unwrap();
770
771 let cloned = client.clone();
772 drop(client);
773 let response = cloned.get("http://example.com").send().unwrap();
774 assert_eq!(
775 response.try_into_json::<Output>().unwrap(),
776 Output { conn: 0, resp: 0 }
777 );
778 }
779
780 #[test]
781 fn default_blocking_http_client_is_cloneable_and_pooled() {
782 fn assert_default_client(_: &DefaultHttpWebClient) {}
783
784 let client = EasyHttpWebClient::try_blocking().unwrap();
785 assert_default_client(client.get_ref());
786 let cloned = client.clone();
787 drop(client);
788 let request = cloned.get("https://example.com").build().unwrap();
789 assert_eq!(request.uri(), &"https://example.com".parse().unwrap());
790 }
791
792 #[cfg(feature = "ws")]
793 #[test]
794 fn default_blocking_http_client_builds_websocket_requests() {
795 use crate::http::ws::handshake::client::BlockingHttpClientWebSocketExt as _;
796
797 let client = EasyHttpWebClient::try_blocking().unwrap();
798 let _from_url = client
799 .websocket("wss://example.com/chat")
800 .with_header("authorization", "Bearer secret");
801
802 let request = Request::builder()
803 .uri("wss://example.com/chat")
804 .body(Body::empty())
805 .unwrap();
806 let _from_request = client.websocket_with_request(request);
807 }
808
809 #[tokio::test]
810 async fn no_pool_tries_proxy_routes_in_order() {
811 let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
812 let transport = service_fn({
813 let attempts = attempts.clone();
814 let direct = dummy_server::<ConnectRequest>();
815 move |input: ConnectRequest| {
816 let attempts = attempts.clone();
817 let direct = direct.clone();
818 async move {
819 let route = input.extensions.get_ref::<ProxyRoute>().unwrap();
820 attempts.lock().push(route.clone());
821 if route.proxy_address().is_some() {
822 Err(ConnectionError::transport(
823 BoxError::from_static_str("proxy unavailable"),
824 ConnectionErrorKind::Unavailable,
825 ))
826 } else {
827 direct.connect(input).await
828 }
829 }
830 }
831 });
832 let client = EasyHttpWebClient::connector_builder()
833 .with_custom_transport_connector(transport)
834 .without_dns_connector()
835 .without_tls_proxy_support()
836 .with_custom_proxy_connector(())
837 .without_tls_support()
838 .with_default_http_connector(Executor::default())
839 .without_connection_pool()
840 .build_client();
841 let proxy = ProxyRoute::Proxy("http://proxy.example:8080".parse::<ProxyAddress>().unwrap());
842 let request = || {
843 let request = Request::builder()
844 .uri("http://example.com")
845 .body(Body::empty())
846 .unwrap();
847 request
848 .extensions()
849 .insert(ProxyRoutes::new([proxy.clone(), ProxyRoute::Direct]));
850 request
851 };
852
853 for _ in 0..2 {
854 client
855 .serve(request())
856 .await
857 .context("serve request through direct fallback")
858 .unwrap();
859 }
860
861 assert_eq!(
862 attempts.lock().as_slice(),
863 [proxy, ProxyRoute::Direct, ProxyRoute::Direct]
864 );
865 }
866
867 #[tokio::test]
868 async fn no_proxy_tls_support_rejects_https_proxy() {
869 let client = EasyHttpWebClient::connector_builder()
870 .with_custom_transport_connector(dummy_server())
871 .without_dns_connector()
872 .without_tls_proxy_support()
873 .with_proxy_support()
874 .without_tls_support()
875 .with_default_http_connector(Executor::default())
876 .without_connection_pool()
877 .build_client();
878 let request = Request::builder()
879 .uri("http://example.com")
880 .body(Body::empty())
881 .unwrap();
882 request.extensions().insert(ProxyRoutes::new([
883 ProxyRoute::Proxy(
884 "https://proxy.example:8443"
885 .parse::<ProxyAddress>()
886 .unwrap(),
887 ),
888 ProxyRoute::Direct,
889 ]));
890
891 let error =
892 ConnectionError::from(client.serve(request).await.unwrap_err().into_box_error());
893 assert_eq!(error.domain(), ConnectionErrorDomain::Transport);
894 assert_eq!(error.kind(), ConnectionErrorKind::Protocol);
895 }
896
897 #[tokio::test]
898 async fn easy_client_pools_plaintext_proxy_versions_separately() {
899 let proxy: ProxyAddress = "http://proxy.example:8080".parse().unwrap();
900 let dials = Arc::new(AtomicUsize::new(0));
901 let transport = service_fn({
902 let inner = dummy_server::<ConnectRequest>();
903 let proxy = proxy.clone();
904 let dials = dials.clone();
905 move |input: ConnectRequest| {
906 let inner = inner.clone();
907 let proxy = proxy.clone();
908 let dials = dials.clone();
909 async move {
910 assert_eq!(
911 input.extensions.get_ref::<ConnectorTarget>(),
912 Some(&ConnectorTarget(proxy.address.clone())),
913 );
914 assert_eq!(
915 input
916 .extensions
917 .get_ref::<ProxyRoute>()
918 .and_then(ProxyRoute::proxy_address),
919 Some(&proxy),
920 );
921 dials.fetch_add(1, Ordering::Relaxed);
922 inner.connect(input).await
923 }
924 }
925 });
926 let client = EasyHttpWebClient::connector_builder()
927 .with_custom_transport_connector(transport)
928 .without_dns_connector()
929 .without_tls_proxy_support()
930 .with_proxy_support()
931 .without_tls_support()
932 .with_default_http_connector(Executor::default())
933 .with_default_connection_pool()
934 .build_client();
935
936 for (version, expected_conn) in [(Version::HTTP_11, 0), (Version::HTTP_2, 1)] {
937 let request = Request::builder()
938 .uri("http://example.com")
939 .version(version)
940 .body(Body::empty())
941 .unwrap();
942 request
943 .extensions()
944 .insert(ProxyRoutes::from(proxy.clone()));
945
946 let response = client.serve(request).await.unwrap();
947 assert_eq!(response.version(), version);
948 assert_eq!(
949 response.try_into_json::<Output>().await.unwrap(),
950 Output {
951 conn: expected_conn,
952 resp: 0,
953 },
954 );
955 }
956 assert_eq!(dials.load(Ordering::Relaxed), 2);
957 }
958
959 #[cfg(feature = "socks5")]
960 #[tokio::test]
961 async fn umbrella_proxy_connector_falls_back_across_supported_plan() {
962 let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
963 let transport = service_fn({
964 let attempts = attempts.clone();
965 let direct = dummy_server::<ConnectRequest>();
966 move |input: ConnectRequest| {
967 let attempts = attempts.clone();
968 let direct = direct.clone();
969 async move {
970 let route = input.extensions.get_ref::<ProxyRoute>().unwrap().clone();
971 attempts.lock().push(route.clone());
972 if route.proxy_address().is_some() {
973 Err(ConnectionError::transport(
974 BoxError::from_static_str("proxy unavailable"),
975 ConnectionErrorKind::Unavailable,
976 ))
977 } else {
978 direct.connect(input).await
979 }
980 }
981 }
982 });
983 let client = EasyHttpWebClient::connector_builder()
984 .with_custom_transport_connector(transport)
985 .without_dns_connector()
986 .without_tls_proxy_support()
987 .with_proxy_support()
988 .without_tls_support()
989 .with_default_http_connector(Executor::default())
990 .without_connection_pool()
991 .build_client();
992 let request = Request::builder()
993 .uri("http://example.com")
994 .body(Body::empty())
995 .unwrap();
996 let socks = ProxyRoute::Proxy(
997 "socks5://socks.example:1080"
998 .parse::<ProxyAddress>()
999 .unwrap(),
1000 );
1001 let http = ProxyRoute::Proxy("http://http.example:8080".parse::<ProxyAddress>().unwrap());
1002 request.extensions().insert(ProxyRoutes::new([
1003 socks.clone(),
1004 http.clone(),
1005 ProxyRoute::Direct,
1006 ]));
1007
1008 let response = client.serve(request).await.unwrap();
1009 let output = response.try_into_json::<Output>().await.unwrap();
1010 assert_eq!(output, Output { conn: 0, resp: 0 });
1011 assert_eq!(
1012 attempts.lock().as_slice(),
1013 [socks, http, ProxyRoute::Direct]
1014 );
1015 }
1016
1017 #[tokio::test]
1018 async fn default_pool_caches_failed_route_and_reuses_selected_connection() {
1019 let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
1020 let transport = service_fn({
1021 let attempts = attempts.clone();
1022 let direct = dummy_server::<ConnectRequest>();
1023 move |input: ConnectRequest| {
1024 let attempts = attempts.clone();
1025 let direct = direct.clone();
1026 async move {
1027 let route = input.extensions.get_ref::<ProxyRoute>().unwrap();
1028 attempts.lock().push(route.clone());
1029 if route.proxy_address().is_some() {
1030 Err(ConnectionError::transport(
1031 BoxError::from_static_str("proxy unavailable"),
1032 ConnectionErrorKind::Unavailable,
1033 ))
1034 } else {
1035 direct.connect(input).await
1036 }
1037 }
1038 }
1039 });
1040 let client = EasyHttpWebClient::connector_builder()
1041 .with_custom_transport_connector(transport)
1042 .without_dns_connector()
1043 .without_tls_proxy_support()
1044 .with_custom_proxy_connector(())
1045 .without_tls_support()
1046 .with_default_http_connector(Executor::default())
1047 .with_default_connection_pool()
1048 .build_client();
1049 let proxy = ProxyRoute::Proxy("http://proxy.example:8080".parse::<ProxyAddress>().unwrap());
1050 let request = || {
1051 let request = Request::builder()
1052 .uri("http://example.com")
1053 .body(Body::empty())
1054 .unwrap();
1055 request
1056 .extensions()
1057 .insert(ProxyRoutes::new([proxy.clone(), ProxyRoute::Direct]));
1058 request
1059 };
1060
1061 for expected_response_index in 0..2 {
1062 let response = client.serve(request()).await.unwrap();
1063 let output = response.try_into_json::<Output>().await.unwrap();
1064 assert_eq!(output.conn, 0);
1065 assert_eq!(output.resp, expected_response_index);
1066 }
1067
1068 assert_eq!(attempts.lock().as_slice(), [proxy, ProxyRoute::Direct]);
1069 }
1070
1071 #[tokio::test]
1072 async fn easy_client_can_disable_proxy_route_failure_cache() {
1073 let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
1074 let transport = service_fn({
1075 let attempts = attempts.clone();
1076 let direct = dummy_server::<ConnectRequest>();
1077 move |input: ConnectRequest| {
1078 let attempts = attempts.clone();
1079 let direct = direct.clone();
1080 async move {
1081 let route = input.extensions.get_ref::<ProxyRoute>().unwrap();
1082 attempts.lock().push(route.clone());
1083 if route.proxy_address().is_some() {
1084 Err(ConnectionError::transport(
1085 BoxError::from_static_str("proxy unavailable"),
1086 ConnectionErrorKind::Unavailable,
1087 ))
1088 } else {
1089 direct.connect(input).await
1090 }
1091 }
1092 }
1093 });
1094 let client = EasyHttpWebClient::connector_builder()
1095 .with_custom_transport_connector(transport)
1096 .without_dns_connector()
1097 .without_tls_proxy_support()
1098 .with_custom_proxy_connector(())
1099 .without_tls_support()
1100 .with_default_http_connector(Executor::default())
1101 .without_proxy_route_failure_cache()
1102 .without_connection_pool()
1103 .build_client();
1104 let proxy = ProxyRoute::Proxy("http://proxy.example:8080".parse().unwrap());
1105
1106 for _ in 0..2 {
1107 let request = Request::builder()
1108 .uri("http://example.com")
1109 .body(Body::empty())
1110 .unwrap();
1111 request
1112 .extensions()
1113 .insert(ProxyRoutes::new([proxy.clone(), ProxyRoute::Direct]));
1114 client.serve(request).await.unwrap();
1115 }
1116
1117 assert_eq!(
1118 attempts.lock().as_slice(),
1119 [proxy.clone(), ProxyRoute::Direct, proxy, ProxyRoute::Direct]
1120 );
1121 }
1122
1123 #[tokio::test]
1124 async fn proxy_free_easy_client_omits_proxy_route_failure_cache() {
1125 let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
1126 let transport = service_fn({
1127 let attempts = attempts.clone();
1128 let direct = dummy_server::<ConnectRequest>();
1129 move |input: ConnectRequest| {
1130 let attempts = attempts.clone();
1131 let direct = direct.clone();
1132 async move {
1133 let route = input.extensions.get_ref::<ProxyRoute>().unwrap();
1134 attempts.lock().push(route.clone());
1135 if route.proxy_address().is_some() {
1136 Err(ConnectionError::transport(
1137 BoxError::from_static_str("proxy unavailable"),
1138 ConnectionErrorKind::Unavailable,
1139 ))
1140 } else {
1141 direct.connect(input).await
1142 }
1143 }
1144 }
1145 });
1146 let client = EasyHttpWebClient::connector_builder()
1147 .with_custom_transport_connector(transport)
1148 .without_dns_connector()
1149 .without_tls_proxy_support()
1150 .without_proxy_support()
1151 .without_tls_support()
1152 .with_default_http_connector(Executor::default())
1153 .without_connection_pool()
1154 .build_client();
1155 let proxy = ProxyRoute::Proxy("http://proxy.example:8080".parse().unwrap());
1156
1157 for _ in 0..2 {
1158 let request = Request::builder()
1159 .uri("http://example.com")
1160 .body(Body::empty())
1161 .unwrap();
1162 request
1163 .extensions()
1164 .insert(ProxyRoutes::new([proxy.clone(), ProxyRoute::Direct]));
1165 client.serve(request).await.unwrap();
1166 }
1167
1168 assert_eq!(
1169 attempts.lock().as_slice(),
1170 [proxy.clone(), ProxyRoute::Direct, proxy, ProxyRoute::Direct]
1171 );
1172 }
1173
1174 #[tokio::test]
1175 async fn easy_client_accepts_custom_proxy_route_failure_cache() {
1176 let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
1177 let transport = service_fn({
1178 let attempts = attempts.clone();
1179 let direct = dummy_server::<ConnectRequest>();
1180 move |input: ConnectRequest| {
1181 let attempts = attempts.clone();
1182 let direct = direct.clone();
1183 async move {
1184 let route = input.extensions.get_ref::<ProxyRoute>().unwrap();
1185 attempts.lock().push(route.clone());
1186 if route.proxy_address().is_some() {
1187 Err(ConnectionError::transport(
1188 BoxError::from_static_str("proxy unavailable"),
1189 ConnectionErrorKind::Unavailable,
1190 ))
1191 } else {
1192 direct.connect(input).await
1193 }
1194 }
1195 }
1196 });
1197 let mut failure_cache_config = ProxyRouteFailureCacheConfig::default();
1198 failure_cache_config.scope = ProxyRouteFailureCacheScope::PerProxy;
1199 let failure_cache = ProxyRouteFailureCache::try_new(failure_cache_config).unwrap();
1200 let client = EasyHttpWebClient::connector_builder()
1201 .with_custom_transport_connector(transport)
1202 .without_dns_connector()
1203 .without_tls_proxy_support()
1204 .without_proxy_support()
1205 .without_tls_support()
1206 .with_default_http_connector(Executor::default())
1207 .with_proxy_route_failure_cache(failure_cache)
1208 .without_connection_pool()
1209 .build_client();
1210 let proxy = ProxyRoute::Proxy("http://proxy.example:8080".parse().unwrap());
1211
1212 for destination in ["one.example", "two.example"] {
1213 let request = Request::builder()
1214 .uri(format!("http://{destination}"))
1215 .body(Body::empty())
1216 .unwrap();
1217 request
1218 .extensions()
1219 .insert(ProxyRoutes::new([proxy.clone(), ProxyRoute::Direct]));
1220 client.serve(request).await.unwrap();
1221 }
1222
1223 assert_eq!(
1224 attempts.lock().as_slice(),
1225 [proxy, ProxyRoute::Direct, ProxyRoute::Direct]
1226 );
1227 }
1228
1229 #[cfg(all(feature = "rustls", any(feature = "aws-lc", feature = "ring")))]
1230 #[tokio::test]
1231 async fn rustls_https_proxy_alpn_is_scoped_across_connect() {
1232 use crate::{
1233 extensions::ExtensionsRef as _,
1234 net::{
1235 Protocol,
1236 address::HostWithPort,
1237 client::{EstablishedClientConnection, ProxyRoute},
1238 stream::service::EchoService,
1239 },
1240 tls::{
1241 client::{NegotiatedTlsParameters, TlsClientConfig},
1242 rustls::{client::TlsConnector, server::TlsAcceptorLayer},
1243 server::{GeneratedServerAuthConfig, ServerAuthData, TlsServerConfig},
1244 },
1245 };
1246 use rama_core::ServiceInput;
1247 use rama_crypto::cert::generate_server_auth;
1248 use rama_http::io::upgrade::handle_upgrade;
1249 use rama_http_backend::client::proxy::layer::HttpProxyConnectorLayer;
1250 use rama_net::http::TargetHttpVersion;
1251 use std::sync::Arc;
1252
1253 let (proxy_chain, proxy_key) =
1254 generate_server_auth(GeneratedServerAuthConfig::default()).expect("proxy auth");
1255 let proxy_trust = proxy_chain.last().expect("proxy trust anchor").clone();
1256 let (origin_chain, origin_key) =
1257 generate_server_auth(GeneratedServerAuthConfig::default()).expect("origin auth");
1258 let origin_trust = origin_chain.last().expect("origin trust anchor").clone();
1259
1260 let origin_server =
1261 TlsAcceptorLayer::new(TlsServerConfig::new().with_single_cert(ServerAuthData {
1262 cert_chain: origin_chain,
1263 private_key: origin_key,
1264 ocsp: None,
1265 }))
1266 .into_layer(EchoService::new());
1267 let (origin_done_tx, origin_done_rx) = tokio::sync::oneshot::channel();
1268 let origin_done_tx = Arc::new(parking_lot::Mutex::new(Some(origin_done_tx)));
1269
1270 let connect_version = Arc::new(parking_lot::Mutex::new(None));
1271 let observed_version = connect_version.clone();
1272 let proxy_http =
1273 HttpServer::auto(Executor::default()).service(service_fn(move |req: Request| {
1274 let origin_server = origin_server.clone();
1275 let origin_done_tx = origin_done_tx.clone();
1276 let observed_version = observed_version.clone();
1277 async move {
1278 assert_eq!(req.method(), rama_http::Method::CONNECT);
1279 *observed_version.lock() = Some(req.version());
1280 let upgrade = handle_upgrade(&req);
1281 tokio::spawn(async move {
1282 let tunnel = upgrade.await.expect("server CONNECT upgrade");
1283 let _origin_result = origin_server.serve(tunnel).await;
1287 if let Some(tx) = origin_done_tx.lock().take() {
1288 tx.send(()).expect("origin completion receiver");
1289 }
1290 });
1291 Ok::<_, Infallible>(Response::new(Body::empty()))
1292 }
1293 }));
1294 let proxy_server = TlsAcceptorLayer::new(
1295 TlsServerConfig::new()
1296 .with_single_cert(ServerAuthData {
1297 cert_chain: proxy_chain,
1298 private_key: proxy_key,
1299 ocsp: None,
1300 })
1301 .with_alpn_http_2(),
1302 )
1303 .into_layer(proxy_http);
1304
1305 let (client_io, server_io) = tokio::io::duplex(64 * 1024);
1306 let client_io = Arc::new(parking_lot::Mutex::new(Some(client_io)));
1307 let transport = service_fn(move |input: ConnectRequest| {
1308 let conn = ServiceInput::new(client_io.lock().take().expect("one proxy connection"));
1309 async move { Ok::<_, ConnectionError>(EstablishedClientConnection { input, conn }) }
1310 });
1311
1312 let proxy_config = TlsClientConfig::new()
1313 .with_alpn_http_2()
1314 .with_server_name(crate::net::address::Host::from_static("localhost"))
1315 .try_with_server_trust_anchors([proxy_trust])
1316 .expect("proxy trust");
1317 let proxy_tls = TlsConnector::tunnel(transport, None).with_base_config(proxy_config);
1318 let proxy = HttpProxyConnectorLayer::default().into_layer(proxy_tls);
1319 let origin_config = TlsClientConfig::new()
1320 .with_alpn(Default::default())
1321 .with_server_name(crate::net::address::Host::from_static("localhost"))
1322 .try_with_server_trust_anchors([origin_trust])
1323 .expect("origin trust");
1324 let connector = TlsConnector::auto(proxy).with_base_config(origin_config);
1325
1326 let input = ConnectRequest::new(HostWithPort::try_from("localhost:443").unwrap())
1327 .with_application_protocol(Protocol::HTTPS);
1328 input
1329 .extensions
1330 .insert(ProxyRoute::Proxy("https://localhost:8443".parse().unwrap()));
1331 let client = async move {
1332 let established = Box::pin(connector.serve(input))
1333 .await
1334 .expect("two TLS handshakes");
1335
1336 assert_eq!(*connect_version.lock(), Some(Version::HTTP_2));
1337 assert_eq!(
1338 established
1339 .conn
1340 .extensions()
1341 .get_ref::<NegotiatedTlsParameters>()
1342 .expect("origin TLS parameters")
1343 .application_layer_protocol,
1344 None
1345 );
1346 assert!(
1347 established
1348 .conn
1349 .extensions()
1350 .get_ref::<TargetHttpVersion>()
1351 .is_none(),
1352 "proxy HTTP/2 must not leak past CONNECT into a no-ALPN origin"
1353 );
1354 drop(established);
1355 };
1356 let (proxy_result, ()) =
1357 Box::pin(tokio::time::timeout(Duration::from_secs(5), async move {
1358 tokio::join!(proxy_server.serve(ServiceInput::new(server_io)), client)
1359 }))
1360 .await
1361 .expect("proxy/origin exchange");
1362 proxy_result.expect("proxy server");
1363 tokio::time::timeout(Duration::from_secs(5), origin_done_rx)
1364 .await
1365 .expect("origin server shutdown")
1366 .expect("origin completion signal");
1367 }
1368
1369 #[cfg(feature = "boring")]
1370 #[tokio::test]
1371 async fn boring_https_proxy_alpn_is_scoped_across_connect() {
1372 use crate::{
1373 extensions::ExtensionsRef as _,
1374 net::{
1375 Protocol,
1376 address::HostWithPort,
1377 client::{EstablishedClientConnection, ProxyRoute},
1378 stream::service::EchoService,
1379 },
1380 tls::{
1381 boring::{client::TlsConnector, server::TlsAcceptorLayer},
1382 client::{NegotiatedTlsParameters, TlsClientConfig},
1383 server::{GeneratedServerAuthConfig, ServerAuthData, TlsServerConfig},
1384 },
1385 };
1386 use rama_core::ServiceInput;
1387 use rama_crypto::cert::generate_server_auth;
1388 use rama_http::io::upgrade::handle_upgrade;
1389 use rama_http_backend::client::proxy::layer::HttpProxyConnectorLayer;
1390 use rama_net::http::TargetHttpVersion;
1391 use std::sync::Arc;
1392
1393 let (proxy_chain, proxy_key) =
1394 generate_server_auth(GeneratedServerAuthConfig::default()).expect("proxy auth");
1395 let proxy_trust = proxy_chain.last().expect("proxy trust anchor").clone();
1396 let (origin_chain, origin_key) =
1397 generate_server_auth(GeneratedServerAuthConfig::default()).expect("origin auth");
1398 let origin_trust = origin_chain.last().expect("origin trust anchor").clone();
1399
1400 let origin_server =
1401 TlsAcceptorLayer::new(TlsServerConfig::new().with_single_cert(ServerAuthData {
1402 cert_chain: origin_chain,
1403 private_key: origin_key,
1404 ocsp: None,
1405 }))
1406 .into_layer(EchoService::new());
1407 let (origin_done_tx, origin_done_rx) = tokio::sync::oneshot::channel();
1408 let origin_done_tx = Arc::new(parking_lot::Mutex::new(Some(origin_done_tx)));
1409
1410 let connect_version = Arc::new(parking_lot::Mutex::new(None));
1411 let observed_version = connect_version.clone();
1412 let proxy_http =
1413 HttpServer::auto(Executor::default()).service(service_fn(move |req: Request| {
1414 let origin_server = origin_server.clone();
1415 let origin_done_tx = origin_done_tx.clone();
1416 let observed_version = observed_version.clone();
1417 async move {
1418 assert_eq!(req.method(), rama_http::Method::CONNECT);
1419 *observed_version.lock() = Some(req.version());
1420 let upgrade = handle_upgrade(&req);
1421 tokio::spawn(async move {
1422 let tunnel = upgrade.await.expect("server CONNECT upgrade");
1423 let _origin_result = origin_server.serve(tunnel).await;
1427 if let Some(tx) = origin_done_tx.lock().take() {
1428 tx.send(()).expect("origin completion receiver");
1429 }
1430 });
1431 Ok::<_, Infallible>(Response::new(Body::empty()))
1432 }
1433 }));
1434 let proxy_server = TlsAcceptorLayer::new(
1435 TlsServerConfig::new()
1436 .with_single_cert(ServerAuthData {
1437 cert_chain: proxy_chain,
1438 private_key: proxy_key,
1439 ocsp: None,
1440 })
1441 .with_alpn_http_2(),
1442 )
1443 .into_layer(proxy_http);
1444
1445 let (client_io, server_io) = tokio::io::duplex(64 * 1024);
1446 let client_io = Arc::new(parking_lot::Mutex::new(Some(client_io)));
1447 let transport = service_fn(move |input: ConnectRequest| {
1448 let conn = ServiceInput::new(client_io.lock().take().expect("one proxy connection"));
1449 async move { Ok::<_, ConnectionError>(EstablishedClientConnection { input, conn }) }
1450 });
1451
1452 let proxy_config = TlsClientConfig::new()
1453 .with_alpn_http_2()
1454 .with_server_name(crate::net::address::Host::from_static("localhost"))
1455 .try_with_server_trust_anchors([proxy_trust])
1456 .expect("proxy trust");
1457 let proxy_tls = TlsConnector::tunnel(transport, None).with_base_config(proxy_config);
1458 let proxy = HttpProxyConnectorLayer::default().into_layer(proxy_tls);
1459 let origin_config = TlsClientConfig::new()
1460 .with_alpn(Default::default())
1461 .with_server_name(crate::net::address::Host::from_static("localhost"))
1462 .try_with_server_trust_anchors([origin_trust])
1463 .expect("origin trust");
1464 let connector = TlsConnector::auto(proxy).with_base_config(origin_config);
1465
1466 let input = ConnectRequest::new(HostWithPort::try_from("localhost:443").unwrap())
1467 .with_application_protocol(Protocol::HTTPS);
1468 input
1469 .extensions
1470 .insert(ProxyRoute::Proxy("https://localhost:8443".parse().unwrap()));
1471 let client = async move {
1472 let established = connector.serve(input).await.expect("two TLS handshakes");
1473
1474 assert_eq!(*connect_version.lock(), Some(Version::HTTP_2));
1475 assert_eq!(
1476 established
1477 .conn
1478 .extensions()
1479 .get_ref::<NegotiatedTlsParameters>()
1480 .expect("origin TLS parameters")
1481 .application_layer_protocol,
1482 None
1483 );
1484 assert!(
1485 established
1486 .conn
1487 .extensions()
1488 .get_ref::<TargetHttpVersion>()
1489 .is_none(),
1490 "proxy HTTP/2 must not leak past CONNECT into a no-ALPN origin"
1491 );
1492 drop(established);
1493 };
1494 let (proxy_result, ()) =
1495 Box::pin(tokio::time::timeout(Duration::from_secs(5), async move {
1496 tokio::join!(proxy_server.serve(ServiceInput::new(server_io)), client)
1497 }))
1498 .await
1499 .expect("proxy/origin exchange");
1500 proxy_result.expect("proxy server");
1501 tokio::time::timeout(Duration::from_secs(5), origin_done_rx)
1502 .await
1503 .expect("origin server shutdown")
1504 .expect("origin completion signal");
1505 }
1506
1507 #[cfg(feature = "boring")]
1508 #[test]
1509 fn proxy_failure_cache_keeps_tls_client_future_bounded() {
1510 let client = EasyHttpWebClient::connector_builder()
1511 .with_default_transport_connector()
1512 .with_default_dns_connector()
1513 .without_tls_proxy_support()
1514 .with_proxy_support()
1515 .with_tls_support_using_boringssl_and_default_http_version(
1516 crate::tls::client::TlsClientConfig::default_http(),
1517 Version::HTTP_11,
1518 )
1519 .with_default_http_connector(Executor::default())
1520 .without_connection_pool()
1521 .build_client();
1522 let request = Request::builder()
1523 .uri("https://example.com")
1524 .body(Body::empty())
1525 .unwrap();
1526
1527 let future = client.serve(request);
1528 let future_size = std::mem::size_of_val(&future);
1529
1530 assert!(
1531 future_size < 64 * 1024,
1532 "easy TLS client future is unexpectedly large: {future_size} bytes"
1533 );
1534 }
1535
1536 #[tokio::test]
1537 async fn connection_is_in_use_until_response_body_is_consumed() {
1538 let client = EasyHttpWebClient::connector_builder()
1539 .with_custom_transport_connector(dummy_server())
1540 .without_dns_connector()
1541 .without_tls_proxy_support()
1542 .without_proxy_support()
1543 .without_tls_support()
1544 .with_default_http_connector(Executor::default())
1545 .try_with_connection_pool(HttpPooledConnectorConfig {
1546 max_concurrent_streams: 1,
1547 max_total: 4,
1548 ..Default::default()
1549 })
1550 .unwrap()
1551 .build_client();
1552
1553 let req = || {
1554 Request::builder()
1555 .uri("http://example.com")
1556 .version(Version::HTTP_2)
1557 .body(Body::empty())
1558 .unwrap()
1559 };
1560
1561 let res1 = client.serve(req()).await.unwrap();
1565 let res2 = client.serve(req()).await.unwrap();
1566
1567 let out2 = res2.try_into_json::<Output>().await.unwrap();
1569 let out1 = res1.try_into_json::<Output>().await.unwrap();
1570
1571 assert_eq!(out1.conn, 0, "first request uses the first connection");
1572 assert_eq!(
1575 out2.conn, 1,
1576 "second request must not reuse a connection whose response body is still in flight"
1577 );
1578 }
1579
1580 #[tokio::test]
1584 async fn default_pool_multiplexes_on_h2() {
1585 let client = EasyHttpWebClient::connector_builder()
1586 .with_custom_transport_connector(dummy_server())
1587 .without_dns_connector()
1588 .without_tls_proxy_support()
1589 .without_proxy_support()
1590 .without_tls_support()
1591 .with_default_http_connector(Executor::default())
1592 .with_default_connection_pool()
1593 .build_client();
1594
1595 let req = || {
1596 Request::builder()
1597 .uri("http://example.com")
1598 .version(Version::HTTP_2)
1599 .body(Body::empty())
1600 .unwrap()
1601 };
1602 let (res1, res2, res3) = tokio::join!(
1603 client.serve(req()),
1604 client.serve(req()),
1605 client.serve(req()),
1606 );
1607
1608 for (i, res) in [res1, res2, res3].into_iter().enumerate() {
1610 let out = res.unwrap().try_into_json::<Output>().await.unwrap();
1611 assert_eq!(out.conn, 0);
1612 assert_eq!(out.resp, i);
1613 }
1614 }
1615
1616 #[tokio::test]
1617 async fn default_pool_does_not_multiplexes_on_h1() {
1618 let client = EasyHttpWebClient::connector_builder()
1619 .with_custom_transport_connector(dummy_server())
1620 .without_dns_connector()
1621 .without_tls_proxy_support()
1622 .without_proxy_support()
1623 .without_tls_support()
1624 .with_default_http_connector(Executor::default())
1625 .with_default_connection_pool()
1626 .build_client();
1627
1628 let req = || {
1629 Request::builder()
1630 .uri("http://example.com")
1631 .version(Version::HTTP_11)
1632 .body(Body::empty())
1633 .unwrap()
1634 };
1635 let (res1, res2, res3) = tokio::join!(
1636 client.serve(req()),
1637 client.serve(req()),
1638 client.serve(req()),
1639 );
1640
1641 for (i, res) in [res1, res2, res3].into_iter().enumerate() {
1644 let out = res.unwrap().try_into_json::<Output>().await.unwrap();
1645 assert_eq!(out.conn, i);
1646 assert_eq!(out.resp, 0);
1647 }
1648 }
1649
1650 #[tokio::test]
1651 async fn multiplex_on_h2_respects_limits() {
1652 let client = EasyHttpWebClient::connector_builder()
1653 .with_custom_transport_connector(dummy_server())
1654 .without_dns_connector()
1655 .without_tls_proxy_support()
1656 .without_proxy_support()
1657 .without_tls_support()
1658 .with_default_http_connector(Executor::default())
1659 .try_with_connection_pool(HttpPooledConnectorConfig {
1660 max_concurrent_streams: 2,
1661 ..Default::default()
1662 })
1663 .unwrap()
1664 .build_client();
1665
1666 let req = || {
1667 Request::builder()
1668 .uri("http://example.com")
1669 .version(Version::HTTP_2)
1670 .body(Body::empty())
1671 .unwrap()
1672 };
1673 let (res1, res2, res3, res4) = tokio::join!(
1674 client.serve(req()),
1675 client.serve(req()),
1676 client.serve(req()),
1677 client.serve(req()),
1678 );
1679
1680 for (i, res) in [res1, res2, res3, res4].into_iter().enumerate() {
1682 let out = res.unwrap().try_into_json::<Output>().await.unwrap();
1683 assert_eq!(out.conn, i / 2);
1684 assert_eq!(out.resp, i % 2);
1685 }
1686 }
1687}