assay/lua/policy/
apply.rs1use std::sync::Arc;
7
8use mlua::{Lua, MultiValue, Table, Value};
9
10use super::{guard_http, is_redacted_header, redact_json_text, redact_keys, response_limit};
11
12const VERBS: &[&str] = &["get", "post", "put", "patch", "delete"];
13
14type Target = Arc<dyn Fn(&MultiValue) -> Option<(String, String)>>;
16
17pub fn apply(lua: &Lua) -> mlua::Result<()> {
18 let Some(http) = lua.globals().get::<Option<Table>>("http")? else {
19 return Ok(());
20 };
21 for verb in VERBS {
22 wrap(lua, &http, verb, verb_target(verb))?;
23 }
24 wrap(lua, &http, "_client_request", client_request_target())?;
27 wrap(lua, &http, "download", verb_target("get"))?;
28 Ok(())
29}
30
31fn verb_target(verb: &'static str) -> Target {
32 Arc::new(move |args| arg_string(args, 0).map(|url| (verb.to_string(), url)))
33}
34
35fn client_request_target() -> Target {
36 Arc::new(|args| Some((arg_string(args, 1)?, arg_string(args, 2)?)))
37}
38
39fn arg_string(args: &MultiValue, at: usize) -> Option<String> {
40 match args.iter().nth(at) {
41 Some(Value::String(s)) => s.to_str().ok().map(|s| s.to_string()),
42 _ => None,
43 }
44}
45
46fn wrap(lua: &Lua, http: &Table, name: &str, target: Target) -> mlua::Result<()> {
47 let Value::Function(inner) = http.get::<Value>(name)? else {
48 return Ok(());
49 };
50 let wrapper = lua.create_async_function(move |lua, args: MultiValue| {
51 let inner = inner.clone();
52 let target = Arc::clone(&target);
53 async move {
54 if let Some((method, url)) = target(&args) {
55 guard_http(&lua, &method, &url)?;
56 }
57 let result = inner.call_async::<Value>(args).await?;
58 sanitize(&lua, result)
59 }
60 })?;
61 http.set(name, wrapper)
62}
63
64fn sanitize(lua: &Lua, result: Value) -> mlua::Result<Value> {
68 let Value::Table(table) = &result else {
69 return Ok(result);
70 };
71 if let Some(limit) = response_limit(lua)
72 && let Ok(body) = table.get::<mlua::String>("body")
73 && body.as_bytes().len() > limit
74 {
75 return Err(mlua::Error::runtime(format!(
76 "policy: response body exceeds max_response_bytes ({limit})"
77 )));
78 }
79
80 let keys = redact_keys(lua);
81 if keys.is_empty() {
82 return Ok(result);
83 }
84 redact_body(lua, table, &keys)?;
85 redact_headers(table, &keys)?;
86 Ok(result)
87}
88
89fn redact_body(lua: &Lua, table: &Table, keys: &[String]) -> mlua::Result<()> {
90 let Ok(body) = table.get::<mlua::String>("body") else {
91 return Ok(());
92 };
93 let bytes = body.as_bytes();
94 let Some(redacted) = std::str::from_utf8(&bytes)
95 .ok()
96 .and_then(|text| redact_json_text(text, keys))
97 else {
98 return Ok(());
99 };
100 table.set("body", lua.create_string(redacted.as_bytes())?)
101}
102
103fn redact_headers(table: &Table, keys: &[String]) -> mlua::Result<()> {
104 let Ok(headers) = table.get::<Table>("headers") else {
105 return Ok(());
106 };
107 let names: Vec<String> = headers
108 .clone()
109 .pairs::<String, Value>()
110 .filter_map(|pair| pair.ok().map(|(name, _)| name))
111 .filter(|name| is_redacted_header(name, keys))
112 .collect();
113 for name in names {
114 headers.set(name, "[redacted]")?;
115 }
116 Ok(())
117}