Skip to main content

hammer/
http.rs

1//! HTTP requests, through Extism's `http_request`. Hammer allows only the hosts the manifest names
2//! under `permissions.http`, which the user approves at install; a request to any other fails.
3
4use serde::Serialize;
5use std::collections::BTreeMap;
6
7#[link(wasm_import_module = "extism:host/env")]
8unsafe extern "C" {
9    fn http_request(request: u64, body: u64) -> u64;
10    fn http_status_code() -> i32;
11}
12
13pub struct Response {
14    pub status: u16,
15    pub body: Vec<u8>,
16}
17
18impl Response {
19    pub fn is_success(&self) -> bool {
20        (200..300).contains(&self.status)
21    }
22}
23
24/// Sends a request and waits for its response. A response of any status is Ok; Err means there was
25/// none, such as for a host the manifest does not name or a network that is down.
26pub fn request(method: &str, url: &str, headers: &[(&str, &str)], body: Option<&[u8]>) -> crate::Result<Response> {
27    #[derive(Serialize)]
28    struct Request<'a> {
29        url: &'a str,
30        method: &'a str,
31        headers: BTreeMap<&'a str, &'a str>,
32    }
33    let request = Request { url, method, headers: headers.iter().copied().collect() };
34    let request = crate::to_kernel(&serde_json::to_vec(&request)?);
35    let body = body.filter(|it| !it.is_empty()).map_or(0, crate::to_kernel);
36    let reply = unsafe { http_request(request, body) };
37    let status = unsafe { http_status_code() };
38    unsafe {
39        crate::free(request);
40        if body != 0 {
41            crate::free(body);
42        }
43    }
44    let body = if reply == 0 { Vec::new() } else { crate::from_kernel(reply) };
45    if reply != 0 {
46        unsafe { crate::free(reply) };
47    }
48    match u16::try_from(status) {
49        Ok(status) if status > 0 => Ok(Response { status, body }),
50        _ => Err(format!("No response from {url}").into()),
51    }
52}
53
54/// `application/x-www-form-urlencoded` form data from its fields.
55pub fn form(fields: &[(&str, &str)]) -> String {
56    fields.iter().map(|(name, value)| format!("{}={}", encode(name), encode(value))).collect::<Vec<_>>().join("&")
57}
58
59fn encode(text: &str) -> String {
60    let mut encoded = String::with_capacity(text.len());
61    for byte in text.bytes() {
62        match byte {
63            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'*' => encoded.push(byte as char),
64            b' ' => encoded.push('+'),
65            _ => encoded.push_str(&format!("%{byte:02X}")),
66        }
67    }
68    encoded
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn form_encodes_reserved_and_non_ascii() {
77        assert_eq!(form(&[("text", "a b&c=d"), ("x", "é")]), "text=a+b%26c%3Dd&x=%C3%A9");
78    }
79}