use std::io::Read;
use wasmsh_browser::WorkerRuntime;
use wasmsh_protocol::{HostCommand, WorkerEvent};
use wasmsh_utils::net_types::{
HostAllowlist, HttpRequest, HttpResponse, NetworkBackend, NetworkError,
};
struct NativeNetworkBackend {
allowlist: HostAllowlist,
}
impl NativeNetworkBackend {
fn new(allowed_hosts: Vec<String>) -> Self {
Self {
allowlist: HostAllowlist::new(allowed_hosts),
}
}
}
impl NetworkBackend for NativeNetworkBackend {
fn check_url(&self, url: &str) -> Result<(), NetworkError> {
self.allowlist.check(url)
}
fn fetch(&self, request: &HttpRequest) -> Result<HttpResponse, NetworkError> {
self.allowlist.check(&request.url)?;
let ureq_req = ureq::request(&request.method, &request.url);
let mut req = ureq_req;
for (key, value) in &request.headers {
req = req.set(key, value);
}
let result = if let Some(ref body) = request.body {
req.send_bytes(body)
} else {
req.call()
};
match result {
Ok(resp) => {
let status = resp.status();
let mut headers = Vec::new();
for name in resp.headers_names() {
if let Some(value) = resp.header(&name) {
headers.push((name, value.to_string()));
}
}
let mut body = Vec::new();
resp.into_reader()
.take(10 * 1024 * 1024) .read_to_end(&mut body)
.unwrap_or(0);
Ok(HttpResponse {
status,
headers,
body,
})
}
Err(ureq::Error::Status(status, resp)) => {
let mut body = Vec::new();
resp.into_reader()
.take(1024 * 1024)
.read_to_end(&mut body)
.unwrap_or(0);
Ok(HttpResponse {
status,
headers: vec![],
body,
})
}
Err(e) => Err(NetworkError::ConnectionFailed(e.to_string())),
}
}
}
fn extract_stdout(events: &[WorkerEvent]) -> String {
let mut out = Vec::new();
for event in events {
if let WorkerEvent::Stdout(data) = event {
out.extend_from_slice(data);
}
}
String::from_utf8_lossy(&out).to_string()
}
fn extract_stderr(events: &[WorkerEvent]) -> String {
let mut out = Vec::new();
for event in events {
if let WorkerEvent::Stderr(data) = event {
out.extend_from_slice(data);
}
}
String::from_utf8_lossy(&out).to_string()
}
fn extract_exit_code(events: &[WorkerEvent]) -> Option<i32> {
for event in events {
if let WorkerEvent::Exit(code) = event {
return Some(*code);
}
}
None
}
fn mayflower_reachable() -> bool {
ureq::get("https://mayflower.de")
.set("User-Agent", "wasmsh-test/1.0")
.call()
.is_ok()
}
fn init_runtime_with_network(allowed_hosts: Vec<String>) -> WorkerRuntime {
let mut rt = WorkerRuntime::new();
let backend = NativeNetworkBackend::new(allowed_hosts.clone());
rt.set_network_backend(Box::new(backend));
rt.handle_command(HostCommand::Init {
step_budget: 0,
allowed_hosts,
});
rt
}
#[test]
fn curl_allowed_host_succeeds() {
if !mayflower_reachable() {
return; }
let mut rt = init_runtime_with_network(vec!["mayflower.de".into()]);
let events = rt.handle_command(HostCommand::Run {
input: "curl -sL https://mayflower.de".into(),
});
let stdout = extract_stdout(&events);
let exit_code = extract_exit_code(&events).unwrap();
assert_eq!(exit_code, 0, "curl to allowed host should succeed");
assert!(
!stdout.is_empty(),
"curl to allowed host should return content"
);
assert!(
stdout.contains("<!") || stdout.contains("<html") || stdout.contains("<HTML"),
"expected HTML from mayflower.de, got: {}...",
&stdout[..stdout.len().min(200)]
);
}
#[test]
fn wget_allowed_host_succeeds() {
if !mayflower_reachable() {
return; }
let mut rt = init_runtime_with_network(vec!["mayflower.de".into()]);
let events = rt.handle_command(HostCommand::Run {
input: "wget -qO - https://mayflower.de".into(),
});
let stdout = extract_stdout(&events);
let exit_code = extract_exit_code(&events).unwrap();
assert_eq!(exit_code, 0, "wget to allowed host should succeed");
assert!(
!stdout.is_empty(),
"wget to allowed host should return content"
);
}
#[test]
fn curl_denied_host_blocked() {
let mut rt = init_runtime_with_network(vec!["mayflower.de".into()]);
let events = rt.handle_command(HostCommand::Run {
input: "curl https://example.com".into(),
});
let stderr = extract_stderr(&events);
let exit_code = extract_exit_code(&events).unwrap();
assert_ne!(exit_code, 0, "curl to denied host must fail");
assert!(
stderr.contains("denied"),
"stderr should mention 'denied', got: {stderr}"
);
}
#[test]
fn wget_denied_host_blocked() {
let mut rt = init_runtime_with_network(vec!["mayflower.de".into()]);
let events = rt.handle_command(HostCommand::Run {
input: "wget -qO - https://example.com".into(),
});
let stderr = extract_stderr(&events);
let exit_code = extract_exit_code(&events).unwrap();
assert_ne!(exit_code, 0, "wget to denied host must fail");
assert!(
stderr.contains("denied"),
"stderr should mention 'denied', got: {stderr}"
);
}
#[test]
fn curl_denied_host_with_subdomain() {
let mut rt = init_runtime_with_network(vec!["mayflower.de".into()]);
let events = rt.handle_command(HostCommand::Run {
input: "curl https://evil.mayflower.de".into(),
});
let exit_code = extract_exit_code(&events).unwrap();
assert_ne!(
exit_code, 0,
"curl to subdomain of allowed host must fail (exact match only)"
);
}
#[test]
fn curl_denied_similar_hostname() {
let mut rt = init_runtime_with_network(vec!["mayflower.de".into()]);
for host in [
"https://notmayflower.de",
"https://mayflower.de.evil.com",
"https://mayflower.com",
] {
let events = rt.handle_command(HostCommand::Run {
input: format!("curl {host}"),
});
let exit_code = extract_exit_code(&events).unwrap();
assert_ne!(exit_code, 0, "curl to '{host}' must be blocked");
}
}
#[test]
fn curl_wildcard_allows_subdomains_but_not_apex() {
if !mayflower_reachable() {
return; }
let mut rt = init_runtime_with_network(vec!["*.mayflower.de".into()]);
let events = rt.handle_command(HostCommand::Run {
input: "curl -s -o /dev/null -w '%{http_code}' https://www.mayflower.de".into(),
});
assert_eq!(
extract_exit_code(&events).unwrap(),
0,
"www.mayflower.de should be allowed by *.mayflower.de"
);
let events = rt.handle_command(HostCommand::Run {
input: "curl https://mayflower.de".into(),
});
assert_ne!(
extract_exit_code(&events).unwrap(),
0,
"apex mayflower.de must NOT be covered by *.mayflower.de"
);
let events = rt.handle_command(HostCommand::Run {
input: "curl https://example.com".into(),
});
assert_ne!(
extract_exit_code(&events).unwrap(),
0,
"example.com must still be blocked"
);
}
#[test]
fn curl_explicit_apex_plus_wildcard_covers_both() {
if !mayflower_reachable() {
return;
}
let mut rt = init_runtime_with_network(vec!["mayflower.de".into(), "*.mayflower.de".into()]);
let events = rt.handle_command(HostCommand::Run {
input: "curl -sL https://mayflower.de".into(),
});
assert_eq!(
extract_exit_code(&events).unwrap(),
0,
"explicit apex should be allowed"
);
}
#[test]
fn curl_empty_allowlist_blocks_everything() {
let mut rt = init_runtime_with_network(vec![]);
let events = rt.handle_command(HostCommand::Run {
input: "curl https://mayflower.de".into(),
});
let stderr = extract_stderr(&events);
let exit_code = extract_exit_code(&events).unwrap();
assert_ne!(exit_code, 0, "empty allowlist must block all requests");
assert!(
stderr.contains("denied") || stderr.contains("allowlist"),
"stderr should mention denial: {stderr}"
);
}
#[test]
fn curl_no_backend_returns_error() {
let mut rt = WorkerRuntime::new();
rt.handle_command(HostCommand::Init {
step_budget: 0,
allowed_hosts: vec![],
});
let events = rt.handle_command(HostCommand::Run {
input: "curl https://mayflower.de".into(),
});
let stderr = extract_stderr(&events);
let exit_code = extract_exit_code(&events).unwrap();
assert_ne!(exit_code, 0);
assert!(
stderr.contains("network access not available"),
"should report no network access: {stderr}"
);
}
#[test]
fn curl_output_to_file_allowed_host() {
if !mayflower_reachable() {
return; }
let mut rt = init_runtime_with_network(vec!["mayflower.de".into()]);
let events = rt.handle_command(HostCommand::Run {
input: "curl -sLo /tmp/mayflower.html https://mayflower.de".into(),
});
let exit_code = extract_exit_code(&events).unwrap();
assert_eq!(exit_code, 0, "curl -o to allowed host should succeed");
let events = rt.handle_command(HostCommand::Run {
input: "wc -c /tmp/mayflower.html".into(),
});
let stdout = extract_stdout(&events);
let byte_count: usize = stdout
.split_whitespace()
.next()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
assert!(
byte_count > 100,
"downloaded file should have substantial content, got {byte_count} bytes"
);
}
#[test]
fn curl_write_out_http_code() {
if !mayflower_reachable() {
return; }
let mut rt = init_runtime_with_network(vec!["mayflower.de".into()]);
let events = rt.handle_command(HostCommand::Run {
input: "curl -sL -o /dev/null -w '%{http_code}' https://mayflower.de".into(),
});
let stdout = extract_stdout(&events);
let exit_code = extract_exit_code(&events).unwrap();
assert_eq!(exit_code, 0);
assert!(
stdout.contains("200"),
"expected HTTP 200 from mayflower.de, got: {stdout}"
);
}
#[test]
fn curl_pipe_to_shell_command() {
if !mayflower_reachable() {
return; }
let mut rt = init_runtime_with_network(vec!["mayflower.de".into()]);
let events = rt.handle_command(HostCommand::Run {
input: "curl -sL https://mayflower.de | wc -l".into(),
});
let stdout = extract_stdout(&events);
let exit_code = extract_exit_code(&events).unwrap();
assert_eq!(exit_code, 0);
let line_count: usize = stdout.trim().parse().unwrap_or(0);
assert!(
line_count > 5,
"expected multiple lines from mayflower.de, got {line_count}"
);
}