T29: use QuickJS and verify the HTTP hook and guest networking

This commit is contained in:
2026-11-06 23:38:21 +00:00
parent 7c7656c4a8
commit bb162abd4d
6 changed files with 86 additions and 48 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ impl Exec for FakeExec {
options: Options,
output: &mut (dyn for<'chunk> FnMut(Stream, &'chunk [u8]) + Send),
) -> anyhow::Result<i32> {
assert!(options.conversation.is_some(), "missing conversation id");
assert!(!options.conversations.is_empty(), "missing conversation id");
let Command::Shell(command) = command else {
anyhow::bail!("expected shell command")
};
+57 -28
View File
@@ -51,6 +51,8 @@ async fn identity_injection_denial_and_ambiguity() {
let world = World::open(temp.path()).unwrap();
std::fs::create_dir_all(temp.path().join("world/bin")).unwrap();
std::fs::copy(guest, temp.path().join("world/bin/guest.wasm")).unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let c = Arc::new(cred::Cred::open(&world.host_dir).unwrap());
let caller = cred::Caller::default();
let item = c
@@ -61,6 +63,7 @@ async fn identity_injection_denial_and_ambiguity() {
version: 0,
free_use: true,
rules: vec![Rule {
port: Some(port),
scheme: "http".into(),
hosts: vec!["127.0.0.1".into()],
path_prefixes: vec!["/free".into()],
@@ -79,12 +82,18 @@ async fn identity_injection_denial_and_ambiguity() {
}],
},
&caller,
"test",
c.unlocked().unwrap(),
)
.unwrap();
let token = uuid::Uuid::new_v4().to_string();
c.set_field(&item.id, &item.fields[0].field_id, &token, &caller, "test")
.unwrap();
c.set_field(
&item.id,
&item.fields[0].field_id,
&token,
&caller,
c.unlocked().unwrap(),
)
.unwrap();
let runtime = Runtime::new_with_hooks(world, c.clone(), Arc::new(Fake), None).unwrap();
let identity = run(
&runtime,
@@ -117,8 +126,6 @@ async fn identity_injection_denial_and_ambiguity() {
assert_eq!(code, 0);
let alias_out = String::from_utf8(alias_out).unwrap();
assert!(alias_out.contains("/bin/guest.wasm") && !alias_out.contains("/bin/alias"));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let server_token = token.clone();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
@@ -149,23 +156,35 @@ async fn identity_injection_denial_and_ambiguity() {
let out = run(&runtime, url.clone(), vec!["root".into()]).await;
assert!(out.contains("status=200") && !out.contains(&token));
server.await.unwrap();
let (redirect_url, redirect) = upstream(Some(token.clone()), 0, 302).await;
assert!(
run(&runtime, format!("{redirect_url}/free"), vec![])
.await
.contains("status=302")
);
redirect.await.unwrap();
let (public_url, public) = upstream(None, 0, 200).await;
assert!(
run(
&runtime,
format!("{}/public", public_url.replace("127.0.0.1", "localhost")),
vec![]
let (public_url, public) = upstream(None, 0, 200, 0, None).await;
let location = format!("{}/public", public_url.replace("127.0.0.1", "localhost"));
let (redirect_url, redirect) =
upstream(Some(token.clone()), 0, 302, port, Some(location)).await;
let mut follow_out = Vec::new();
let code = runtime
.exec(
Command::Argv(vec![
"/bin/guest.wasm".into(),
"follow".into(),
format!("{redirect_url}/free"),
]),
Options {
cwd: "/".into(),
timeout: Some(std::time::Duration::from_secs(30)),
..Default::default()
},
&mut |_, b| follow_out.extend_from_slice(b),
)
.await
.contains("status=200")
.unwrap();
assert_eq!(code, 0);
let follow_out = String::from_utf8(follow_out).unwrap();
assert!(
follow_out.contains("first-status=302")
&& follow_out.contains("status=200")
&& !follow_out.contains(&token)
);
redirect.await.unwrap();
public.await.unwrap();
let mut changed = item.clone();
changed.free_use = false;
@@ -173,7 +192,9 @@ async fn identity_injection_denial_and_ambiguity() {
changed.id = String::new();
changed.name = "second".into();
changed.rules[0].path_prefixes = vec!["/free".into(), "/deny".into()];
let second = c.create_item(changed, &caller, "test").unwrap();
let second = c
.create_item(changed, &caller, c.unlocked().unwrap())
.unwrap();
assert!(
run(&runtime, url.clone(), vec![])
.await
@@ -190,17 +211,17 @@ async fn identity_injection_denial_and_ambiguity() {
&second.fields[0].field_id,
&token,
&caller,
"test",
c.unlocked().unwrap(),
)
.unwrap();
c.approve(
&c.request_list().unwrap()[0].id,
"allow",
cred::webauthn::Decision::Approve,
cred::requests::Overrides::default(),
Some("test"),
c.unlocked().unwrap(),
)
.unwrap();
let (url, task) = upstream(Some(token.clone()), 0, 200).await;
let (url, task) = upstream(Some(token.clone()), 0, 200, port, None).await;
let out = run(
&runtime,
format!("{url}/deny"),
@@ -209,7 +230,7 @@ async fn identity_injection_denial_and_ambiguity() {
.await;
assert!(out.contains("status=200") && !out.contains(&token));
task.await.unwrap();
let (child_url, task) = upstream(Some(token.clone()), 0, 200).await;
let (child_url, task) = upstream(Some(token.clone()), 0, 200, port, None).await;
let mut child_out = Vec::new();
let code = runtime
.exec(
@@ -240,15 +261,20 @@ async fn upstream(
token: Option<String>,
length: usize,
status: u16,
port: u16,
location: Option<String>,
) -> (String, tokio::task::JoinHandle<()>) {
use http_body_util::{BodyExt, Full};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let listener = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port))
.await
.unwrap();
let url = format!("http://{}", listener.local_addr().unwrap());
let task = tokio::spawn(async move {
let (socket, _) = listener.accept().await.unwrap();
let service =
hyper::service::service_fn(move |request: http::Request<hyper::body::Incoming>| {
let token = token.clone();
let location = location.clone();
async move {
match token {
Some(token) => assert!(
@@ -271,7 +297,10 @@ async fn upstream(
Ok::<_, std::convert::Infallible>(
http::Response::builder()
.status(status)
.header("location", "http://localhost/public")
.header(
"location",
location.as_deref().unwrap_or("http://localhost/public"),
)
.body(Full::new(bytes::Bytes::from_static(b"ok")))
.unwrap(),
)
@@ -294,7 +323,7 @@ async fn streamed_upload_redirect_and_http_admin_denial() {
std::fs::create_dir_all(temp.path().join("world/bin")).unwrap();
std::fs::copy(guest, temp.path().join("world/bin/guest.wasm")).unwrap();
let runtime = Runtime::new(world).unwrap();
let (url, task) = upstream(None, 4 * 1024 * 1024, 302).await;
let (url, task) = upstream(None, 4 * 1024 * 1024, 302, 0, None).await;
let mut out = Vec::new();
let code = runtime
.exec(
+9
View File
@@ -25,6 +25,15 @@ mod bindings { wit_bindgen::generate!({path:"wit",world:"program",generate_all})
async fn main() -> anyhow::Result<()> {
let args: Vec<_> = std::env::args().collect();
match args[1].as_str() {
"follow" => {
let response=Client::new().send(Request::get(&args[2]).body(Body::empty())?).await?;
println!("first-status={}",response.status().as_u16());
let location=response.headers()["location"].to_str()?.to_owned();
response.into_body().into_boxed_body().collect().await?;
let response=Client::new().send(Request::get(location).body(Body::empty())?).await?;
println!("status={}",response.status().as_u16());
response.into_body().into_boxed_body().collect().await?;
},
"spawn" => {
use bindings::sausage::process::host::{self, Request, Stdio};
let child=host::spawn(&Request {program:"/bin/guest.wasm".into(),argv:vec!["guest".into(),args[2].clone(),"GET".into()],cwd:"/".into(),env:vec![],group:None,stdin:Stdio::Null,stdout:Stdio::Inherit,stderr:Stdio::Inherit}).unwrap();
+5 -5
View File
@@ -93,7 +93,7 @@ async fn guest_tcp_and_host_denial() {
Command::Argv(vec![
"/bin/guest.wasm".into(),
"listen".into(),
address.to_string(),
format!("0.0.0.0:{}", address.port()),
]),
Options {
cwd: "/".into(),
@@ -102,10 +102,10 @@ async fn guest_tcp_and_host_denial() {
},
&mut |_, chunk| {
bytes.extend_from_slice(chunk);
if bytes.windows(5).any(|w| w == b"ready") {
if let Some(tx) = tx.take() {
let _ = tx.send(());
}
if bytes.windows(5).any(|w| w == b"ready")
&& let Some(tx) = tx.take()
{
let _ = tx.send(());
}
},
)