#![cfg(feature = "rhai-runtime")]
use std::sync::Arc;
use std::time::Duration;
use rhai::Engine;
use uni_plugin::{Capability, CapabilitySet, HttpEgress};
use crate::host_fn_impls::{require_allowed, require_service, rt_err};
use crate::host_fns::RhaiHostFnSpec;
use crate::loader::RhaiLoader;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
const MAX_RESPONSE_BYTES: usize = 8 * 1024 * 1024;
pub fn register(loader: &mut RhaiLoader) {
let http = loader.http();
let placeholder = Capability::Network {
allow: vec!["**".into()],
};
let get_http = http.clone();
loader.host_fns_mut().register(RhaiHostFnSpec::gated(
"uni.http.get",
placeholder.clone(),
"HTTP GET against a URL in the granted allow-list.",
move |engine: &mut Engine, caps: &CapabilitySet| {
let http = get_http.clone();
let caps = caps.clone();
engine.register_fn(
"uni_http_get",
move |url: &str| -> Result<String, Box<rhai::EvalAltResult>> {
http_request(&http, &caps, url, None)
},
);
},
));
loader.host_fns_mut().register(RhaiHostFnSpec::gated(
"uni.http.post",
placeholder,
"HTTP POST against a URL in the granted allow-list.",
move |engine: &mut Engine, caps: &CapabilitySet| {
let http = http.clone();
let caps = caps.clone();
engine.register_fn(
"uni_http_post",
move |url: &str, body: &str| -> Result<String, Box<rhai::EvalAltResult>> {
http_request(&http, &caps, url, Some(body.as_bytes()))
},
);
},
));
}
fn http_request(
http: &Option<Arc<dyn HttpEgress>>,
caps: &CapabilitySet,
url: &str,
body: Option<&[u8]>,
) -> Result<String, Box<rhai::EvalAltResult>> {
require_allowed(
caps,
|c| c.network_allows(url),
format!("uni.http: url `{url}` not in granted Network allow-list"),
)?;
let egress = require_service(http, "uni.http: no HTTP egress configured")?;
let timeout = caps
.iter()
.find_map(|c| match c {
Capability::WallClockMillisPerCall(ms) => Some(Duration::from_millis(*ms)),
_ => None,
})
.unwrap_or(DEFAULT_TIMEOUT);
let traceparent = uni_plugin::observability::current_trace_context().to_traceparent();
let tp = traceparent.as_deref();
let response = match body {
Some(b) => egress.post(url, b, timeout, MAX_RESPONSE_BYTES, tp),
None => egress.get(url, timeout, MAX_RESPONSE_BYTES, tp),
}
.map_err(|e| rt_err(format!("uni.http(`{url}`): {e}")))?;
if response.status >= 400 {
return Err(rt_err(format!(
"uni.http(`{url}`): HTTP status {}",
response.status
)));
}
Ok(String::from_utf8_lossy(&response.body).into_owned())
}