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}