#![cfg(feature = "extism-runtime")]
use std::sync::Arc;
use extism::{Function, UserData, ValType};
use serde::Serialize;
use serde::de::DeserializeOwned;
use uni_plugin::secrets::SecretStore;
use uni_plugin::{Capability, CapabilitySet, FnError, HttpEgress, KmsProvider};
use crate::host_fns::HostFnSpec;
use crate::loader::ExtismLoader;
pub mod kms;
pub mod net;
pub mod secret;
pub(crate) const FN_KMS_SIGN: &str = "uni_kms_sign";
pub(crate) const FN_KMS_VERIFY: &str = "uni_kms_verify";
pub(crate) const FN_SECRET_ACQUIRE: &str = "uni_secret_acquire";
pub(crate) const FN_HTTP_GET: &str = "uni_http_get";
pub(crate) const FN_HTTP_POST: &str = "uni_http_post";
#[derive(Clone)]
pub(crate) struct HostSvcCtx {
pub effective: CapabilitySet,
pub kms: Option<Arc<dyn KmsProvider>>,
pub secrets: Option<Arc<SecretStore>>,
pub http: Option<Arc<dyn HttpEgress>>,
}
pub(crate) fn to_hex(bytes: &[u8]) -> String {
use std::fmt::Write as _;
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
let _ = write!(s, "{b:02x}");
}
s
}
pub(crate) fn from_hex(s: &str) -> Result<Vec<u8>, String> {
if !s.len().is_multiple_of(2) {
return Err("odd-length hex string".to_owned());
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string()))
.collect()
}
pub(crate) fn dispatch_json<Req, Resp, F>(
ctx: &HostSvcCtx,
req_json: &str,
label: &str,
f: F,
) -> Result<String, FnError>
where
Req: DeserializeOwned,
Resp: Serialize,
F: FnOnce(&HostSvcCtx, Req) -> Result<Resp, FnError>,
{
let req: Req = serde_json::from_str(req_json)
.map_err(|e| FnError::new(0xC30, format!("{label}: bad request json: {e}")))?;
let resp = f(ctx, req)?;
serde_json::to_string(&resp)
.map_err(|e| FnError::new(0xC31, format!("{label}: response json: {e}")))
}
pub(crate) fn build_service_fn(name: &str, ctx: &HostSvcCtx) -> Option<Function> {
let f = match name {
FN_KMS_SIGN => Function::new(
FN_KMS_SIGN,
[ValType::I64],
[ValType::I64],
UserData::new(ctx.clone()),
kms::uni_kms_sign,
),
FN_KMS_VERIFY => Function::new(
FN_KMS_VERIFY,
[ValType::I64],
[ValType::I64],
UserData::new(ctx.clone()),
kms::uni_kms_verify,
),
FN_SECRET_ACQUIRE => Function::new(
FN_SECRET_ACQUIRE,
[ValType::I64],
[ValType::I64],
UserData::new(ctx.clone()),
secret::uni_secret_acquire,
),
FN_HTTP_GET => Function::new(
FN_HTTP_GET,
[ValType::I64],
[ValType::I64],
UserData::new(ctx.clone()),
net::uni_http_get,
),
FN_HTTP_POST => Function::new(
FN_HTTP_POST,
[ValType::I64],
[ValType::I64],
UserData::new(ctx.clone()),
net::uni_http_post,
),
_ => return None,
};
Some(f)
}
pub fn register_default_host_svc(loader: &mut ExtismLoader) {
let specs = [
(
FN_KMS_SIGN,
Capability::Kms {
key_ids: Vec::new(),
},
"Sign bytes with a host-managed key (hex in/out).",
),
(
FN_KMS_VERIFY,
Capability::Kms {
key_ids: Vec::new(),
},
"Verify a hex signature against a host-managed key.",
),
(
FN_SECRET_ACQUIRE,
Capability::Secret { ids: Vec::new() },
"Acquire an opaque handle for a named secret.",
),
(
FN_HTTP_GET,
Capability::Network { allow: Vec::new() },
"HTTP GET against a URL in the granted allow-list.",
),
(
FN_HTTP_POST,
Capability::Network { allow: Vec::new() },
"HTTP POST against a URL in the granted allow-list.",
),
];
for (name, cap, docs) in specs {
loader.host_fns_mut().register(HostFnSpec {
name: name.to_owned(),
required_capability: Some(cap),
docs: docs.to_owned(),
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hex_round_trips() {
let bytes = [0x00u8, 0x0f, 0xa5, 0xff];
let hex = to_hex(&bytes);
assert_eq!(hex, "000fa5ff");
assert_eq!(from_hex(&hex).unwrap(), bytes);
}
#[test]
fn from_hex_rejects_odd_length() {
assert!(from_hex("abc").is_err());
}
#[test]
fn register_default_host_svc_registers_five_specs() {
let mut loader = ExtismLoader::new();
register_default_host_svc(&mut loader);
assert_eq!(loader.host_fns().len(), 5);
assert!(loader.host_fns().get(FN_KMS_SIGN).is_some());
assert!(loader.host_fns().get(FN_HTTP_POST).is_some());
}
#[test]
fn build_service_fn_unknown_name_is_none() {
let ctx = HostSvcCtx {
effective: CapabilitySet::new(),
kms: None,
secrets: None,
http: None,
};
assert!(build_service_fn("not_a_service_fn", &ctx).is_none());
assert!(build_service_fn(FN_KMS_SIGN, &ctx).is_some());
}
}