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