Skip to main content

mcpr_core/proxy/
pipeline_builder.rs

1//! Default middleware chain construction for the pipeline.
2//!
3//! One call to [`build_default_pipeline`] produces the ordered chain
4//! from `PIPELINE.md` §Middleware. Order matters:
5//! schema ingest runs before CSP rewrite so the schema store captures
6//! the raw upstream CSP (cloud-backend contract), and envelope seal runs
7//! last so content-inspecting middlewares operate on `McpBuffered`
8//! before it becomes a sealed `Raw`.
9
10use std::sync::Arc;
11
12use arc_swap::ArcSwap;
13
14use super::RewriteConfig;
15use super::pipeline::driver::Pipeline;
16use super::pipeline::middleware::{RequestMiddleware, ResponseMiddleware};
17use super::pipeline::middlewares::{
18    ClientInfoInjectMiddleware, CspRewriteMiddleware, EnvelopeSealMiddleware,
19    HealthTrackMiddleware, SchemaIngestMiddleware, SchemaStaleMiddleware, SessionDeleteMiddleware,
20    SessionRecordMiddleware, SessionTouchMiddleware, TargetExtractMiddleware, UrlMapMiddleware,
21};
22use super::router::ProxyRouter;
23use super::transport::ProxyTransport;
24
25/// Concrete `Pipeline` instantiation used in production. Middleware
26/// traits are object-safe (`Box<dyn …>`), but the router and transport
27/// are generic parameters that want concrete types at construction.
28pub type ProxyPipeline = Pipeline<ProxyRouter, ProxyTransport>;
29
30/// Build the baseline pipeline. `rewrite_config` is shared with the
31/// middlewares that read it (`CspRewrite`, `UrlMap`) — swapping the
32/// inner `Arc` via `.store()` hot-reloads rules without restart.
33pub fn build_default_pipeline(rewrite_config: Arc<ArcSwap<RewriteConfig>>) -> ProxyPipeline {
34    let request_chain: Vec<Box<dyn RequestMiddleware>> = vec![
35        Box::new(SessionDeleteMiddleware),
36        Box::new(SessionTouchMiddleware),
37        Box::new(ClientInfoInjectMiddleware),
38        Box::new(TargetExtractMiddleware),
39    ];
40    let response_chain: Vec<Box<dyn ResponseMiddleware>> = vec![
41        // `SchemaIngest` reads the raw upstream result BEFORE `CspRewrite`
42        // mutates it — the schema store must capture the untouched CSP.
43        Box::new(SchemaIngestMiddleware),
44        Box::new(SchemaStaleMiddleware),
45        Box::new(CspRewriteMiddleware::new(rewrite_config.clone())),
46        Box::new(SessionRecordMiddleware),
47        Box::new(HealthTrackMiddleware),
48        // `UrlMap` only touches `OauthJson` / `Raw-with-JSON-content-type`
49        // that came from the passthrough dispatch — ordering it before
50        // `EnvelopeSeal` keeps it from also re-rewriting the sealed
51        // `McpBuffered → Raw` bytes.
52        Box::new(UrlMapMiddleware::new(rewrite_config)),
53        Box::new(EnvelopeSealMiddleware),
54    ];
55
56    for mw in &request_chain {
57        tracing::info!(chain = "request", name = mw.name(), "middleware registered");
58    }
59    for mw in &response_chain {
60        tracing::info!(
61            chain = "response",
62            name = mw.name(),
63            "middleware registered"
64        );
65    }
66
67    Pipeline::new(request_chain, response_chain, ProxyRouter, ProxyTransport)
68}
69
70#[cfg(test)]
71#[allow(non_snake_case)]
72mod tests {
73    use super::*;
74
75    use crate::proxy::pipeline::middlewares::test_support::test_proxy_state;
76
77    #[tokio::test]
78    async fn build_default_pipeline__registers_expected_chain_names_in_order() {
79        let proxy = test_proxy_state();
80        let pipeline = build_default_pipeline(proxy.rewrite_config.clone());
81
82        assert_eq!(
83            pipeline.request_chain_names(),
84            vec![
85                "session_delete",
86                "session_touch",
87                "client_info_inject",
88                "target_extract",
89            ],
90        );
91        assert_eq!(
92            pipeline.response_chain_names(),
93            vec![
94                "schema_ingest",
95                "schema_stale",
96                "csp_rewrite",
97                "session_record",
98                "health_track",
99                "url_map",
100                "envelope_seal",
101            ],
102        );
103    }
104}