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
+10 -6
View File
@@ -37,7 +37,9 @@ fn response(response: Response<Bytes>) -> (Response<WasiBody>, 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<WasiBody>,
@@ -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<T> 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<impl Body<Data = Bytes, Error = Error> + Send + 'static>,
async fn send_request<T: Body<Data = Bytes, Error = Error> + Send + 'static>(
mut req: Request<T>,
options: Option<RequestOptions>,
policy: &super::net::NetPolicy,
) -> Result<
(
Response<impl Body<Data = Bytes, Error = Error>>,
impl Future<Output = Result<(), Error>> + Send,
Response<impl Body<Data = Bytes, Error = Error> + use<T>>,
impl Future<Output = Result<(), Error>> + Send + use<T>,
),
Error,
> {
+4 -8
View File
@@ -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<dyn crate::cred::script::Script> {
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<crate::cred::Cred>,
script: Arc<dyn crate::cred::script::Script>,
router: Option<
fn(
&crate::cred::Caller,
::http::Request<bytes::Bytes>,
) -> ::http::Response<bytes::Bytes>,
>,
router: Option<Router>,
) -> anyhow::Result<Arc<Self>> {
Self::with_hooks(
world,
+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(());
}
},
)