rama_http/layer/upgrade/http_proxy_connect/
eager.rs1use 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#[derive(Debug, Clone)]
49pub struct EagerHttpProxyConnector<C, S> {
50 connector: C,
51 relay_service: S,
52}
53
54impl<C, S> EagerHttpProxyConnector<C, S> {
55 #[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}