Skip to main content

synapse_proxy/transform/
mod.rs

1//! The transform pipeline: traits a request/response step implements, the
2//! mutable request/response views they operate on, and the registry of
3//! code-registered custom transforms.
4
5pub mod error_remap;
6pub mod inject;
7pub mod wrap;
8
9use std::collections::HashMap;
10use std::sync::Arc;
11
12use async_trait::async_trait;
13use axum::http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode};
14use serde_json::Value;
15
16use crate::context::ResolvedContext;
17
18/// A step's failure: `Reject` short-circuits the pipeline with a contract body.
19#[derive(Debug, Clone)]
20pub enum TransformError {
21    Reject {
22        status: StatusCode,
23        error: String,
24        detail: String,
25    },
26    Internal(String),
27}
28
29/// Mutable view of the request a `RequestTransform` may edit before forwarding.
30/// `body` is JSON when present; non-JSON bodies are exposed as raw bytes and are
31/// not editable as JSON.
32pub struct ProxyRequest {
33    pub method: Method,
34    pub path: String,
35    pub query: Option<String>,
36    pub headers: HeaderMap,
37    body: Body,
38}
39
40enum Body {
41    Bytes(Vec<u8>),
42    Json(Value),
43}
44
45impl ProxyRequest {
46    pub fn from_parts(
47        method: Method,
48        path: String,
49        query: Option<String>,
50        headers: HeaderMap,
51        bytes: Vec<u8>,
52    ) -> Self {
53        Self {
54            method,
55            path,
56            query,
57            headers,
58            body: Body::Bytes(bytes),
59        }
60    }
61
62    /// Overwrite a header (skips silently if name/value is invalid).
63    pub fn set_header(&mut self, name: &str, value: &str) {
64        if let (Ok(n), Ok(v)) = (HeaderName::try_from(name), HeaderValue::try_from(value)) {
65            self.headers.insert(n, v);
66        }
67    }
68
69    /// Remove a header entirely (used to strip a caller value when an injected
70    /// context key is absent — fail-safe identity handling).
71    pub fn remove_header(&mut self, name: &str) {
72        if let Ok(n) = HeaderName::try_from(name) {
73            self.headers.remove(n);
74        }
75    }
76
77    /// Mutable JSON body, parsing the raw bytes on first access. Errors if the
78    /// body is not valid JSON (a body transform on a non-JSON body is a reject).
79    pub fn body_json_mut(&mut self) -> Result<&mut Value, TransformError> {
80        if let Body::Bytes(b) = &self.body {
81            let parsed = if b.is_empty() {
82                Value::Object(Default::default())
83            } else {
84                serde_json::from_slice(b).map_err(|e| TransformError::Reject {
85                    status: StatusCode::BAD_REQUEST,
86                    error: "invalid_body".into(),
87                    detail: format!("expected JSON body: {e}"),
88                })?
89            };
90            self.body = Body::Json(parsed);
91        }
92        match &mut self.body {
93            Body::Json(v) => Ok(v),
94            Body::Bytes(_) => unreachable!(),
95        }
96    }
97
98    /// Serialize the (possibly transformed) body back to bytes for forwarding.
99    pub fn into_body_bytes(self) -> Vec<u8> {
100        match self.body {
101            Body::Bytes(b) => b,
102            Body::Json(v) => serde_json::to_vec(&v).unwrap_or_default(),
103        }
104    }
105}
106
107/// Mutable view of the upstream response. v1 transforms touch status/headers and
108/// may replace the body with a small JSON value; if no replacement is set the
109/// handler streams the upstream body unchanged.
110pub struct ProxyResponse {
111    pub status: StatusCode,
112    pub headers: HeaderMap,
113    replacement: Option<Value>,
114}
115
116impl ProxyResponse {
117    pub fn new(status: StatusCode, headers: HeaderMap) -> Self {
118        Self {
119            status,
120            headers,
121            replacement: None,
122        }
123    }
124    /// Replace the response body with `value` (sent instead of streaming upstream).
125    pub fn replace_body(&mut self, value: Value) {
126        self.replacement = Some(value);
127    }
128    pub fn replacement(&self) -> Option<&Value> {
129        self.replacement.as_ref()
130    }
131}
132
133#[async_trait]
134pub trait RequestTransform: Send + Sync {
135    async fn apply(
136        &self,
137        ctx: &ResolvedContext,
138        req: &mut ProxyRequest,
139    ) -> Result<(), TransformError>;
140}
141
142#[async_trait]
143pub trait ResponseTransform: Send + Sync {
144    async fn apply(
145        &self,
146        ctx: &ResolvedContext,
147        resp: &mut ProxyResponse,
148    ) -> Result<(), TransformError>;
149}
150
151/// Code-registered custom transforms, looked up by `{ transform = "name" }`.
152#[derive(Default, Clone)]
153pub struct TransformRegistry {
154    pub(crate) request: HashMap<String, Arc<dyn RequestTransform>>,
155    pub(crate) response: HashMap<String, Arc<dyn ResponseTransform>>,
156}
157
158impl TransformRegistry {
159    pub fn register_request(&mut self, name: impl Into<String>, t: Arc<dyn RequestTransform>) {
160        self.request.insert(name.into(), t);
161    }
162    pub fn register_response(&mut self, name: impl Into<String>, t: Arc<dyn ResponseTransform>) {
163        self.response.insert(name.into(), t);
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    fn req() -> ProxyRequest {
172        ProxyRequest::from_parts(
173            Method::POST,
174            "/x".into(),
175            None,
176            HeaderMap::new(),
177            b"{\"a\":1}".to_vec(),
178        )
179    }
180
181    #[test]
182    fn set_header_overwrites() {
183        let mut r = req();
184        r.set_header("x-test", "v1");
185        r.set_header("x-test", "v2");
186        assert_eq!(r.headers.get("x-test").unwrap(), "v2");
187    }
188
189    #[test]
190    fn body_json_mut_parses_and_serializes() {
191        let mut r = req();
192        r.body_json_mut().unwrap()["b"] = serde_json::json!(2);
193        let bytes = r.into_body_bytes();
194        let v: Value = serde_json::from_slice(&bytes).unwrap();
195        assert_eq!(v["a"], 1);
196        assert_eq!(v["b"], 2);
197    }
198
199    #[test]
200    fn body_json_mut_rejects_non_json() {
201        let mut r = ProxyRequest::from_parts(
202            Method::POST,
203            "/x".into(),
204            None,
205            HeaderMap::new(),
206            b"not json".to_vec(),
207        );
208        assert!(matches!(
209            r.body_json_mut(),
210            Err(TransformError::Reject { .. })
211        ));
212    }
213}