T29: use QuickJS and verify the HTTP hook and guest networking
This commit is contained in:
@@ -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,
|
||||
> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
@@ -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(());
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user