1use std::path::Path;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5
6use crate::error::Error;
7use crate::middleware::MiddlewareKind;
8
9#[derive(Debug, Clone)]
13pub struct PluginExport {
14 pub name: String,
15 pub kind: MiddlewareKind,
16 pub stateless: bool,
17 pub needs_body: bool,
18 pub inspects: Vec<String>,
19}
20
21#[derive(Debug)]
23pub struct PluginMetadata {
24 pub name: String,
25 pub version: String,
26 pub abi_version: String,
27 pub exports: Vec<PluginExport>,
28}
29
30#[derive(Clone, Debug, Eq, PartialEq, Hash)]
35pub struct ModuleId(pub Arc<str>);
36
37#[derive(Debug, Clone)]
39pub enum ContextValue {
40 Text(String),
41 Bytes(Vec<u8>),
42 Int64(i64),
43 Uint64(u64),
44 Boolean(bool),
45 ListText(Vec<String>),
46}
47
48#[derive(Debug, Clone)]
50pub struct ContextEntry {
51 pub path: String,
52 pub value: ContextValue,
53}
54
55#[derive(Debug, Clone)]
59pub struct Header {
60 pub name: String,
61 pub value: String,
62}
63
64#[derive(Debug, Clone)]
66pub struct BytesView {
67 pub data: Vec<u8>,
68 pub truncated: bool,
69}
70
71pub struct L4PeekInput {
73 pub peek: Vec<u8>,
74 pub context: Vec<ContextEntry>,
75}
76
77#[derive(Debug)]
79pub enum L4PeekDecision {
80 Continue,
81 Close,
82}
83
84pub struct L4BytesInput {
86 pub bytes: BytesView,
87 pub context: Vec<ContextEntry>,
88}
89
90#[derive(Debug)]
92pub enum L4BytesDecision {
93 Continue,
94 Tunnel,
95 Close,
96}
97
98pub struct L7RequestInput {
100 pub method: String,
101 pub uri: String,
102 pub headers: Vec<Header>,
103 pub body: Option<BytesView>,
104 pub context: Vec<ContextEntry>,
105}
106
107#[derive(Debug, Clone)]
109pub struct SynthResponse {
110 pub status: u16,
111 pub headers: Vec<Header>,
112 pub body: Vec<u8>,
113}
114
115#[derive(Debug)]
117pub enum L7RequestDecision {
118 Continue,
119 Short(SynthResponse),
120 Close,
121}
122
123pub struct L7ResponseInput {
125 pub status: u16,
126 pub headers: Vec<Header>,
127 pub body: Option<BytesView>,
128 pub context: Vec<ContextEntry>,
129}
130
131#[derive(Debug, Clone)]
133pub struct ModifiedResponse {
134 pub status: Option<u16>,
135 pub headers: Option<Vec<Header>>,
136 pub body: Option<Vec<u8>>,
137}
138
139#[derive(Debug)]
141pub enum L7ResponseDecision {
142 Continue,
143 Modify(ModifiedResponse),
144 Abort,
145}
146
147#[derive(Debug)]
152pub enum PluginError {
153 Plugin { code: String, message: String, on_error_hint: Option<String> },
154 Trap(String),
155 Exhausted,
156}
157
158#[async_trait]
165pub trait WasmRuntime: Send + Sync {
166 async fn load_component(&self, path: &Path) -> Result<Arc<PluginMetadata>, Error>;
172
173 async fn invoke_l4_peek(
182 &self,
183 module_id: &ModuleId,
184 export_name: &str,
185 args_json: &str,
186 input: L4PeekInput,
187 ) -> Result<L4PeekDecision, PluginError>;
188
189 async fn invoke_l4_bytes(
191 &self,
192 module_id: &ModuleId,
193 export_name: &str,
194 args_json: &str,
195 input: L4BytesInput,
196 ) -> Result<L4BytesDecision, PluginError>;
197
198 async fn invoke_l7_request(
200 &self,
201 module_id: &ModuleId,
202 export_name: &str,
203 args_json: &str,
204 input: L7RequestInput,
205 ) -> Result<L7RequestDecision, PluginError>;
206
207 async fn invoke_l7_response(
209 &self,
210 module_id: &ModuleId,
211 export_name: &str,
212 args_json: &str,
213 input: L7ResponseInput,
214 ) -> Result<L7ResponseDecision, PluginError>;
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
222pub struct WasmPoolSummary {
223 pub kind: String,
226 pub key: String,
229 pub export: String,
231 pub capacity: usize,
234 pub available: usize,
238 pub total_allocations: u64,
241 pub failures: u64,
244}
245
246pub trait WasmPoolStats: Send + Sync {
252 fn snapshot(&self) -> Vec<WasmPoolSummary>;
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
271pub struct PluginHttpPolicy {
272 #[serde(default)]
276 pub allow_insecure: bool,
277 #[serde(default)]
283 pub allowed_hosts: Vec<String>,
284 #[serde(default = "default_max_body_size")]
288 pub max_body_size: u32,
289 #[serde(default = "default_timeout_ms")]
292 pub default_timeout_ms: u32,
293 #[serde(default = "default_follow_redirects")]
296 pub default_follow_redirects: u32,
297}
298
299const fn default_max_body_size() -> u32 {
300 1024 * 1024
301}
302
303const fn default_timeout_ms() -> u32 {
304 30_000
305}
306
307const fn default_follow_redirects() -> u32 {
308 5
309}
310
311impl Default for PluginHttpPolicy {
312 fn default() -> Self {
313 Self {
314 allow_insecure: false,
315 allowed_hosts: Vec::new(),
316 max_body_size: default_max_body_size(),
317 default_timeout_ms: default_timeout_ms(),
318 default_follow_redirects: default_follow_redirects(),
319 }
320 }
321}
322
323#[derive(Debug, Clone, Default)]
327pub struct PluginPolicyTable {
328 pub policies: std::collections::HashMap<String, PluginHttpPolicy>,
329}
330
331impl PluginPolicyTable {
332 #[must_use]
333 pub fn new() -> Self {
334 Self { policies: std::collections::HashMap::new() }
335 }
336
337 pub fn from_json(s: &str) -> Result<Self, Error> {
346 let policies: std::collections::HashMap<String, PluginHttpPolicy> =
347 serde_json::from_str(s).map_err(|e| Error::compile(format!("wasm/policy.json: {e}")))?;
348 Ok(Self { policies })
349 }
350
351 pub fn load_from_dir(wasm_dir: &std::path::Path) -> Result<Self, Error> {
359 let path = wasm_dir.join("policy.json");
360 match std::fs::read_to_string(&path) {
361 Ok(s) => Self::from_json(&s),
362 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::new()),
363 Err(e) => Err(Error::compile(format!("wasm/policy.json: read {}: {e}", path.display()))),
364 }
365 }
366
367 #[must_use]
370 pub fn get_or_default(&self, stem: &str) -> PluginHttpPolicy {
371 self.policies.get(stem).cloned().unwrap_or_default()
372 }
373}
374
375#[cfg(test)]
376mod policy_tests {
377 use super::*;
378
379 #[test]
380 fn default_policy_is_deny_all() {
381 let p = PluginHttpPolicy::default();
382 assert!(!p.allow_insecure);
383 assert!(p.allowed_hosts.is_empty(), "deny-all by default");
384 assert_eq!(p.max_body_size, 1024 * 1024);
385 assert_eq!(p.default_timeout_ms, 30_000);
386 assert_eq!(p.default_follow_redirects, 5);
387 }
388
389 #[test]
390 fn policy_table_round_trips_explicit_fields() {
391 let json = r#"{
392 "edge": {
393 "allow_insecure": true,
394 "allowed_hosts": ["api.internal", "*.example.com"],
395 "max_body_size": 65536,
396 "default_timeout_ms": 5000,
397 "default_follow_redirects": 0
398 }
399 }"#;
400 let t = PluginPolicyTable::from_json(json).expect("parse");
401 let p = t.get_or_default("edge");
402 assert!(p.allow_insecure);
403 assert_eq!(p.allowed_hosts, vec!["api.internal".to_string(), "*.example.com".to_string()]);
404 assert_eq!(p.max_body_size, 65_536);
405 assert_eq!(p.default_timeout_ms, 5000);
406 assert_eq!(p.default_follow_redirects, 0);
407 }
408
409 #[test]
410 fn policy_table_partial_entry_fills_defaults() {
411 let json = r#"{ "edge": { "allowed_hosts": ["x.y"] } }"#;
412 let t = PluginPolicyTable::from_json(json).expect("parse");
413 let p = t.get_or_default("edge");
414 assert_eq!(p.allowed_hosts, vec!["x.y".to_string()]);
415 assert_eq!(p.max_body_size, 1024 * 1024, "default fills");
416 assert_eq!(p.default_timeout_ms, 30_000);
417 }
418
419 #[test]
420 fn policy_table_missing_plugin_returns_deny_all_default() {
421 let t = PluginPolicyTable::from_json(r#"{ "other": {} }"#).expect("parse");
422 let p = t.get_or_default("missing");
423 assert_eq!(p, PluginHttpPolicy::default());
424 }
425
426 #[test]
427 fn policy_table_load_from_dir_handles_absent_file() {
428 let tmp = tempfile::tempdir().expect("tempdir");
429 let t = PluginPolicyTable::load_from_dir(tmp.path()).expect("absent ok");
430 assert!(t.policies.is_empty());
431 }
432
433 #[test]
434 fn policy_table_load_from_dir_parses_json() {
435 let tmp = tempfile::tempdir().expect("tempdir");
436 std::fs::write(tmp.path().join("policy.json"), r#"{ "x": { "allowed_hosts": ["*"] } }"#)
437 .expect("write");
438 let t = PluginPolicyTable::load_from_dir(tmp.path()).expect("parse");
439 assert_eq!(t.get_or_default("x").allowed_hosts, vec!["*".to_string()]);
440 }
441
442 #[test]
443 fn policy_table_load_from_dir_propagates_parse_errors() {
444 let tmp = tempfile::tempdir().expect("tempdir");
445 std::fs::write(tmp.path().join("policy.json"), "{ this is not json").expect("write");
446 let err = PluginPolicyTable::load_from_dir(tmp.path()).expect_err("must fail");
447 assert!(err.to_string().contains("policy.json"));
448 }
449}