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