harn-vm 0.10.173

Async bytecode virtual machine for the Harn programming language
use super::*;

pub(in crate::http) fn build_http_download_response(
    status: i64,
    headers: crate::value::DictMap,
    bytes_written: u64,
) -> VmValue {
    let mut result = crate::value::DictMap::new();
    result.put_int("status", status);
    result.put("headers", VmValue::dict(headers));
    result.put_int("bytes_written", u64_to_vm_int(bytes_written));
    result.put_bool("ok", (200..300).contains(&(status as u16)));
    VmValue::dict(result)
}

pub(in crate::http) fn build_http_response(
    status: i64,
    headers: crate::value::DictMap,
    body: String,
    final_url: &str,
    private_filter: &crate::session_inputs::OutputFilter,
) -> VmValue {
    let mut result = crate::value::DictMap::new();
    result.put_int("status", status);
    let headers: crate::value::DictMap = headers
        .into_iter()
        .map(|(name, value)| {
            (
                crate::value::intern_key(&private_filter.redact(name.as_str())),
                VmValue::string(private_filter.redact(&value.display())),
            )
        })
        .collect();
    result.put("headers", VmValue::dict(headers));
    result.put_str("body", private_filter.redact(&body));
    result.put_str("final_url", private_filter.redact(final_url));
    result.put_bool("ok", (200..300).contains(&(status as u16)));
    VmValue::dict(result)
}

pub(super) fn apply_session_secret_headers(
    options: &crate::value::DictMap,
    url: &str,
    parts: &mut HttpRequestParts,
) -> Result<(), VmError> {
    let Some(raw) = options.get("secret_headers") else {
        return Ok(());
    };
    let bindings = raw
        .as_dict()
        .ok_or_else(|| vm_error("secret_headers must map header names to named inputs"))?;
    if parts.headers.contains_key(reqwest::header::HOST) {
        return Err(vm_error(
            "Secret headers cannot be combined with a custom Host header",
        ));
    }
    let destination = url::Url::parse(url)
        .map_err(|_| vm_error("A valid HTTPS destination is required for secret headers"))?;
    if destination.scheme() != "https"
        || destination.host_str().is_none()
        || !destination.username().is_empty()
        || destination.password().is_some()
        || bindings.len() > 32
    {
        return Err(vm_error("Secret headers require an HTTPS destination without URL credentials and at most 32 bindings"));
    }
    let usage = if ["proxy", "proxy_auth", "no_proxy", "tls"]
        .iter()
        .any(|key| options.contains_key(*key))
    {
        crate::session_inputs::InputUse::HttpWithCustomTransport(url)
    } else {
        crate::session_inputs::InputUse::Http(url)
    };
    for (header, binding) in bindings {
        let header_name = reqwest::header::HeaderName::from_bytes(header.as_bytes())
            .map_err(|_| vm_error("A secret header name is invalid"))?;
        if header_name == reqwest::header::HOST {
            return Err(vm_error("A named input cannot supply the Host header"));
        }
        if parts.headers.contains_key(&header_name) {
            return Err(vm_error(
                "A secret header cannot replace an ordinary header",
            ));
        }
        let binding = binding
            .as_dict()
            .ok_or_else(|| vm_error("A secret header binding requires name and optional prefix"))?;
        if binding.keys().any(|key| key != "name" && key != "prefix") {
            return Err(vm_error(
                "A secret header binding accepts only name and prefix",
            ));
        }
        let Some(VmValue::String(name)) = binding.get("name") else {
            return Err(vm_error("A secret header binding requires an input name"));
        };
        let prefix = match binding.get("prefix") {
            None => "",
            Some(VmValue::String(value)) => value.as_ref(),
            _ => return Err(vm_error("A secret header prefix must be a string")),
        };
        let value = zeroize::Zeroizing::new(
            crate::session_inputs::http_header(name.as_str(), usage, prefix)
                .map_err(|error| vm_error(error.to_string()))?,
        );
        let mut value = reqwest::header::HeaderValue::from_str(&value)
            .map_err(|_| vm_error("The named input cannot be used as an HTTP header"))?;
        value.set_sensitive(true);
        parts.headers.insert(header_name, value);
        parts
            .recorded_headers
            .put_str(header.as_str(), "[redacted]");
    }
    Ok(())
}