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