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