Skip to main content

rama/http/client/
proxy_connector.rs

1use crate::{
2    Layer, Service,
3    error::{BoxError, BoxErrorExt},
4    extensions::{Extensions, ExtensionsRef},
5    http::client::proxy::layer::{
6        HttpProxyConnector, HttpProxyConnectorLayer, MaybeHttpProxiedConnection,
7    },
8    io::Io,
9    net::{
10        AuthorityInputExt, HttpVersionInputExt, Protocol, ProtocolInputExt,
11        TargetHttpVersionInputExt,
12        client::{
13            ConnectionError, ConnectionErrorKind, ConnectorService, EstablishedClientConnection,
14            EstablishedProxyRoute, ProxyRoute,
15        },
16    },
17    proxy::socks5::{Socks5ProxyConnector, Socks5ProxyConnectorLayer},
18    telemetry::tracing,
19};
20use pin_project_lite::pin_project;
21use std::{
22    fmt::Debug,
23    pin::Pin,
24    task::{self, Poll},
25};
26use tokio::io::{AsyncRead, AsyncWrite};
27
28/// Proxy connector which supports http(s) and socks5(h) proxy address
29///
30/// Connector will look at [`ProxyRoute`] to determine which proxy
31/// connector to use if one is configured
32#[derive(Debug, Clone)]
33pub struct ProxyConnector<S> {
34    inner: S,
35    socks: Socks5ProxyConnector<S>,
36    http: HttpProxyConnector<S>,
37    required: bool,
38}
39
40impl<S: Clone> ProxyConnector<S> {
41    /// Creates a new [`ProxyConnector`].
42    fn new(
43        inner: S,
44        socks_proxy_layer: Socks5ProxyConnectorLayer,
45        http_proxy_layer: HttpProxyConnectorLayer,
46        required: bool,
47    ) -> Self {
48        Self {
49            socks: socks_proxy_layer.into_layer(inner.clone()),
50            http: http_proxy_layer.into_layer(inner.clone()),
51            inner,
52            required,
53        }
54    }
55
56    #[inline]
57    /// Creates a new required [`ProxyConnector`].
58    ///
59    /// This connector will fail unless a proxied [`ProxyRoute`] is configured.
60    pub fn required(
61        inner: S,
62        socks_proxy_layer: Socks5ProxyConnectorLayer,
63        http_proxy_layer: HttpProxyConnectorLayer,
64    ) -> Self {
65        Self::new(inner, socks_proxy_layer, http_proxy_layer, true)
66    }
67
68    #[inline]
69    /// Creates a new optional [`ProxyConnector`].
70    ///
71    /// This connector will forward to the inner connector for a direct or missing [`ProxyRoute`].
72    pub fn optional(
73        inner: S,
74        socks_proxy_layer: Socks5ProxyConnectorLayer,
75        http_proxy_layer: HttpProxyConnectorLayer,
76    ) -> Self {
77        Self::new(inner, socks_proxy_layer, http_proxy_layer, false)
78    }
79}
80
81impl<Input, S> Service<Input> for ProxyConnector<S>
82where
83    S: ConnectorService<Input, Connection: Io + Unpin>,
84    Input: AuthorityInputExt
85        + ProtocolInputExt
86        + HttpVersionInputExt
87        + TargetHttpVersionInputExt
88        + Send
89        + ExtensionsRef
90        + 'static,
91{
92    type Output = EstablishedClientConnection<MaybeProxiedConnection<S::Connection>, Input>;
93    type Error = ConnectionError;
94
95    async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
96        let route = input.extensions().get_ref::<ProxyRoute>();
97        let route_requested = route.is_some();
98
99        match route {
100            None | Some(ProxyRoute::Direct) => {
101                if self.required {
102                    return Err(ConnectionError::local(
103                        BoxError::from_static_str("proxy required but none is defined"),
104                        ConnectionErrorKind::InvalidInput,
105                    ));
106                }
107                tracing::trace!("no proxy detected in ctx, using inner connector");
108                let EstablishedClientConnection { input, conn } = self.inner.connect(input).await?;
109
110                if route_requested {
111                    conn.extensions().insert(EstablishedProxyRoute::Direct);
112                }
113
114                let conn = MaybeProxiedConnection::direct(conn);
115                Ok(EstablishedClientConnection { input, conn })
116            }
117            Some(ProxyRoute::Proxy(proxy)) => {
118                let protocol = proxy.protocol.as_ref();
119                tracing::trace!(?protocol, "proxy detected in ctx");
120
121                let protocol = protocol.unwrap_or_else(|| {
122                    tracing::trace!("no protocol detected, using http as protocol");
123                    &Protocol::HTTP
124                });
125
126                if protocol.is_socks5() {
127                    tracing::trace!(
128                        target = %&proxy.address,
129                        "using socks proxy connector",
130                    );
131
132                    let EstablishedClientConnection { input, conn } =
133                        self.socks.connect(input).await?;
134
135                    let conn = MaybeProxiedConnection::socks(conn);
136                    Ok(EstablishedClientConnection { input, conn })
137                } else if protocol.is_http() {
138                    tracing::trace!(
139                        target = %&proxy.address,
140                        "using http proxy connector"
141                    );
142
143                    let EstablishedClientConnection { input, conn } =
144                        self.http.connect(input).await?;
145
146                    let conn = MaybeProxiedConnection::http(conn);
147                    Ok(EstablishedClientConnection { input, conn })
148                } else {
149                    Err(ConnectionError::transport(
150                        BoxError::from_static_str("received unsupported proxy protocol"),
151                        ConnectionErrorKind::Protocol,
152                    )
153                    .context_debug_field("protocol", protocol.clone()))
154                }
155            }
156        }
157    }
158}
159
160pin_project! {
161    /// A connection which will be proxied if a proxied [`ProxyRoute`] was configured.
162    pub struct MaybeProxiedConnection<S> {
163        #[pin]
164        inner: Connection<S>,
165    }
166}
167
168impl<S: ExtensionsRef> MaybeProxiedConnection<S> {
169    pub fn direct(conn: S) -> Self {
170        Self {
171            inner: Connection::Direct { conn },
172        }
173    }
174
175    pub fn socks(conn: S) -> Self {
176        Self {
177            inner: Connection::Socks { conn },
178        }
179    }
180
181    pub fn http(conn: MaybeHttpProxiedConnection<S>) -> Self {
182        Self {
183            inner: Connection::Http { conn },
184        }
185    }
186}
187
188impl<S: Debug> Debug for MaybeProxiedConnection<S> {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        f.debug_struct("MaybeProxiedConnection")
191            .field("inner", &self.inner)
192            .finish()
193    }
194}
195
196impl<S: ExtensionsRef> ExtensionsRef for MaybeProxiedConnection<S> {
197    fn extensions(&self) -> &Extensions {
198        match &self.inner {
199            Connection::Direct { conn } | Connection::Socks { conn } => conn.extensions(),
200            Connection::Http { conn } => conn.extensions(),
201        }
202    }
203}
204
205pin_project! {
206    #[project = ConnectionProj]
207    enum Connection<S> {
208        Direct{ #[pin] conn: S },
209        Socks{ #[pin] conn: S },
210        Http{ #[pin] conn: MaybeHttpProxiedConnection<S> },
211
212    }
213}
214
215impl<S: Debug> Debug for Connection<S> {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        match self {
218            Self::Direct { conn } => f.debug_struct("Direct").field("conn", conn).finish(),
219            Self::Socks { conn } => f.debug_struct("Socks").field("conn", conn).finish(),
220            Self::Http { conn } => f.debug_struct("Http").field("conn", conn).finish(),
221        }
222    }
223}
224
225#[warn(clippy::missing_trait_methods)]
226impl<Conn: AsyncWrite> AsyncWrite for MaybeProxiedConnection<Conn> {
227    fn poll_write(
228        self: Pin<&mut Self>,
229        cx: &mut task::Context<'_>,
230        buf: &[u8],
231    ) -> Poll<Result<usize, std::io::Error>> {
232        match self.project().inner.project() {
233            ConnectionProj::Direct { conn } | ConnectionProj::Socks { conn } => {
234                conn.poll_write(cx, buf)
235            }
236            ConnectionProj::Http { conn } => conn.poll_write(cx, buf),
237        }
238    }
239
240    fn poll_flush(
241        self: Pin<&mut Self>,
242        cx: &mut task::Context<'_>,
243    ) -> Poll<Result<(), std::io::Error>> {
244        match self.project().inner.project() {
245            ConnectionProj::Direct { conn } | ConnectionProj::Socks { conn } => conn.poll_flush(cx),
246            ConnectionProj::Http { conn } => conn.poll_flush(cx),
247        }
248    }
249
250    fn poll_shutdown(
251        self: Pin<&mut Self>,
252        cx: &mut task::Context<'_>,
253    ) -> Poll<Result<(), std::io::Error>> {
254        match self.project().inner.project() {
255            ConnectionProj::Direct { conn } | ConnectionProj::Socks { conn } => {
256                conn.poll_shutdown(cx)
257            }
258            ConnectionProj::Http { conn } => conn.poll_shutdown(cx),
259        }
260    }
261
262    fn is_write_vectored(&self) -> bool {
263        match &self.inner {
264            Connection::Direct { conn } | Connection::Socks { conn } => conn.is_write_vectored(),
265            Connection::Http { conn } => conn.is_write_vectored(),
266        }
267    }
268
269    fn poll_write_vectored(
270        self: Pin<&mut Self>,
271        cx: &mut task::Context<'_>,
272        bufs: &[std::io::IoSlice<'_>],
273    ) -> Poll<Result<usize, std::io::Error>> {
274        match self.project().inner.project() {
275            ConnectionProj::Direct { conn } | ConnectionProj::Socks { conn } => {
276                conn.poll_write_vectored(cx, bufs)
277            }
278            ConnectionProj::Http { conn } => conn.poll_write_vectored(cx, bufs),
279        }
280    }
281}
282
283#[warn(clippy::missing_trait_methods)]
284impl<Conn: AsyncRead> AsyncRead for MaybeProxiedConnection<Conn> {
285    fn poll_read(
286        self: Pin<&mut Self>,
287        cx: &mut task::Context<'_>,
288        buf: &mut tokio::io::ReadBuf<'_>,
289    ) -> Poll<std::io::Result<()>> {
290        match self.project().inner.project() {
291            ConnectionProj::Direct { conn } | ConnectionProj::Socks { conn } => {
292                conn.poll_read(cx, buf)
293            }
294            ConnectionProj::Http { conn } => conn.poll_read(cx, buf),
295        }
296    }
297}
298
299/// Proxy connector layer which supports http(s) and socks5(h) proxy address
300///
301/// Connector will look at [`ProxyRoute`] to determine which proxy
302/// connector to use if one is configured
303pub struct ProxyConnectorLayer {
304    socks_layer: Socks5ProxyConnectorLayer,
305    http_layer: HttpProxyConnectorLayer,
306    required: bool,
307}
308
309impl ProxyConnectorLayer {
310    #[must_use]
311    /// Creates a new required [`ProxyConnectorLayer`].
312    ///
313    /// This connector will fail unless a proxied [`ProxyRoute`] is configured.
314    pub fn required(
315        socks_proxy_layer: Socks5ProxyConnectorLayer,
316        http_proxy_layer: HttpProxyConnectorLayer,
317    ) -> Self {
318        Self {
319            socks_layer: socks_proxy_layer,
320            http_layer: http_proxy_layer,
321            required: true,
322        }
323    }
324
325    #[must_use]
326    /// Creates a new optional [`ProxyConnectorLayer`].
327    ///
328    /// This connector will forward to the inner connector for a direct or missing [`ProxyRoute`].
329    pub fn optional(
330        socks_proxy_layer: Socks5ProxyConnectorLayer,
331        http_proxy_layer: HttpProxyConnectorLayer,
332    ) -> Self {
333        Self {
334            socks_layer: socks_proxy_layer,
335            http_layer: http_proxy_layer,
336            required: false,
337        }
338    }
339}
340
341impl<S: Clone> Layer<S> for ProxyConnectorLayer {
342    type Service = ProxyConnector<S>;
343
344    fn layer(&self, inner: S) -> Self::Service {
345        ProxyConnector::new(
346            inner,
347            self.socks_layer.clone(),
348            self.http_layer.clone(),
349            self.required,
350        )
351    }
352
353    fn into_layer(self, inner: S) -> Self::Service {
354        ProxyConnector::new(inner, self.socks_layer, self.http_layer, self.required)
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use crate::{
362        net::{proxy::IoForwardService, test_utils::client::MockSocket},
363        proxy::socks5::{
364            Socks5ProxyConnectorLayer,
365            server::{Connector as EagerSocks5Connector, Socks5Connector},
366        },
367        tcp::client::service::TcpConnector,
368    };
369
370    fn assert_socks5_connector<S, C: Socks5Connector<S>>(_: &C) {}
371
372    #[test]
373    fn eager_socks5_accepts_combined_proxy_connection() {
374        let proxy_connector = ProxyConnectorLayer::optional(
375            Socks5ProxyConnectorLayer::optional(),
376            HttpProxyConnectorLayer::optional(),
377        )
378        .into_layer(TcpConnector::new());
379        let connector = EagerSocks5Connector::new(proxy_connector, IoForwardService::default());
380
381        assert_socks5_connector::<MockSocket, _>(&connector);
382    }
383}