Skip to main content

rama/http/client/
proxy_connector.rs

1use crate::utils::macros::define_inner_service_accessors;
2use crate::{
3    Layer, Service,
4    error::{BoxError, BoxErrorExt},
5    extensions::{Extensions, ExtensionsRef},
6    http::client::proxy::layer::{
7        HttpProxyConnector, HttpProxyConnectorLayer, MaybeHttpProxiedConnection,
8    },
9    io::Io,
10    net::{
11        AuthorityInputExt, HttpVersionInputExt, Protocol, ProtocolInputExt,
12        TargetHttpVersionInputExt,
13        client::{
14            ConnectionError, ConnectionErrorKind, ConnectorService, EstablishedClientConnection,
15            EstablishedProxyRoute, ProxyRoute,
16        },
17    },
18    proxy::socks5::{Socks5ProxyConnector, Socks5ProxyConnectorLayer},
19    telemetry::tracing,
20};
21#[cfg(feature = "tls")]
22use crate::{net::client::ConnectionPolicyScope, tls::TlsTunnel};
23use pin_project_lite::pin_project;
24use std::{
25    fmt::Debug,
26    pin::Pin,
27    task::{self, Poll},
28};
29use tokio::io::{AsyncRead, AsyncWrite};
30
31/// Proxy connector which supports http(s), masque and socks5(h) proxy address
32///
33/// Connector will look at [`ProxyRoute`] to determine which proxy
34/// connector to use if one is configured
35#[derive(Debug, Clone, ExtensionsRef)]
36pub struct ProxyConnector<S> {
37    inner: S,
38    socks: Socks5ProxyConnector<S>,
39    http: HttpProxyConnector<S>,
40    required: bool,
41}
42
43impl<S: Clone> ProxyConnector<S> {
44    define_inner_service_accessors!();
45
46    /// Creates a new [`ProxyConnector`].
47    fn new(
48        inner: S,
49        socks_proxy_layer: Socks5ProxyConnectorLayer,
50        http_proxy_layer: HttpProxyConnectorLayer,
51        required: bool,
52    ) -> Self {
53        Self {
54            socks: socks_proxy_layer.into_layer(inner.clone()),
55            http: http_proxy_layer.into_layer(inner.clone()),
56            inner,
57            required,
58        }
59    }
60
61    #[inline]
62    /// Creates a new required [`ProxyConnector`].
63    ///
64    /// This connector will fail unless a proxied [`ProxyRoute`] is configured.
65    pub fn required(
66        inner: S,
67        socks_proxy_layer: Socks5ProxyConnectorLayer,
68        http_proxy_layer: HttpProxyConnectorLayer,
69    ) -> Self {
70        Self::new(inner, socks_proxy_layer, http_proxy_layer, true)
71    }
72
73    #[inline]
74    /// Creates a new optional [`ProxyConnector`].
75    ///
76    /// This connector will forward to the inner connector for a direct or missing [`ProxyRoute`].
77    pub fn optional(
78        inner: S,
79        socks_proxy_layer: Socks5ProxyConnectorLayer,
80        http_proxy_layer: HttpProxyConnectorLayer,
81    ) -> Self {
82        Self::new(inner, socks_proxy_layer, http_proxy_layer, false)
83    }
84}
85
86impl<Input, S> Service<Input> for ProxyConnector<S>
87where
88    S: ConnectorService<Input, Connection: Io + Unpin>,
89    Input: AuthorityInputExt
90        + ProtocolInputExt
91        + HttpVersionInputExt
92        + TargetHttpVersionInputExt
93        + Send
94        + ExtensionsRef
95        + 'static,
96{
97    type Output = EstablishedClientConnection<MaybeProxiedConnection<S::Connection>, Input>;
98    type Error = ConnectionError;
99
100    async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
101        let route = input.extensions().get_ref::<ProxyRoute>();
102        let route_requested = route.is_some();
103
104        match route {
105            None | Some(ProxyRoute::Direct) => {
106                if self.required {
107                    return Err(ConnectionError::local(
108                        BoxError::from_static_str("proxy required but none is defined"),
109                        ConnectionErrorKind::InvalidInput,
110                    ));
111                }
112                tracing::trace!("no proxy detected in ctx, using inner connector");
113                let EstablishedClientConnection { input, conn } = self.inner.connect(input).await?;
114
115                if route_requested {
116                    conn.extensions().insert(EstablishedProxyRoute::Direct);
117                }
118
119                let conn = MaybeProxiedConnection::direct(conn);
120                Ok(EstablishedClientConnection { input, conn })
121            }
122            Some(ProxyRoute::Proxy(proxy)) => {
123                let protocol = proxy.protocol.as_ref();
124                tracing::trace!(?protocol, "proxy detected in ctx");
125
126                let protocol = protocol.unwrap_or_else(|| {
127                    tracing::trace!("no protocol detected, using http as protocol");
128                    &Protocol::HTTP
129                });
130
131                if protocol.is_socks5() {
132                    tracing::trace!(
133                        target = %&proxy.address,
134                        "using socks proxy connector",
135                    );
136
137                    // SOCKS does not supply a tunnel TLS policy of its own.
138                    // An explicit request policy must not poison shared routes.
139                    #[cfg(feature = "tls")]
140                    let request_tunnel = input.extensions().contains::<TlsTunnel>();
141                    let established = self.socks.connect(input).await;
142                    #[cfg(feature = "tls")]
143                    let established = established.map_err(|error| {
144                        if request_tunnel {
145                            error.with_policy_scope(ConnectionPolicyScope::Request)
146                        } else {
147                            error
148                        }
149                    });
150                    let EstablishedClientConnection { input, conn } = established?;
151
152                    let conn = MaybeProxiedConnection::socks(conn);
153                    Ok(EstablishedClientConnection { input, conn })
154                } else if protocol.is_http() || protocol.is_masque() {
155                    // A MASQUE proxy is an HTTPS proxy for TCP, and receives its own requests
156                    // as a forward proxy; see `ProxyRoute::addresses_proxy`.
157                    tracing::trace!(
158                        target = %&proxy.address,
159                        "using http proxy connector"
160                    );
161
162                    let EstablishedClientConnection { input, conn } =
163                        self.http.connect(input).await?;
164
165                    let conn = MaybeProxiedConnection::http(conn);
166                    Ok(EstablishedClientConnection { input, conn })
167                } else {
168                    Err(ConnectionError::transport(
169                        BoxError::from_static_str("received unsupported proxy protocol"),
170                        ConnectionErrorKind::Protocol,
171                    )
172                    .context_debug_field("protocol", protocol.clone()))
173                }
174            }
175        }
176    }
177}
178
179pin_project! {
180    /// A connection which will be proxied if a proxied [`ProxyRoute`] was configured.
181    pub struct MaybeProxiedConnection<S> {
182        #[pin]
183        inner: Connection<S>,
184    }
185}
186
187impl<S: ExtensionsRef> MaybeProxiedConnection<S> {
188    pub fn direct(conn: S) -> Self {
189        Self {
190            inner: Connection::Direct { conn },
191        }
192    }
193
194    pub fn socks(conn: S) -> Self {
195        Self {
196            inner: Connection::Socks { conn },
197        }
198    }
199
200    pub fn http(conn: MaybeHttpProxiedConnection<S>) -> Self {
201        Self {
202            inner: Connection::Http { conn },
203        }
204    }
205}
206
207impl<S: Debug> Debug for MaybeProxiedConnection<S> {
208    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209        f.debug_struct("MaybeProxiedConnection")
210            .field("inner", &self.inner)
211            .finish()
212    }
213}
214
215impl<S: ExtensionsRef> ExtensionsRef for MaybeProxiedConnection<S> {
216    fn extensions(&self) -> &Extensions {
217        match &self.inner {
218            Connection::Direct { conn } | Connection::Socks { conn } => conn.extensions(),
219            Connection::Http { conn } => conn.extensions(),
220        }
221    }
222}
223
224pin_project! {
225    #[project = ConnectionProj]
226    enum Connection<S> {
227        Direct{ #[pin] conn: S },
228        Socks{ #[pin] conn: S },
229        Http{ #[pin] conn: MaybeHttpProxiedConnection<S> },
230
231    }
232}
233
234impl<S: Debug> Debug for Connection<S> {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        match self {
237            Self::Direct { conn } => f.debug_struct("Direct").field("conn", conn).finish(),
238            Self::Socks { conn } => f.debug_struct("Socks").field("conn", conn).finish(),
239            Self::Http { conn } => f.debug_struct("Http").field("conn", conn).finish(),
240        }
241    }
242}
243
244#[warn(clippy::missing_trait_methods)]
245impl<Conn: AsyncWrite> AsyncWrite for MaybeProxiedConnection<Conn> {
246    fn poll_write(
247        self: Pin<&mut Self>,
248        cx: &mut task::Context<'_>,
249        buf: &[u8],
250    ) -> Poll<Result<usize, std::io::Error>> {
251        match self.project().inner.project() {
252            ConnectionProj::Direct { conn } | ConnectionProj::Socks { conn } => {
253                conn.poll_write(cx, buf)
254            }
255            ConnectionProj::Http { conn } => conn.poll_write(cx, buf),
256        }
257    }
258
259    fn poll_flush(
260        self: Pin<&mut Self>,
261        cx: &mut task::Context<'_>,
262    ) -> Poll<Result<(), std::io::Error>> {
263        match self.project().inner.project() {
264            ConnectionProj::Direct { conn } | ConnectionProj::Socks { conn } => conn.poll_flush(cx),
265            ConnectionProj::Http { conn } => conn.poll_flush(cx),
266        }
267    }
268
269    fn poll_shutdown(
270        self: Pin<&mut Self>,
271        cx: &mut task::Context<'_>,
272    ) -> Poll<Result<(), std::io::Error>> {
273        match self.project().inner.project() {
274            ConnectionProj::Direct { conn } | ConnectionProj::Socks { conn } => {
275                conn.poll_shutdown(cx)
276            }
277            ConnectionProj::Http { conn } => conn.poll_shutdown(cx),
278        }
279    }
280
281    fn is_write_vectored(&self) -> bool {
282        match &self.inner {
283            Connection::Direct { conn } | Connection::Socks { conn } => conn.is_write_vectored(),
284            Connection::Http { conn } => conn.is_write_vectored(),
285        }
286    }
287
288    fn poll_write_vectored(
289        self: Pin<&mut Self>,
290        cx: &mut task::Context<'_>,
291        bufs: &[std::io::IoSlice<'_>],
292    ) -> Poll<Result<usize, std::io::Error>> {
293        match self.project().inner.project() {
294            ConnectionProj::Direct { conn } | ConnectionProj::Socks { conn } => {
295                conn.poll_write_vectored(cx, bufs)
296            }
297            ConnectionProj::Http { conn } => conn.poll_write_vectored(cx, bufs),
298        }
299    }
300}
301
302#[warn(clippy::missing_trait_methods)]
303impl<Conn: AsyncRead> AsyncRead for MaybeProxiedConnection<Conn> {
304    fn poll_read(
305        self: Pin<&mut Self>,
306        cx: &mut task::Context<'_>,
307        buf: &mut tokio::io::ReadBuf<'_>,
308    ) -> Poll<std::io::Result<()>> {
309        match self.project().inner.project() {
310            ConnectionProj::Direct { conn } | ConnectionProj::Socks { conn } => {
311                conn.poll_read(cx, buf)
312            }
313            ConnectionProj::Http { conn } => conn.poll_read(cx, buf),
314        }
315    }
316}
317
318/// Proxy connector layer which supports http(s), masque and socks5(h) proxy address
319///
320/// Connector will look at [`ProxyRoute`] to determine which proxy
321/// connector to use if one is configured
322pub struct ProxyConnectorLayer {
323    socks_layer: Socks5ProxyConnectorLayer,
324    http_layer: HttpProxyConnectorLayer,
325    required: bool,
326}
327
328impl ProxyConnectorLayer {
329    #[must_use]
330    /// Creates a new required [`ProxyConnectorLayer`].
331    ///
332    /// This connector will fail unless a proxied [`ProxyRoute`] is configured.
333    pub fn required(
334        socks_proxy_layer: Socks5ProxyConnectorLayer,
335        http_proxy_layer: HttpProxyConnectorLayer,
336    ) -> Self {
337        Self {
338            socks_layer: socks_proxy_layer,
339            http_layer: http_proxy_layer,
340            required: true,
341        }
342    }
343
344    #[must_use]
345    /// Creates a new optional [`ProxyConnectorLayer`].
346    ///
347    /// This connector will forward to the inner connector for a direct or missing [`ProxyRoute`].
348    pub fn optional(
349        socks_proxy_layer: Socks5ProxyConnectorLayer,
350        http_proxy_layer: HttpProxyConnectorLayer,
351    ) -> Self {
352        Self {
353            socks_layer: socks_proxy_layer,
354            http_layer: http_proxy_layer,
355            required: false,
356        }
357    }
358}
359
360impl<S: Clone> Layer<S> for ProxyConnectorLayer {
361    type Service = ProxyConnector<S>;
362
363    fn layer(&self, inner: S) -> Self::Service {
364        ProxyConnector::new(
365            inner,
366            self.socks_layer.clone(),
367            self.http_layer.clone(),
368            self.required,
369        )
370    }
371
372    fn into_layer(self, inner: S) -> Self::Service {
373        ProxyConnector::new(inner, self.socks_layer, self.http_layer, self.required)
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380    use crate::{
381        net::{proxy::IoForwardService, test_utils::client::MockSocket},
382        proxy::socks5::{
383            Socks5ProxyConnectorLayer,
384            server::{Connector as EagerSocks5Connector, Socks5Connector},
385        },
386        tcp::client::service::TcpConnector,
387    };
388
389    #[cfg(feature = "tls")]
390    use {
391        crate::{
392            net::{
393                address::HostWithPort,
394                address::ProxyAddress,
395                client::{
396                    ConnectRequest, EstablishedProxyRoute, ProxyRouteFailureCache,
397                    ProxyRouteFailureCacheConfig, ProxyRouteFailureCacheConnector,
398                    ProxyRouteFailureCacheScope,
399                },
400            },
401            service::service_fn,
402        },
403        std::sync::{
404            Arc,
405            atomic::{AtomicUsize, Ordering},
406        },
407    };
408
409    fn assert_socks5_connector<S, C: Socks5Connector<S>>(_: &C) {}
410
411    #[cfg(feature = "tls")]
412    #[tokio::test]
413    async fn a_masque_proxy_receives_its_own_requests_directly_as_forwarded() {
414        let dialed = Arc::new(parking_lot::Mutex::new(None));
415        let transport = service_fn({
416            let dialed = dialed.clone();
417            move |input: ConnectRequest| {
418                *dialed.lock() = Some(input.authority.clone());
419                async move {
420                    let (stream, _peer) = tokio::io::duplex(64);
421                    Ok::<_, ConnectionError>(EstablishedClientConnection {
422                        input,
423                        conn: MockSocket::new(stream),
424                    })
425                }
426            }
427        });
428        let connector = ProxyConnector::required(
429            transport,
430            Socks5ProxyConnectorLayer::required(),
431            HttpProxyConnectorLayer::required(),
432        );
433        let proxy: ProxyAddress = "masque://user:secret@proxy.example:443".parse().unwrap();
434        let input = ConnectRequest::new(HostWithPort::try_from("proxy.example:443").unwrap())
435            .with_application_protocol(Protocol::HTTPS);
436        input.extensions().insert(ProxyRoute::Proxy(proxy.clone()));
437        let established = connector.serve(input).await.unwrap();
438        assert_eq!(
439            dialed.lock().take().unwrap(),
440            HostWithPort::try_from("proxy.example:443").unwrap()
441        );
442        assert_eq!(
443            established
444                .conn
445                .extensions()
446                .get_ref::<EstablishedProxyRoute>(),
447            Some(&EstablishedProxyRoute::Forward(proxy))
448        );
449    }
450
451    #[cfg(feature = "tls")]
452    #[tokio::test]
453    async fn request_tunnel_failure_does_not_poison_socks_proxy_route() {
454        for scope in [
455            ProxyRouteFailureCacheScope::PerDestination,
456            ProxyRouteFailureCacheScope::PerProxy,
457        ] {
458            let attempts = Arc::new(AtomicUsize::new(0));
459            let transport = service_fn({
460                let attempts = attempts.clone();
461                move |input: ConnectRequest| {
462                    attempts.fetch_add(1, Ordering::Relaxed);
463                    async move {
464                        Err::<EstablishedClientConnection<MockSocket, ConnectRequest>, _>(
465                            ConnectionError::transport(
466                                BoxError::from_static_str("scripted proxy failure"),
467                                if input.extensions().contains::<TlsTunnel>() {
468                                    ConnectionErrorKind::Protocol
469                                } else {
470                                    ConnectionErrorKind::Unavailable
471                                },
472                            ),
473                        )
474                    }
475                }
476            });
477            let mut config = ProxyRouteFailureCacheConfig::default();
478            config.scope = scope;
479            let connector = ProxyRouteFailureCacheConnector::new(
480                ProxyConnector::required(
481                    transport,
482                    Socks5ProxyConnectorLayer::required(),
483                    HttpProxyConnectorLayer::required(),
484                ),
485                ProxyRouteFailureCache::try_new(config).unwrap(),
486            );
487            let request = || {
488                let input = ConnectRequest::new(HostWithPort::example_domain_http())
489                    .with_application_protocol(Protocol::HTTP);
490                input.extensions().insert(ProxyRoute::Proxy(
491                    "socks5://127.0.0.1:1080".parse().unwrap(),
492                ));
493                input
494            };
495            let customized = request();
496            customized.extensions().insert(TlsTunnel {
497                server_identity: Some("proxy.example".parse().unwrap()),
498                application_protocol: None,
499                alpn: None,
500            });
501            let error = connector.serve(customized).await.unwrap_err();
502            assert_eq!(error.kind(), ConnectionErrorKind::Protocol);
503            assert_eq!(error.policy_scope(), ConnectionPolicyScope::Request);
504
505            let error = connector.serve(request()).await.unwrap_err();
506            assert_eq!(error.kind(), ConnectionErrorKind::Unavailable);
507            assert_eq!(error.policy_scope(), ConnectionPolicyScope::Unknown);
508            assert_eq!(attempts.load(Ordering::Relaxed), 2);
509            _ = connector.serve(request()).await.unwrap_err();
510            assert_eq!(
511                attempts.load(Ordering::Relaxed),
512                2,
513                "ordinary failures remain cached"
514            );
515        }
516    }
517
518    #[test]
519    fn eager_socks5_accepts_combined_proxy_connection() {
520        let proxy_connector = ProxyConnectorLayer::optional(
521            Socks5ProxyConnectorLayer::optional(),
522            HttpProxyConnectorLayer::optional(),
523        )
524        .into_layer(TcpConnector::new());
525        let connector = EagerSocks5Connector::new(proxy_connector, IoForwardService::default());
526
527        assert_socks5_connector::<MockSocket, _>(&connector);
528    }
529}