Skip to main content

rama/http/client/
builder.rs

1use rama_core::rt::Executor;
2
3use super::HttpConnector;
4use crate::{
5    Layer, Service,
6    dns::client::resolver::DnsAddressResolver,
7    error::BoxError,
8    extensions::ExtensionsRef,
9    http::{
10        Request, StreamingBody, client::proxy::layer::HttpProxyConnector,
11        layer::version_adapter::RequestVersionAdapter,
12    },
13    net::client::{
14        EstablishedClientConnection,
15        pool::{
16            LruDropPool, PooledConnector,
17            http::{BasicHttpConId, BasicHttpConnIdentifier, HttpPooledConnectorConfig},
18        },
19    },
20    tcp::client::service::TcpConnector,
21};
22use std::{marker::PhantomData, time::Duration};
23
24#[cfg(feature = "boring")]
25use crate::tls::boring::client as boring_client;
26
27#[cfg(any(feature = "rustls", feature = "boring"))]
28use crate::net::tls::client::TlsClientConfig;
29#[cfg(feature = "rustls")]
30use crate::tls::rustls::client as rustls_client;
31
32#[cfg(feature = "socks5")]
33use crate::{http::client::proxy_connector::ProxyConnector, proxy::socks5::Socks5ProxyConnector};
34
35/// Builder that is designed to easily create a connoector for [`super::EasyHttpWebClient`] from most basic use cases
36#[derive(Default)]
37pub struct EasyHttpConnectorBuilder<C = (), S = ()> {
38    connector: C,
39    _phantom: PhantomData<S>,
40}
41
42#[non_exhaustive]
43#[derive(Debug)]
44pub struct TransportStage;
45#[non_exhaustive]
46#[derive(Debug)]
47pub struct ProxyTunnelStage;
48#[non_exhaustive]
49#[derive(Debug)]
50pub struct ProxyStage;
51#[non_exhaustive]
52#[derive(Debug)]
53pub struct TlsStage;
54#[non_exhaustive]
55#[derive(Debug)]
56pub struct HttpStage;
57#[non_exhaustive]
58#[derive(Debug)]
59pub struct PoolStage;
60
61impl EasyHttpConnectorBuilder {
62    #[must_use]
63    pub fn new() -> Self {
64        Self::default()
65    }
66
67    #[must_use]
68    pub fn with_default_transport_connector(
69        self,
70    ) -> EasyHttpConnectorBuilder<TcpConnector, TransportStage> {
71        let connector = TcpConnector::default();
72        EasyHttpConnectorBuilder {
73            connector,
74            _phantom: PhantomData,
75        }
76    }
77
78    /// Add a custom transport connector that will be used by this client for the transport layer
79    pub fn with_custom_transport_connector<C>(
80        self,
81        connector: C,
82    ) -> EasyHttpConnectorBuilder<C, TransportStage> {
83        EasyHttpConnectorBuilder {
84            connector,
85            _phantom: PhantomData,
86        }
87    }
88}
89
90impl<T, Stage> EasyHttpConnectorBuilder<T, Stage> {
91    /// Add a custom connector to this Stage.
92    ///
93    /// Adding a custom connector to a stage will not change the state
94    /// so this can be used to modify behaviour at a specific stage.
95    pub fn with_custom_connector<L>(
96        self,
97        connector_layer: L,
98    ) -> EasyHttpConnectorBuilder<L::Service, Stage>
99    where
100        L: Layer<T>,
101    {
102        self.map_connector(|c| connector_layer.into_layer(c))
103    }
104
105    /// Map the current connector using the given fn.
106    ///
107    /// Mapping a connector to a stage will not change the state
108    /// so this can be used to modify behaviour at a specific stage.
109    pub fn map_connector<T2>(
110        self,
111        map_fn: impl FnOnce(T) -> T2,
112    ) -> EasyHttpConnectorBuilder<T2, Stage> {
113        let connector = map_fn(self.connector);
114        EasyHttpConnectorBuilder {
115            connector,
116            _phantom: PhantomData,
117        }
118    }
119}
120
121impl EasyHttpConnectorBuilder<TcpConnector, TransportStage> {
122    /// Add a custom [`DnsAddressResolver`] that will be
123    /// used by this client for address resolution.
124    pub fn with_dns_address_resolver<T: DnsAddressResolver + Clone>(
125        self,
126        resolver: T,
127    ) -> EasyHttpConnectorBuilder<TcpConnector<T>, TransportStage> {
128        let connector = self.connector.with_dns(resolver);
129        EasyHttpConnectorBuilder {
130            connector,
131            _phantom: PhantomData,
132        }
133    }
134}
135
136impl<T> EasyHttpConnectorBuilder<T, TransportStage> {
137    #[cfg(any(feature = "rustls", feature = "boring"))]
138    /// Add a custom proxy tls connector that will be used to setup a tls connection to the proxy
139    pub fn with_custom_tls_proxy_connector<L>(
140        self,
141        connector_layer: L,
142    ) -> EasyHttpConnectorBuilder<L::Service, ProxyTunnelStage>
143    where
144        L: Layer<T>,
145    {
146        let connector = connector_layer.into_layer(self.connector);
147        EasyHttpConnectorBuilder {
148            connector,
149            _phantom: PhantomData,
150        }
151    }
152
153    #[cfg(feature = "boring")]
154    #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
155    /// Support a tls tunnel to the proxy itself using boringssl
156    ///
157    /// Note that a tls proxy is not needed to make a https connection
158    /// to the final target. It only has an influence on the initial connection
159    /// to the proxy itself
160    pub fn with_tls_proxy_support_using_boringssl(
161        self,
162    ) -> EasyHttpConnectorBuilder<
163        boring_client::TlsConnector<T, boring_client::ConnectorKindTunnel>,
164        ProxyTunnelStage,
165    > {
166        let connector = boring_client::TlsConnector::tunnel(self.connector, None);
167        EasyHttpConnectorBuilder {
168            connector,
169            _phantom: PhantomData,
170        }
171    }
172
173    #[cfg(feature = "boring")]
174    #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
175    /// Support a tls tunnel to the proxy itself using boringssl and the provided config
176    ///
177    /// Note that a tls proxy is not needed to make a https connection
178    /// to the final target. It only has an influence on the initial connection
179    /// to the proxy itself
180    pub fn with_tls_proxy_support_using_boringssl_config(
181        self,
182        config: TlsClientConfig,
183    ) -> EasyHttpConnectorBuilder<
184        boring_client::TlsConnector<T, boring_client::ConnectorKindTunnel>,
185        ProxyTunnelStage,
186    > {
187        let connector =
188            boring_client::TlsConnector::tunnel(self.connector, None).with_base_config(config);
189        EasyHttpConnectorBuilder {
190            connector,
191            _phantom: PhantomData,
192        }
193    }
194
195    #[cfg(feature = "rustls")]
196    #[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
197    /// Support a tls tunnel to the proxy itself using rustls
198    ///
199    /// Note that a tls proxy is not needed to make a https connection
200    /// to the final target. It only has an influence on the initial connection
201    /// to the proxy itself
202    pub fn with_tls_proxy_support_using_rustls(
203        self,
204    ) -> EasyHttpConnectorBuilder<
205        rustls_client::TlsConnector<T, rustls_client::ConnectorKindTunnel>,
206        ProxyTunnelStage,
207    > {
208        let connector = rustls_client::TlsConnector::tunnel(self.connector, None);
209
210        EasyHttpConnectorBuilder {
211            connector,
212            _phantom: PhantomData,
213        }
214    }
215
216    #[cfg(feature = "rustls")]
217    #[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
218    /// Support a tls tunnel to the proxy itself using rustls and the provided config
219    ///
220    /// Note that a tls proxy is not needed to make a https connection
221    /// to the final target. It only has an influence on the initial connection
222    /// to the proxy itself
223    pub fn with_tls_proxy_support_using_rustls_config(
224        self,
225        config: TlsClientConfig,
226    ) -> EasyHttpConnectorBuilder<
227        rustls_client::TlsConnector<T, rustls_client::ConnectorKindTunnel>,
228        ProxyTunnelStage,
229    > {
230        let connector =
231            rustls_client::TlsConnector::tunnel(self.connector, None).with_base_config(config);
232
233        EasyHttpConnectorBuilder {
234            connector,
235            _phantom: PhantomData,
236        }
237    }
238
239    /// Don't support a tls tunnel to the proxy itself
240    ///
241    /// Note that a tls proxy is not needed to make a https connection
242    /// to the final target. It only has an influence on the initial connection
243    /// to the proxy itself
244    pub fn without_tls_proxy_support(self) -> EasyHttpConnectorBuilder<T, ProxyTunnelStage> {
245        EasyHttpConnectorBuilder {
246            connector: self.connector,
247            _phantom: PhantomData,
248        }
249    }
250}
251
252impl<T> EasyHttpConnectorBuilder<T, ProxyTunnelStage> {
253    /// Add a custom proxy connector that will be used by this client
254    pub fn with_custom_proxy_connector<L>(
255        self,
256        connector_layer: L,
257    ) -> EasyHttpConnectorBuilder<L::Service, ProxyStage>
258    where
259        L: Layer<T>,
260    {
261        let connector = connector_layer.into_layer(self.connector);
262        EasyHttpConnectorBuilder {
263            connector,
264            _phantom: PhantomData,
265        }
266    }
267
268    #[cfg(not(feature = "socks5"))]
269    /// Add support for usage of a http(s) [`ProxyAddress`] to this client
270    ///
271    /// Note that a tls proxy is not needed to make a https connection
272    /// to the final target. It only has an influence on the initial connection
273    /// to the proxy itself
274    ///
275    /// Note to also enable socks proxy support enable feature `socks5`
276    ///
277    /// [`ProxyAddress`]: rama_net::address::ProxyAddress
278    pub fn with_proxy_support(self) -> EasyHttpConnectorBuilder<HttpProxyConnector<T>, ProxyStage> {
279        self.with_http_proxy_support()
280    }
281
282    /// Add support for usage of a http(s) [`ProxyAddress`] to this client
283    ///
284    /// Note that a tls proxy is not needed to make a https connection
285    /// to the final target. It only has an influence on the initial connection
286    /// to the proxy itself
287    ///
288    /// [`ProxyAddress`]: rama_net::address::ProxyAddress
289    pub fn with_http_proxy_support(
290        self,
291    ) -> EasyHttpConnectorBuilder<HttpProxyConnector<T>, ProxyStage> {
292        let connector = HttpProxyConnector::optional(self.connector);
293
294        EasyHttpConnectorBuilder {
295            connector,
296            _phantom: PhantomData,
297        }
298    }
299
300    #[cfg(feature = "socks5")]
301    #[cfg_attr(docsrs, doc(cfg(feature = "socks5")))]
302    /// Add support for usage of a socks5(h) [`ProxyAddress`] to this client
303    ///
304    /// [`ProxyAddress`]: rama_net::address::ProxyAddress
305    pub fn with_socks5_proxy_support(
306        self,
307    ) -> EasyHttpConnectorBuilder<Socks5ProxyConnector<T>, ProxyStage> {
308        let connector = Socks5ProxyConnector::optional(self.connector);
309
310        EasyHttpConnectorBuilder {
311            connector,
312            _phantom: PhantomData,
313        }
314    }
315
316    /// Make a client without proxy support
317    pub fn without_proxy_support(self) -> EasyHttpConnectorBuilder<T, ProxyStage> {
318        EasyHttpConnectorBuilder {
319            connector: self.connector,
320            _phantom: PhantomData,
321        }
322    }
323}
324
325impl<T: Clone> EasyHttpConnectorBuilder<T, ProxyTunnelStage> {
326    #[cfg(feature = "socks5")]
327    #[cfg_attr(docsrs, doc(cfg(feature = "socks5")))]
328    /// Add support for usage of a http(s) and socks5(h) [`ProxyAddress`] to this client
329    ///
330    /// Note that a tls proxy is not needed to make a https connection
331    /// to the final target. It only has an influence on the initial connection
332    /// to the proxy itself
333    ///
334    /// [`ProxyAddress`]: rama_net::address::ProxyAddress
335    pub fn with_proxy_support(self) -> EasyHttpConnectorBuilder<ProxyConnector<T>, ProxyStage> {
336        use rama_http_backend::client::proxy::layer::HttpProxyConnectorLayer;
337        use rama_socks5::Socks5ProxyConnectorLayer;
338
339        let connector = ProxyConnector::optional(
340            self.connector,
341            Socks5ProxyConnectorLayer::required(),
342            HttpProxyConnectorLayer::required(),
343        );
344
345        EasyHttpConnectorBuilder {
346            connector,
347            _phantom: PhantomData,
348        }
349    }
350}
351
352impl<T> EasyHttpConnectorBuilder<T, ProxyStage> {
353    #[cfg(any(feature = "rustls", feature = "boring"))]
354    /// Add a custom tls connector that will be used by the client
355    ///
356    /// Note: when using a tls_connector you probably want to also
357    /// add a [`RequestVersionAdapter`] which applies the negotiated
358    /// http version from tls alpn. This can be achieved by using
359    /// [`Self::with_custom_connector`] just after adding the tls connector.
360    pub fn with_custom_tls_connector<L>(
361        self,
362        connector_layer: L,
363    ) -> EasyHttpConnectorBuilder<L::Service, TlsStage>
364    where
365        L: Layer<T>,
366    {
367        let connector = connector_layer.into_layer(self.connector);
368
369        EasyHttpConnectorBuilder {
370            connector,
371            _phantom: PhantomData,
372        }
373    }
374
375    #[cfg(feature = "boring")]
376    #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
377    /// Support https connections by using boringssl for tls
378    ///
379    /// Note: this also adds a [`RequestVersionAdapter`] to automatically change the
380    /// request version to the one configured with tls alpn. If this is not
381    /// wanted, use [`Self::with_custom_tls_connector`] instead.
382    pub fn with_tls_support_using_boringssl(
383        self,
384        config: TlsClientConfig,
385    ) -> EasyHttpConnectorBuilder<RequestVersionAdapter<boring_client::TlsConnector<T>>, TlsStage>
386    {
387        let connector = boring_client::TlsConnector::auto(self.connector).with_base_config(config);
388        let connector = RequestVersionAdapter::new(connector);
389
390        EasyHttpConnectorBuilder {
391            connector,
392            _phantom: PhantomData,
393        }
394    }
395
396    #[cfg(feature = "boring")]
397    #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
398    /// Same as [`Self::with_tls_support_using_boringssl`] but also
399    /// setting the default `TargetHttpVersion` in case no ALPN is negotiated.
400    ///
401    /// This is a fairly important detail for proxy purposes given otherwise
402    /// you might come in situations where the ingress traffic is negotiated to `h2`,
403    /// but the egress traffic has no negotiation which would without a default
404    /// http version remain on h2... In such a case you can get failed
405    /// requests if the egress server does not handle multiple http versions.
406    pub fn with_tls_support_using_boringssl_and_default_http_version(
407        self,
408        config: TlsClientConfig,
409        default_http_version: rama_http::Version,
410    ) -> EasyHttpConnectorBuilder<RequestVersionAdapter<boring_client::TlsConnector<T>>, TlsStage>
411    {
412        let connector = boring_client::TlsConnector::auto(self.connector).with_base_config(config);
413        let connector =
414            RequestVersionAdapter::new(connector).with_default_version(default_http_version);
415
416        EasyHttpConnectorBuilder {
417            connector,
418            _phantom: PhantomData,
419        }
420    }
421
422    #[cfg(feature = "rustls")]
423    #[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
424    /// Support https connections by using ruslts for tls
425    ///
426    /// Note: this also adds a [`RequestVersionAdapter`] to automatically change the
427    /// request version to the one configured with tls alpn. If this is not
428    /// wanted, use [`Self::with_custom_tls_connector`] instead.
429    pub fn with_tls_support_using_rustls(
430        self,
431        config: TlsClientConfig,
432    ) -> EasyHttpConnectorBuilder<RequestVersionAdapter<rustls_client::TlsConnector<T>>, TlsStage>
433    {
434        let connector = rustls_client::TlsConnector::auto(self.connector).with_base_config(config);
435        let connector = RequestVersionAdapter::new(connector);
436
437        EasyHttpConnectorBuilder {
438            connector,
439            _phantom: PhantomData,
440        }
441    }
442
443    #[cfg(feature = "rustls")]
444    #[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
445    /// Same as [`Self::with_tls_support_using_rustls`] but also
446    /// setting the default `TargetHttpVersion` in case no ALPN is negotiated.
447    ///
448    /// This is a fairly important detail for proxy purposes given otherwise
449    /// you might come in situations where the ingress traffic is negotiated to `h2`,
450    /// but the egress traffic has no negotiation which would without a default
451    /// http version remain on h2... In such a case you can get failed
452    /// requests if the egress server does not handle multiple http versions.
453    pub fn with_tls_support_using_rustls_and_default_http_version(
454        self,
455        config: TlsClientConfig,
456        default_http_version: rama_http::Version,
457    ) -> EasyHttpConnectorBuilder<RequestVersionAdapter<rustls_client::TlsConnector<T>>, TlsStage>
458    {
459        let connector = rustls_client::TlsConnector::auto(self.connector).with_base_config(config);
460        let connector =
461            RequestVersionAdapter::new(connector).with_default_version(default_http_version);
462
463        EasyHttpConnectorBuilder {
464            connector,
465            _phantom: PhantomData,
466        }
467    }
468
469    /// Dont support https on this connector
470    pub fn without_tls_support(self) -> EasyHttpConnectorBuilder<T, TlsStage> {
471        EasyHttpConnectorBuilder {
472            connector: self.connector,
473            _phantom: PhantomData,
474        }
475    }
476}
477
478impl<T> EasyHttpConnectorBuilder<T, TlsStage> {
479    /// Add http support to this connector
480    pub fn with_default_http_connector<Body>(
481        self,
482        exec: Executor,
483    ) -> EasyHttpConnectorBuilder<HttpConnector<T, Body>, HttpStage> {
484        let connector = HttpConnector::new(self.connector, exec);
485
486        EasyHttpConnectorBuilder {
487            connector,
488            _phantom: PhantomData,
489        }
490    }
491
492    /// Add a custom http connector that will be run just after tls
493    pub fn with_custom_http_connector<L>(
494        self,
495        connector_layer: L,
496    ) -> EasyHttpConnectorBuilder<L::Service, HttpStage>
497    where
498        L: Layer<T>,
499    {
500        let connector = connector_layer.into_layer(self.connector);
501
502        EasyHttpConnectorBuilder {
503            connector,
504            _phantom: PhantomData,
505        }
506    }
507}
508
509type DefaultConnectionPoolBuilder<T, C> = EasyHttpConnectorBuilder<
510    RequestVersionAdapter<
511        PooledConnector<T, LruDropPool<C, BasicHttpConId>, BasicHttpConnIdentifier>,
512    >,
513    PoolStage,
514>;
515
516impl<T> EasyHttpConnectorBuilder<T, HttpStage> {
517    /// Use the default connection pool for this [`super::EasyHttpWebClient`]
518    ///
519    /// This will create a [`LruDropPool`] using the provided limits
520    /// and will use [`BasicHttpConnIdentifier`] to group connection on protocol
521    /// and authority, which should cover most common use cases
522    ///
523    /// Use `wait_for_pool_timeout` to limit how long we wait for the pool to give us a connection
524    ///
525    /// If you need a different pool or custom way to group connection you can
526    /// use [`EasyHttpConnectorBuilder::with_custom_connection_pool()`] to provide
527    /// you own.
528    ///
529    /// This also applies a [`RequestVersionAdapter`] layer to make sure that request versions
530    /// are adapted when pooled connections are used, which you almost always need, but in case
531    /// that is unwanted, you can use [`Self::with_custom_connection_pool`] instead.
532    pub fn try_with_connection_pool<C: ExtensionsRef>(
533        self,
534        config: HttpPooledConnectorConfig,
535    ) -> Result<DefaultConnectionPoolBuilder<T, C>, BoxError> {
536        let connector = config.build_connector(self.connector)?;
537        let connector = RequestVersionAdapter::new(connector);
538
539        Ok(EasyHttpConnectorBuilder {
540            connector,
541            _phantom: PhantomData,
542        })
543    }
544
545    #[inline(always)]
546    /// Same as [`Self::try_with_connection_pool`] but using the default [`HttpPooledConnectorConfig`].
547    pub fn try_with_default_connection_pool<C: ExtensionsRef>(
548        self,
549    ) -> Result<DefaultConnectionPoolBuilder<T, C>, BoxError> {
550        self.try_with_connection_pool(Default::default())
551    }
552
553    /// Configure this client to use the provided [`Pool`] and [`ReqToConnId`]
554    ///
555    /// Use `wait_for_pool_timeout` to limit how long we wait for the pool to give us a connection
556    ///
557    /// Warning: this does not apply a [`RequestVersionAdapter`] layer to make sure that request versions
558    /// are adapted when pooled connections are used, which you almost always. This should be manually added
559    /// by using [`Self::with_custom_connector`] after configuring this pool and providing a [`RequestVersionAdapter`] there.
560    ///
561    /// [`Pool`]: rama_net::client::pool::Pool
562    /// [`ReqToConnId`]: rama_net::client::pool::ReqToConnID
563    pub fn with_custom_connection_pool<P, R>(
564        self,
565        pool: P,
566        req_to_conn_id: R,
567        wait_for_pool_timeout: Option<Duration>,
568    ) -> EasyHttpConnectorBuilder<PooledConnector<T, P, R>, PoolStage> {
569        let connector = PooledConnector::new(self.connector, pool, req_to_conn_id)
570            .maybe_with_wait_for_pool_timeout(wait_for_pool_timeout);
571
572        EasyHttpConnectorBuilder {
573            connector,
574            _phantom: PhantomData,
575        }
576    }
577}
578
579impl<T, S> EasyHttpConnectorBuilder<T, S> {
580    /// Build a [`super::EasyHttpWebClient`] using the currently configured connector
581    pub fn build_client<Body, ModifiedBody, ConnResponse>(
582        self,
583    ) -> super::EasyHttpWebClient<Body, T::Output, ()>
584    where
585        Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
586        ModifiedBody:
587            StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
588        T: Service<
589                Request<Body>,
590                Output = EstablishedClientConnection<ConnResponse, Request<ModifiedBody>>,
591                Error: Into<BoxError>,
592            >,
593        ConnResponse: ExtensionsRef,
594    {
595        super::EasyHttpWebClient::new(self.connector)
596    }
597
598    /// Build a connector from the currently configured setup
599    pub fn build_connector(self) -> T {
600        self.connector
601    }
602}