Skip to main content

aws_smithy_http_client/client/
connect.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5use crate::client::connect;
6use crate::proxy;
7use aws_smithy_runtime_api::box_error::BoxError;
8use http_1x::{Extensions, HeaderMap, HeaderValue, Uri};
9use hyper::rt::{Read, ReadBufCursor, Write};
10use hyper_util::client::legacy::connect::{Connected, Connection};
11use hyper_util::client::proxy::matcher::Matcher;
12use pin_project_lite::pin_project;
13use std::fmt;
14use std::future::Future;
15use std::io;
16use std::io::IoSlice;
17use std::pin::Pin;
18use std::sync::Arc;
19use std::task::{Context, Poll};
20
21pub(crate) trait AsyncConn:
22    Read + Write + Connection + Send + Sync + Unpin + 'static
23{
24}
25
26impl<T: Read + Write + Connection + Send + Sync + Unpin + 'static> AsyncConn for T {}
27
28pub(crate) type BoxConn = Box<dyn AsyncConn>;
29
30// Future for connecting
31pub(crate) type Connecting = Pin<Box<dyn Future<Output = Result<Conn, BoxError>> + Send>>;
32
33/// Authorization selected for cleartext HTTP requests sent through a proxy.
34#[derive(Clone)]
35pub(crate) struct ProxyAuthorization(HeaderValue);
36
37impl fmt::Debug for ProxyAuthorization {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        f.write_str("ProxyAuthorization(** redacted **)")
40    }
41}
42
43impl ProxyAuthorization {
44    pub(crate) fn new(value: HeaderValue) -> Self {
45        Self(value)
46    }
47
48    /// Applies the selected authorization unless the caller supplied one.
49    pub(crate) fn apply(&self, headers: &mut HeaderMap) {
50        headers
51            .entry(http_1x::header::PROXY_AUTHORIZATION)
52            .or_insert_with(|| self.0.clone());
53    }
54}
55
56/// How an established transport reaches its origin.
57///
58/// The classification contains no proxy credentials. It describes the path
59/// selected by the built-in connector or reported by a custom connector.
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61#[non_exhaustive]
62pub enum ConnectPath {
63    /// The transport connects to the origin without an HTTP proxy.
64    Direct,
65    /// HTTP requests are sent to a forward proxy in absolute form.
66    ForwardProxy,
67    /// The connector established an HTTP `CONNECT` tunnel to the origin.
68    ProxyTunnel,
69}
70
71impl ConnectPath {
72    /// Returns whether an HTTP proxy participates in the connection path.
73    pub fn is_proxied(&self) -> bool {
74        !matches!(self, Self::Direct)
75    }
76}
77
78/// Connector-owned connection path with request-time proxy state.
79#[derive(Clone, Debug)]
80pub(crate) enum ConnectPathInner {
81    /// The transport reaches the origin without a configured HTTP proxy.
82    Direct,
83    /// Requests are sent to a forward proxy; HTTP/1 uses absolute-form targets.
84    ForwardProxy {
85        authorization: Option<ProxyAuthorization>,
86    },
87    /// The transport reaches the origin through an established CONNECT tunnel.
88    ProxyTunnel,
89}
90
91impl ConnectPathInner {
92    pub(crate) fn forward_proxy(authorization: Option<HeaderValue>) -> Self {
93        Self::ForwardProxy {
94            authorization: authorization.map(ProxyAuthorization::new),
95        }
96    }
97
98    /// Recovers the full path when available and preserves custom connector behavior.
99    pub(crate) fn from_connected(connected: &Connected, extras: &Extensions) -> Self {
100        extras
101            .get::<Self>()
102            .cloned()
103            .or_else(|| extras.get::<ConnectPath>().copied().map(Self::from_public))
104            .unwrap_or_else(|| {
105                if connected.is_proxied() {
106                    Self::forward_proxy(None)
107                } else {
108                    Self::Direct
109                }
110            })
111    }
112
113    /// Returns the credential-free public path classification.
114    pub(crate) fn public(&self) -> ConnectPath {
115        match self {
116            Self::Direct => ConnectPath::Direct,
117            Self::ForwardProxy { .. } => ConnectPath::ForwardProxy,
118            Self::ProxyTunnel => ConnectPath::ProxyTunnel,
119        }
120    }
121
122    /// Creates connector state from a credential-free custom-connector value.
123    fn from_public(path: ConnectPath) -> Self {
124        match path {
125            ConnectPath::Direct => Self::Direct,
126            ConnectPath::ForwardProxy => Self::forward_proxy(None),
127            ConnectPath::ProxyTunnel => Self::ProxyTunnel,
128        }
129    }
130
131    /// Returns whether HTTP/1 requests require an absolute-form target.
132    pub(crate) fn uses_absolute_form(&self) -> bool {
133        matches!(self, Self::ForwardProxy { .. })
134    }
135
136    /// Applies configured forwarding credentials unless the request supplied them.
137    pub(crate) fn apply_proxy_authorization(&self, headers: &mut HeaderMap) {
138        if let Self::ForwardProxy {
139            authorization: Some(authorization),
140        } = self
141        {
142            authorization.apply(headers);
143        }
144    }
145}
146
147pin_project! {
148    pub(crate) struct Conn {
149        #[pin]
150        pub(super)inner: BoxConn,
151        pub(super) connect_path: ConnectPathInner,
152    }
153}
154
155impl Connection for Conn {
156    fn connected(&self) -> Connected {
157        self.inner
158            .connected()
159            .proxy(self.connect_path.uses_absolute_form())
160            .extra(self.connect_path.clone())
161    }
162}
163
164impl Read for Conn {
165    fn poll_read(
166        self: Pin<&mut Self>,
167        cx: &mut Context<'_>,
168        buf: ReadBufCursor<'_>,
169    ) -> Poll<io::Result<()>> {
170        let this = self.project();
171        Read::poll_read(this.inner, cx, buf)
172    }
173}
174
175impl Write for Conn {
176    fn poll_write(
177        self: Pin<&mut Self>,
178        cx: &mut Context<'_>,
179        buf: &[u8],
180    ) -> Poll<Result<usize, io::Error>> {
181        let this = self.project();
182        Write::poll_write(this.inner, cx, buf)
183    }
184
185    fn poll_write_vectored(
186        self: Pin<&mut Self>,
187        cx: &mut Context<'_>,
188        bufs: &[IoSlice<'_>],
189    ) -> Poll<Result<usize, io::Error>> {
190        let this = self.project();
191        Write::poll_write_vectored(this.inner, cx, bufs)
192    }
193
194    fn is_write_vectored(&self) -> bool {
195        self.inner.is_write_vectored()
196    }
197
198    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
199        let this = self.project();
200        Write::poll_flush(this.inner, cx)
201    }
202
203    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
204        let this = self.project();
205        Write::poll_shutdown(this.inner, cx)
206    }
207}
208
209/// HTTP-only proxy connector for handling HTTP requests through HTTP proxies
210///
211/// This connector handles the HTTP proxy logic when no TLS provider is selected,
212/// including request URL modification and proxy authentication.
213#[derive(Debug, Clone)]
214pub(crate) struct HttpProxyConnector<C> {
215    inner: C,
216    proxy_matcher: Option<Arc<Matcher>>,
217}
218
219impl<C> HttpProxyConnector<C> {
220    pub(crate) fn new(inner: C, proxy_config: proxy::ProxyConfig) -> Self {
221        let proxy_matcher = if proxy_config.is_disabled() {
222            None
223        } else {
224            Some(Arc::new(proxy_config.into_hyper_util_matcher()))
225        };
226        Self {
227            inner,
228            proxy_matcher,
229        }
230    }
231}
232
233impl<C> tower::Service<Uri> for HttpProxyConnector<C>
234where
235    C: tower::Service<Uri> + Clone + Send + 'static,
236    C::Response: hyper::rt::Read
237        + hyper::rt::Write
238        + hyper_util::client::legacy::connect::Connection
239        + Send
240        + Sync
241        + Unpin
242        + 'static,
243    C::Future: Send + 'static,
244    C::Error: Into<BoxError>,
245{
246    type Response = connect::Conn;
247    type Error = BoxError;
248    type Future = connect::Connecting;
249
250    fn poll_ready(
251        &mut self,
252        cx: &mut std::task::Context<'_>,
253    ) -> std::task::Poll<Result<(), Self::Error>> {
254        self.inner.poll_ready(cx).map_err(Into::into)
255    }
256
257    fn call(&mut self, dst: Uri) -> Self::Future {
258        let proxy_intercept = self
259            .proxy_matcher
260            .as_ref()
261            .and_then(|matcher| matcher.intercept(&dst));
262
263        if let Some(intercept) = proxy_intercept {
264            // HTTP through proxy: Connect to proxy server
265            let proxy_uri = intercept.uri().clone();
266            let connect_path = ConnectPathInner::forward_proxy(intercept.basic_auth().cloned());
267            let fut = self.inner.call(proxy_uri);
268            Box::pin(async move {
269                let conn = fut.await.map_err(Into::into)?;
270                Ok(connect::Conn {
271                    inner: Box::new(conn),
272                    connect_path,
273                })
274            })
275        } else {
276            // Direct connection
277            let fut = self.inner.call(dst);
278            Box::pin(async move {
279                let conn = fut.await.map_err(Into::into)?;
280                Ok(connect::Conn {
281                    inner: Box::new(conn),
282                    connect_path: ConnectPathInner::Direct,
283                })
284            })
285        }
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    #[test]
294    fn custom_connector_path_extra_preserves_public_classification() {
295        for path in [
296            ConnectPath::Direct,
297            ConnectPath::ForwardProxy,
298            ConnectPath::ProxyTunnel,
299        ] {
300            let connected = Connected::new().extra(path);
301            let mut extras = Extensions::new();
302            connected.get_extras(&mut extras);
303
304            assert_eq!(
305                path,
306                ConnectPathInner::from_connected(&connected, &extras).public()
307            );
308        }
309    }
310
311    #[test]
312    fn generic_proxy_metadata_maps_to_forward_proxy() {
313        let connected = Connected::new().proxy(true);
314        let mut extras = Extensions::new();
315        connected.get_extras(&mut extras);
316
317        assert_eq!(
318            ConnectPath::ForwardProxy,
319            ConnectPathInner::from_connected(&connected, &extras).public()
320        );
321    }
322}