Skip to main content

rama/http/client/
builder.rs

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