1use globset::Glob;
28use hmac::{Hmac, Mac};
29use mlua::prelude::*;
30use sha2::{Digest, Sha256};
31use std::path::Path;
32use std::time::Duration;
33
34pub(crate) fn register_all_host_apis(
49 lua: &Lua,
50 script_name: &str,
51 sandbox_root: &Path,
52) -> LuaResult<()> {
53 sandbox_globals(lua)?;
54 register_http_api(lua)?;
55 register_json_api(lua)?;
56 register_env_api(lua)?;
57 register_log_api(lua, script_name)?;
58 register_fs_api(lua, sandbox_root)?;
59 register_base64_api(lua)?;
60 register_crypto_api(lua)?;
61 register_sleep(lua)?;
62 Ok(())
63}
64
65pub(crate) fn sandbox_globals(lua: &Lua) -> LuaResult<()> {
71 let globals = lua.globals();
72 globals.set("os", LuaValue::Nil)?;
73 globals.set("io", LuaValue::Nil)?;
74 globals.set("loadfile", LuaValue::Nil)?;
75 globals.set("dofile", LuaValue::Nil)?;
76 globals.set("debug", LuaValue::Nil)?;
77 Ok(())
78}
79
80fn register_http_api(lua: &Lua) -> LuaResult<()> {
85 let client = reqwest::blocking::Client::builder()
86 .timeout(Duration::from_secs(30))
87 .redirect(reqwest::redirect::Policy::limited(10))
88 .build()
89 .map_err(mlua::Error::external)?;
90
91 let http = lua.create_table()?;
92
93 let c = client.clone();
95 http.set(
96 "get",
97 lua.create_function(move |lua, (url, opts): (String, Option<LuaTable>)| {
98 do_http_request(lua, &c, "GET", &url, None, opts)
99 })?,
100 )?;
101
102 let c = client.clone();
104 http.set(
105 "post",
106 lua.create_function(
107 move |lua, (url, body, opts): (String, String, Option<LuaTable>)| {
108 do_http_request(lua, &c, "POST", &url, Some(&body), opts)
109 },
110 )?,
111 )?;
112
113 let c = client.clone();
115 http.set(
116 "put",
117 lua.create_function(
118 move |lua, (url, body, opts): (String, String, Option<LuaTable>)| {
119 do_http_request(lua, &c, "PUT", &url, Some(&body), opts)
120 },
121 )?,
122 )?;
123
124 lua.globals().set("http", http)?;
125 Ok(())
126}
127
128fn do_http_request(
130 lua: &Lua,
131 client: &reqwest::blocking::Client,
132 method: &str,
133 url: &str,
134 body: Option<&str>,
135 opts: Option<LuaTable>,
136) -> LuaResult<LuaTable> {
137 let mut builder = match method {
138 "GET" => client.get(url),
139 "POST" => client.post(url),
140 "PUT" => client.put(url),
141 "DELETE" => client.delete(url),
142 "PATCH" => client.patch(url),
143 _ => {
144 return Err(mlua::Error::external(anyhow::anyhow!(
145 "unsupported HTTP method: {}",
146 method
147 )))
148 }
149 };
150
151 if let Some(ref opts) = opts {
152 if let Ok(headers) = opts.get::<LuaTable>("headers") {
154 for pair in headers.pairs::<String, String>() {
155 let (k, v) = pair?;
156 builder = builder.header(k, v);
157 }
158 }
159
160 if let Ok(params) = opts.get::<LuaTable>("params") {
162 let mut param_vec: Vec<(String, String)> = Vec::new();
163 for pair in params.pairs::<String, String>() {
164 let (k, v) = pair?;
165 param_vec.push((k, v));
166 }
167 builder = builder.query(¶m_vec);
168 }
169
170 if let Ok(timeout) = opts.get::<f64>("timeout") {
172 builder = builder.timeout(Duration::from_secs_f64(timeout));
173 }
174 }
175
176 if let Some(body) = body {
178 builder = builder.body(body.to_string());
179 }
180
181 let response = builder.send().map_err(|e| {
183 mlua::Error::external(anyhow::anyhow!("HTTP {} {} failed: {}", method, url, e))
184 })?;
185
186 let status = response.status().as_u16();
187 let ok = response.status().is_success();
188
189 let headers_table = lua.create_table()?;
191 for (name, value) in response.headers() {
192 if let Ok(v) = value.to_str() {
193 headers_table.set(name.as_str(), v.to_string())?;
194 }
195 }
196
197 let body_text = response.text().map_err(|e| {
199 mlua::Error::external(anyhow::anyhow!("Failed to read response body: {}", e))
200 })?;
201
202 let json_value = serde_json::from_str::<serde_json::Value>(&body_text).ok();
204
205 let result = lua.create_table()?;
207 result.set("status", status)?;
208 result.set("headers", headers_table)?;
209 result.set("body", body_text)?;
210 result.set("ok", ok)?;
211 if let Some(json) = json_value {
212 result.set("json", json_value_to_lua(lua, &json)?)?;
213 }
214
215 Ok(result)
216}
217
218fn register_json_api(lua: &Lua) -> LuaResult<()> {
223 let json_table = lua.create_table()?;
224
225 json_table.set(
226 "parse",
227 lua.create_function(|lua, s: String| {
228 let value: serde_json::Value = serde_json::from_str(&s)
229 .map_err(|e| mlua::Error::external(anyhow::anyhow!("json.parse: {}", e)))?;
230 json_value_to_lua(lua, &value)
231 })?,
232 )?;
233
234 json_table.set(
235 "encode",
236 lua.create_function(|_lua, value: LuaValue| {
237 let json = lua_value_to_json(value)?;
238 serde_json::to_string(&json)
239 .map_err(|e| mlua::Error::external(anyhow::anyhow!("json.encode: {}", e)))
240 })?,
241 )?;
242
243 lua.globals().set("json", json_table)?;
244 Ok(())
245}
246
247fn register_env_api(lua: &Lua) -> LuaResult<()> {
252 let env = lua.create_table()?;
253
254 env.set(
255 "get",
256 lua.create_function(|_lua, name: String| Ok(std::env::var(&name).ok()))?,
257 )?;
258
259 lua.globals().set("env", env)?;
260 Ok(())
261}
262
263fn register_log_api(lua: &Lua, script_name: &str) -> LuaResult<()> {
268 let log = lua.create_table()?;
269
270 let n = script_name.to_string();
271 log.set(
272 "info",
273 lua.create_function(move |_lua, msg: String| {
274 eprintln!("[{}] INFO: {}", n, msg);
275 Ok(())
276 })?,
277 )?;
278
279 let n = script_name.to_string();
280 log.set(
281 "warn",
282 lua.create_function(move |_lua, msg: String| {
283 eprintln!("[{}] WARN: {}", n, msg);
284 Ok(())
285 })?,
286 )?;
287
288 let n = script_name.to_string();
289 log.set(
290 "error",
291 lua.create_function(move |_lua, msg: String| {
292 eprintln!("[{}] ERROR: {}", n, msg);
293 Ok(())
294 })?,
295 )?;
296
297 let n = script_name.to_string();
298 log.set(
299 "debug",
300 lua.create_function(move |_lua, msg: String| {
301 eprintln!("[{}] DEBUG: {}", n, msg);
302 Ok(())
303 })?,
304 )?;
305
306 lua.globals().set("log", log)?;
307 Ok(())
308}
309
310fn register_fs_api(lua: &Lua, sandbox_root: &Path) -> LuaResult<()> {
315 let fs = lua.create_table()?;
316 let root = sandbox_root
317 .canonicalize()
318 .unwrap_or_else(|_| sandbox_root.to_path_buf());
319
320 let r = root.clone();
322 fs.set(
323 "read",
324 lua.create_function(move |_lua, path: String| {
325 let target = r.join(&path);
326 let canonical = target
327 .canonicalize()
328 .map_err(|e| mlua::Error::external(anyhow::anyhow!("fs.read: {}: {}", path, e)))?;
329 if !canonical.starts_with(&r) {
330 return Err(mlua::Error::external(anyhow::anyhow!(
331 "fs.read: path escapes sandbox: {}",
332 path
333 )));
334 }
335 std::fs::read_to_string(&canonical)
336 .map_err(|e| mlua::Error::external(anyhow::anyhow!("fs.read: {}: {}", path, e)))
337 })?,
338 )?;
339
340 let r = root.clone();
342 fs.set(
343 "list",
344 lua.create_function(move |lua, (dir, glob_pattern): (String, Option<String>)| {
345 let target = r.join(&dir);
346 let canonical = target
347 .canonicalize()
348 .map_err(|e| mlua::Error::external(anyhow::anyhow!("fs.list: {}: {}", dir, e)))?;
349 if !canonical.starts_with(&r) {
350 return Err(mlua::Error::external(anyhow::anyhow!(
351 "fs.list: path escapes sandbox: {}",
352 dir
353 )));
354 }
355
356 let matcher = glob_pattern
357 .as_deref()
358 .map(|p| {
359 Glob::new(p)
360 .map_err(|e| {
361 mlua::Error::external(anyhow::anyhow!("fs.list: bad glob: {}", e))
362 })
363 .map(|g| g.compile_matcher())
364 })
365 .transpose()?;
366
367 let entries = lua.create_table()?;
368 let mut idx = 1i64;
369
370 let dir_entries = std::fs::read_dir(&canonical)
371 .map_err(|e| mlua::Error::external(anyhow::anyhow!("fs.list: {}: {}", dir, e)))?;
372
373 for entry in dir_entries {
374 let entry = entry.map_err(mlua::Error::external)?;
375 let path = entry.path();
376
377 if let Some(ref m) = matcher {
378 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
379 if !m.is_match(name) {
380 continue;
381 }
382 }
383
384 let metadata = entry.metadata().map_err(mlua::Error::external)?;
385 let item = lua.create_table()?;
386 item.set("path", path.to_string_lossy().to_string())?;
387 item.set("size", metadata.len())?;
388 item.set(
389 "modified",
390 metadata
391 .modified()
392 .ok()
393 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
394 .map(|d| d.as_secs())
395 .unwrap_or(0),
396 )?;
397 entries.set(idx, item)?;
398 idx += 1;
399 }
400
401 Ok(entries)
402 })?,
403 )?;
404
405 lua.globals().set("fs", fs)?;
406 Ok(())
407}
408
409fn register_base64_api(lua: &Lua) -> LuaResult<()> {
414 use base64::{engine::general_purpose::STANDARD, Engine as _};
415
416 let b64 = lua.create_table()?;
417
418 b64.set(
419 "encode",
420 lua.create_function(|_lua, data: String| Ok(STANDARD.encode(data.as_bytes())))?,
421 )?;
422
423 b64.set(
424 "decode",
425 lua.create_function(|_lua, data: String| {
426 let bytes = STANDARD
427 .decode(data.as_bytes())
428 .map_err(|e| mlua::Error::external(anyhow::anyhow!("base64.decode: {}", e)))?;
429 String::from_utf8(bytes)
430 .map_err(|e| mlua::Error::external(anyhow::anyhow!("base64.decode: {}", e)))
431 })?,
432 )?;
433
434 lua.globals().set("base64", b64)?;
435 Ok(())
436}
437
438fn register_crypto_api(lua: &Lua) -> LuaResult<()> {
443 let crypto = lua.create_table()?;
444
445 crypto.set(
446 "sha256",
447 lua.create_function(|_lua, data: String| {
448 let mut hasher = Sha256::new();
449 hasher.update(data.as_bytes());
450 Ok(format!("{:x}", hasher.finalize()))
451 })?,
452 )?;
453
454 crypto.set(
455 "hmac_sha256",
456 lua.create_function(|_lua, (key, data): (String, String)| {
457 type HmacSha256 = Hmac<Sha256>;
458 let mut mac = HmacSha256::new_from_slice(key.as_bytes())
459 .map_err(|e| mlua::Error::external(anyhow::anyhow!("crypto.hmac_sha256: {}", e)))?;
460 mac.update(data.as_bytes());
461 Ok(hex::encode(mac.finalize().into_bytes()))
462 })?,
463 )?;
464
465 lua.globals().set("crypto", crypto)?;
466 Ok(())
467}
468
469fn register_sleep(lua: &Lua) -> LuaResult<()> {
474 lua.globals().set(
475 "sleep",
476 lua.create_function(|_lua, seconds: f64| {
477 std::thread::sleep(Duration::from_secs_f64(seconds));
478 Ok(())
479 })?,
480 )?;
481 Ok(())
482}
483
484pub(crate) fn toml_value_to_lua(lua: &Lua, value: &toml::Value) -> LuaResult<LuaValue> {
490 match value {
491 toml::Value::String(s) => {
492 let expanded = expand_env_vars(s);
493 lua.create_string(&expanded).map(LuaValue::String)
494 }
495 toml::Value::Integer(i) => Ok(LuaValue::Integer(*i)),
496 toml::Value::Float(f) => Ok(LuaValue::Number(*f)),
497 toml::Value::Boolean(b) => Ok(LuaValue::Boolean(*b)),
498 toml::Value::Array(arr) => {
499 let table = lua.create_table()?;
500 for (i, v) in arr.iter().enumerate() {
501 table.set(i as i64 + 1, toml_value_to_lua(lua, v)?)?;
502 }
503 Ok(LuaValue::Table(table))
504 }
505 toml::Value::Table(map) => {
506 let t = toml_table_to_lua(lua, map)?;
507 Ok(LuaValue::Table(t))
508 }
509 _ => Ok(LuaValue::Nil),
510 }
511}
512
513pub(crate) fn toml_table_to_lua(lua: &Lua, table: &toml::Table) -> LuaResult<LuaTable> {
515 let lua_table = lua.create_table()?;
516 for (k, v) in table {
517 lua_table.set(k.as_str(), toml_value_to_lua(lua, v)?)?;
518 }
519 Ok(lua_table)
520}
521
522pub(crate) fn expand_env_vars(s: &str) -> String {
524 let mut result = s.to_string();
525 while let Some(start) = result.find("${") {
526 let end = match result[start..].find('}') {
527 Some(pos) => start + pos,
528 None => break,
529 };
530 let var_name = &result[start + 2..end];
531 let value = std::env::var(var_name).unwrap_or_default();
532 result = format!("{}{}{}", &result[..start], value, &result[end + 1..]);
533 }
534 result
535}
536
537pub(crate) fn json_value_to_lua(lua: &Lua, value: &serde_json::Value) -> LuaResult<LuaValue> {
543 match value {
544 serde_json::Value::Null => Ok(LuaValue::Nil),
545 serde_json::Value::Bool(b) => Ok(LuaValue::Boolean(*b)),
546 serde_json::Value::Number(n) => {
547 if let Some(i) = n.as_i64() {
548 Ok(LuaValue::Integer(i))
549 } else {
550 Ok(LuaValue::Number(n.as_f64().unwrap_or(0.0)))
551 }
552 }
553 serde_json::Value::String(s) => lua.create_string(s).map(LuaValue::String),
554 serde_json::Value::Array(arr) => {
555 let table = lua.create_table()?;
556 for (i, v) in arr.iter().enumerate() {
557 table.set(i as i64 + 1, json_value_to_lua(lua, v)?)?;
558 }
559 Ok(LuaValue::Table(table))
560 }
561 serde_json::Value::Object(map) => {
562 let table = lua.create_table()?;
563 for (k, v) in map {
564 table.set(k.as_str(), json_value_to_lua(lua, v)?)?;
565 }
566 Ok(LuaValue::Table(table))
567 }
568 }
569}
570
571pub(crate) fn lua_value_to_json(value: LuaValue) -> LuaResult<serde_json::Value> {
573 match value {
574 LuaValue::Nil => Ok(serde_json::Value::Null),
575 LuaValue::Boolean(b) => Ok(serde_json::Value::Bool(b)),
576 LuaValue::Integer(i) => Ok(serde_json::Value::Number(i.into())),
577 LuaValue::Number(n) => Ok(serde_json::Number::from_f64(n)
578 .map(serde_json::Value::Number)
579 .unwrap_or(serde_json::Value::Null)),
580 LuaValue::String(s) => Ok(serde_json::Value::String(s.to_str()?.to_string())),
581 LuaValue::Table(t) => {
582 let len = t.raw_len();
584 if len > 0 {
585 let mut arr = Vec::new();
586 for i in 1..=len {
587 let v: LuaValue = t.raw_get(i)?;
588 arr.push(lua_value_to_json(v)?);
589 }
590 Ok(serde_json::Value::Array(arr))
591 } else {
592 let mut map = serde_json::Map::new();
593 for pair in t.pairs::<String, LuaValue>() {
594 let (k, v) = pair?;
595 map.insert(k, lua_value_to_json(v)?);
596 }
597 Ok(serde_json::Value::Object(map))
598 }
599 }
600 _ => Ok(serde_json::Value::Null),
601 }
602}