Skip to main content

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}