Skip to main content

featherbit/context/
mod.rs

1//! The per-request `Context` object that flows through every node of a policy
2//! graph, carrying the inbound request, the response under construction,
3//! free-form inter-node state, and any errors accumulated along the way.
4//! Serializable so it can be marshalled to and from Lua scripts.
5
6use bytes::Bytes;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::net::SocketAddr;
10
11/// Holds all state for one request as it travels through a policy graph.
12///
13/// Each plugin receives the context, may mutate any part of it, and passes it
14/// on through its success or error port. The engine sends `response` back to
15/// the client once graph execution finishes.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Context {
18    /// The inbound HTTP request as received by the listener (plugins may rewrite it before proxying).
19    pub request: GatewayRequest,
20    /// The response being built; whatever is here when the graph finishes is sent to the client.
21    pub response: GatewayResponse,
22    /// Free-form key/value scratch space for passing data between nodes (e.g. auth claims).
23    pub message: HashMap<String, serde_json::Value>,
24    /// Errors recorded by nodes; routing through a node's `error` port appends here.
25    pub errors: Vec<GatewayError>,
26}
27
28/// Protocol-agnostic snapshot of the inbound request, decoupled from hyper types.
29///
30/// Multi-valued headers and query parameters are preserved as `Vec<String>`.
31/// The body is fully buffered; it serializes as base64 (see `bytes_serde`),
32/// which is how it crosses into Lua scripts.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct GatewayRequest {
35    /// HTTP method (`GET`, `POST`, ...).
36    pub method: String,
37    /// Request path without the query string.
38    pub path: String,
39    /// Value of the `Host` header (empty string when absent).
40    pub host: String,
41    /// URI scheme, defaulting to `http` when the URI carries none.
42    pub scheme: String,
43    /// Header name → list of values (headers may repeat).
44    pub headers: HashMap<String, Vec<String>>,
45    /// Query parameter name → list of values (parameters may repeat).
46    pub query_params: HashMap<String, Vec<String>>,
47    /// Fully buffered request body, serialized as base64.
48    #[serde(with = "bytes_serde")]
49    pub body: Bytes,
50    /// Client socket address as `ip:port`.
51    pub remote_addr: String,
52    /// Wire protocol the request arrived on.
53    pub protocol: Protocol,
54}
55
56/// The response under construction, ultimately returned to the client.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct GatewayResponse {
59    /// HTTP status code; `0` until a node (e.g. upstream or error-handler) sets it.
60    pub status_code: u16,
61    /// Header name → list of values.
62    pub headers: HashMap<String, Vec<String>>,
63    /// Response body, serialized as base64.
64    #[serde(with = "bytes_serde")]
65    pub body: Bytes,
66}
67
68/// An error recorded by a node during graph execution.
69///
70/// Errors do not abort the pipeline by themselves; the graph engine routes
71/// the context through the failing node's `error` port (or the policy's
72/// error handler) with this record appended to `Context::errors`.
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
74pub struct GatewayError {
75    /// Id of the node that produced the error.
76    pub node_id: String,
77    /// Machine-readable error code (e.g. `unauthorized`, `rate_limited`).
78    pub code: String,
79    /// Human-readable description.
80    pub message: String,
81    /// Optional structured details; defaults to empty when absent.
82    #[serde(default)]
83    pub metadata: HashMap<String, serde_json::Value>,
84}
85
86/// Wire protocol of the inbound connection.
87///
88/// Serialized in lowercase (`http1`, `http2`, ...). Only `Http1` and `Http2`
89/// are currently produced; the remaining variants are reserved for planned
90/// WebSocket/TCP/UDP proxying.
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
92#[serde(rename_all = "lowercase")]
93pub enum Protocol {
94    Http1,
95    Http2,
96    WebSocket,
97    Tcp,
98    Udp,
99}
100
101impl Context {
102    /// Creates a fresh context for an inbound request.
103    ///
104    /// The response starts empty with `status_code == 0` (i.e. "unset"),
105    /// and `message`/`errors` start empty.
106    pub fn new(request: GatewayRequest) -> Self {
107        Self {
108            request,
109            response: GatewayResponse {
110                status_code: 0,
111                headers: HashMap::new(),
112                body: Bytes::new(),
113            },
114            message: HashMap::new(),
115            errors: Vec::new(),
116        }
117    }
118}
119
120impl GatewayRequest {
121    /// Builds a `GatewayRequest` from parsed hyper request parts and a buffered body.
122    ///
123    /// Non-UTF-8 header values become empty strings, the query string is split
124    /// on `&`/`=` without percent-decoding, and the protocol is classified as
125    /// HTTP/2 only for `http::Version::HTTP_2` (HTTP/1.x otherwise).
126    pub fn from_hyper(req: &http::request::Parts, body: Bytes, remote_addr: SocketAddr) -> Self {
127        let mut headers: HashMap<String, Vec<String>> = HashMap::new();
128        for (name, value) in req.headers.iter() {
129            headers
130                .entry(name.as_str().to_string())
131                .or_default()
132                .push(value.to_str().unwrap_or("").to_string());
133        }
134
135        let mut query_params: HashMap<String, Vec<String>> = HashMap::new();
136        if let Some(query) = req.uri.query() {
137            for pair in query.split('&') {
138                let mut parts = pair.splitn(2, '=');
139                let key = parts.next().unwrap_or("").to_string();
140                let value = parts.next().unwrap_or("").to_string();
141                query_params.entry(key).or_default().push(value);
142            }
143        }
144
145        let host = req
146            .headers
147            .get("host")
148            .and_then(|v| v.to_str().ok())
149            .unwrap_or("")
150            .to_string();
151
152        let scheme = req.uri.scheme_str().unwrap_or("http").to_string();
153
154        let protocol = if req.version == http::Version::HTTP_2 {
155            Protocol::Http2
156        } else {
157            Protocol::Http1
158        };
159
160        Self {
161            method: req.method.as_str().to_string(),
162            path: req.uri.path().to_string(),
163            host,
164            scheme,
165            headers,
166            query_params,
167            body,
168            remote_addr: remote_addr.to_string(),
169            protocol,
170        }
171    }
172}
173
174/// Serde adapter that encodes `Bytes` as a base64 string, keeping binary
175/// bodies intact through JSON/Lua round-trips.
176mod bytes_serde {
177    use base64::{engine::general_purpose::STANDARD, Engine};
178    use bytes::Bytes;
179    use serde::{self, Deserialize, Deserializer, Serializer};
180
181    pub fn serialize<S>(bytes: &Bytes, serializer: S) -> Result<S::Ok, S::Error>
182    where
183        S: Serializer,
184    {
185        serializer.serialize_str(&STANDARD.encode(bytes))
186    }
187
188    pub fn deserialize<'de, D>(deserializer: D) -> Result<Bytes, D::Error>
189    where
190        D: Deserializer<'de>,
191    {
192        let s = String::deserialize(deserializer)?;
193        STANDARD
194            .decode(&s)
195            .map(Bytes::from)
196            .map_err(serde::de::Error::custom)
197    }
198}