1use std::{fmt, io};
7
8use crate::{
9 Layer, Service,
10 error::BoxError,
11 extensions::ExtensionsRef,
12 graceful::WeakScope,
13 http::{Body as ResponseBody, Request, Response, StreamingBody},
14 net::client::EstablishedClientConnection,
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::tls::boring::client::TlsClientSessionCache, std::sync::Arc};
43
44#[cfg(all(feature = "boring", feature = "quic"))]
45use crate::quic::tls::BoringTlsProvider;
46
47#[cfg(all(
48 feature = "quic",
49 any(
50 feature = "boring",
51 feature = "masque",
52 all(feature = "rustls", any(feature = "ring", feature = "aws-lc"))
53 )
54))]
55use crate::quic::TransportConfig;
56
57#[cfg(all(feature = "masque", feature = "quic"))]
58use crate::{
59 proxy::masque::connect_udp::{OUTER_IDLE_TIMEOUT, OUTER_INITIAL_MTU},
60 quic::{IdleTimeout, proto::VarInt},
61};
62
63pub mod builder;
64#[doc(inline)]
65pub use builder::EasyHttpConnectorBuilder;
66#[cfg(all(feature = "masque", feature = "quic"))]
67#[doc(inline)]
68pub use builder::MasqueQuicEndpoints;
69
70#[cfg(feature = "socks5")]
71mod proxy_connector;
72#[cfg(feature = "socks5")]
73#[cfg_attr(docsrs, doc(cfg(feature = "socks5")))]
74#[doc(inline)]
75pub use proxy_connector::{MaybeProxiedConnection, ProxyConnector, ProxyConnectorLayer};
76
77pub struct EasyHttpWebClient<BodyIn, ConnResponse, L> {
94 connector: BoxService<Request<BodyIn>, ConnResponse, OpaqueError>,
95 forward_proxy_layer: HttpForwardProxyLayer,
96 plaintext_http_proxy_mode: Option<PlaintextHttpProxyMode>,
97 jit_layers: L,
98}
99
100impl<BodyIn, ConnResponse, L> fmt::Debug for EasyHttpWebClient<BodyIn, ConnResponse, L> {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.debug_struct("EasyHttpWebClient").finish()
103 }
104}
105
106impl<BodyIn, ConnResponse, L: Clone> Clone for EasyHttpWebClient<BodyIn, ConnResponse, L> {
107 fn clone(&self) -> Self {
108 Self {
109 connector: self.connector.clone(),
110 forward_proxy_layer: self.forward_proxy_layer.clone(),
111 plaintext_http_proxy_mode: self.plaintext_http_proxy_mode,
112 jit_layers: self.jit_layers.clone(),
113 }
114 }
115}
116
117impl EasyHttpWebClient<(), (), ()> {
118 #[must_use]
120 pub fn connector_builder() -> EasyHttpConnectorBuilder {
121 EasyHttpConnectorBuilder::new()
122 }
123
124 pub fn try_blocking() -> io::Result<BlockingHttpWebClient> {
143 BlockingHttpClient::try_new(EasyHttpWebClient::default())
144 }
145}
146
147pub type DefaultHttpWebClient<Body = crate::http::Body> = EasyHttpWebClient<
158 Body,
159 EstablishedClientConnection<
160 rama_http::layer::alt_svc::AltSvc<
161 BindBodyToConn<
162 crate::net::client::pool::MultiplexedConnection<
163 HttpClientService<Body>,
164 HttpConnId,
165 >,
166 >,
167 >,
168 Request<Body>,
169 >,
170 (),
171>;
172
173pub type BlockingHttpWebClient = BlockingHttpClient<DefaultHttpWebClient>;
175
176impl<Body> Default for DefaultHttpWebClient<Body>
177where
178 Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
179{
180 #[inline(always)]
181 fn default() -> Self {
182 Self::default_with_scope(WeakScope::detached())
183 }
184}
185
186impl<Body> DefaultHttpWebClient<Body>
187where
188 Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
189{
190 core::cfg_select! {
191 all(feature = "boring", feature = "quic") => {
192 pub fn default_with_scope(scope: impl Into<WeakScope>) -> Self {
193 let scope = scope.into();
194 #[expect(
195 clippy::expect_used,
196 reason = "fixed default H3 limits and BoringSSL TLS configuration are valid"
197 )]
198 Self::boringssl_default(scope, TlsClientConfig::default_http(), None)
199 .expect("default BoringSSL HTTP/3 configuration is valid")
200 }
201
202 #[cfg(feature = "masque")]
203 #[cfg_attr(docsrs, doc(cfg(feature = "masque")))]
204 pub fn default_masque_proxy_client_with_scope(scope: impl Into<WeakScope>) -> Self {
208 let scope = scope.into();
209 #[expect(
210 clippy::expect_used,
211 reason = "fixed default H3 limits and BoringSSL TLS configuration are valid"
212 )]
213 Self::try_default_masque_proxy_client_using_boringssl(
214 scope,
215 TlsClientConfig::default_http(),
216 )
217 .expect("default BoringSSL HTTP/3 configuration is valid")
218 }
219
220 #[cfg(feature = "masque")]
221 #[cfg_attr(docsrs, doc(cfg(feature = "masque")))]
222 pub fn try_default_masque_proxy_client_using_boringssl(
226 scope: impl Into<WeakScope>,
227 proxy_tls: TlsClientConfig,
228 ) -> Result<Self, BoxError> {
229 let scope = scope.into();
230 Self::boringssl_default(scope, proxy_tls, Some(masque_quic_transport()))
231 }
232
233 #[cfg(feature = "masque")]
234 #[cfg_attr(docsrs, doc(cfg(feature = "masque")))]
235 pub fn default_masque_with_scope(scope: impl Into<WeakScope>) -> Self {
243 let scope = scope.into();
244 #[expect(
245 clippy::expect_used,
246 reason = "fixed default H3 limits and BoringSSL TLS configuration are valid"
247 )]
248 Self::try_default_masque_using_boringssl(
249 scope,
250 TlsClientConfig::default_http(),
251 TlsClientConfig::default_http(),
252 )
253 .expect("default BoringSSL HTTP/3 configuration is valid")
254 }
255
256 #[cfg(feature = "masque")]
257 #[cfg_attr(docsrs, doc(cfg(feature = "masque")))]
258 pub fn try_default_masque_using_boringssl(
261 scope: impl Into<WeakScope>,
262 tls: TlsClientConfig,
263 proxy_tls: TlsClientConfig,
264 ) -> Result<Self, BoxError> {
265 let scope = scope.into();
266 let h3 = Http3Connector::builder(scope.clone())
267 .with_tls_provider(Arc::new(BoringTlsProvider))
268 .with_tls_config(tls.clone());
269 let proxy_client =
270 DefaultHttpWebClient::<crate::http::Body>::try_default_masque_proxy_client_using_boringssl(
271 scope.clone(),
272 proxy_tls.clone(),
273 )?;
274 Ok(EasyHttpConnectorBuilder::new()
275 .with_default_transport_connector()
276 .with_default_dns_connector()
277 .with_tls_proxy_support_using_boringssl_config(proxy_tls)
278 .with_proxy_support()
279 .with_tls_support_using_boringssl(tls)
280 .with_tls_session_store(Arc::new(TlsClientSessionCache::default()))
281 .with_default_http_connector(scope)
282 .with_http3_masque_support(h3, proxy_client)?
283 .with_default_connection_pool()
284 .build_client())
285 }
286
287 fn boringssl_default(
288 scope: WeakScope,
289 tls: TlsClientConfig,
290 transport: Option<TransportConfig>,
291 ) -> Result<Self, BoxError> {
292 let h3 = Http3Connector::builder(scope.clone())
293 .with_tls_provider(Arc::new(BoringTlsProvider))
294 .with_tls_config(tls.clone())
295 .maybe_with_transport(transport)
296 .build_lazy()?;
297 Ok(EasyHttpConnectorBuilder::new()
298 .with_default_transport_connector()
299 .with_default_dns_connector()
300 .with_tls_proxy_support_using_boringssl()
301 .with_proxy_support()
302 .with_tls_support_using_boringssl(tls)
303 .with_tls_session_store(Arc::new(TlsClientSessionCache::default()))
304 .with_default_http_connector(scope)
305 .with_http3_support(h3)
306 .with_default_connection_pool()
307 .build_client())
308 }
309 }
310 all(feature = "rustls", any(feature = "ring", feature = "aws-lc"), feature = "quic") => {
311 pub fn default_with_scope(scope: impl Into<WeakScope>) -> Self {
312 let scope = scope.into();
313 #[expect(
314 clippy::expect_used,
315 reason = "fixed default H3 limits and Rustls TLS configuration are valid"
316 )]
317 Self::rustls_default(scope, TlsClientConfig::default_http(), None)
318 .expect("default Rustls HTTP/3 configuration is valid")
319 }
320
321 #[cfg(feature = "masque")]
322 #[cfg_attr(docsrs, doc(cfg(feature = "masque")))]
323 pub fn default_masque_proxy_client_with_scope(scope: impl Into<WeakScope>) -> Self {
327 let scope = scope.into();
328 #[expect(
329 clippy::expect_used,
330 reason = "fixed default H3 limits and Rustls TLS configuration are valid"
331 )]
332 Self::try_default_masque_proxy_client_using_rustls(
333 scope,
334 TlsClientConfig::default_http(),
335 )
336 .expect("default Rustls HTTP/3 configuration is valid")
337 }
338
339 #[cfg(feature = "masque")]
340 #[cfg_attr(docsrs, doc(cfg(feature = "masque")))]
341 pub fn try_default_masque_proxy_client_using_rustls(
345 scope: impl Into<WeakScope>,
346 proxy_tls: TlsClientConfig,
347 ) -> Result<Self, BoxError> {
348 let scope = scope.into();
349 Self::rustls_default(scope, proxy_tls, Some(masque_quic_transport()))
350 }
351
352 #[cfg(feature = "masque")]
353 #[cfg_attr(docsrs, doc(cfg(feature = "masque")))]
354 pub fn default_masque_with_scope(scope: impl Into<WeakScope>) -> Self {
362 let scope = scope.into();
363 #[expect(
364 clippy::expect_used,
365 reason = "fixed default H3 limits and Rustls TLS configuration are valid"
366 )]
367 Self::try_default_masque_using_rustls(
368 scope,
369 TlsClientConfig::default_http(),
370 TlsClientConfig::default_http(),
371 )
372 .expect("default Rustls HTTP/3 configuration is valid")
373 }
374
375 #[cfg(feature = "masque")]
376 #[cfg_attr(docsrs, doc(cfg(feature = "masque")))]
377 pub fn try_default_masque_using_rustls(
380 scope: impl Into<WeakScope>,
381 tls: TlsClientConfig,
382 proxy_tls: TlsClientConfig,
383 ) -> Result<Self, BoxError> {
384 let scope = scope.into();
385 let h3 = Http3Connector::builder(scope.clone())
386 .with_tls_config(tls.clone());
387 let proxy_client =
388 DefaultHttpWebClient::<crate::http::Body>::try_default_masque_proxy_client_using_rustls(
389 scope.clone(),
390 proxy_tls.clone(),
391 )?;
392 Ok(EasyHttpConnectorBuilder::new()
393 .with_default_transport_connector()
394 .with_default_dns_connector()
395 .with_tls_proxy_support_using_rustls_config(proxy_tls)
396 .with_proxy_support()
397 .with_tls_support_using_rustls(tls)
398 .with_default_http_connector(scope)
399 .with_http3_masque_support(h3, proxy_client)?
400 .with_default_connection_pool()
401 .build_client())
402 }
403
404 fn rustls_default(
405 scope: WeakScope,
406 tls: TlsClientConfig,
407 transport: Option<TransportConfig>,
408 ) -> Result<Self, BoxError> {
409 let h3 = Http3Connector::builder(scope.clone())
410 .with_tls_config(tls.clone())
411 .maybe_with_transport(transport)
412 .build_lazy()?;
413 Ok(EasyHttpConnectorBuilder::new()
414 .with_default_transport_connector()
415 .with_default_dns_connector()
416 .with_tls_proxy_support_using_rustls()
417 .with_proxy_support()
418 .with_tls_support_using_rustls(tls)
419 .with_default_http_connector(scope)
420 .with_http3_support(h3)
421 .with_default_connection_pool()
422 .build_client())
423 }
424 }
425 feature = "boring" => {
426 pub fn default_with_scope(scope: impl Into<WeakScope>) -> Self {
427 let scope = scope.into();
428 EasyHttpConnectorBuilder::new()
429 .with_default_transport_connector()
430 .with_default_dns_connector()
431 .with_tls_proxy_support_using_boringssl()
432 .with_proxy_support()
433 .with_tls_support_using_boringssl(TlsClientConfig::default_http())
434 .with_tls_session_store(Arc::new(TlsClientSessionCache::default()))
435 .with_default_http_connector(scope)
436 .with_default_connection_pool()
437 .build_client()
438 }
439 }
440 feature = "rustls" => {
441 pub fn default_with_scope(scope: impl Into<WeakScope>) -> Self {
442 let scope = scope.into();
443 EasyHttpConnectorBuilder::new()
444 .with_default_transport_connector()
445 .with_default_dns_connector()
446 .with_tls_proxy_support_using_rustls()
447 .with_proxy_support()
448 .with_tls_support_using_rustls(TlsClientConfig::default_http())
449 .with_default_http_connector(scope)
450 .with_default_connection_pool()
451 .build_client()
452 }
453 }
454 _ => {
455 pub fn default_with_scope(scope: impl Into<WeakScope>) -> Self {
456 let scope = scope.into();
457 EasyHttpConnectorBuilder::new()
458 .with_default_transport_connector()
459 .with_default_dns_connector()
460 .without_tls_proxy_support()
461 .with_proxy_support()
462 .without_tls_support()
463 .with_default_http_connector(scope)
464 .with_default_connection_pool()
465 .build_client()
466 }
467 }
468 }
469}
470
471#[cfg(all(feature = "masque", feature = "quic"))]
472#[cfg_attr(docsrs, doc(cfg(all(feature = "masque", feature = "quic"))))]
473#[must_use]
478pub fn masque_quic_transport() -> TransportConfig {
479 let idle = u32::try_from(OUTER_IDLE_TIMEOUT.as_millis()).unwrap_or(u32::MAX);
480 TransportConfig::default()
481 .with_initial_mtu(OUTER_INITIAL_MTU)
482 .with_max_idle_timeout(IdleTimeout::from(VarInt::from_u32(idle)))
483}
484
485impl<BodyIn, ConnResponse> EasyHttpWebClient<BodyIn, ConnResponse, ()>
486where
487 BodyIn: Send + 'static,
488{
489 #[must_use]
495 pub fn new<S>(connector: S) -> Self
496 where
497 S: Service<Request<BodyIn>, Output = ConnResponse, Error: Into<BoxError>>,
498 {
499 Self {
500 connector: MapErr::into_opaque_error(connector).boxed(),
501 forward_proxy_layer: HttpForwardProxyLayer::new(),
502 plaintext_http_proxy_mode: None,
503 jit_layers: (),
504 }
505 }
506}
507
508impl<BodyIn, ConnResponse, L> EasyHttpWebClient<BodyIn, ConnResponse, L> {
509 pub fn try_into_blocking(self) -> io::Result<BlockingHttpClient<Self>> {
512 BlockingHttpClient::try_new(self)
513 }
514
515 #[must_use]
518 pub fn into_blocking_with_runtime(
519 self,
520 runtime: &crate::rt::blocking::Runtime,
521 ) -> BlockingHttpClient<Self> {
522 BlockingHttpClient::with_runtime(self, runtime)
523 }
524
525 #[must_use]
529 pub fn with_connector<S, BodyInNew, ConnResponseNew>(
530 self,
531 connector: S,
532 ) -> EasyHttpWebClient<BodyInNew, ConnResponseNew, L>
533 where
534 S: Service<Request<BodyInNew>, Output = ConnResponseNew, Error: Into<BoxError>>,
535 BodyInNew: Send + 'static,
536 {
537 EasyHttpWebClient {
538 connector: MapErr::into_opaque_error(connector).boxed(),
539 forward_proxy_layer: self.forward_proxy_layer,
540 plaintext_http_proxy_mode: self.plaintext_http_proxy_mode,
541 jit_layers: self.jit_layers,
542 }
543 }
544
545 pub fn with_jit_layer<T>(self, jit_layers: T) -> EasyHttpWebClient<BodyIn, ConnResponse, T> {
557 EasyHttpWebClient {
558 connector: self.connector,
559 forward_proxy_layer: self.forward_proxy_layer,
560 plaintext_http_proxy_mode: self.plaintext_http_proxy_mode,
561 jit_layers,
562 }
563 }
564
565 crate::utils::macros::generate_set_and_with! {
566 pub fn forward_proxy_auth(mut self, enabled: bool) -> Self {
578 self.forward_proxy_layer.set_proxy_auth(enabled);
579 self
580 }
581 }
582
583 #[must_use]
587 pub fn without_forward_proxy_auth(self) -> Self {
588 self.with_forward_proxy_auth(false)
589 }
590
591 crate::utils::macros::generate_set_and_with! {
592 pub fn tunnel_plaintext_http(mut self, enabled: bool) -> Self {
604 self.plaintext_http_proxy_mode = Some(if enabled {
605 PlaintextHttpProxyMode::Tunnel
606 } else {
607 PlaintextHttpProxyMode::Forward
608 });
609 self
610 }
611 }
612
613 crate::utils::macros::generate_set_and_with! {
614 pub fn isolate_forward_proxy_auth_error(mut self, enabled: bool) -> Self {
621 self.forward_proxy_layer.set_isolate_auth_error(enabled);
622 self
623 }
624 }
625}
626
627impl<Body, ConnectionBody, Connection, IncomingBody, L> Service<Request<Body>>
628 for EasyHttpWebClient<Body, EstablishedClientConnection<Connection, Request<ConnectionBody>>, L>
629where
630 Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
631 Connection: Service<Request<ConnectionBody>, Output = Response<IncomingBody>, Error = BoxError>
632 + ExtensionsRef,
633 IncomingBody: Send + Sync + 'static,
634 ResponseBody: From<IncomingBody>,
635 ConnectionBody:
638 StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
639 L: Layer<
640 HttpForwardProxyService<MapResponseBody<Connection, fn(IncomingBody) -> ResponseBody>>,
641 Service: Service<Request<ConnectionBody>, Output = Response, Error = BoxError>,
642 > + Send
643 + Sync
644 + 'static,
645{
646 type Output = Response;
647 type Error = OpaqueError;
648
649 async fn serve(&self, req: Request<Body>) -> Result<Self::Output, Self::Error> {
650 let uri = req.uri().clone();
651
652 if let Some(mode) = self.plaintext_http_proxy_mode {
653 req.extensions().insert(mode);
654 }
655
656 let EstablishedClientConnection {
657 input: req,
658 conn: http_connection,
659 } = self.connector.serve(req).await.into_opaque_error()?;
660
661 req.extensions()
665 .insert(Egress(http_connection.extensions().clone()));
666
667 let http_connection = MapResponseBody::into_boxed_streaming_body(http_connection);
668 let http_connection = self.forward_proxy_layer.layer(http_connection);
669 let http_connection = self.jit_layers.layer(http_connection);
670
671 tracing::trace!(url.full = %uri, "send http req to connector stack");
673
674 let result = http_connection.serve(req).await;
675
676 match result {
677 Ok(resp) => {
678 tracing::trace!(url.full = %uri, "response received from connector stack");
679 Ok(resp)
680 }
681 Err(err) => Err(err
682 .context("http request failure")
683 .context_field("uri", uri)
684 .into_opaque_error()),
685 }
686 }
687}
688
689#[cfg(test)]
690mod tests {
691 use std::{
692 convert::Infallible,
693 num::NonZeroUsize,
694 sync::{
695 Arc,
696 atomic::{AtomicUsize, Ordering},
697 },
698 time::Duration,
699 };
700
701 use rama_core::extensions::Extensions;
702 use rama_core::{error::BoxErrorExt as _, service::service_fn};
703 use rama_http::{Body, BodyExtractExt, Version};
704 use rama_http_backend::server::HttpServer;
705 use rama_net::{
706 address::ProxyAddress,
707 client::{
708 ConnectRequest, ConnectionError, ConnectionErrorDomain, ConnectionErrorKind,
709 ConnectorService, ConnectorTarget, EstablishedProxyRoute, ProxyRoute,
710 ProxyRouteFailureCache, ProxyRouteFailureCacheConfig, ProxyRouteFailureCacheScope,
711 ProxyRoutes,
712 },
713 test_utils::client::{MockConnectorService, MockSocket},
714 };
715 use rama_utils::octets::kib;
716 use serde::{Deserialize, Serialize};
717 use tokio::time::sleep;
718
719 #[cfg(feature = "boring")]
720 use {
721 crate::{
722 net::address::SocketAddress,
723 tcp::{TcpStream, server::TcpListener},
724 tls::{
725 boring::{TlsStream, server::TlsAcceptorLayer},
726 client::{NegotiatedTlsParameters, ServerVerifyMode, TlsServerVerify},
727 server::{GeneratedServerAuthConfig, ServerAuthData, TlsServerConfig},
728 },
729 },
730 rama_core::{extensions::ExtensionsRef as _, layer::MapInputLayer},
731 rama_http::header::CONNECTION,
732 };
733
734 use super::*;
735
736 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
737 struct Output {
738 conn: usize,
739 resp: usize,
740 }
741
742 #[tokio::test]
743 async fn default_client_request_future_stays_within_stack_budget() {
744 let client = DefaultHttpWebClient::<Body>::default();
745 let request = Request::builder()
746 .uri("https://example.com/")
747 .body(Body::empty())
748 .unwrap();
749 let request = client.serve(request);
750 let inner_size = std::mem::size_of_val(&request);
751 let request = tokio::time::timeout(Duration::from_secs(1), request);
752 let size = std::mem::size_of_val(&request);
753 eprintln!("default client request: {inner_size} bytes; with timeout: {size} bytes");
754 assert!(
755 size <= kib(16),
756 "default client request future is {size} bytes"
757 );
758 }
759
760 #[test]
761 fn default_client_can_be_constructed_and_cloned_without_a_runtime() {
762 let client = DefaultHttpWebClient::<Body>::default();
763 drop(client.clone());
764 drop(client);
765 }
766
767 #[derive(Debug, Clone, Default, ExtensionsRef)]
768 struct EmptyHttpConnection {
769 extensions: Extensions,
770 }
771
772 impl Service<Request> for EmptyHttpConnection {
773 type Output = Response;
774 type Error = BoxError;
775
776 async fn serve(&self, _request: Request) -> Result<Self::Output, Self::Error> {
777 Ok(Response::new(Body::empty()))
778 }
779 }
780
781 #[derive(Debug, Clone)]
782 struct InspectConnectionRouteLayer(EstablishedProxyRoute);
783
784 impl<S: ExtensionsRef> Layer<S> for InspectConnectionRouteLayer {
785 type Service = S;
786
787 fn layer(&self, inner: S) -> Self::Service {
788 assert_eq!(
789 inner.extensions().get_ref::<EstablishedProxyRoute>(),
790 Some(&self.0),
791 );
792 inner
793 }
794 }
795
796 fn dummy_server<Input: Send + 'static>()
797 -> impl Service<
798 Input,
799 Output = EstablishedClientConnection<MockSocket, Input>,
800 Error = Infallible,
801 > + Clone {
802 let created_connections = Arc::new(AtomicUsize::new(0));
803 MockConnectorService::new(move || {
804 let created_connections = created_connections.clone();
805 let conn = created_connections.fetch_add(1, Ordering::Relaxed);
806
807 let created_response = Arc::new(AtomicUsize::new(0));
809
810 HttpServer::auto(WeakScope::detached()).service(service_fn(move |_req: Request| {
811 let created_response = created_response.clone();
812 let resp = created_response.fetch_add(1, Ordering::Relaxed);
813 async move {
814 sleep(Duration::from_millis(5)).await;
815 let out = Output { conn, resp };
816 let resp = Response::new(Body::from(serde_json::to_vec(&out).unwrap()));
817 Ok::<_, Infallible>(resp)
818 }
819 }))
820 })
821 }
822
823 #[tokio::test]
824 async fn custom_connector_receives_plaintext_http_proxy_mode() {
825 let connector = service_fn(|request: Request| async move {
826 assert_eq!(
827 request.extensions().get_ref::<PlaintextHttpProxyMode>(),
828 Some(&PlaintextHttpProxyMode::Tunnel)
829 );
830
831 let conn = EmptyHttpConnection::default();
832 Ok::<_, Infallible>(EstablishedClientConnection {
833 input: request,
834 conn,
835 })
836 });
837 let client = EasyHttpWebClient::new(connector).with_tunnel_plaintext_http(true);
838 let request = Request::builder()
839 .uri("http://example.com/")
840 .body(Body::empty())
841 .unwrap();
842
843 let response = client.serve(request).await.unwrap();
844 assert_eq!(response.status(), crate::http::StatusCode::OK);
845 }
846
847 #[tokio::test]
848 async fn jit_layer_can_read_established_connection_extensions() {
849 let connector = service_fn(|request: Request| async move {
850 let conn = EmptyHttpConnection::default();
851 conn.extensions().insert(EstablishedProxyRoute::Direct);
852 Ok::<_, Infallible>(EstablishedClientConnection {
853 input: request,
854 conn,
855 })
856 });
857 let client = EasyHttpWebClient::new(connector)
858 .with_jit_layer(InspectConnectionRouteLayer(EstablishedProxyRoute::Direct));
859 let request = Request::builder()
860 .uri("http://example.com/")
861 .extension(ProxyRoute::Direct)
862 .body(Body::empty())
863 .unwrap();
864
865 let response = client.serve(request).await.unwrap();
866 assert_eq!(response.status(), crate::http::StatusCode::OK);
867 }
868
869 #[tokio::test]
870 async fn jit_request_metadata_cannot_change_proxy_credentials_target_or_challenge_isolation() {
871 use rama_core::layer::{MapInputLayer, MapOutputLayer};
872 use rama_http::{HeaderValue, StatusCode, header::PROXY_AUTHORIZATION};
873
874 #[derive(Debug, Clone, ExtensionsRef)]
875 struct InspectProxyConnection {
876 extensions: Extensions,
877 }
878
879 impl Service<Request> for InspectProxyConnection {
880 type Output = Response;
881 type Error = BoxError;
882
883 async fn serve(&self, request: Request) -> Result<Response, BoxError> {
884 let route = self.extensions.get_ref::<EstablishedProxyRoute>();
885 let is_forward = route.is_some_and(EstablishedProxyRoute::is_http_forward);
886 assert_eq!(
887 request
888 .extensions()
889 .egress()
890 .unwrap()
891 .0
892 .get_ref::<EstablishedProxyRoute>(),
893 route,
894 );
895 assert_eq!(
896 request.extensions().get_ref::<ProxyRoute>(),
897 Some(&ProxyRoute::Proxy(
898 "http://wrong:request-secret@requested.example:8080"
899 .parse()
900 .unwrap(),
901 )),
902 "forward policy must preserve the caller's requested route",
903 );
904 let mut target = Vec::new();
905 rama_http::proto::h1::head::encode_request_target(
906 request.method(),
907 request.uri(),
908 request.extensions(),
909 &mut target,
910 )
911 .unwrap();
912 if is_forward {
913 assert_eq!(
914 request.headers().get(PROXY_AUTHORIZATION).unwrap(),
915 "Basic dXBzdHJlYW06c2VjcmV0",
916 );
917 assert_eq!(&target[..], b"http://origin.example/resource");
918 } else {
919 assert!(request.headers().get(PROXY_AUTHORIZATION).is_none());
920 assert_eq!(&target[..], b"/resource");
921 }
922 Ok(Response::builder()
923 .status(StatusCode::PROXY_AUTHENTICATION_REQUIRED)
924 .header("proxy-authenticate", "Basic realm=private-upstream")
925 .body(Body::from("private upstream challenge"))
926 .unwrap())
927 }
928 }
929
930 let proxy: ProxyAddress = "http://upstream:secret@proxy.example:8080".parse().unwrap();
931 for isolate in [false, true] {
932 for route in [
933 None,
934 Some(EstablishedProxyRoute::Direct),
935 Some(EstablishedProxyRoute::Tunnel(proxy.clone())),
936 Some(EstablishedProxyRoute::Tunnel(
937 "socks5://proxy.example:1080".parse().unwrap(),
938 )),
939 Some(EstablishedProxyRoute::Forward(proxy.clone())),
940 ] {
941 let is_forward = route
942 .as_ref()
943 .is_some_and(EstablishedProxyRoute::is_http_forward);
944 let connector = service_fn(move |request: Request| {
945 let route = route.clone();
946 async move {
947 let extensions = Extensions::new();
948 if let Some(route) = route {
949 extensions.insert(route);
950 }
951 Ok::<_, Infallible>(EstablishedClientConnection {
952 input: request,
953 conn: InspectProxyConnection { extensions },
954 })
955 }
956 });
957 let stale_route = if is_forward {
958 EstablishedProxyRoute::Direct
959 } else {
960 EstablishedProxyRoute::Forward(proxy.clone())
961 };
962 let observed_responses = Arc::new(AtomicUsize::new(0));
963 let client = EasyHttpWebClient::new(connector)
964 .with_isolate_forward_proxy_auth_error(isolate)
965 .with_jit_layer((
966 MapInputLayer::new(move |mut request: Request| {
967 request.extensions().insert(stale_route.clone());
968 let stale_egress = Extensions::new();
969 stale_egress.insert(stale_route.clone());
970 request.extensions().insert(Egress(stale_egress));
971 request.headers_mut().insert(
972 PROXY_AUTHORIZATION,
973 HeaderValue::from_static("Basic downstream-secret"),
974 );
975 request
976 }),
977 MapOutputLayer::new({
978 let observed_responses = observed_responses.clone();
979 move |response: Response| {
980 observed_responses.fetch_add(1, Ordering::Relaxed);
981 response
982 }
983 }),
984 ));
985 let request = Request::builder()
986 .uri("http://origin.example/resource")
987 .body(Body::empty())
988 .unwrap();
989 request.extensions().insert(ProxyRoute::Proxy(
990 "http://wrong:request-secret@requested.example:8080"
991 .parse()
992 .unwrap(),
993 ));
994 let result = client.serve(request).await;
995 if isolate && is_forward {
996 assert!(result.is_err());
997 assert_eq!(observed_responses.load(Ordering::Relaxed), 0);
998 } else {
999 assert_eq!(
1000 result.unwrap().status(),
1001 StatusCode::PROXY_AUTHENTICATION_REQUIRED
1002 );
1003 assert_eq!(observed_responses.load(Ordering::Relaxed), 1);
1004 }
1005 }
1006 }
1007 }
1008
1009 #[test]
1010 fn blocking_client_drives_the_composed_http_stack() {
1011 let client = EasyHttpWebClient::connector_builder()
1012 .with_custom_transport_connector(dummy_server())
1013 .without_dns_connector()
1014 .without_tls_proxy_support()
1015 .without_proxy_support()
1016 .without_tls_support()
1017 .with_default_http_connector(WeakScope::detached())
1018 .without_connection_pool()
1019 .build_client()
1020 .try_into_blocking()
1021 .unwrap();
1022
1023 let cloned = client.clone();
1024 drop(client);
1025 let response = cloned.get("http://example.com").send().unwrap();
1026 assert_eq!(
1027 response.try_into_json::<Output>().unwrap(),
1028 Output { conn: 0, resp: 0 }
1029 );
1030 }
1031
1032 #[test]
1033 fn default_blocking_http_client_is_cloneable_and_pooled() {
1034 fn assert_default_client(_: &DefaultHttpWebClient) {}
1035
1036 let client = EasyHttpWebClient::try_blocking().unwrap();
1037 assert_default_client(client.get_ref());
1038 let cloned = client.clone();
1039 drop(client);
1040 let request = cloned.get("https://example.com").build().unwrap();
1041 assert_eq!(request.uri(), &"https://example.com".parse().unwrap());
1042 }
1043
1044 #[cfg(feature = "ws")]
1045 #[test]
1046 fn default_blocking_http_client_builds_websocket_requests() {
1047 use crate::http::ws::handshake::client::BlockingHttpClientWebSocketExt as _;
1048
1049 let client = EasyHttpWebClient::try_blocking().unwrap();
1050 let _from_url = client
1051 .websocket("wss://example.com/chat")
1052 .with_header("authorization", "Bearer secret");
1053
1054 let request = Request::builder()
1055 .uri("wss://example.com/chat")
1056 .body(Body::empty())
1057 .unwrap();
1058 let _from_request = client.websocket_with_request(request);
1059 }
1060
1061 #[tokio::test]
1062 async fn no_pool_tries_proxy_routes_in_order() {
1063 let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
1064 let transport = service_fn({
1065 let attempts = attempts.clone();
1066 let direct = dummy_server::<ConnectRequest>();
1067 move |input: ConnectRequest| {
1068 let attempts = attempts.clone();
1069 let direct = direct.clone();
1070 async move {
1071 let route = input.extensions.get_ref::<ProxyRoute>().unwrap();
1072 attempts.lock().push(route.clone());
1073 if route.proxy_address().is_some() {
1074 Err(ConnectionError::transport(
1075 BoxError::from_static_str("proxy unavailable"),
1076 ConnectionErrorKind::Unavailable,
1077 ))
1078 } else {
1079 direct.connect(input).await
1080 }
1081 }
1082 }
1083 });
1084 let client = EasyHttpWebClient::connector_builder()
1085 .with_custom_transport_connector(transport)
1086 .without_dns_connector()
1087 .without_tls_proxy_support()
1088 .with_custom_proxy_connector(())
1089 .without_tls_support()
1090 .with_default_http_connector(WeakScope::detached())
1091 .without_connection_pool()
1092 .build_client();
1093 let proxy = ProxyRoute::Proxy("http://proxy.example:8080".parse::<ProxyAddress>().unwrap());
1094 let request = || {
1095 let request = Request::builder()
1096 .uri("http://example.com")
1097 .body(Body::empty())
1098 .unwrap();
1099 request
1100 .extensions()
1101 .insert(ProxyRoutes::new([proxy.clone(), ProxyRoute::Direct]));
1102 request
1103 };
1104
1105 for _ in 0..2 {
1106 client
1107 .serve(request())
1108 .await
1109 .context("serve request through direct fallback")
1110 .unwrap();
1111 }
1112
1113 assert_eq!(
1114 attempts.lock().as_slice(),
1115 [proxy, ProxyRoute::Direct, ProxyRoute::Direct]
1116 );
1117 }
1118
1119 #[tokio::test]
1120 async fn no_proxy_tls_support_rejects_https_proxy() {
1121 let client = EasyHttpWebClient::connector_builder()
1122 .with_custom_transport_connector(dummy_server())
1123 .without_dns_connector()
1124 .without_tls_proxy_support()
1125 .with_proxy_support()
1126 .without_tls_support()
1127 .with_default_http_connector(WeakScope::detached())
1128 .without_connection_pool()
1129 .build_client();
1130 let request = Request::builder()
1131 .uri("http://example.com")
1132 .body(Body::empty())
1133 .unwrap();
1134 request.extensions().insert(ProxyRoutes::new([
1135 ProxyRoute::Proxy(
1136 "https://proxy.example:8443"
1137 .parse::<ProxyAddress>()
1138 .unwrap(),
1139 ),
1140 ProxyRoute::Direct,
1141 ]));
1142
1143 let error =
1144 ConnectionError::from(client.serve(request).await.unwrap_err().into_box_error());
1145 assert_eq!(error.domain(), ConnectionErrorDomain::Transport);
1146 assert_eq!(error.kind(), ConnectionErrorKind::Protocol);
1147 }
1148
1149 #[tokio::test]
1150 async fn easy_client_pools_plaintext_proxy_versions_separately() {
1151 let proxy: ProxyAddress = "http://proxy.example:8080".parse().unwrap();
1152 let dials = Arc::new(AtomicUsize::new(0));
1153 let transport = service_fn({
1154 let inner = dummy_server::<ConnectRequest>();
1155 let proxy = proxy.clone();
1156 let dials = dials.clone();
1157 move |input: ConnectRequest| {
1158 let inner = inner.clone();
1159 let proxy = proxy.clone();
1160 let dials = dials.clone();
1161 async move {
1162 assert_eq!(
1163 input.extensions.get_ref::<ConnectorTarget>(),
1164 Some(&ConnectorTarget(proxy.address.clone())),
1165 );
1166 assert_eq!(
1167 input
1168 .extensions
1169 .get_ref::<ProxyRoute>()
1170 .and_then(ProxyRoute::proxy_address),
1171 Some(&proxy),
1172 );
1173 dials.fetch_add(1, Ordering::Relaxed);
1174 inner.connect(input).await
1175 }
1176 }
1177 });
1178 let client = EasyHttpWebClient::connector_builder()
1179 .with_custom_transport_connector(transport)
1180 .without_dns_connector()
1181 .without_tls_proxy_support()
1182 .with_proxy_support()
1183 .without_tls_support()
1184 .with_default_http_connector(WeakScope::detached())
1185 .with_default_connection_pool()
1186 .build_client();
1187
1188 for (version, expected_conn) in [(Version::HTTP_11, 0), (Version::HTTP_2, 1)] {
1189 let request = Request::builder()
1190 .uri("http://example.com")
1191 .version(version)
1192 .body(Body::empty())
1193 .unwrap();
1194 request
1195 .extensions()
1196 .insert(ProxyRoutes::from(proxy.clone()));
1197
1198 let response = client.serve(request).await.unwrap();
1199 assert_eq!(response.version(), version);
1200 assert_eq!(
1201 response.try_into_json::<Output>().await.unwrap(),
1202 Output {
1203 conn: expected_conn,
1204 resp: 0,
1205 },
1206 );
1207 }
1208 assert_eq!(dials.load(Ordering::Relaxed), 2);
1209 }
1210
1211 #[cfg(feature = "socks5")]
1212 #[tokio::test]
1213 async fn umbrella_proxy_connector_falls_back_across_supported_plan() {
1214 let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
1215 let transport = service_fn({
1216 let attempts = attempts.clone();
1217 let direct = dummy_server::<ConnectRequest>();
1218 move |input: ConnectRequest| {
1219 let attempts = attempts.clone();
1220 let direct = direct.clone();
1221 async move {
1222 let route = input.extensions.get_ref::<ProxyRoute>().unwrap().clone();
1223 attempts.lock().push(route.clone());
1224 if route.proxy_address().is_some() {
1225 Err(ConnectionError::transport(
1226 BoxError::from_static_str("proxy unavailable"),
1227 ConnectionErrorKind::Unavailable,
1228 ))
1229 } else {
1230 direct.connect(input).await
1231 }
1232 }
1233 }
1234 });
1235 let client = EasyHttpWebClient::connector_builder()
1236 .with_custom_transport_connector(transport)
1237 .without_dns_connector()
1238 .without_tls_proxy_support()
1239 .with_proxy_support()
1240 .without_tls_support()
1241 .with_default_http_connector(WeakScope::detached())
1242 .without_connection_pool()
1243 .build_client();
1244 let request = Request::builder()
1245 .uri("http://example.com")
1246 .body(Body::empty())
1247 .unwrap();
1248 let socks = ProxyRoute::Proxy(
1249 "socks5://socks.example:1080"
1250 .parse::<ProxyAddress>()
1251 .unwrap(),
1252 );
1253 let http = ProxyRoute::Proxy("http://http.example:8080".parse::<ProxyAddress>().unwrap());
1254 request.extensions().insert(ProxyRoutes::new([
1255 socks.clone(),
1256 http.clone(),
1257 ProxyRoute::Direct,
1258 ]));
1259
1260 let response = client.serve(request).await.unwrap();
1261 let output = response.try_into_json::<Output>().await.unwrap();
1262 assert_eq!(output, Output { conn: 0, resp: 0 });
1263 assert_eq!(
1264 attempts.lock().as_slice(),
1265 [socks, http, ProxyRoute::Direct]
1266 );
1267 }
1268
1269 #[tokio::test]
1270 async fn default_pool_caches_failed_route_and_reuses_selected_connection() {
1271 let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
1272 let transport = service_fn({
1273 let attempts = attempts.clone();
1274 let direct = dummy_server::<ConnectRequest>();
1275 move |input: ConnectRequest| {
1276 let attempts = attempts.clone();
1277 let direct = direct.clone();
1278 async move {
1279 let route = input.extensions.get_ref::<ProxyRoute>().unwrap();
1280 attempts.lock().push(route.clone());
1281 if route.proxy_address().is_some() {
1282 Err(ConnectionError::transport(
1283 BoxError::from_static_str("proxy unavailable"),
1284 ConnectionErrorKind::Unavailable,
1285 ))
1286 } else {
1287 direct.connect(input).await
1288 }
1289 }
1290 }
1291 });
1292 let client = EasyHttpWebClient::connector_builder()
1293 .with_custom_transport_connector(transport)
1294 .without_dns_connector()
1295 .without_tls_proxy_support()
1296 .with_custom_proxy_connector(())
1297 .without_tls_support()
1298 .with_default_http_connector(WeakScope::detached())
1299 .with_default_connection_pool()
1300 .build_client();
1301 let proxy = ProxyRoute::Proxy("http://proxy.example:8080".parse::<ProxyAddress>().unwrap());
1302 let request = || {
1303 let request = Request::builder()
1304 .uri("http://example.com")
1305 .body(Body::empty())
1306 .unwrap();
1307 request
1308 .extensions()
1309 .insert(ProxyRoutes::new([proxy.clone(), ProxyRoute::Direct]));
1310 request
1311 };
1312
1313 for expected_response_index in 0..2 {
1314 let response = client.serve(request()).await.unwrap();
1315 let output = response.try_into_json::<Output>().await.unwrap();
1316 assert_eq!(output.conn, 0);
1317 assert_eq!(output.resp, expected_response_index);
1318 }
1319
1320 assert_eq!(attempts.lock().as_slice(), [proxy, ProxyRoute::Direct]);
1321 }
1322
1323 #[tokio::test]
1324 async fn easy_client_can_disable_proxy_route_failure_cache() {
1325 let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
1326 let transport = service_fn({
1327 let attempts = attempts.clone();
1328 let direct = dummy_server::<ConnectRequest>();
1329 move |input: ConnectRequest| {
1330 let attempts = attempts.clone();
1331 let direct = direct.clone();
1332 async move {
1333 let route = input.extensions.get_ref::<ProxyRoute>().unwrap();
1334 attempts.lock().push(route.clone());
1335 if route.proxy_address().is_some() {
1336 Err(ConnectionError::transport(
1337 BoxError::from_static_str("proxy unavailable"),
1338 ConnectionErrorKind::Unavailable,
1339 ))
1340 } else {
1341 direct.connect(input).await
1342 }
1343 }
1344 }
1345 });
1346 let client = EasyHttpWebClient::connector_builder()
1347 .with_custom_transport_connector(transport)
1348 .without_dns_connector()
1349 .without_tls_proxy_support()
1350 .with_custom_proxy_connector(())
1351 .without_tls_support()
1352 .with_default_http_connector(WeakScope::detached())
1353 .without_proxy_route_failure_cache()
1354 .without_connection_pool()
1355 .build_client();
1356 let proxy = ProxyRoute::Proxy("http://proxy.example:8080".parse().unwrap());
1357
1358 for _ in 0..2 {
1359 let request = Request::builder()
1360 .uri("http://example.com")
1361 .body(Body::empty())
1362 .unwrap();
1363 request
1364 .extensions()
1365 .insert(ProxyRoutes::new([proxy.clone(), ProxyRoute::Direct]));
1366 client.serve(request).await.unwrap();
1367 }
1368
1369 assert_eq!(
1370 attempts.lock().as_slice(),
1371 [proxy.clone(), ProxyRoute::Direct, proxy, ProxyRoute::Direct]
1372 );
1373 }
1374
1375 #[tokio::test]
1376 async fn proxy_free_easy_client_omits_proxy_route_failure_cache() {
1377 let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
1378 let transport = service_fn({
1379 let attempts = attempts.clone();
1380 let direct = dummy_server::<ConnectRequest>();
1381 move |input: ConnectRequest| {
1382 let attempts = attempts.clone();
1383 let direct = direct.clone();
1384 async move {
1385 let route = input.extensions.get_ref::<ProxyRoute>().unwrap();
1386 attempts.lock().push(route.clone());
1387 if route.proxy_address().is_some() {
1388 Err(ConnectionError::transport(
1389 BoxError::from_static_str("proxy unavailable"),
1390 ConnectionErrorKind::Unavailable,
1391 ))
1392 } else {
1393 direct.connect(input).await
1394 }
1395 }
1396 }
1397 });
1398 let client = EasyHttpWebClient::connector_builder()
1399 .with_custom_transport_connector(transport)
1400 .without_dns_connector()
1401 .without_tls_proxy_support()
1402 .without_proxy_support()
1403 .without_tls_support()
1404 .with_default_http_connector(WeakScope::detached())
1405 .without_connection_pool()
1406 .build_client();
1407 let proxy = ProxyRoute::Proxy("http://proxy.example:8080".parse().unwrap());
1408
1409 for _ in 0..2 {
1410 let request = Request::builder()
1411 .uri("http://example.com")
1412 .body(Body::empty())
1413 .unwrap();
1414 request
1415 .extensions()
1416 .insert(ProxyRoutes::new([proxy.clone(), ProxyRoute::Direct]));
1417 client.serve(request).await.unwrap();
1418 }
1419
1420 assert_eq!(
1421 attempts.lock().as_slice(),
1422 [proxy.clone(), ProxyRoute::Direct, proxy, ProxyRoute::Direct]
1423 );
1424 }
1425
1426 #[tokio::test]
1427 async fn easy_client_accepts_custom_proxy_route_failure_cache() {
1428 let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
1429 let transport = service_fn({
1430 let attempts = attempts.clone();
1431 let direct = dummy_server::<ConnectRequest>();
1432 move |input: ConnectRequest| {
1433 let attempts = attempts.clone();
1434 let direct = direct.clone();
1435 async move {
1436 let route = input.extensions.get_ref::<ProxyRoute>().unwrap();
1437 attempts.lock().push(route.clone());
1438 if route.proxy_address().is_some() {
1439 Err(ConnectionError::transport(
1440 BoxError::from_static_str("proxy unavailable"),
1441 ConnectionErrorKind::Unavailable,
1442 ))
1443 } else {
1444 direct.connect(input).await
1445 }
1446 }
1447 }
1448 });
1449 let mut failure_cache_config = ProxyRouteFailureCacheConfig::default();
1450 failure_cache_config.scope = ProxyRouteFailureCacheScope::PerProxy;
1451 let failure_cache = ProxyRouteFailureCache::try_new(failure_cache_config).unwrap();
1452 let client = EasyHttpWebClient::connector_builder()
1453 .with_custom_transport_connector(transport)
1454 .without_dns_connector()
1455 .without_tls_proxy_support()
1456 .without_proxy_support()
1457 .without_tls_support()
1458 .with_default_http_connector(WeakScope::detached())
1459 .with_proxy_route_failure_cache(failure_cache)
1460 .without_connection_pool()
1461 .build_client();
1462 let proxy = ProxyRoute::Proxy("http://proxy.example:8080".parse().unwrap());
1463
1464 for destination in ["one.example", "two.example"] {
1465 let request = Request::builder()
1466 .uri(format!("http://{destination}"))
1467 .body(Body::empty())
1468 .unwrap();
1469 request
1470 .extensions()
1471 .insert(ProxyRoutes::new([proxy.clone(), ProxyRoute::Direct]));
1472 client.serve(request).await.unwrap();
1473 }
1474
1475 assert_eq!(
1476 attempts.lock().as_slice(),
1477 [proxy, ProxyRoute::Direct, ProxyRoute::Direct]
1478 );
1479 }
1480
1481 #[cfg(all(feature = "rustls", any(feature = "aws-lc", feature = "ring")))]
1482 #[tokio::test]
1483 async fn rustls_https_proxy_alpn_is_scoped_across_connect() {
1484 use crate::{
1485 extensions::ExtensionsRef as _,
1486 net::{
1487 Protocol,
1488 address::HostWithPort,
1489 client::{EstablishedClientConnection, ProxyRoute},
1490 stream::service::EchoService,
1491 },
1492 tls::{
1493 client::{NegotiatedTlsParameters, TlsClientConfig},
1494 rustls::{client::TlsConnector, server::TlsAcceptorLayer},
1495 server::{GeneratedServerAuthConfig, ServerAuthData, TlsServerConfig},
1496 },
1497 };
1498 use rama_core::ServiceInput;
1499 use rama_crypto::cert::generate_server_auth;
1500 use rama_http::io::upgrade::handle_upgrade;
1501 use rama_http_backend::client::proxy::layer::HttpProxyConnectorLayer;
1502 use rama_net::http::TargetHttpVersion;
1503 use std::sync::Arc;
1504
1505 let (proxy_chain, proxy_key) =
1506 generate_server_auth(GeneratedServerAuthConfig::default()).expect("proxy auth");
1507 let proxy_trust = proxy_chain.last().expect("proxy trust anchor").clone();
1508 let (origin_chain, origin_key) =
1509 generate_server_auth(GeneratedServerAuthConfig::default()).expect("origin auth");
1510 let origin_trust = origin_chain.last().expect("origin trust anchor").clone();
1511
1512 let origin_server =
1513 TlsAcceptorLayer::new(TlsServerConfig::new().with_single_cert(ServerAuthData {
1514 cert_chain: origin_chain,
1515 private_key: origin_key,
1516 ocsp: None,
1517 }))
1518 .into_layer(EchoService::new());
1519 let (origin_done_tx, origin_done_rx) = tokio::sync::oneshot::channel();
1520 let origin_done_tx = Arc::new(parking_lot::Mutex::new(Some(origin_done_tx)));
1521
1522 let connect_version = Arc::new(parking_lot::Mutex::new(None));
1523 let observed_version = connect_version.clone();
1524 let proxy_http =
1525 HttpServer::auto(WeakScope::detached()).service(service_fn(move |req: Request| {
1526 let origin_server = origin_server.clone();
1527 let origin_done_tx = origin_done_tx.clone();
1528 let observed_version = observed_version.clone();
1529 async move {
1530 assert_eq!(req.method(), rama_http::Method::CONNECT);
1531 *observed_version.lock() = Some(req.version());
1532 let upgrade = handle_upgrade(&req);
1533 tokio::spawn(async move {
1534 let tunnel = upgrade.await.expect("server CONNECT upgrade");
1535 let _origin_result = origin_server.serve(tunnel).await;
1539 if let Some(tx) = origin_done_tx.lock().take() {
1540 tx.send(()).expect("origin completion receiver");
1541 }
1542 });
1543 Ok::<_, Infallible>(Response::new(Body::empty()))
1544 }
1545 }));
1546 let proxy_server = TlsAcceptorLayer::new(
1547 TlsServerConfig::new()
1548 .with_single_cert(ServerAuthData {
1549 cert_chain: proxy_chain,
1550 private_key: proxy_key,
1551 ocsp: None,
1552 })
1553 .with_alpn_http_2(),
1554 )
1555 .into_layer(proxy_http);
1556
1557 let (client_io, server_io) = tokio::io::duplex(64 * 1024);
1558 let client_io = Arc::new(parking_lot::Mutex::new(Some(client_io)));
1559 let transport = service_fn(move |input: ConnectRequest| {
1560 let conn = ServiceInput::new(client_io.lock().take().expect("one proxy connection"));
1561 async move { Ok::<_, ConnectionError>(EstablishedClientConnection { input, conn }) }
1562 });
1563
1564 let proxy_config = TlsClientConfig::new()
1565 .with_alpn_http_2()
1566 .with_server_name(crate::net::address::Host::from_static("localhost"))
1567 .try_with_server_trust_anchors([proxy_trust])
1568 .expect("proxy trust");
1569 let proxy_tls = TlsConnector::tunnel(transport, None).with_base_config(proxy_config);
1570 let proxy = HttpProxyConnectorLayer::default().into_layer(proxy_tls);
1571 let origin_config = TlsClientConfig::new()
1572 .with_alpn(Default::default())
1573 .with_server_name(crate::net::address::Host::from_static("localhost"))
1574 .try_with_server_trust_anchors([origin_trust])
1575 .expect("origin trust");
1576 let connector = TlsConnector::auto(proxy).with_base_config(origin_config);
1577
1578 let input = ConnectRequest::new(HostWithPort::try_from("localhost:443").unwrap())
1579 .with_application_protocol(Protocol::HTTPS);
1580 input
1581 .extensions
1582 .insert(ProxyRoute::Proxy("https://localhost:8443".parse().unwrap()));
1583 let client = async move {
1584 let established = Box::pin(connector.serve(input))
1585 .await
1586 .expect("two TLS handshakes");
1587
1588 assert_eq!(*connect_version.lock(), Some(Version::HTTP_2));
1589 assert_eq!(
1590 established
1591 .conn
1592 .extensions()
1593 .get_ref::<NegotiatedTlsParameters>()
1594 .expect("origin TLS parameters")
1595 .application_layer_protocol,
1596 None
1597 );
1598 assert!(
1599 established
1600 .conn
1601 .extensions()
1602 .get_ref::<TargetHttpVersion>()
1603 .is_none(),
1604 "proxy HTTP/2 must not leak past CONNECT into a no-ALPN origin"
1605 );
1606 drop(established);
1607 };
1608 let (proxy_result, ()) =
1609 Box::pin(tokio::time::timeout(Duration::from_secs(5), async move {
1610 tokio::join!(proxy_server.serve(ServiceInput::new(server_io)), client)
1611 }))
1612 .await
1613 .expect("proxy/origin exchange");
1614 proxy_result.expect("proxy server");
1615 tokio::time::timeout(Duration::from_secs(5), origin_done_rx)
1616 .await
1617 .expect("origin server shutdown")
1618 .expect("origin completion signal");
1619 }
1620
1621 #[cfg(feature = "boring")]
1622 #[tokio::test]
1623 async fn boring_https_proxy_alpn_is_scoped_across_connect() {
1624 use crate::{
1625 extensions::ExtensionsRef as _,
1626 net::{
1627 Protocol,
1628 address::HostWithPort,
1629 client::{EstablishedClientConnection, ProxyRoute},
1630 stream::service::EchoService,
1631 },
1632 tls::{
1633 boring::{client::TlsConnector, server::TlsAcceptorLayer},
1634 client::{NegotiatedTlsParameters, TlsClientConfig},
1635 server::{GeneratedServerAuthConfig, ServerAuthData, TlsServerConfig},
1636 },
1637 };
1638 use rama_core::ServiceInput;
1639 use rama_crypto::cert::generate_server_auth;
1640 use rama_http::io::upgrade::handle_upgrade;
1641 use rama_http_backend::client::proxy::layer::HttpProxyConnectorLayer;
1642 use rama_net::http::TargetHttpVersion;
1643 use std::sync::Arc;
1644
1645 let (proxy_chain, proxy_key) =
1646 generate_server_auth(GeneratedServerAuthConfig::default()).expect("proxy auth");
1647 let proxy_trust = proxy_chain.last().expect("proxy trust anchor").clone();
1648 let (origin_chain, origin_key) =
1649 generate_server_auth(GeneratedServerAuthConfig::default()).expect("origin auth");
1650 let origin_trust = origin_chain.last().expect("origin trust anchor").clone();
1651
1652 let origin_server =
1653 TlsAcceptorLayer::new(TlsServerConfig::new().with_single_cert(ServerAuthData {
1654 cert_chain: origin_chain,
1655 private_key: origin_key,
1656 ocsp: None,
1657 }))
1658 .into_layer(EchoService::new());
1659 let (origin_done_tx, origin_done_rx) = tokio::sync::oneshot::channel();
1660 let origin_done_tx = Arc::new(parking_lot::Mutex::new(Some(origin_done_tx)));
1661
1662 let connect_version = Arc::new(parking_lot::Mutex::new(None));
1663 let observed_version = connect_version.clone();
1664 let proxy_http =
1665 HttpServer::auto(WeakScope::detached()).service(service_fn(move |req: Request| {
1666 let origin_server = origin_server.clone();
1667 let origin_done_tx = origin_done_tx.clone();
1668 let observed_version = observed_version.clone();
1669 async move {
1670 assert_eq!(req.method(), rama_http::Method::CONNECT);
1671 *observed_version.lock() = Some(req.version());
1672 let upgrade = handle_upgrade(&req);
1673 tokio::spawn(async move {
1674 let tunnel = upgrade.await.expect("server CONNECT upgrade");
1675 let _origin_result = origin_server.serve(tunnel).await;
1679 if let Some(tx) = origin_done_tx.lock().take() {
1680 tx.send(()).expect("origin completion receiver");
1681 }
1682 });
1683 Ok::<_, Infallible>(Response::new(Body::empty()))
1684 }
1685 }));
1686 let proxy_server = TlsAcceptorLayer::new(
1687 TlsServerConfig::new()
1688 .with_single_cert(ServerAuthData {
1689 cert_chain: proxy_chain,
1690 private_key: proxy_key,
1691 ocsp: None,
1692 })
1693 .with_alpn_http_2(),
1694 )
1695 .into_layer(proxy_http);
1696
1697 let (client_io, server_io) = tokio::io::duplex(64 * 1024);
1698 let client_io = Arc::new(parking_lot::Mutex::new(Some(client_io)));
1699 let transport = service_fn(move |input: ConnectRequest| {
1700 let conn = ServiceInput::new(client_io.lock().take().expect("one proxy connection"));
1701 async move { Ok::<_, ConnectionError>(EstablishedClientConnection { input, conn }) }
1702 });
1703
1704 let proxy_config = TlsClientConfig::new()
1705 .with_alpn_http_2()
1706 .with_server_name(crate::net::address::Host::from_static("localhost"))
1707 .try_with_server_trust_anchors([proxy_trust])
1708 .expect("proxy trust");
1709 let proxy_tls = TlsConnector::tunnel(transport, None).with_base_config(proxy_config);
1710 let proxy = HttpProxyConnectorLayer::default().into_layer(proxy_tls);
1711 let origin_config = TlsClientConfig::new()
1712 .with_alpn(Default::default())
1713 .with_server_name(crate::net::address::Host::from_static("localhost"))
1714 .try_with_server_trust_anchors([origin_trust])
1715 .expect("origin trust");
1716 let connector = TlsConnector::auto(proxy).with_base_config(origin_config);
1717
1718 let input = ConnectRequest::new(HostWithPort::try_from("localhost:443").unwrap())
1719 .with_application_protocol(Protocol::HTTPS);
1720 input
1721 .extensions
1722 .insert(ProxyRoute::Proxy("https://localhost:8443".parse().unwrap()));
1723 let client = async move {
1724 let established = connector.serve(input).await.expect("two TLS handshakes");
1725
1726 assert_eq!(*connect_version.lock(), Some(Version::HTTP_2));
1727 assert_eq!(
1728 established
1729 .conn
1730 .extensions()
1731 .get_ref::<NegotiatedTlsParameters>()
1732 .expect("origin TLS parameters")
1733 .application_layer_protocol,
1734 None
1735 );
1736 assert!(
1737 established
1738 .conn
1739 .extensions()
1740 .get_ref::<TargetHttpVersion>()
1741 .is_none(),
1742 "proxy HTTP/2 must not leak past CONNECT into a no-ALPN origin"
1743 );
1744 drop(established);
1745 };
1746 let (proxy_result, ()) =
1747 Box::pin(tokio::time::timeout(Duration::from_secs(5), async move {
1748 tokio::join!(proxy_server.serve(ServiceInput::new(server_io)), client)
1749 }))
1750 .await
1751 .expect("proxy/origin exchange");
1752 proxy_result.expect("proxy server");
1753 tokio::time::timeout(Duration::from_secs(5), origin_done_rx)
1754 .await
1755 .expect("origin server shutdown")
1756 .expect("origin completion signal");
1757 }
1758
1759 #[cfg(feature = "boring")]
1760 #[tokio::test]
1761 async fn default_client_resumes_tls_sessions() {
1762 let scope = WeakScope::detached();
1763 let resumed = Arc::new(parking_lot::Mutex::new(Vec::new()));
1764 let record = resumed.clone();
1765 let server = TlsAcceptorLayer::new(
1766 TlsServerConfig::new()
1767 .with_server_auth(
1768 ServerAuthData::new_generated(GeneratedServerAuthConfig::default()).unwrap(),
1769 )
1770 .with_alpn_http_1(),
1771 )
1772 .with_session_resumption(true)
1773 .into_layer(
1774 MapInputLayer::new(move |stream: TlsStream<TcpStream>| {
1775 let params = stream.extensions().get_ref::<NegotiatedTlsParameters>();
1776 record.lock().push(params.and_then(|params| params.resumed));
1777 stream
1778 })
1779 .into_layer(HttpServer::new_http1(scope.clone()).service(service_fn(
1780 async |_: Request| {
1781 Ok::<_, Infallible>(
1783 Response::builder()
1784 .header(CONNECTION, "close")
1785 .body(Body::empty())
1786 .unwrap(),
1787 )
1788 },
1789 ))),
1790 );
1791 let listener = TcpListener::bind_address(SocketAddress::local_ipv4(0), scope)
1792 .await
1793 .unwrap();
1794 let port = listener.local_addr().unwrap().port();
1795 let server = tokio::spawn(listener.serve(server));
1796
1797 let client = DefaultHttpWebClient::<Body>::default();
1798 for _ in 0..2 {
1799 let request = Request::builder()
1800 .uri(format!("https://localhost:{port}/"))
1801 .body(Body::empty())
1802 .unwrap();
1803 request
1804 .extensions()
1805 .insert(TlsServerVerify(ServerVerifyMode::Disable));
1806 let response = tokio::time::timeout(Duration::from_secs(5), client.serve(request))
1807 .await
1808 .unwrap()
1809 .unwrap();
1810 response.try_into_string().await.unwrap();
1811 }
1812 server.abort();
1813 assert_eq!(*resumed.lock(), [Some(false), Some(true)]);
1814 }
1815
1816 #[cfg(feature = "boring")]
1817 #[test]
1818 fn proxy_failure_cache_keeps_tls_client_future_bounded() {
1819 let client = EasyHttpWebClient::connector_builder()
1820 .with_default_transport_connector()
1821 .with_default_dns_connector()
1822 .without_tls_proxy_support()
1823 .with_proxy_support()
1824 .with_tls_support_using_boringssl_and_default_http_version(
1825 crate::tls::client::TlsClientConfig::default_http(),
1826 Version::HTTP_11,
1827 )
1828 .with_default_http_connector(WeakScope::detached())
1829 .without_connection_pool()
1830 .build_client();
1831 let request = Request::builder()
1832 .uri("https://example.com")
1833 .body(Body::empty())
1834 .unwrap();
1835
1836 let future = client.serve(request);
1837 let future_size = std::mem::size_of_val(&future);
1838
1839 assert!(
1840 future_size < 64 * 1024,
1841 "easy TLS client future is unexpectedly large: {future_size} bytes"
1842 );
1843 }
1844
1845 #[tokio::test]
1846 async fn connection_is_in_use_until_response_body_is_consumed() {
1847 let client = EasyHttpWebClient::connector_builder()
1848 .with_custom_transport_connector(dummy_server())
1849 .without_dns_connector()
1850 .without_tls_proxy_support()
1851 .without_proxy_support()
1852 .without_tls_support()
1853 .with_default_http_connector(WeakScope::detached())
1854 .with_connection_pool(HttpPooledConnectorConfig {
1855 max_streams_per_connection: NonZeroUsize::new(1),
1856 max_connections_total: NonZeroUsize::new(4),
1857 ..Default::default()
1858 })
1859 .build_client();
1860
1861 let req = || {
1862 Request::builder()
1863 .uri("http://example.com")
1864 .version(Version::HTTP_2)
1865 .body(Body::empty())
1866 .unwrap()
1867 };
1868
1869 let res1 = client.serve(req()).await.unwrap();
1873 let res2 = client.serve(req()).await.unwrap();
1874
1875 let out2 = res2.try_into_json::<Output>().await.unwrap();
1877 let out1 = res1.try_into_json::<Output>().await.unwrap();
1878
1879 assert_eq!(out1.conn, 0, "first request uses the first connection");
1880 assert_eq!(
1883 out2.conn, 1,
1884 "second request must not reuse a connection whose response body is still in flight"
1885 );
1886 }
1887
1888 #[tokio::test]
1892 async fn default_pool_multiplexes_on_h2() {
1893 let client = EasyHttpWebClient::connector_builder()
1894 .with_custom_transport_connector(dummy_server())
1895 .without_dns_connector()
1896 .without_tls_proxy_support()
1897 .without_proxy_support()
1898 .without_tls_support()
1899 .with_default_http_connector(WeakScope::detached())
1900 .with_default_connection_pool()
1901 .build_client();
1902
1903 let req = || {
1904 Request::builder()
1905 .uri("http://example.com")
1906 .version(Version::HTTP_2)
1907 .body(Body::empty())
1908 .unwrap()
1909 };
1910 let (res1, res2, res3) = tokio::join!(
1911 client.serve(req()),
1912 client.serve(req()),
1913 client.serve(req()),
1914 );
1915
1916 for (i, res) in [res1, res2, res3].into_iter().enumerate() {
1918 let out = res.unwrap().try_into_json::<Output>().await.unwrap();
1919 assert_eq!(out.conn, 0);
1920 assert_eq!(out.resp, i);
1921 }
1922 }
1923
1924 #[tokio::test]
1925 async fn default_pool_does_not_multiplexes_on_h1() {
1926 let client = EasyHttpWebClient::connector_builder()
1927 .with_custom_transport_connector(dummy_server())
1928 .without_dns_connector()
1929 .without_tls_proxy_support()
1930 .without_proxy_support()
1931 .without_tls_support()
1932 .with_default_http_connector(WeakScope::detached())
1933 .with_default_connection_pool()
1934 .build_client();
1935
1936 let req = || {
1937 Request::builder()
1938 .uri("http://example.com")
1939 .version(Version::HTTP_11)
1940 .body(Body::empty())
1941 .unwrap()
1942 };
1943 let (res1, res2, res3) = tokio::join!(
1944 client.serve(req()),
1945 client.serve(req()),
1946 client.serve(req()),
1947 );
1948
1949 for (i, res) in [res1, res2, res3].into_iter().enumerate() {
1952 let out = res.unwrap().try_into_json::<Output>().await.unwrap();
1953 assert_eq!(out.conn, i);
1954 assert_eq!(out.resp, 0);
1955 }
1956 }
1957
1958 #[tokio::test]
1959 async fn multiplex_on_h2_respects_limits() {
1960 let client = EasyHttpWebClient::connector_builder()
1961 .with_custom_transport_connector(dummy_server())
1962 .without_dns_connector()
1963 .without_tls_proxy_support()
1964 .without_proxy_support()
1965 .without_tls_support()
1966 .with_default_http_connector(WeakScope::detached())
1967 .with_connection_pool(HttpPooledConnectorConfig {
1968 max_streams_per_connection: NonZeroUsize::new(2),
1969 ..Default::default()
1970 })
1971 .build_client();
1972
1973 let req = || {
1974 Request::builder()
1975 .uri("http://example.com")
1976 .version(Version::HTTP_2)
1977 .body(Body::empty())
1978 .unwrap()
1979 };
1980 let (res1, res2, res3, res4) = tokio::join!(
1981 client.serve(req()),
1982 client.serve(req()),
1983 client.serve(req()),
1984 client.serve(req()),
1985 );
1986
1987 for (i, res) in [res1, res2, res3, res4].into_iter().enumerate() {
1989 let out = res.unwrap().try_into_json::<Output>().await.unwrap();
1990 assert_eq!(out.conn, i / 2);
1991 assert_eq!(out.resp, i % 2);
1992 }
1993 }
1994}