From bb162abd4d116263b3e5b91c496bd62b989f4912 Mon Sep 17 00:00:00 2001 From: Surma Date: Fri, 6 Nov 2026 23:38:21 +0000 Subject: [PATCH] T29: use QuickJS and verify the HTTP hook and guest networking --- host/src/process/http.rs | 16 +++++--- host/src/process/mod.rs | 12 ++---- host/tests/harness.rs | 2 +- host/tests/hook.rs | 85 +++++++++++++++++++++++++++------------- host/tests/hook_guest.rs | 9 +++++ host/tests/net.rs | 10 ++--- 6 files changed, 86 insertions(+), 48 deletions(-) diff --git a/host/src/process/http.rs b/host/src/process/http.rs index 2c3e9cd..e72380b 100644 --- a/host/src/process/http.rs +++ b/host/src/process/http.rs @@ -37,7 +37,9 @@ fn response(response: Response) -> (Response, Io) { } impl WasiHttpHooks for Hooks { // Admit every scheme until the URI supplies the host name. - fn is_supported_scheme(&mut self, _: &http::uri::Scheme) -> bool { true } + fn is_supported_scheme(&mut self, _: &http::uri::Scheme) -> bool { + true + } fn send_request( &mut self, request: Request, @@ -61,7 +63,9 @@ impl WasiHttpHooks for Hooks { return Ok(response(router(&caller, Request::from_parts(parts, bytes)))); } if !matches!(parts.uri.scheme_str(), Some("http" | "https")) { - while let Some(frame) = body.frame().await { frame?; } + while let Some(frame) = body.frame().await { + frame?; + } return Err(Error::HttpRequestUriInvalid); } let result = @@ -144,14 +148,14 @@ impl TokioStream for T where T: AsyncRead + AsyncWrite + Send + Sync + Unpin /// Preserve Wasmtime's HTTP/1 transport and request deadlines, but connect only to checked addresses. /// The returned future drives the connection after the response headers arrive. -async fn send_request( - mut req: Request + Send + 'static>, +async fn send_request + Send + 'static>( + mut req: Request, options: Option, policy: &super::net::NetPolicy, ) -> Result< ( - Response>, - impl Future> + Send, + Response + use>, + impl Future> + Send + use, ), Error, > { diff --git a/host/src/process/mod.rs b/host/src/process/mod.rs index c995588..ae3e35b 100644 --- a/host/src/process/mod.rs +++ b/host/src/process/mod.rs @@ -47,10 +47,11 @@ mod bindings { use bindings::sausage::process::host::{Host, HostChild, Request, Stdio}; mod cache; mod http; +pub use http::Router; pub mod net; -/// Temporary default until T28 supplies the bounded QuickJS evaluator. +/// Credential scripts use T28's bounded QuickJS component. pub fn default_script() -> Arc { - Arc::new(crate::cred::v8::V8) + Arc::new(crate::cred::quickjs::QuickJs::default()) } pub use cache::configure as configure_cache; mod services; @@ -167,12 +168,7 @@ impl Runtime { world: World, cred: Arc, script: Arc, - router: Option< - fn( - &crate::cred::Caller, - ::http::Request, - ) -> ::http::Response, - >, + router: Option, ) -> anyhow::Result> { Self::with_hooks( world, diff --git a/host/tests/harness.rs b/host/tests/harness.rs index 2c68791..d4c08ae 100644 --- a/host/tests/harness.rs +++ b/host/tests/harness.rs @@ -16,7 +16,7 @@ impl Exec for FakeExec { options: Options, output: &mut (dyn for<'chunk> FnMut(Stream, &'chunk [u8]) + Send), ) -> anyhow::Result { - 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") }; diff --git a/host/tests/hook.rs b/host/tests/hook.rs index a25e407..d75d76d 100644 --- a/host/tests/hook.rs +++ b/host/tests/hook.rs @@ -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, length: usize, status: u16, + port: u16, + location: Option, ) -> (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| { 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( diff --git a/host/tests/hook_guest.rs b/host/tests/hook_guest.rs index 0344d34..d14adb8 100644 --- a/host/tests/hook_guest.rs +++ b/host/tests/hook_guest.rs @@ -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(); diff --git a/host/tests/net.rs b/host/tests/net.rs index b923957..51a6998 100644 --- a/host/tests/net.rs +++ b/host/tests/net.rs @@ -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(()); } }, )