makepad/libs/flow/src/engine/http_seam.rs
Admin 2b98a1f048 flow: the run engine — instances with an inputs table, one VM per run with live Fn closures, a dataflow scheduler, five executors (chat over the hub session, gen over the fleet, fn, http under an egress policy, ask that parks a run), content-addressed values with a RAM budget and spill
Real adapters by default (HubChat, FleetGen, HubHttp); seams for tests; the
gen path is proven end to end against the in-process testpattern hub service.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 17:01:53 +02:00

95 lines
3.4 KiB
Rust

use super::executors::http::{HttpReq, HttpResp, HttpSeam};
use makepad_ai_hub::http_client::{http_fetch_no_redirect, HttpClientRequest};
/// Real bounded HTTP seam: the hub client supplies plain HTTP and platform
/// TLS, while `http_fetch_no_redirect` preserves flow's redirect-refusal law.
pub struct HubHttp;
impl HttpSeam for HubHttp {
fn request(&self, req: HttpReq) -> Result<HttpResp, String> {
if std::time::Instant::now() >= req.deadline {
return Err("HTTP deadline elapsed".to_string());
}
let body = (!req.body.is_empty()).then_some((req.content_type.as_str(), req.body.as_slice()));
let request = HttpClientRequest {
method: &req.method,
url: &req.url,
range_from: None,
range_to: None,
bearer: None,
body,
extra_headers: &req.headers,
};
let response = http_fetch_no_redirect(&request).map_err(|error| error.to_string())?;
let status = response.status;
let headers = response.headers.clone();
let body = response
.read_body_to_vec(32 * 1024 * 1024)
.map_err(|error| error.to_string())?;
if std::time::Instant::now() >= req.deadline {
return Err("HTTP deadline elapsed".to_string());
}
Ok(HttpResp {
status,
headers,
body,
})
}
}
impl crate::engine::NetPolicy {
pub fn check(&self, url: &str) -> Result<(), String> {
let parsed = makepad_ai_hub::http_client::parse_url(url)
.map_err(|error| format!("refused by policy: {error}"))?;
let host = parsed.host.to_ascii_lowercase();
if !self.allow.iter().any(|pattern| host_matches(&host, pattern)) {
return Err(format!("refused by policy: host `{host}` is not allowed"));
}
if self.deny_private && is_private(&host) {
return Err(format!("refused by policy: private host `{host}`"));
}
if self.deny_private {
use std::net::ToSocketAddrs;
let addresses = (host.as_str(), parsed.port)
.to_socket_addrs()
.map_err(|error| format!("refused by policy: cannot resolve `{host}`: {error}"))?;
for address in addresses {
if is_private(&address.ip().to_string()) {
return Err(format!(
"refused by policy: `{host}` resolves to private address {}",
address.ip()
));
}
}
}
Ok(())
}
}
fn host_matches(host: &str, pattern: &str) -> bool {
let pattern = pattern.to_ascii_lowercase();
if pattern == "*" {
return true;
}
if let Some(suffix) = pattern.strip_prefix("*.") {
return host == suffix || host.ends_with(&format!(".{suffix}"));
}
host == pattern
}
fn is_private(host: &str) -> bool {
if host == "localhost" || host.ends_with(".localhost") {
return true;
}
let Ok(ip) = host.parse::<std::net::IpAddr>() else {
return false;
};
match ip {
std::net::IpAddr::V4(ip) => {
ip.is_private() || ip.is_loopback() || ip.is_link_local() || ip.is_unspecified()
}
std::net::IpAddr::V6(ip) => {
ip.is_loopback() || ip.is_unspecified() || (ip.segments()[0] & 0xfe00) == 0xfc00
}
}
}