rama_http/layer/upgrade/mitm/svc.rs
1use std::fmt;
2use std::sync::Arc;
3
4use crate::{
5 Body, Request, Response, StreamingBody, io::upgrade::Upgraded,
6 opentelemetry::version_as_protocol_version, service::web::response::IntoResponse,
7};
8use rama_core::{
9 Service, bytes,
10 error::{BoxError, ErrorExt as _},
11 error_sink::{ErrorSink, TracingErrorSink},
12 extensions::ExtensionsRef,
13 io::BridgeIo,
14 matcher::service::{ServiceMatch, ServiceMatcher},
15 rt::Executor,
16 telemetry::tracing::{self, Instrument as _},
17};
18
19#[derive(Clone)]
20/// Http middleware that can be used by MITM proxies,
21/// such as transparent (L4) proxies to relay a HTTP upgrade-request
22/// as-is and pipe the upgraded upgrade request on both ends
23/// via the upgrade (bridgeIo) svc.
24pub struct HttpUpgradeMitmRelay<M, S> {
25 exec: Executor,
26 nested_matcher_svc: M,
27 inner_svc: S,
28 error_sink: Arc<dyn ErrorSink>,
29}
30
31impl<M, S> HttpUpgradeMitmRelay<M, S> {
32 #[inline(always)]
33 #[must_use]
34 /// Create a new [`HttpUpgradeMitmRelay`].
35 pub fn new(exec: Executor, nested_matcher_svc: M, inner_svc: S) -> Self {
36 Self {
37 exec,
38 nested_matcher_svc,
39 inner_svc,
40 error_sink: Arc::new(TracingErrorSink::default()),
41 }
42 }
43
44 /// Set a custom [`ErrorSink`] used to observe errors from the detached
45 /// relay task (relay-service failures and upgrade failures on either side).
46 ///
47 /// Defaults to [`TracingErrorSink::default`] (traces at DEBUG level).
48 #[must_use]
49 pub fn with_error_sink(mut self, sink: impl ErrorSink) -> Self {
50 self.error_sink = Arc::new(sink);
51 self
52 }
53}
54
55impl<M: fmt::Debug, S: fmt::Debug> fmt::Debug for HttpUpgradeMitmRelay<M, S> {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 f.debug_struct("HttpUpgradeMitmRelay")
58 .field("exec", &self.exec)
59 .field("nested_matcher_svc", &self.nested_matcher_svc)
60 .field("inner_svc", &self.inner_svc)
61 .finish()
62 }
63}
64
65impl<M, S, ReqBody, ModReqBody, ResBody, ModResBody> Service<Request<ReqBody>>
66 for HttpUpgradeMitmRelay<M, S>
67where
68 M: ServiceMatcher<
69 Request<ReqBody>,
70 Error: IntoResponse,
71 ModifiedInput = Request<ModReqBody>,
72 Service: ServiceMatcher<
73 Response<ResBody>,
74 Error: Into<S::Error>,
75 ModifiedInput = Response<ModResBody>,
76 Service: Service<BridgeIo<Upgraded, Upgraded>, Output = (), Error: Into<BoxError>>,
77 >,
78 >,
79 S: Service<Request, Output = Response<ResBody>>,
80 ReqBody: StreamingBody<Data = bytes::Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
81 ModReqBody: StreamingBody<Data = bytes::Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
82 ResBody: StreamingBody<Data = bytes::Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
83 ModResBody: StreamingBody<Data = bytes::Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
84{
85 type Output = Response;
86 type Error = S::Error;
87
88 async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
89 let ServiceMatch {
90 input: req,
91 service: maybe_res_svc_matcher,
92 } = match self.nested_matcher_svc.match_service(req).await {
93 Ok(sm) => sm,
94 Err(err) => return Ok(err.into_response()),
95 };
96
97 if let Some(res_svc_matcher) = maybe_res_svc_matcher {
98 tracing::debug!("HttpUpgradeMitmRelay: upgrade MITM relay req match made...");
99
100 let on_upgrade_ingress = crate::io::upgrade::handle_upgrade(&req);
101
102 let relay_upgrade_span = tracing::trace_root_span!(
103 "upgrade::mitm_relay::serve",
104 otel.kind = "server",
105 http.request.method = %req.method().as_str(),
106 url.full = %req.request_uri(),
107 url.path = %req.uri().path_or_root().as_ref(),
108 url.query = %req.uri().query_or_empty().as_ref(),
109 url.scheme = %req.uri().scheme_str().unwrap_or_default(),
110 network.protocol.name = "http",
111 network.protocol.version = version_as_protocol_version(req.version()),
112 );
113
114 tracing::trace!(
115 "HttpUpgradeMitmRelay: matched req flow: request response from inner svc"
116 );
117
118 let res = self.inner_svc.serve(req.map(Body::new)).await?;
119
120 tracing::trace!(
121 "HttpUpgradeMitmRelay: matched req flow: received res from inner flow... continue match making"
122 );
123
124 let ServiceMatch {
125 input: res,
126 service: maybe_relay_svc,
127 } = res_svc_matcher
128 .into_match_service(res)
129 .await
130 .map_err(Into::into)?;
131
132 if let Some(relay_svc) = maybe_relay_svc {
133 tracing::debug!(
134 "HttpUpgradeMitmRelay: upgrade MITM relay res match made... spawning relay task..."
135 );
136
137 let on_upgrade_egress = crate::io::upgrade::handle_upgrade(&res);
138 // The relay service reads its negotiated config from the
139 // upgraded EGRESS stream's extensions (a WS relay's
140 // `RelayWebSocketConfig`, carrying the agreed permessage-deflate
141 // params). On HTTP/1 the upgraded stream is fulfilled from the
142 // bare connection io and does NOT inherit the response
143 // extensions — only the h2 client threads `res.extensions()`
144 // into its upgraded io. Graft them on here so h1 and h2 behave
145 // identically; without it an h1 WS relay builds its sockets
146 // WITHOUT deflate and resets the first compressed frame.
147 //
148 // On h2 this graft is technically redundant — the upgraded
149 // io's extension store and `res.extensions()` share the same
150 // top-level `Arc`, so `extend` duplicates the top-level
151 // entries into the same `AppendOnlyVec`. `get_ref` walks
152 // newest-first and returns the (identical) duplicate, so it's
153 // correctness-neutral; the cost is one extra entry per
154 // top-level item per WS upgrade. Filtering to the specific
155 // entries the relay reads would couple this layer to
156 // rama-ws-side types (e.g. `RelayWebSocketConfig`), which
157 // we'd rather not — the over-graft is bounded and benign.
158 //
159 // NOTE: grafted per call site rather than inside
160 // `handle_upgrade` ON PURPOSE, and it grafts the ENTIRE
161 // top-level of `res.extensions()` (not a curated subset).
162 // This is safe because `extend` copies only the TOP-LEVEL
163 // entries of the source (it does NOT walk the parent chain),
164 // and a client-received response's top level is a `fork()` of
165 // the request (h1: `conn.rs` client branch; h2: the stream
166 // equivalent) — so it does NOT carry the connection's own
167 // `Ingress`/`Egress(self.io.extensions())` self-wrapper. That
168 // wrapper, when present, lives in the parent chain, which
169 // `extend` skips. So grafting the response top level onto the
170 // egress upgraded stream cannot introduce a back-pointer to
171 // that stream's own store.
172 //
173 // Centralizing this inside `handle_upgrade` is NOT safe: it
174 // would also run on the server-acceptor path, where the
175 // request's TOP level *does* carry
176 // `Ingress(self.io.extensions())` (`conn.rs:221`) and
177 // `Upgraded::new` shares that same io extension `Arc`. The
178 // graft would then make the io's store contain a wrapper
179 // pointing back at itself — a self-referential `Extensions`
180 // cycle → stack overflow on `get_ref` traversal (confirmed
181 // empirically: centralizing SIGABRTs the WS suite).
182 let egress_msg_ext = res.extensions().clone();
183 let error_sink = self.error_sink.clone();
184 tracing::trace!("HttpUpgradeMitmRelay: spawn relay svc on its own task");
185
186 self.exec.spawn_task(async move {
187 tracing::debug!(
188 "HttpUpgradeMitmRelay: spawned task active"
189 );
190
191 let (ingress_stream, egress_stream) = match tokio::try_join!(on_upgrade_ingress, on_upgrade_egress) {
192 Ok(streams) => streams,
193 Err(err) => {
194 // routed to the sink instead of swallowed: the relay
195 // task is detached, so this is its only error outlet.
196 error_sink.sink_error(
197 err.context("mitm relay: upgrade failed on one or both sides"),
198 );
199 return;
200 }
201 };
202
203 egress_stream.extensions().extend(&egress_msg_ext);
204
205 tracing::trace!(
206 "HttpUpgradeMitmRelay: relay task: bidirectional upgrade complete: continue serving via upgrade relay svc"
207 );
208 if let Err(err) = relay_svc.serve(BridgeIo(ingress_stream, egress_stream)).await {
209 error_sink.sink_error(err.context("mitm relay handler failed"));
210 }
211 }.instrument(relay_upgrade_span));
212
213 Ok(res.map(Body::new))
214 } else {
215 tracing::debug!(
216 "HttpUpgradeMitmRelay: aborted: req was matched... but no response match"
217 );
218 Ok(res.map(Body::new))
219 }
220 } else {
221 let res = self.inner_svc.serve(req.map(Body::new)).await?;
222 Ok(res.map(Body::new))
223 }
224 }
225}