Skip to main content

rama_http/layer/upgrade/http_proxy_connect/
eager.rs

1use crate::{
2    Request, Response, StatusCode,
3    layer::upgrade::{UpgradeOutput, UpgradeResponse, Upgraded},
4    service::web::response::IntoResponse as _,
5};
6use rama_core::{
7    Service,
8    error::BoxError,
9    extensions::ExtensionsRef as _,
10    io::{BridgeIo, Io},
11    telemetry::tracing,
12};
13use rama_net::{
14    ConnectorTargetInputExt, Protocol,
15    client::{
16        ConnectRequest, ConnectionErrorKind, ConnectorService, ConnectorTarget,
17        EstablishedClientConnection,
18    },
19};
20
21/// HTTP proxy CONNECT service which establishes the egress connection before
22/// acknowledging the CONNECT request.
23///
24/// Register it directly with an [`UpgradeLayer`](crate::layer::upgrade::UpgradeLayer):
25///
26/// ```ignore
27/// let connect = EagerHttpProxyConnector::new(connector, relay_service);
28/// let layer = UpgradeLayer::new(
29///     executor,
30///     MethodMatcher::CONNECT,
31///     connect,
32/// );
33/// ```
34///
35/// A successful response is returned only after `connector` established the
36/// requested egress connection. The established connection is carried across
37/// the HTTP upgrade and passed to `relay_service` together with the upgraded
38/// ingress connection as a [`BridgeIo`].
39///
40/// Use
41/// [`LazyHttpProxyConnectReplyService`](super::LazyHttpProxyConnectReplyService)
42/// together with a handler that establishes its own connection when the CONNECT
43/// request must be acknowledged before an egress connection can be made.
44///
45/// Connection policy belongs in the connector stack. For example, wrap the
46/// connector in a [`TimeoutLayer`](rama_core::layer::TimeoutLayer) to bound the
47/// time spent establishing egress.
48#[derive(Debug, Clone)]
49pub struct EagerHttpProxyConnector<C, S> {
50    connector: C,
51    relay_service: S,
52}
53
54impl<C, S> EagerHttpProxyConnector<C, S> {
55    /// Create an HTTP proxy CONNECT service that establishes egress before replying.
56    #[must_use]
57    pub fn new(connector: C, relay_service: S) -> Self {
58        Self {
59            connector,
60            relay_service,
61        }
62    }
63}
64
65impl<C, S, Body> Service<Request<Body>> for EagerHttpProxyConnector<C, S>
66where
67    C: ConnectorService<ConnectRequest, Connection: Io + Unpin>,
68    S: Service<BridgeIo<Upgraded, C::Connection>, Error: Into<BoxError>> + Clone,
69    Body: Send + 'static,
70{
71    type Output = UpgradeOutput<Request<Body>, Response>;
72    type Error = Response;
73
74    async fn serve(&self, req: Request<Body>) -> Result<Self::Output, Self::Error> {
75        let Some(authority) = req.connector_target_with_default_port(Protocol::HTTP_DEFAULT_PORT)
76        else {
77            tracing::error!("http proxy, error extracting connector target");
78            return Err(StatusCode::BAD_REQUEST.into_response());
79        };
80
81        tracing::info!(
82            server.address = %authority.host,
83            server.port = authority.port,
84            "accept CONNECT: establish egress connection",
85        );
86
87        let EstablishedClientConnection {
88            input: _,
89            conn: egress,
90        } = match self
91            .connector
92            .connect(ConnectRequest::new_with_extensions(
93                authority.clone(),
94                req.extensions().fork(),
95            ))
96            .await
97        {
98            Ok(established) => established,
99            Err(err) => return Err(connect_error_response(&authority, &err)),
100        };
101
102        let relay_service = self.relay_service.clone();
103        Ok(UpgradeResponse::new(req, StatusCode::OK.into_response())
104            .with_extension(ConnectorTarget(authority))
105            .with_handler(move |upgraded| async move {
106                relay_service
107                    .serve(BridgeIo(upgraded, egress))
108                    .await
109                    .map(drop)
110                    .map_err(|err| -> BoxError { err.into() })
111            }))
112    }
113}
114
115fn connect_error_response(
116    authority: &rama_net::address::HostWithPort,
117    err: &rama_net::client::ConnectionError,
118) -> Response {
119    tracing::debug!(
120        server.address = %authority.host,
121        server.port = authority.port,
122        error = ?err,
123        "HTTP proxy CONNECT egress connection failed",
124    );
125
126    if err.kind() == ConnectionErrorKind::Timeout {
127        StatusCode::GATEWAY_TIMEOUT.into_response()
128    } else {
129        StatusCode::BAD_GATEWAY.into_response()
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use std::{
136        pin::Pin,
137        sync::{
138            Arc,
139            atomic::{AtomicBool, Ordering},
140        },
141        task::{Context, Poll},
142        time::Duration,
143    };
144
145    use rama_core::{
146        Layer, ServiceInput,
147        error::BoxErrorExt as _,
148        extensions::{Extensions, ExtensionsRef},
149        layer::TimeoutLayer,
150        service::service_fn,
151    };
152    use rama_http_types::Body;
153    use rama_net::client::{ConnectionError, EstablishedClientConnection};
154    use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
155
156    use super::*;
157
158    fn connect_request() -> Request<()> {
159        Request::builder()
160            .method("CONNECT")
161            .uri(rama_net::uri::Uri::parse_authority_form("example.com:443").unwrap())
162            .body(())
163            .unwrap()
164    }
165
166    fn test_io() -> ServiceInput<tokio_test::io::Mock> {
167        ServiceInput::new(tokio_test::io::Builder::new().build())
168    }
169
170    struct DropTrackedIo {
171        inner: ServiceInput<tokio_test::io::Mock>,
172        dropped: Arc<AtomicBool>,
173    }
174
175    impl Drop for DropTrackedIo {
176        fn drop(&mut self) {
177            self.dropped.store(true, Ordering::SeqCst);
178        }
179    }
180
181    impl ExtensionsRef for DropTrackedIo {
182        fn extensions(&self) -> &Extensions {
183            self.inner.extensions()
184        }
185    }
186
187    impl AsyncRead for DropTrackedIo {
188        fn poll_read(
189            mut self: Pin<&mut Self>,
190            cx: &mut Context<'_>,
191            buf: &mut ReadBuf<'_>,
192        ) -> Poll<std::io::Result<()>> {
193            Pin::new(&mut self.inner).poll_read(cx, buf)
194        }
195    }
196
197    impl AsyncWrite for DropTrackedIo {
198        fn poll_write(
199            mut self: Pin<&mut Self>,
200            cx: &mut Context<'_>,
201            buf: &[u8],
202        ) -> Poll<Result<usize, std::io::Error>> {
203            Pin::new(&mut self.inner).poll_write(cx, buf)
204        }
205
206        fn poll_flush(
207            mut self: Pin<&mut Self>,
208            cx: &mut Context<'_>,
209        ) -> Poll<Result<(), std::io::Error>> {
210            Pin::new(&mut self.inner).poll_flush(cx)
211        }
212
213        fn poll_shutdown(
214            mut self: Pin<&mut Self>,
215            cx: &mut Context<'_>,
216        ) -> Poll<Result<(), std::io::Error>> {
217            Pin::new(&mut self.inner).poll_shutdown(cx)
218        }
219    }
220
221    #[tokio::test]
222    async fn missing_connector_target_is_rejected_before_connecting() {
223        let connected = Arc::new(AtomicBool::new(false));
224        let connector = service_fn({
225            let connected = connected.clone();
226            move |_req: ConnectRequest| {
227                connected.store(true, Ordering::SeqCst);
228                std::future::ready(Ok::<_, ConnectionError>(EstablishedClientConnection {
229                    input: _req,
230                    conn: test_io(),
231                }))
232            }
233        });
234        let service = EagerHttpProxyConnector::new(connector, ());
235        let request = Request::builder()
236            .method("CONNECT")
237            .uri("/")
238            .body(())
239            .unwrap();
240
241        let response = service.serve(request).await.unwrap_err();
242
243        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
244        assert!(!connected.load(Ordering::SeqCst));
245    }
246
247    #[tokio::test]
248    async fn connection_failure_is_returned_instead_of_connect_success() {
249        let connector = service_fn(|_req: ConnectRequest| async move {
250            Err::<EstablishedClientConnection<ServiceInput<tokio_test::io::Mock>, ConnectRequest>, _>(
251                ConnectionError::unknown(BoxError::from_static_str("egress unavailable")),
252            )
253        });
254        let service = EagerHttpProxyConnector::new(connector, ());
255
256        let response = Service::<Request<()>>::serve(&service, connect_request())
257            .await
258            .unwrap_err();
259
260        assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
261    }
262
263    #[tokio::test]
264    async fn connection_timeout_is_reported_as_gateway_timeout() {
265        let connector = service_fn(|_req: ConnectRequest| async move {
266            Err::<EstablishedClientConnection<ServiceInput<tokio_test::io::Mock>, ConnectRequest>, _>(
267                ConnectionError::transport(
268                    BoxError::from_static_str("egress timed out"),
269                    ConnectionErrorKind::Timeout,
270                ),
271            )
272        });
273        let service = EagerHttpProxyConnector::new(connector, ());
274
275        let response = Service::<Request<()>>::serve(&service, connect_request())
276            .await
277            .unwrap_err();
278
279        assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT);
280    }
281
282    #[tokio::test]
283    async fn connector_timeout_layer_is_reported_as_gateway_timeout() {
284        let connector = service_fn(|_req: ConnectRequest| async move {
285            std::future::pending::<
286                Result<
287                    EstablishedClientConnection<ServiceInput<tokio_test::io::Mock>, ConnectRequest>,
288                    ConnectionError,
289                >,
290            >()
291            .await
292        });
293        let connector = TimeoutLayer::new(Duration::ZERO).into_layer(connector);
294        let service = EagerHttpProxyConnector::new(connector, ());
295
296        let response = Service::<Request<()>>::serve(&service, connect_request())
297            .await
298            .unwrap_err();
299
300        assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT);
301    }
302
303    #[tokio::test]
304    async fn established_connection_is_reused_by_upgrade_handler() {
305        let connected = Arc::new(AtomicBool::new(false));
306        let relayed = Arc::new(AtomicBool::new(false));
307
308        let connector = service_fn({
309            let connected = connected.clone();
310            move |input: ConnectRequest| {
311                connected.store(true, Ordering::SeqCst);
312                async move {
313                    Ok::<_, ConnectionError>(EstablishedClientConnection {
314                        input,
315                        conn: test_io(),
316                    })
317                }
318            }
319        });
320        let relay = service_fn({
321            let connected = connected.clone();
322            let relayed = relayed.clone();
323            move |bridge: BridgeIo<Upgraded, ServiceInput<tokio_test::io::Mock>>| {
324                assert!(
325                    connected.load(Ordering::SeqCst),
326                    "egress must be connected before the relay starts"
327                );
328                assert_eq!(
329                    bridge.0.extensions().get_ref::<ConnectorTarget>(),
330                    Some(&ConnectorTarget("example.com:443".parse().unwrap())),
331                );
332                relayed.store(true, Ordering::SeqCst);
333                std::future::ready(Ok::<_, BoxError>(()))
334            }
335        });
336        let connect = EagerHttpProxyConnector::new(connector, relay);
337        let inner = service_fn(|_req: Request| async move {
338            Ok::<_, std::convert::Infallible>(Response::new(Body::empty()))
339        });
340        let service = crate::layer::upgrade::UpgradeLayer::new(
341            rama_core::rt::Executor::default(),
342            true,
343            connect,
344        )
345        .into_layer(inner);
346        let (pending_upgrade, on_upgrade) = crate::io::upgrade::pending();
347        let request = connect_request().map(|()| Body::empty());
348        request.extensions().insert(on_upgrade);
349
350        let response = service.serve(request).await.unwrap();
351        assert_eq!(response.status(), StatusCode::OK);
352        assert!(connected.load(Ordering::SeqCst));
353        assert!(!relayed.load(Ordering::SeqCst));
354
355        pending_upgrade.fulfill(Upgraded::new(test_io(), Default::default()));
356        tokio::time::timeout(Duration::from_secs(5), async {
357            while !relayed.load(Ordering::SeqCst) {
358                tokio::task::yield_now().await;
359            }
360        })
361        .await
362        .expect("relay should receive the established egress connection");
363    }
364
365    #[tokio::test]
366    async fn established_egress_is_dropped_when_upgrade_is_abandoned() {
367        let dropped = Arc::new(AtomicBool::new(false));
368        let connector = service_fn({
369            let dropped = dropped.clone();
370            move |input: ConnectRequest| {
371                let dropped = dropped.clone();
372                async move {
373                    Ok::<_, ConnectionError>(EstablishedClientConnection {
374                        input,
375                        conn: DropTrackedIo {
376                            inner: test_io(),
377                            dropped,
378                        },
379                    })
380                }
381            }
382        });
383        let connect = EagerHttpProxyConnector::new(connector, ());
384        let inner = service_fn(|_req: Request| async move {
385            Ok::<_, std::convert::Infallible>(Response::new(Body::empty()))
386        });
387        let service = crate::layer::upgrade::UpgradeLayer::new(
388            rama_core::rt::Executor::default(),
389            true,
390            connect,
391        )
392        .into_layer(inner);
393        let (pending_upgrade, on_upgrade) = crate::io::upgrade::pending();
394        let request = connect_request().map(|()| Body::empty());
395        request.extensions().insert(on_upgrade);
396
397        let response = service.serve(request).await.unwrap();
398        assert_eq!(response.status(), StatusCode::OK);
399        assert!(!dropped.load(Ordering::SeqCst));
400
401        drop(pending_upgrade);
402        tokio::time::timeout(Duration::from_secs(5), async {
403            while !dropped.load(Ordering::SeqCst) {
404                tokio::task::yield_now().await;
405            }
406        })
407        .await
408        .expect("abandoned upgrade should release established egress");
409    }
410}