diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9be7950741..0ae06d1cea 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -455,6 +455,12 @@ jobs: RUST_BACKTRACE: 1 if: matrix.target == '' && matrix.os != 'windows-latest' && needs.determine.outputs.test-capi + # Ensure wit definitions are in sync: both wasmtime-wasi and wasmtime-wasi-http need their own + # copy of the wit definitions so publishing works, but we need to ensure they are identical copies. + - name: Check that the wasi and wasi-http wit directories agree + run: | + diff -ru crates/wasi/wit crates/wasi-http/wit + # Build and test all features - run: ./ci/run-tests.sh --locked env: diff --git a/Cargo.lock b/Cargo.lock index 6cb389c561..d56b805a22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1206,6 +1206,7 @@ checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1228,6 +1229,17 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" +[[package]] +name = "futures-executor" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.28" @@ -1252,11 +1264,15 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -3027,6 +3043,7 @@ name = "wasi-http-tests" version = "0.0.0" dependencies = [ "anyhow", + "tokio", "wit-bindgen", ] @@ -3757,7 +3774,9 @@ name = "wasmtime-wasi-http" version = "13.0.0" dependencies = [ "anyhow", + "async-trait", "bytes", + "futures", "http", "http-body", "http-body-util", @@ -3767,6 +3786,7 @@ dependencies = [ "tokio", "tokio-rustls", "wasmtime", + "wasmtime-wasi", "webpki-roots", ] diff --git a/ci/run-tests.sh b/ci/run-tests.sh index 4fac7b1cfb..f8c52d241d 100755 --- a/ci/run-tests.sh +++ b/ci/run-tests.sh @@ -7,6 +7,7 @@ cargo test \ --workspace \ --exclude 'wasmtime-wasi-*' \ --exclude wasi-tests \ + --exclude wasi-http-tests \ --exclude command-tests \ --exclude reactor-tests \ $@ diff --git a/crates/test-programs/Cargo.toml b/crates/test-programs/Cargo.toml index bd9de15696..b2f7ddd4fc 100644 --- a/crates/test-programs/Cargo.toml +++ b/crates/test-programs/Cargo.toml @@ -14,14 +14,23 @@ wit-component = { workspace = true } heck = { workspace = true } [dependencies] +anyhow = { workspace = true } +http = { version = "0.2.9" } +http-body = "1.0.0-rc.2" +http-body-util = "0.1.0-rc.2" +hyper = { version = "1.0.0-rc.3", features = ["full"] } is-terminal = { workspace = true } +tokio = { workspace = true, features = ["net", "rt-multi-thread", "macros"] } [dev-dependencies] anyhow = { workspace = true } tempfile = { workspace = true } test-log = { version = "0.2", default-features = false, features = ["trace"] } tracing = { workspace = true } -tracing-subscriber = { version = "0.3.1", default-features = false, features = ['fmt', 'env-filter'] } +tracing-subscriber = { version = "0.3.1", default-features = false, features = [ + 'fmt', + 'env-filter', +] } lazy_static = "1" wasmtime = { workspace = true, features = ['cranelift', 'component-model'] } @@ -30,14 +39,9 @@ wasi-cap-std-sync = { workspace = true } wasmtime-wasi = { workspace = true, features = ["tokio"] } cap-std = { workspace = true } cap-rand = { workspace = true } -tokio = { version = "1.8.0", features = ["net", "rt-multi-thread", "macros"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } wasmtime-wasi-http = { workspace = true } -hyper = { version = "1.0.0-rc.3", features = ["full"] } -http = { version = "0.2.9" } -http-body = "1.0.0-rc.2" -http-body-util = "0.1.0-rc.2" [features] test_programs = [] -test_programs_http = [ "wasmtime/component-model" ] diff --git a/crates/test-programs/build.rs b/crates/test-programs/build.rs index eccf8e0d6e..8d16007fc6 100644 --- a/crates/test-programs/build.rs +++ b/crates/test-programs/build.rs @@ -59,10 +59,7 @@ fn build_and_generate_tests() { if BUILD_WASI_HTTP_TESTS { modules_rs(&meta, "wasi-http-tests", "bin", &out_dir); - // FIXME this is broken at the moment because guest bindgen is embedding the proxy world type, - // so wit-component expects the module to contain the proxy's exports. we need a different - // world to pass guest bindgen that is just "a command that also can do outbound http" - //components_rs(&meta, "wasi-http-tests", "bin", &command_adapter, &out_dir); + components_rs(&meta, "wasi-http-tests", "bin", &reactor_adapter, &out_dir); } components_rs(&meta, "command-tests", "bin", &command_adapter, &out_dir); @@ -70,7 +67,7 @@ fn build_and_generate_tests() { } // Creates an `${out_dir}/${package}_modules.rs` file that exposes a `get_module(&str) -> Module`, -// and a contains a `use self::{module} as _;` for each module that ensures that the user defines +// and contains a `use self::{module} as _;` for each module that ensures that the user defines // a symbol (ideally a #[test]) corresponding to each module. fn modules_rs(meta: &cargo_metadata::Metadata, package: &str, kind: &str, out_dir: &PathBuf) { let modules = targets_in_package(meta, package, kind) diff --git a/crates/test-programs/src/http_server.rs b/crates/test-programs/src/http_server.rs new file mode 100644 index 0000000000..6d3e6231f5 --- /dev/null +++ b/crates/test-programs/src/http_server.rs @@ -0,0 +1,96 @@ +use http_body_util::{combinators::BoxBody, BodyExt, Full}; +use hyper::{body::Bytes, service::service_fn, Request, Response}; +use std::{ + net::{SocketAddr, TcpListener, TcpStream}, + sync::OnceLock, +}; + +async fn test( + mut req: Request, +) -> http::Result>> { + let method = req.method().to_string(); + let body = req.body_mut().collect().await.unwrap(); + let buf = body.to_bytes(); + + Response::builder() + .status(http::StatusCode::OK) + .header("x-wasmtime-test-method", method) + .header("x-wasmtime-test-uri", req.uri().to_string()) + .body(Full::::from(buf).boxed()) +} + +async fn serve_http1_connection(stream: TcpStream) -> Result<(), hyper::Error> { + let mut builder = hyper::server::conn::http1::Builder::new(); + let http = builder.keep_alive(false).pipeline_flush(true); + stream.set_nonblocking(true).unwrap(); + let io = tokio::net::TcpStream::from_std(stream).unwrap(); + http.serve_connection(io, service_fn(test)).await +} + +#[derive(Clone)] +/// An Executor that uses the tokio runtime. +pub struct TokioExecutor; + +impl hyper::rt::Executor for TokioExecutor +where + F: std::future::Future + Send + 'static, + F::Output: Send + 'static, +{ + fn execute(&self, fut: F) { + tokio::task::spawn(fut); + } +} + +async fn serve_http2_connection(stream: TcpStream) -> Result<(), hyper::Error> { + let mut builder = hyper::server::conn::http2::Builder::new(TokioExecutor); + let http = builder.max_concurrent_streams(20); + let io = tokio::net::TcpStream::from_std(stream).unwrap(); + http.serve_connection(io, service_fn(test)).await +} + +pub async fn setup_http1( + future: impl std::future::Future>, +) -> Result<(), anyhow::Error> { + static CELL_HTTP1: OnceLock = OnceLock::new(); + let listener = CELL_HTTP1.get_or_init(|| { + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + TcpListener::bind(addr).unwrap() + }); + + let thread = tokio::task::spawn(async move { + let (stream, _) = listener.accept().unwrap(); + let conn = serve_http1_connection(stream).await; + if let Err(err) = conn { + eprintln!("Error serving connection: {:?}", err); + } + }); + + let (future_result, thread_result) = tokio::join!(future, thread); + future_result?; + thread_result.unwrap(); + + Ok(()) +} + +pub async fn setup_http2( + future: impl std::future::Future>, +) -> anyhow::Result<()> { + static CELL_HTTP2: OnceLock = OnceLock::new(); + let listener = CELL_HTTP2.get_or_init(|| { + let addr = SocketAddr::from(([127, 0, 0, 1], 3001)); + TcpListener::bind(addr).unwrap() + }); + let thread = tokio::task::spawn(async move { + let (stream, _) = listener.accept().unwrap(); + let conn = serve_http2_connection(stream).await; + if let Err(err) = conn { + eprintln!("Error serving connection: {:?}", err); + } + }); + + let (future_result, thread_result) = tokio::join!(future, thread); + future_result?; + thread_result.unwrap(); + + Ok(()) +} diff --git a/crates/test-programs/src/lib.rs b/crates/test-programs/src/lib.rs index 11eca7f286..cd3b710703 100644 --- a/crates/test-programs/src/lib.rs +++ b/crates/test-programs/src/lib.rs @@ -1,6 +1,9 @@ ///! This crate exists to build crates for wasm32-wasi in build.rs, and execute ///! these wasm programs in harnesses defined under tests/. +#[cfg(all(feature = "test_programs", not(skip_wasi_http_tests)))] +pub mod http_server; + /// The wasi-tests binaries use these environment variables to determine their /// expected behavior. /// Used by all of the tests/ which execute the wasi-tests binaries. diff --git a/crates/test-programs/tests/wasi-http-components.rs b/crates/test-programs/tests/wasi-http-components.rs new file mode 100644 index 0000000000..4e0257cde2 --- /dev/null +++ b/crates/test-programs/tests/wasi-http-components.rs @@ -0,0 +1,140 @@ +#![cfg(all(feature = "test_programs", not(skip_wasi_http_tests)))] +use wasmtime::{ + component::{Component, Linker}, + Config, Engine, Store, +}; +use wasmtime_wasi::preview2::{ + command::{add_to_linker, Command}, + Table, WasiCtx, WasiCtxBuilder, WasiView, +}; +use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; + +use test_programs::http_server::{setup_http1, setup_http2}; + +lazy_static::lazy_static! { + static ref ENGINE: Engine = { + let mut config = Config::new(); + config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable); + config.wasm_component_model(true); + config.async_support(true); + let engine = Engine::new(&config).unwrap(); + engine + }; +} +// uses ENGINE, creates a fn get_module(&str) -> Module +include!(concat!(env!("OUT_DIR"), "/wasi_http_tests_components.rs")); + +struct Ctx { + table: Table, + wasi: WasiCtx, + http: WasiHttpCtx, +} + +impl WasiView for Ctx { + fn table(&self) -> &Table { + &self.table + } + fn table_mut(&mut self) -> &mut Table { + &mut self.table + } + fn ctx(&self) -> &WasiCtx { + &self.wasi + } + fn ctx_mut(&mut self) -> &mut WasiCtx { + &mut self.wasi + } +} + +impl WasiHttpView for Ctx { + fn http_ctx(&self) -> &WasiHttpCtx { + &self.http + } + fn http_ctx_mut(&mut self) -> &mut WasiHttpCtx { + &mut self.http + } +} + +async fn instantiate_component( + component: Component, + ctx: Ctx, +) -> Result<(Store, Command), anyhow::Error> { + let mut linker = Linker::new(&ENGINE); + add_to_linker(&mut linker)?; + wasmtime_wasi_http::add_to_component_linker(&mut linker)?; + + let mut store = Store::new(&ENGINE, ctx); + + let (command, _instance) = Command::instantiate_async(&mut store, &component, &linker).await?; + Ok((store, command)) +} + +async fn run(name: &str) -> anyhow::Result<()> { + let mut table = Table::new(); + let component = get_component(name); + + // Create our wasi context. + let wasi = WasiCtxBuilder::new() + .inherit_stdio() + .arg(name) + .build(&mut table)?; + let http = WasiHttpCtx::new(); + + let (mut store, command) = instantiate_component(component, Ctx { table, wasi, http }).await?; + command + .wasi_cli_run() + .call_run(&mut store) + .await + .map_err(|e| anyhow::anyhow!("wasm failed with {e:?}"))? + .map_err(|e| anyhow::anyhow!("command returned with failing exit status {e:?}")) +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn outbound_request_get() { + setup_http1(run("outbound_request_get")).await.unwrap(); +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +#[ignore = "test is currently flaky in ci and needs to be debugged"] +async fn outbound_request_post() { + setup_http1(run("outbound_request_post")).await.unwrap(); +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn outbound_request_put() { + setup_http1(run("outbound_request_put")).await.unwrap(); +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +#[cfg_attr( + windows, + ignore = "test is currently flaky in ci and needs to be debugged" +)] +async fn outbound_request_invalid_version() { + setup_http2(run("outbound_request_invalid_version")) + .await + .unwrap(); +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn outbound_request_unknown_method() { + run("outbound_request_unknown_method").await.unwrap(); +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn outbound_request_unsupported_scheme() { + run("outbound_request_unsupported_scheme").await.unwrap(); +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn outbound_request_invalid_port() { + run("outbound_request_invalid_port").await.unwrap(); +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +#[cfg_attr( + windows, + ignore = "test is currently flaky in ci and needs to be debugged" +)] +async fn outbound_request_invalid_dnsname() { + run("outbound_request_invalid_dnsname").await.unwrap(); +} diff --git a/crates/test-programs/tests/wasi-http-modules.rs b/crates/test-programs/tests/wasi-http-modules.rs new file mode 100644 index 0000000000..98e40662ef --- /dev/null +++ b/crates/test-programs/tests/wasi-http-modules.rs @@ -0,0 +1,153 @@ +#![cfg(all(feature = "test_programs", not(skip_wasi_http_tests)))] +use wasmtime::{Config, Engine, Func, Linker, Module, Store}; +use wasmtime_wasi::preview2::{ + preview1::{WasiPreview1Adapter, WasiPreview1View}, + Table, WasiCtx, WasiCtxBuilder, WasiView, +}; +use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; + +use test_programs::http_server::{setup_http1, setup_http2}; + +lazy_static::lazy_static! { + static ref ENGINE: Engine = { + let mut config = Config::new(); + config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable); + config.wasm_component_model(true); + config.async_support(true); + + let engine = Engine::new(&config).unwrap(); + engine + }; +} +// uses ENGINE, creates a fn get_module(&str) -> Module +include!(concat!(env!("OUT_DIR"), "/wasi_http_tests_modules.rs")); + +struct Ctx { + table: Table, + wasi: WasiCtx, + adapter: WasiPreview1Adapter, + http: WasiHttpCtx, +} + +impl WasiView for Ctx { + fn table(&self) -> &Table { + &self.table + } + fn table_mut(&mut self) -> &mut Table { + &mut self.table + } + fn ctx(&self) -> &WasiCtx { + &self.wasi + } + fn ctx_mut(&mut self) -> &mut WasiCtx { + &mut self.wasi + } +} +impl WasiPreview1View for Ctx { + fn adapter(&self) -> &WasiPreview1Adapter { + &self.adapter + } + fn adapter_mut(&mut self) -> &mut WasiPreview1Adapter { + &mut self.adapter + } +} +impl WasiHttpView for Ctx { + fn http_ctx(&self) -> &WasiHttpCtx { + &self.http + } + fn http_ctx_mut(&mut self) -> &mut WasiHttpCtx { + &mut self.http + } +} + +async fn instantiate_module(module: Module, ctx: Ctx) -> Result<(Store, Func), anyhow::Error> { + let mut linker = Linker::new(&ENGINE); + wasmtime_wasi_http::add_to_linker(&mut linker)?; + wasmtime_wasi::preview2::preview1::add_to_linker(&mut linker)?; + + let mut store = Store::new(&ENGINE, ctx); + + let instance = linker.instantiate_async(&mut store, &module).await?; + let command = instance.get_func(&mut store, "wasi:cli/run#run").unwrap(); + Ok((store, command)) +} + +async fn run(name: &str) -> anyhow::Result<()> { + let mut table = Table::new(); + let module = get_module(name); + + // Create our wasi context. + let wasi = WasiCtxBuilder::new() + .inherit_stdio() + .arg(name) + .build(&mut table)?; + let http = WasiHttpCtx::new(); + + let adapter = WasiPreview1Adapter::new(); + + let (mut store, command) = instantiate_module( + module, + Ctx { + table, + wasi, + http, + adapter, + }, + ) + .await?; + command + .call_async(&mut store, &[], &mut [wasmtime::Val::null()]) + .await + .map_err(|e| anyhow::anyhow!("command returned with failing exit status {e:?}")) +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn outbound_request_get() { + setup_http1(run("outbound_request_get")).await.unwrap(); +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +#[ignore = "test is currently flaky in ci and needs to be debugged"] +async fn outbound_request_post() { + setup_http1(run("outbound_request_post")).await.unwrap(); +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn outbound_request_put() { + setup_http1(run("outbound_request_put")).await.unwrap(); +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +#[cfg_attr( + windows, + ignore = "test is currently flaky in ci and needs to be debugged" +)] +async fn outbound_request_invalid_version() { + setup_http2(run("outbound_request_invalid_version")) + .await + .unwrap(); +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn outbound_request_unknown_method() { + run("outbound_request_unknown_method").await.unwrap(); +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn outbound_request_unsupported_scheme() { + run("outbound_request_unsupported_scheme").await.unwrap(); +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn outbound_request_invalid_port() { + run("outbound_request_invalid_port").await.unwrap(); +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +#[cfg_attr( + windows, + ignore = "test is currently flaky in ci and needs to be debugged" +)] +async fn outbound_request_invalid_dnsname() { + run("outbound_request_invalid_dnsname").await.unwrap(); +} diff --git a/crates/test-programs/tests/wasi-http.rs b/crates/test-programs/tests/wasi-http.rs deleted file mode 100644 index e6a5480e7d..0000000000 --- a/crates/test-programs/tests/wasi-http.rs +++ /dev/null @@ -1,97 +0,0 @@ -#![cfg(all(feature = "test_programs", not(skip_wasi_http_tests)))] -use wasmtime::{Config, Engine, Linker, Store}; -use wasmtime_wasi::{sync::WasiCtxBuilder, WasiCtx}; -use wasmtime_wasi_http::WasiHttp; - -use http_body_util::combinators::BoxBody; -use http_body_util::BodyExt; -use hyper::server::conn::http1; -use hyper::{body::Bytes, service::service_fn, Request, Response}; -use std::{error::Error, net::SocketAddr}; -use tokio::net::TcpListener; - -lazy_static::lazy_static! { - static ref ENGINE: Engine = { - let mut config = Config::new(); - config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable); - let engine = Engine::new(&config).unwrap(); - engine - }; -} -// uses ENGINE, creates a fn get_module(&str) -> Module -include!(concat!(env!("OUT_DIR"), "/wasi_http_tests_modules.rs")); - -async fn test( - req: Request, -) -> http::Result>> { - let method = req.method().to_string(); - Response::builder() - .status(http::StatusCode::OK) - .header("x-wasmtime-test-method", method) - .header("x-wasmtime-test-uri", req.uri().to_string()) - .body(req.into_body().boxed()) -} - -async fn async_run_serve() -> Result<(), Box> { - let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); - - let listener = TcpListener::bind(addr).await?; - - loop { - let (stream, _) = listener.accept().await?; - - tokio::task::spawn(async move { - if let Err(err) = http1::Builder::new() - .serve_connection(stream, service_fn(test)) - .await - { - println!("Error serving connection: {:?}", err); - } - }); - } -} - -fn run_server() -> Result<(), Box> { - let rt = tokio::runtime::Runtime::new()?; - let _ent = rt.enter(); - - rt.block_on(async_run_serve())?; - Ok(()) -} - -pub fn run(name: &str) -> anyhow::Result<()> { - let _thread = std::thread::spawn(|| { - run_server().unwrap(); - }); - - let module = get_module(name); - let mut linker = Linker::new(&ENGINE); - - struct Ctx { - wasi: WasiCtx, - http: WasiHttp, - } - - wasmtime_wasi::sync::add_to_linker(&mut linker, |cx: &mut Ctx| &mut cx.wasi)?; - wasmtime_wasi_http::add_to_linker(&mut linker, |cx: &mut Ctx| &mut cx.http)?; - - // Create our wasi context. - let wasi = WasiCtxBuilder::new().inherit_stdio().arg(name)?.build(); - - let mut store = Store::new( - &ENGINE, - Ctx { - wasi, - http: WasiHttp::new(), - }, - ); - - let instance = linker.instantiate(&mut store, &module)?; - let start = instance.get_typed_func::<(), ()>(&mut store, "_start")?; - start.call(&mut store, ()) -} - -#[test_log::test] -fn outbound_request() { - run("outbound_request").unwrap() -} diff --git a/crates/test-programs/wasi-http-tests/Cargo.toml b/crates/test-programs/wasi-http-tests/Cargo.toml index a5b5a7893e..a648458bed 100644 --- a/crates/test-programs/wasi-http-tests/Cargo.toml +++ b/crates/test-programs/wasi-http-tests/Cargo.toml @@ -7,4 +7,7 @@ publish = false [dependencies] anyhow = { workspace = true } -wit-bindgen = { workspace = true, default-features = false, features = ["macros"] } +tokio = { workspace = true, features = ["macros", "rt"] } +wit-bindgen = { workspace = true, default-features = false, features = [ + "macros", +] } diff --git a/crates/test-programs/wasi-http-tests/src/bin/outbound_request.rs b/crates/test-programs/wasi-http-tests/src/bin/outbound_request.rs deleted file mode 100644 index 7987c0dada..0000000000 --- a/crates/test-programs/wasi-http-tests/src/bin/outbound_request.rs +++ /dev/null @@ -1,188 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use std::fmt; -use wasi_http_tests::*; - -struct Response { - status: wasi::http::types::StatusCode, - headers: Vec<(String, Vec)>, - body: Vec, -} -impl fmt::Debug for Response { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut out = f.debug_struct("Response"); - out.field("status", &self.status) - .field("headers", &self.headers); - if let Ok(body) = std::str::from_utf8(&self.body) { - out.field("body", &body); - } else { - out.field("body", &self.body); - } - out.finish() - } -} - -impl Response { - fn header(&self, name: &str) -> Option<&Vec> { - self.headers - .iter() - .find_map(|(k, v)| if k == name { Some(v) } else { None }) - } -} - -fn request( - method: wasi::http::types::Method, - scheme: wasi::http::types::Scheme, - authority: &str, - path_with_query: &str, - body: &[u8], -) -> Result { - let headers = wasi::http::types::new_fields(&[ - ("User-agent".to_string(), "WASI-HTTP/0.0.1".to_string()), - ("Content-type".to_string(), "application/json".to_string()), - ]); - - let request = wasi::http::types::new_outgoing_request( - &method, - Some(&path_with_query), - Some(&scheme), - Some(&authority), - headers, - ); - - let request_body = wasi::http::types::outgoing_request_write(request) - .map_err(|_| anyhow!("outgoing request write failed"))?; - - let mut body_cursor = 0; - while body_cursor < body.len() { - let written = wasi::io::streams::write(request_body, &body[body_cursor..]) - .context("writing request body")?; - body_cursor += written as usize; - } - - let future_response = wasi::http::outgoing_handler::handle(request, None); - - let incoming_response = wasi::http::types::future_incoming_response_get(future_response) - .ok_or_else(|| anyhow!("incoming response is available immediately"))? - // TODO: maybe anything that appears in the Result<_, E> position should impl - // Error? anyway, just use its Debug here: - .map_err(|e| anyhow!("{e:?}"))?; - - // TODO: The current implementation requires this drop after the request is sent. - // The ownership semantics are unclear in wasi-http we should clarify exactly what is - // supposed to happen here. - wasi::io::streams::drop_output_stream(request_body); - - // TODO: we could create a pollable from the future_response and poll on it here to test that - // its available immediately - - wasi::http::types::drop_outgoing_request(request); - - wasi::http::types::drop_future_incoming_response(future_response); - - let status = wasi::http::types::incoming_response_status(incoming_response); - - let headers_handle = wasi::http::types::incoming_response_headers(incoming_response); - let headers = wasi::http::types::fields_entries(headers_handle); - wasi::http::types::drop_fields(headers_handle); - - let body_stream = wasi::http::types::incoming_response_consume(incoming_response) - .map_err(|()| anyhow!("incoming response has no body stream"))?; - - let mut body = Vec::new(); - let mut eof = false; - while !eof { - let (mut body_chunk, stream_ended) = wasi::io::streams::read(body_stream, u64::MAX)?; - eof = stream_ended; - body.append(&mut body_chunk); - } - wasi::io::streams::drop_input_stream(body_stream); - wasi::http::types::drop_incoming_response(incoming_response); - - Ok(Response { - status, - headers, - body, - }) -} - -fn main() -> Result<()> { - let r1 = request( - wasi::http::types::Method::Get, - wasi::http::types::Scheme::Http, - "localhost:3000", - "/get?some=arg?goes=here", - &[], - ) - .context("localhost:3000 /get")?; - - println!("localhost:3000 /get: {r1:?}"); - assert_eq!(r1.status, 200); - let method = r1.header("x-wasmtime-test-method").unwrap(); - assert_eq!(std::str::from_utf8(method).unwrap(), "GET"); - let uri = r1.header("x-wasmtime-test-uri").unwrap(); - assert_eq!( - std::str::from_utf8(uri).unwrap(), - "http://localhost:3000/get?some=arg?goes=here" - ); - assert_eq!(r1.body, b""); - - let r2 = request( - wasi::http::types::Method::Post, - wasi::http::types::Scheme::Http, - "localhost:3000", - "/post", - b"{\"foo\": \"bar\"}", - ) - .context("localhost:3000 /post")?; - - println!("localhost:3000 /post: {r2:?}"); - assert_eq!(r2.status, 200); - let method = r2.header("x-wasmtime-test-method").unwrap(); - assert_eq!(std::str::from_utf8(method).unwrap(), "POST"); - assert_eq!(r2.body, b"{\"foo\": \"bar\"}"); - - let r3 = request( - wasi::http::types::Method::Put, - wasi::http::types::Scheme::Http, - "localhost:3000", - "/put", - &[], - ) - .context("localhost:3000 /put")?; - - println!("localhost:3000 /put: {r3:?}"); - assert_eq!(r3.status, 200); - let method = r3.header("x-wasmtime-test-method").unwrap(); - assert_eq!(std::str::from_utf8(method).unwrap(), "PUT"); - assert_eq!(r3.body, b""); - - let r4 = request( - wasi::http::types::Method::Other("OTHER".to_owned()), - wasi::http::types::Scheme::Http, - "localhost:3000", - "/", - &[], - ); - - let error = r4.unwrap_err(); - assert_eq!( - error.to_string(), - "Error::UnexpectedError(\"unknown method OTHER\")" - ); - - let r5 = request( - wasi::http::types::Method::Get, - wasi::http::types::Scheme::Other("WS".to_owned()), - "localhost:3000", - "/", - &[], - ); - - let error = r5.unwrap_err(); - assert_eq!( - error.to_string(), - "Error::UnexpectedError(\"unsupported scheme WS\")" - ); - - Ok(()) -} diff --git a/crates/test-programs/wasi-http-tests/src/bin/outbound_request_get.rs b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_get.rs new file mode 100644 index 0000000000..617d3db5a4 --- /dev/null +++ b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_get.rs @@ -0,0 +1,41 @@ +use anyhow::{Context, Result}; +use wasi_http_tests::bindings::wasi::http::types::{Method, Scheme}; + +struct Component; + +fn main() {} + +async fn run() -> Result<(), ()> { + let res = wasi_http_tests::request( + Method::Get, + Scheme::Http, + "localhost:3000", + "/get?some=arg&goes=here", + None, + None, + ) + .await + .context("localhost:3000 /get") + .unwrap(); + + println!("localhost:3000 /get: {res:?}"); + assert_eq!(res.status, 200); + let method = res.header("x-wasmtime-test-method").unwrap(); + assert_eq!(std::str::from_utf8(method).unwrap(), "GET"); + let uri = res.header("x-wasmtime-test-uri").unwrap(); + assert_eq!( + std::str::from_utf8(uri).unwrap(), + "http://localhost:3000/get?some=arg&goes=here" + ); + assert_eq!(res.body, b""); + + Ok(()) +} + +impl wasi_http_tests::bindings::exports::wasi::cli::run::Run for Component { + fn run() -> Result<(), ()> { + wasi_http_tests::in_tokio(async { run().await }) + } +} + +wasi_http_tests::export_command_extended!(Component); diff --git a/crates/test-programs/wasi-http-tests/src/bin/outbound_request_invalid_dnsname.rs b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_invalid_dnsname.rs new file mode 100644 index 0000000000..1565defe8a --- /dev/null +++ b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_invalid_dnsname.rs @@ -0,0 +1,31 @@ +use anyhow::Result; +use wasi_http_tests::bindings::wasi::http::types::{Method, Scheme}; + +struct Component; + +fn main() {} + +async fn run() -> Result<(), ()> { + let res = wasi_http_tests::request( + Method::Get, + Scheme::Http, + "some.invalid.dnsname:3000", + "/", + None, + None, + ) + .await; + + let error = res.unwrap_err(); + assert_eq!(error.to_string(), "Error::InvalidUrl(\"invalid dnsname\")"); + + Ok(()) +} + +impl wasi_http_tests::bindings::exports::wasi::cli::run::Run for Component { + fn run() -> Result<(), ()> { + wasi_http_tests::in_tokio(async { run().await }) + } +} + +wasi_http_tests::export_command_extended!(Component); diff --git a/crates/test-programs/wasi-http-tests/src/bin/outbound_request_invalid_port.rs b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_invalid_port.rs new file mode 100644 index 0000000000..87132b93e0 --- /dev/null +++ b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_invalid_port.rs @@ -0,0 +1,34 @@ +use anyhow::Result; +use wasi_http_tests::bindings::wasi::http::types::{Method, Scheme}; + +struct Component; + +fn main() {} + +async fn run() -> Result<(), ()> { + let res = wasi_http_tests::request( + Method::Get, + Scheme::Http, + "localhost:99999", + "/", + None, + None, + ) + .await; + + let error = res.unwrap_err(); + assert_eq!( + error.to_string(), + "Error::InvalidUrl(\"invalid port value\")" + ); + + Ok(()) +} + +impl wasi_http_tests::bindings::exports::wasi::cli::run::Run for Component { + fn run() -> Result<(), ()> { + wasi_http_tests::in_tokio(async { run().await }) + } +} + +wasi_http_tests::export_command_extended!(Component); diff --git a/crates/test-programs/wasi-http-tests/src/bin/outbound_request_invalid_version.rs b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_invalid_version.rs new file mode 100644 index 0000000000..2b75a5f0b7 --- /dev/null +++ b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_invalid_version.rs @@ -0,0 +1,40 @@ +use anyhow::Result; +use wasi_http_tests::bindings::wasi::http::types::{Method, Scheme}; + +struct Component; + +fn main() {} + +async fn run() -> Result<(), ()> { + let res = wasi_http_tests::request( + Method::Connect, + Scheme::Http, + "localhost:3001", + "/", + None, + Some(&[]), + ) + .await; + + let error = res.unwrap_err().to_string(); + if error.ne("Error::ProtocolError(\"invalid HTTP version parsed\")") + && error.ne("Error::ProtocolError(\"operation was canceled\")") + { + panic!( + r#"assertion failed: `(left == right)` + left: `"{error}"`, + right: `"Error::ProtocolError(\"invalid HTTP version parsed\")"` + or `"Error::ProtocolError(\"operation was canceled\")"`)"# + ) + } + + Ok(()) +} + +impl wasi_http_tests::bindings::exports::wasi::cli::run::Run for Component { + fn run() -> Result<(), ()> { + wasi_http_tests::in_tokio(async { run().await }) + } +} + +wasi_http_tests::export_command_extended!(Component); diff --git a/crates/test-programs/wasi-http-tests/src/bin/outbound_request_post.rs b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_post.rs new file mode 100644 index 0000000000..25b67207aa --- /dev/null +++ b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_post.rs @@ -0,0 +1,36 @@ +use anyhow::{Context, Result}; +use wasi_http_tests::bindings::wasi::http::types::{Method, Scheme}; + +struct Component; + +fn main() {} + +async fn run() -> Result<(), ()> { + let res = wasi_http_tests::request( + Method::Post, + Scheme::Http, + "localhost:3000", + "/post", + Some(b"{\"foo\": \"bar\"}"), + None, + ) + .await + .context("localhost:3000 /post") + .unwrap(); + + println!("localhost:3000 /post: {res:?}"); + assert_eq!(res.status, 200); + let method = res.header("x-wasmtime-test-method").unwrap(); + assert_eq!(std::str::from_utf8(method).unwrap(), "POST"); + assert_eq!(res.body, b"{\"foo\": \"bar\"}"); + + Ok(()) +} + +impl wasi_http_tests::bindings::exports::wasi::cli::run::Run for Component { + fn run() -> Result<(), ()> { + wasi_http_tests::in_tokio(async { run().await }) + } +} + +wasi_http_tests::export_command_extended!(Component); diff --git a/crates/test-programs/wasi-http-tests/src/bin/outbound_request_put.rs b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_put.rs new file mode 100644 index 0000000000..6a3446e787 --- /dev/null +++ b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_put.rs @@ -0,0 +1,36 @@ +use anyhow::{Context, Result}; +use wasi_http_tests::bindings::wasi::http::types::{Method, Scheme}; + +struct Component; + +fn main() {} + +async fn run() -> Result<(), ()> { + let res = wasi_http_tests::request( + Method::Put, + Scheme::Http, + "localhost:3000", + "/put", + Some(&[]), + None, + ) + .await + .context("localhost:3000 /put") + .unwrap(); + + println!("localhost:3000 /put: {res:?}"); + assert_eq!(res.status, 200); + let method = res.header("x-wasmtime-test-method").unwrap(); + assert_eq!(std::str::from_utf8(method).unwrap(), "PUT"); + assert_eq!(res.body, b""); + + Ok(()) +} + +impl wasi_http_tests::bindings::exports::wasi::cli::run::Run for Component { + fn run() -> Result<(), ()> { + wasi_http_tests::in_tokio(async { run().await }) + } +} + +wasi_http_tests::export_command_extended!(Component); diff --git a/crates/test-programs/wasi-http-tests/src/bin/outbound_request_unknown_method.rs b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_unknown_method.rs new file mode 100644 index 0000000000..7971b8814d --- /dev/null +++ b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_unknown_method.rs @@ -0,0 +1,34 @@ +use anyhow::Result; +use wasi_http_tests::bindings::wasi::http::types::{Method, Scheme}; + +struct Component; + +fn main() {} + +async fn run() -> Result<(), ()> { + let res = wasi_http_tests::request( + Method::Other("OTHER".to_owned()), + Scheme::Http, + "localhost:3000", + "/", + None, + None, + ) + .await; + + let error = res.unwrap_err(); + assert_eq!( + error.to_string(), + "Error::InvalidUrl(\"unknown method OTHER\")" + ); + + Ok(()) +} + +impl wasi_http_tests::bindings::exports::wasi::cli::run::Run for Component { + fn run() -> Result<(), ()> { + wasi_http_tests::in_tokio(async { run().await }) + } +} + +wasi_http_tests::export_command_extended!(Component); diff --git a/crates/test-programs/wasi-http-tests/src/bin/outbound_request_unsupported_scheme.rs b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_unsupported_scheme.rs new file mode 100644 index 0000000000..5a7b4faa27 --- /dev/null +++ b/crates/test-programs/wasi-http-tests/src/bin/outbound_request_unsupported_scheme.rs @@ -0,0 +1,34 @@ +use anyhow::Result; +use wasi_http_tests::bindings::wasi::http::types::{Method, Scheme}; + +struct Component; + +fn main() {} + +async fn run() -> Result<(), ()> { + let res = wasi_http_tests::request( + Method::Get, + Scheme::Other("WS".to_owned()), + "localhost:3000", + "/", + None, + None, + ) + .await; + + let error = res.unwrap_err(); + assert_eq!( + error.to_string(), + "Error::InvalidUrl(\"unsupported scheme WS\")" + ); + + Ok(()) +} + +impl wasi_http_tests::bindings::exports::wasi::cli::run::Run for Component { + fn run() -> Result<(), ()> { + wasi_http_tests::in_tokio(async { run().await }) + } +} + +wasi_http_tests::export_command_extended!(Component); diff --git a/crates/test-programs/wasi-http-tests/src/lib.rs b/crates/test-programs/wasi-http-tests/src/lib.rs index 7da3c05089..6de349b42d 100644 --- a/crates/test-programs/wasi-http-tests/src/lib.rs +++ b/crates/test-programs/wasi-http-tests/src/lib.rs @@ -1,3 +1,175 @@ -// The macro will generate a macro for defining exports which we won't be reusing -#![allow(unused)] -wit_bindgen::generate!({ path: "../../wasi-http/wasi-http/wit" }); +pub mod bindings { + wit_bindgen::generate!({ + path: "../../wasi-http/wit", + world: "wasmtime:wasi/command-extended", + macro_call_prefix: "::wasi_http_tests::bindings::", + macro_export, + }); +} + +use anyhow::{anyhow, Context, Result}; +use std::fmt; +use std::sync::OnceLock; + +use bindings::wasi::http::{outgoing_handler, types as http_types}; +use bindings::wasi::io::streams; +use bindings::wasi::poll::poll; + +pub struct Response { + pub status: http_types::StatusCode, + pub headers: Vec<(String, Vec)>, + pub body: Vec, +} +impl fmt::Debug for Response { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut out = f.debug_struct("Response"); + out.field("status", &self.status) + .field("headers", &self.headers); + if let Ok(body) = std::str::from_utf8(&self.body) { + out.field("body", &body); + } else { + out.field("body", &self.body); + } + out.finish() + } +} + +impl Response { + pub fn header(&self, name: &str) -> Option<&Vec> { + self.headers + .iter() + .find_map(|(k, v)| if k == name { Some(v) } else { None }) + } +} + +pub async fn request( + method: http_types::Method, + scheme: http_types::Scheme, + authority: &str, + path_with_query: &str, + body: Option<&[u8]>, + additional_headers: Option<&[(String, String)]>, +) -> Result { + let headers = http_types::new_fields( + &[ + &[ + ("User-agent".to_string(), "WASI-HTTP/0.0.1".to_string()), + ("Content-type".to_string(), "application/json".to_string()), + ], + additional_headers.unwrap_or(&[]), + ] + .concat(), + ); + + let request = http_types::new_outgoing_request( + &method, + Some(path_with_query), + Some(&scheme), + Some(authority), + headers, + ); + + let request_body = http_types::outgoing_request_write(request) + .map_err(|_| anyhow!("outgoing request write failed"))?; + + if let Some(body) = body { + let output_stream_pollable = streams::subscribe_to_output_stream(request_body); + let len = body.len(); + if len == 0 { + let (_written, _status) = streams::write(request_body, &[]) + .map_err(|_| anyhow!("request_body stream write failed")) + .context("writing empty request body")?; + } else { + let mut body_cursor = 0; + while body_cursor < body.len() { + let (written, _status) = streams::write(request_body, &body[body_cursor..]) + .map_err(|_| anyhow!("request_body stream write failed")) + .context("writing request body")?; + body_cursor += written as usize; + } + } + + // TODO: enable when working as expected + // let _ = poll::poll_oneoff(&[output_stream_pollable]); + + poll::drop_pollable(output_stream_pollable); + } + + let future_response = outgoing_handler::handle(request, None); + + let incoming_response = match http_types::future_incoming_response_get(future_response) { + Some(result) => result, + None => { + let pollable = http_types::listen_to_future_incoming_response(future_response); + let _ = poll::poll_oneoff(&[pollable]); + http_types::future_incoming_response_get(future_response) + .expect("incoming response available") + } + } + // TODO: maybe anything that appears in the Result<_, E> position should impl + // Error? anyway, just use its Debug here: + .map_err(|e| anyhow!("{e:?}"))?; + + // TODO: The current implementation requires this drop after the request is sent. + // The ownership semantics are unclear in wasi-http we should clarify exactly what is + // supposed to happen here. + streams::drop_output_stream(request_body); + + http_types::drop_outgoing_request(request); + + http_types::drop_future_incoming_response(future_response); + + let status = http_types::incoming_response_status(incoming_response); + + let headers_handle = http_types::incoming_response_headers(incoming_response); + let headers = http_types::fields_entries(headers_handle); + http_types::drop_fields(headers_handle); + + let body_stream = http_types::incoming_response_consume(incoming_response) + .map_err(|()| anyhow!("incoming response has no body stream"))?; + let input_stream_pollable = streams::subscribe_to_input_stream(body_stream); + + let mut body = Vec::new(); + let mut eof = streams::StreamStatus::Open; + while eof != streams::StreamStatus::Ended { + let (mut body_chunk, stream_status) = + streams::read(body_stream, u64::MAX).map_err(|_| anyhow!("body_stream read failed"))?; + eof = if body_chunk.is_empty() { + streams::StreamStatus::Ended + } else { + stream_status + }; + body.append(&mut body_chunk); + } + + poll::drop_pollable(input_stream_pollable); + streams::drop_input_stream(body_stream); + http_types::drop_incoming_response(incoming_response); + + Ok(Response { + status, + headers, + body, + }) +} + +static RUNTIME: OnceLock = OnceLock::new(); + +pub fn in_tokio(f: F) -> F::Output { + match tokio::runtime::Handle::try_current() { + Ok(h) => { + let _enter = h.enter(); + h.block_on(f) + } + Err(_) => { + let runtime = RUNTIME.get_or_init(|| { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + }); + let _enter = runtime.enter(); + runtime.block_on(f) + } + } +} diff --git a/crates/wasi-http/Cargo.toml b/crates/wasi-http/Cargo.toml index a99f9a32e4..ccacfb4824 100644 --- a/crates/wasi-http/Cargo.toml +++ b/crates/wasi-http/Cargo.toml @@ -9,13 +9,22 @@ description = "Experimental HTTP library for WebAssembly in Wasmtime" [dependencies] anyhow = { workspace = true } +async-trait = { workspace = true } bytes = { workspace = true } +futures = { workspace = true, default-features = false, features = [ + "executor", +] } hyper = { version = "=1.0.0-rc.3", features = ["full"] } -tokio = { version = "1", default-features = false, features = ["net", "rt-multi-thread", "time"] } +tokio = { version = "1", default-features = false, features = [ + "net", + "rt-multi-thread", + "time", +] } http = { version = "0.2.9" } http-body = "1.0.0-rc.2" http-body-util = "0.1.0-rc.2" thiserror = { workspace = true } +wasmtime-wasi = { workspace = true } wasmtime = { workspace = true, features = ['component-model'] } # The `ring` crate, used to implement TLS, does not build on riscv64 or s390x diff --git a/crates/wasi-http/src/component_impl.rs b/crates/wasi-http/src/component_impl.rs index b0b27941c8..b660d1f2a3 100644 --- a/crates/wasi-http/src/component_impl.rs +++ b/crates/wasi-http/src/component_impl.rs @@ -1,11 +1,10 @@ -pub use crate::r#struct::WasiHttp; -use crate::wasi::http::outgoing_handler::Host; -use crate::wasi::http::types::{Error, Host as TypesHost, Method, RequestOptions, Scheme}; -use crate::wasi::io::streams::Host as StreamsHost; +use crate::wasi::http::types::{Error, Method, RequestOptions, Scheme}; +use crate::{WasiHttpView, WasiHttpViewExt}; use anyhow::anyhow; use std::str; use std::vec::Vec; use wasmtime::{AsContext, AsContextMut, Caller, Extern, Memory}; +use wasmtime_wasi::preview2::bindings::{io, poll}; const MEMORY: &str = "memory"; @@ -81,7 +80,10 @@ fn read_option_string( } } -fn allocate_guest_pointer(caller: &mut Caller<'_, T>, size: u32) -> anyhow::Result { +async fn allocate_guest_pointer( + caller: &mut Caller<'_, T>, + size: u32, +) -> anyhow::Result { let realloc = caller .get_export("cabi_realloc") .ok_or_else(|| anyhow!("missing required export cabi_realloc"))?; @@ -89,7 +91,9 @@ fn allocate_guest_pointer(caller: &mut Caller<'_, T>, size: u32) -> anyhow::R .into_func() .ok_or_else(|| anyhow!("cabi_realloc must be a func"))?; let typed = func.typed::<(u32, u32, u32, u32), u32>(caller.as_context())?; - Ok(typed.call(caller.as_context_mut(), (0, 0, 4, size))?) + Ok(typed + .call_async(caller.as_context_mut(), (0, 0, 4, size)) + .await?) } fn u32_array_to_u8(arr: &[u32]) -> Vec { @@ -105,9 +109,17 @@ fn u32_array_to_u8(arr: &[u32]) -> Vec { pub fn add_component_to_linker( linker: &mut wasmtime::Linker, - get_cx: impl Fn(&mut T) -> &mut WasiHttp + Send + Sync + Copy + 'static, -) -> anyhow::Result<()> { - linker.func_wrap( + get_cx: impl Fn(&mut T) -> &mut T + Send + Sync + Copy + 'static, +) -> anyhow::Result<()> +where + T: WasiHttpView + + WasiHttpViewExt + + crate::wasi::http::outgoing_handler::Host + + crate::wasi::http::types::Host + + io::streams::Host + + poll::poll::Host, +{ + linker.func_wrap8_async( "wasi:http/outgoing-handler", "handle", move |mut caller: Caller<'_, T>, @@ -118,34 +130,35 @@ pub fn add_component_to_linker( has_first_byte_timeout: i32, first_byte_timeout_ms: u32, has_between_bytes_timeout: i32, - between_bytes_timeout_ms: u32| - -> anyhow::Result { - let options = if has_options == 1 { - Some(RequestOptions { - connect_timeout_ms: if has_timeout == 1 { - Some(timeout_ms) - } else { - None - }, - first_byte_timeout_ms: if has_first_byte_timeout == 1 { - Some(first_byte_timeout_ms) - } else { - None - }, - between_bytes_timeout_ms: if has_between_bytes_timeout == 1 { - Some(between_bytes_timeout_ms) - } else { - None - }, - }) - } else { - None - }; + between_bytes_timeout_ms: u32| { + Box::new(async move { + let options = if has_options == 1 { + Some(RequestOptions { + connect_timeout_ms: if has_timeout == 1 { + Some(timeout_ms) + } else { + None + }, + first_byte_timeout_ms: if has_first_byte_timeout == 1 { + Some(first_byte_timeout_ms) + } else { + None + }, + between_bytes_timeout_ms: if has_between_bytes_timeout == 1 { + Some(between_bytes_timeout_ms) + } else { + None + }, + }) + } else { + None + }; - Ok(get_cx(caller.data_mut()).handle(request, options)?) + get_cx(caller.data_mut()).handle(request, options).await + }) }, )?; - linker.func_wrap( + linker.func_wrap14_async( "wasi:http/types", "new-outgoing-request", move |mut caller: Caller<'_, T>, @@ -162,324 +175,449 @@ pub fn add_component_to_linker( authority_is_some: i32, authority_ptr: u32, authority_len: u32, - headers: u32| - -> anyhow::Result { - let memory = memory_get(&mut caller)?; - let path = read_option_string( - &memory, - caller.as_context_mut(), - path_is_some, - path_ptr, - path_len, - )?; - let authority = read_option_string( - &memory, - caller.as_context_mut(), - authority_is_some, - authority_ptr, - authority_len, - )?; - - let mut s = Scheme::Https; - if scheme_is_some == 1 { - s = match scheme { - 0 => Scheme::Http, - 1 => Scheme::Https, + headers: u32| { + Box::new(async move { + let memory = memory_get(&mut caller)?; + let path = read_option_string( + &memory, + caller.as_context_mut(), + path_is_some, + path_ptr, + path_len, + )?; + let authority = read_option_string( + &memory, + caller.as_context_mut(), + authority_is_some, + authority_ptr, + authority_len, + )?; + + let mut s = Scheme::Https; + if scheme_is_some == 1 { + s = match scheme { + 0 => Scheme::Http, + 1 => Scheme::Https, + _ => { + let value = string_from_memory( + &memory, + caller.as_context_mut(), + scheme_ptr.try_into()?, + scheme_len.try_into()?, + )?; + Scheme::Other(value) + } + }; + } + let m = match method { + 0 => Method::Get, + 1 => Method::Head, + 2 => Method::Post, + 3 => Method::Put, + 4 => Method::Delete, + 5 => Method::Connect, + 6 => Method::Options, + 7 => Method::Trace, + 8 => Method::Patch, _ => { let value = string_from_memory( &memory, caller.as_context_mut(), - scheme_ptr.try_into()?, - scheme_len.try_into()?, + method_ptr.try_into()?, + method_len.try_into()?, )?; - Scheme::Other(value) + Method::Other(value) } }; - } - let m = match method { - 0 => Method::Get, - 1 => Method::Head, - 2 => Method::Post, - 3 => Method::Put, - 4 => Method::Delete, - 5 => Method::Connect, - 6 => Method::Options, - 7 => Method::Trace, - 8 => Method::Patch, - _ => { - let value = string_from_memory( - &memory, - caller.as_context_mut(), - method_ptr.try_into()?, - method_len.try_into()?, - )?; - Method::Other(value) - } - }; - let ctx = get_cx(caller.data_mut()); - Ok(ctx.new_outgoing_request(m, path, Some(s), authority, headers)?) + let ctx = get_cx(caller.data_mut()); + ctx.new_outgoing_request(m, path, Some(s), authority, headers) + .await + }) }, )?; - linker.func_wrap( + linker.func_wrap1_async( "wasi:http/types", "incoming-response-status", - move |mut caller: Caller<'_, T>, id: u32| -> anyhow::Result { - let ctx = get_cx(caller.data_mut()); - Ok(ctx.incoming_response_status(id)?.into()) + move |mut caller: Caller<'_, T>, id: u32| { + Box::new(async move { + let ctx = get_cx(caller.data_mut()); + let result: u32 = ctx.incoming_response_status(id).await?.into(); + Ok(result) + }) }, )?; - linker.func_wrap( + linker.func_wrap1_async( "wasi:http/types", "drop-future-incoming-response", - move |mut caller: Caller<'_, T>, future: u32| -> anyhow::Result<()> { - let ctx = get_cx(caller.data_mut()); - ctx.drop_future_incoming_response(future)?; - Ok(()) + move |mut caller: Caller<'_, T>, future: u32| { + Box::new(async move { + let ctx = get_cx(caller.data_mut()); + ctx.drop_future_incoming_response(future).await + }) }, )?; - linker.func_wrap( + linker.func_wrap2_async( "wasi:http/types", "future-incoming-response-get", - move |mut caller: Caller<'_, T>, future: u32, ptr: i32| -> anyhow::Result<()> { - let ctx = get_cx(caller.data_mut()); - let response = ctx.future_incoming_response_get(future)?.unwrap_or(Ok(0)); - - let memory = memory_get(&mut caller)?; - - // First == is_some - // Second == is_err - // Third == {ok: is_err = false, tag: is_err = true} - // Fourth == string ptr - // Fifth == string len - let result: [u32; 5] = match response { - Ok(value) => [1, 0, value, 0, 0], - Err(error) => { - let (tag, err_string) = match error { - Error::InvalidUrl(e) => (0u32, e), - Error::TimeoutError(e) => (1u32, e), - Error::ProtocolError(e) => (2u32, e), - Error::UnexpectedError(e) => (3u32, e), - }; - let bytes = err_string.as_bytes(); - let len = bytes.len().try_into().unwrap(); - let ptr = allocate_guest_pointer(&mut caller, len)?; - memory.write(caller.as_context_mut(), ptr as _, bytes)?; - [1, 1, tag, ptr, len] - } - }; - let raw = u32_array_to_u8(&result); + move |mut caller: Caller<'_, T>, future: u32, ptr: i32| { + Box::new(async move { + let ctx = get_cx(caller.data_mut()); + let response = ctx.future_incoming_response_get(future).await?; - memory.write(caller.as_context_mut(), ptr as _, &raw)?; - Ok(()) + let memory = memory_get(&mut caller)?; + + // First == is_some + // Second == is_err + // Third == {ok: is_err = false, tag: is_err = true} + // Fourth == string ptr + // Fifth == string len + let result: [u32; 5] = match response { + Some(inner) => match inner { + Ok(value) => [1, 0, value, 0, 0], + Err(error) => { + let (tag, err_string) = match error { + Error::InvalidUrl(e) => (0u32, e), + Error::TimeoutError(e) => (1u32, e), + Error::ProtocolError(e) => (2u32, e), + Error::UnexpectedError(e) => (3u32, e), + }; + let bytes = err_string.as_bytes(); + let len = bytes.len().try_into().unwrap(); + let ptr = allocate_guest_pointer(&mut caller, len).await?; + memory.write(caller.as_context_mut(), ptr as _, bytes)?; + [1, 1, tag, ptr, len] + } + }, + None => [0, 0, 0, 0, 0], + }; + let raw = u32_array_to_u8(&result); + + memory.write(caller.as_context_mut(), ptr as _, &raw)?; + Ok(()) + }) }, )?; - linker.func_wrap( + linker.func_wrap1_async( + "wasi:http/types", + "listen-to-future-incoming-response", + move |mut caller: Caller<'_, T>, future: u32| { + Box::new(async move { + let ctx = get_cx(caller.data_mut()); + ctx.listen_to_future_incoming_response(future).await + }) + }, + )?; + linker.func_wrap2_async( "wasi:http/types", "incoming-response-consume", - move |mut caller: Caller<'_, T>, response: u32, ptr: i32| -> anyhow::Result<()> { - let ctx = get_cx(caller.data_mut()); - let stream = ctx.incoming_response_consume(response)?.unwrap_or(0); - - let memory = memory_get(&mut caller).unwrap(); - - // First == is_some - // Second == stream_id - let result: [u32; 2] = [0, stream]; - let raw = u32_array_to_u8(&result); - - memory.write(caller.as_context_mut(), ptr as _, &raw)?; - Ok(()) + move |mut caller: Caller<'_, T>, response: u32, ptr: i32| { + Box::new(async move { + let ctx = get_cx(caller.data_mut()); + let stream = ctx.incoming_response_consume(response).await?.unwrap_or(0); + + let memory = memory_get(&mut caller).unwrap(); + + // First == is_some + // Second == stream_id + // let result: [u32; 2] = match result { + // Ok(value) => [0, value], + // Err(_) => [1, 0], + // }; + let result: [u32; 2] = [0, stream]; + let raw = u32_array_to_u8(&result); + + memory.write(caller.as_context_mut(), ptr as _, &raw)?; + Ok(()) + }) }, )?; - linker.func_wrap( - "wasi:io/poll", + linker.func_wrap1_async( + "wasi:poll/poll", "drop-pollable", - move |_caller: Caller<'_, T>, _a: i32| -> anyhow::Result<()> { - anyhow::bail!("unimplemented") + move |mut caller: Caller<'_, T>, id: u32| { + Box::new(async move { + let ctx = get_cx(caller.data_mut()); + poll::poll::Host::drop_pollable(ctx, id).await + }) }, )?; - linker.func_wrap( - "wasi:http/types", - "drop-fields", - move |mut caller: Caller<'_, T>, ptr: u32| -> anyhow::Result<()> { - let ctx = get_cx(caller.data_mut()); - ctx.drop_fields(ptr)?; - Ok(()) + linker.func_wrap3_async( + "wasi:poll/poll", + "poll-oneoff", + move |mut caller: Caller<'_, T>, base_ptr: u32, len: u32, out_ptr: u32| { + Box::new(async move { + let memory = memory_get(&mut caller)?; + + let mut vec = Vec::new(); + let mut i = 0; + while i < len { + let ptr = base_ptr + i * 4; + let pollable_ptr = u32_from_memory(&memory, caller.as_context_mut(), ptr)?; + vec.push(pollable_ptr); + i = i + 1; + } + + let ctx = get_cx(caller.data_mut()); + let result = poll::poll::Host::poll_oneoff(ctx, vec).await?; + + let result_len = result.len(); + let result_ptr = + allocate_guest_pointer(&mut caller, (4 * result_len).try_into()?).await?; + let mut ptr = result_ptr; + for item in result.iter() { + let completion: u32 = match item { + true => 1, + false => 0, + }; + memory.write(caller.as_context_mut(), ptr as _, &completion.to_be_bytes())?; + + ptr = ptr + 4; + } + + let result: [u32; 2] = [result_ptr, result_len.try_into()?]; + let raw = u32_array_to_u8(&result); + memory.write(caller.as_context_mut(), out_ptr as _, &raw)?; + Ok(()) + }) }, )?; - linker.func_wrap( + linker.func_wrap1_async( "wasi:io/streams", "drop-input-stream", - move |mut caller: Caller<'_, T>, id: u32| -> anyhow::Result<()> { - let ctx = get_cx(caller.data_mut()); - ctx.drop_input_stream(id)?; - Ok(()) + move |mut caller: Caller<'_, T>, id: u32| { + Box::new(async move { + let ctx = get_cx(caller.data_mut()); + io::streams::Host::drop_input_stream(ctx, id).await + }) }, )?; - linker.func_wrap( + linker.func_wrap1_async( "wasi:io/streams", "drop-output-stream", - move |mut caller: Caller<'_, T>, id: u32| -> anyhow::Result<()> { - let ctx = get_cx(caller.data_mut()); - ctx.drop_output_stream(id)?; - Ok(()) + move |mut caller: Caller<'_, T>, id: u32| { + Box::new(async move { + let ctx = get_cx(caller.data_mut()); + io::streams::Host::drop_output_stream(ctx, id).await + }) + }, + )?; + linker.func_wrap3_async( + "wasi:io/streams", + "read", + move |mut caller: Caller<'_, T>, stream: u32, len: u64, ptr: u32| { + Box::new(async move { + let ctx = get_cx(caller.data_mut()); + + let (bytes, status) = io::streams::Host::read(ctx, stream, len) + .await? + .map_err(|_| anyhow!("read failed"))?; + + let done = match status { + io::streams::StreamStatus::Open => 0, + io::streams::StreamStatus::Ended => 1, + }; + let body_len: u32 = bytes.len().try_into()?; + let out_ptr = allocate_guest_pointer(&mut caller, body_len).await?; + + // First == is_err + // Second == {ok: is_err = false, tag: is_err = true} + // Third == bytes length + // Fourth == enum status + let result: [u32; 4] = [0, out_ptr, body_len, done]; + let raw = u32_array_to_u8(&result); + + let memory = memory_get(&mut caller)?; + memory.write(caller.as_context_mut(), out_ptr as _, &bytes)?; + memory.write(caller.as_context_mut(), ptr as _, &raw)?; + Ok(()) + }) + }, + )?; + linker.func_wrap1_async( + "wasi:io/streams", + "subscribe-to-input-stream", + move |mut caller: Caller<'_, T>, stream: u32| { + Box::new(async move { + let ctx = get_cx(caller.data_mut()); + io::streams::Host::subscribe_to_input_stream(ctx, stream).await + }) + }, + )?; + linker.func_wrap1_async( + "wasi:io/streams", + "subscribe-to-output-stream", + move |mut caller: Caller<'_, T>, stream: u32| { + Box::new(async move { + let ctx = get_cx(caller.data_mut()); + io::streams::Host::subscribe_to_output_stream(ctx, stream).await + }) + }, + )?; + linker.func_wrap4_async( + "wasi:io/streams", + "write", + move |mut caller: Caller<'_, T>, stream: u32, body_ptr: u32, body_len: u32, ptr: u32| { + Box::new(async move { + let memory: Memory = memory_get(&mut caller)?; + let body = + string_from_memory(&memory, caller.as_context_mut(), body_ptr, body_len)?; + + let ctx = get_cx(caller.data_mut()); + + let (len, status) = io::streams::Host::write(ctx, stream, body.into()) + .await? + .map_err(|_| anyhow!("write failed"))?; + let written: u32 = len.try_into()?; + let done: u32 = match status { + io::streams::StreamStatus::Open => 0, + io::streams::StreamStatus::Ended => 1, + }; + + // First == is_err + // Second == {ok: is_err = false, tag: is_err = true} + // Third == amount of bytes written + // Fifth == enum status + let result: [u32; 5] = [0, 0, written, 0, done]; + let raw = u32_array_to_u8(&result); + + memory.write(caller.as_context_mut(), ptr as _, &raw)?; + + Ok(()) + }) + }, + )?; + linker.func_wrap1_async( + "wasi:http/types", + "drop-fields", + move |mut caller: Caller<'_, T>, ptr: u32| { + Box::new(async move { + let ctx = get_cx(caller.data_mut()); + ctx.drop_fields(ptr).await + }) }, )?; - linker.func_wrap( + linker.func_wrap2_async( "wasi:http/types", "outgoing-request-write", - move |mut caller: Caller<'_, T>, request: u32, ptr: u32| -> anyhow::Result<()> { - let ctx = get_cx(caller.data_mut()); - let stream = ctx - .outgoing_request_write(request)? - .map_err(|_| anyhow!("no outgoing stream present"))?; - - let memory = memory_get(&mut caller)?; - // First == is_some - // Second == stream_id - let result: [u32; 2] = [0, stream]; - let raw = u32_array_to_u8(&result); - - memory.write(caller.as_context_mut(), ptr as _, &raw)?; - Ok(()) + move |mut caller: Caller<'_, T>, request: u32, ptr: u32| { + Box::new(async move { + let ctx = get_cx(caller.data_mut()); + let stream = ctx + .outgoing_request_write(request) + .await? + .map_err(|_| anyhow!("no outgoing stream present"))?; + + let memory = memory_get(&mut caller)?; + // First == is_some + // Second == stream_id + let result: [u32; 2] = [0, stream]; + let raw = u32_array_to_u8(&result); + + memory.write(caller.as_context_mut(), ptr as _, &raw)?; + Ok(()) + }) }, )?; - linker.func_wrap( + linker.func_wrap1_async( "wasi:http/types", "drop-outgoing-request", - move |mut caller: Caller<'_, T>, id: u32| -> anyhow::Result<()> { - let ctx = get_cx(caller.data_mut()); - ctx.drop_outgoing_request(id)?; - Ok(()) + move |mut caller: Caller<'_, T>, id: u32| { + Box::new(async move { + let ctx = get_cx(caller.data_mut()); + ctx.drop_outgoing_request(id).await + }) }, )?; - linker.func_wrap( + linker.func_wrap1_async( "wasi:http/types", "drop-incoming-response", - move |mut caller: Caller<'_, T>, id: u32| -> anyhow::Result<()> { - let ctx = get_cx(caller.data_mut()); - ctx.drop_incoming_response(id)?; - Ok(()) + move |mut caller: Caller<'_, T>, id: u32| { + Box::new(async move { + let ctx = get_cx(caller.data_mut()); + ctx.drop_incoming_response(id).await + }) }, )?; - linker.func_wrap( + linker.func_wrap2_async( "wasi:http/types", "new-fields", - move |mut caller: Caller<'_, T>, base_ptr: u32, len: u32| -> anyhow::Result { - let memory = memory_get(&mut caller)?; - - let mut vec = Vec::new(); - let mut i = 0; - // TODO: read this more efficiently as a single block. - while i < len { - let ptr = base_ptr + i * 16; - let name_ptr = u32_from_memory(&memory, caller.as_context_mut(), ptr)?; - let name_len = u32_from_memory(&memory, caller.as_context_mut(), ptr + 4)?; - let value_ptr = u32_from_memory(&memory, caller.as_context_mut(), ptr + 8)?; - let value_len = u32_from_memory(&memory, caller.as_context_mut(), ptr + 12)?; - - let name = - string_from_memory(&memory, caller.as_context_mut(), name_ptr, name_len)?; - let value = - string_from_memory(&memory, caller.as_context_mut(), value_ptr, value_len)?; - - vec.push((name, value)); - i = i + 1; - } - - let ctx = get_cx(caller.data_mut()); - Ok(ctx.new_fields(vec)?) - }, - )?; - linker.func_wrap( - "wasi:io/streams", - "read", - move |mut caller: Caller<'_, T>, stream: u32, len: u64, ptr: u32| -> anyhow::Result<()> { - let ctx = get_cx(caller.data_mut()); - let bytes_tuple = ctx.read(stream, len)??; - let bytes = bytes_tuple.0; - let done = match bytes_tuple.1 { - true => 1, - false => 0, - }; - let body_len: u32 = bytes.len().try_into()?; - let out_ptr = allocate_guest_pointer(&mut caller, body_len)?; - let result: [u32; 4] = [0, out_ptr, body_len, done]; - let raw = u32_array_to_u8(&result); - - let memory = memory_get(&mut caller)?; - memory.write(caller.as_context_mut(), out_ptr as _, &bytes)?; - memory.write(caller.as_context_mut(), ptr as _, &raw)?; - Ok(()) - }, - )?; - linker.func_wrap( - "wasi:io/streams", - "write", - move |mut caller: Caller<'_, T>, - stream: u32, - body_ptr: u32, - body_len: u32, - ptr: u32| - -> anyhow::Result<()> { - let memory = memory_get(&mut caller)?; - let body = string_from_memory(&memory, caller.as_context_mut(), body_ptr, body_len)?; - - let result: [u32; 3] = [0, 0, body_len]; - let raw = u32_array_to_u8(&result); - - let memory = memory_get(&mut caller)?; - memory.write(caller.as_context_mut(), ptr as _, &raw)?; - - let ctx = get_cx(caller.data_mut()); - ctx.write(stream, body.as_bytes().to_vec())??; - Ok(()) + move |mut caller: Caller<'_, T>, base_ptr: u32, len: u32| { + Box::new(async move { + let memory = memory_get(&mut caller)?; + + let mut vec = Vec::new(); + let mut i = 0; + // TODO: read this more efficiently as a single block. + while i < len { + let ptr = base_ptr + i * 16; + let name_ptr = u32_from_memory(&memory, caller.as_context_mut(), ptr)?; + let name_len = u32_from_memory(&memory, caller.as_context_mut(), ptr + 4)?; + let value_ptr = u32_from_memory(&memory, caller.as_context_mut(), ptr + 8)?; + let value_len = u32_from_memory(&memory, caller.as_context_mut(), ptr + 12)?; + + let name = + string_from_memory(&memory, caller.as_context_mut(), name_ptr, name_len)?; + let value = + string_from_memory(&memory, caller.as_context_mut(), value_ptr, value_len)?; + + vec.push((name, value)); + i = i + 1; + } + + let ctx = get_cx(caller.data_mut()); + ctx.new_fields(vec).await + }) }, )?; - linker.func_wrap( + linker.func_wrap2_async( "wasi:http/types", "fields-entries", - move |mut caller: Caller<'_, T>, fields: u32, out_ptr: u32| -> anyhow::Result<()> { - let ctx = get_cx(caller.data_mut()); - let entries = ctx.fields_entries(fields)?; - - let header_len = entries.len(); - let tuple_ptr = allocate_guest_pointer(&mut caller, (16 * header_len).try_into()?)?; - let mut ptr = tuple_ptr; - for item in entries.iter() { - let name = &item.0; - let value = &item.1; - let name_len: u32 = name.len().try_into()?; - let value_len: u32 = value.len().try_into()?; - - let name_ptr = allocate_guest_pointer(&mut caller, name_len)?; - let value_ptr = allocate_guest_pointer(&mut caller, value_len)?; + move |mut caller: Caller<'_, T>, fields: u32, out_ptr: u32| { + Box::new(async move { + let ctx = get_cx(caller.data_mut()); + let entries = ctx.fields_entries(fields).await?; + + let header_len = entries.len(); + let tuple_ptr = + allocate_guest_pointer(&mut caller, (16 * header_len).try_into()?).await?; + let mut ptr = tuple_ptr; + for item in entries.iter() { + let name = &item.0; + let value = &item.1; + let name_len: u32 = name.len().try_into()?; + let value_len: u32 = value.len().try_into()?; + + let name_ptr = allocate_guest_pointer(&mut caller, name_len).await?; + let value_ptr = allocate_guest_pointer(&mut caller, value_len).await?; + + let memory = memory_get(&mut caller)?; + memory.write(caller.as_context_mut(), name_ptr as _, &name.as_bytes())?; + memory.write(caller.as_context_mut(), value_ptr as _, value)?; + + let pair: [u32; 4] = [name_ptr, name_len, value_ptr, value_len]; + let raw_pair = u32_array_to_u8(&pair); + memory.write(caller.as_context_mut(), ptr as _, &raw_pair)?; + + ptr = ptr + 16; + } let memory = memory_get(&mut caller)?; - memory.write(caller.as_context_mut(), name_ptr as _, &name.as_bytes())?; - memory.write(caller.as_context_mut(), value_ptr as _, value)?; - - let pair: [u32; 4] = [name_ptr, name_len, value_ptr, value_len]; - let raw_pair = u32_array_to_u8(&pair); - memory.write(caller.as_context_mut(), ptr as _, &raw_pair)?; - - ptr = ptr + 16; - } - - let memory = memory_get(&mut caller)?; - let result: [u32; 2] = [tuple_ptr, header_len.try_into()?]; - let raw = u32_array_to_u8(&result); - memory.write(caller.as_context_mut(), out_ptr as _, &raw)?; - Ok(()) + let result: [u32; 2] = [tuple_ptr, header_len.try_into()?]; + let raw = u32_array_to_u8(&result); + memory.write(caller.as_context_mut(), out_ptr as _, &raw)?; + Ok(()) + }) }, )?; - linker.func_wrap( + linker.func_wrap1_async( "wasi:http/types", "incoming-response-headers", - move |mut caller: Caller<'_, T>, handle: u32| -> anyhow::Result { - let ctx = get_cx(caller.data_mut()); - Ok(ctx.incoming_response_headers(handle)?) + move |mut caller: Caller<'_, T>, handle: u32| { + Box::new(async move { + let ctx = get_cx(caller.data_mut()); + ctx.incoming_response_headers(handle).await + }) }, )?; Ok(()) diff --git a/crates/wasi-http/src/http_impl.rs b/crates/wasi-http/src/http_impl.rs index 239e54a687..c0f2b7af44 100644 --- a/crates/wasi-http/src/http_impl.rs +++ b/crates/wasi-http/src/http_impl.rs @@ -1,14 +1,10 @@ -use crate::r#struct::{ActiveFuture, ActiveResponse}; -use crate::r#struct::{Stream, WasiHttp}; +use crate::r#struct::{ActiveFields, ActiveFuture, ActiveResponse, HttpResponse, TableHttpExt}; use crate::wasi::http::types::{FutureIncomingResponse, OutgoingRequest, RequestOptions, Scheme}; -#[cfg(not(any(target_arch = "riscv64", target_arch = "s390x")))] -use anyhow::anyhow; -use anyhow::bail; -use bytes::{BufMut, Bytes, BytesMut}; -use http_body_util::{BodyExt, Full}; -use hyper::Method; -use hyper::Request; -use std::collections::HashMap; +pub use crate::{WasiHttpCtx, WasiHttpView}; +use anyhow::Context; +use bytes::{Bytes, BytesMut}; +use http_body_util::{BodyExt, Empty, Full}; +use hyper::{Method, Request}; #[cfg(not(any(target_arch = "riscv64", target_arch = "s390x")))] use std::sync::Arc; use std::time::Duration; @@ -16,17 +12,20 @@ use tokio::net::TcpStream; use tokio::time::timeout; #[cfg(not(any(target_arch = "riscv64", target_arch = "s390x")))] use tokio_rustls::rustls::{self, OwnedTrustAnchor}; +use wasmtime_wasi::preview2::{StreamState, TableStreamExt}; -impl crate::wasi::http::outgoing_handler::Host for WasiHttp { - fn handle( +#[async_trait::async_trait] +impl crate::wasi::http::outgoing_handler::Host for T { + async fn handle( &mut self, request_id: OutgoingRequest, options: Option, ) -> wasmtime::Result { - let future_id = self.future_id_base; - self.future_id_base = self.future_id_base + 1; - let future = ActiveFuture::new(future_id, request_id, options); - self.futures.insert(future_id, future); + let future = ActiveFuture::new(request_id, options); + let future_id = self + .table_mut() + .push_future(Box::new(future)) + .context("[handle] pushing future")?; Ok(future_id) } } @@ -43,12 +42,22 @@ fn port_for_scheme(scheme: &Option) -> &str { } } -impl WasiHttp { - pub(crate) async fn handle_async( +#[async_trait::async_trait] +pub trait WasiHttpViewExt { + async fn handle_async( &mut self, request_id: OutgoingRequest, options: Option, - ) -> wasmtime::Result { + ) -> wasmtime::Result; +} + +#[async_trait::async_trait] +impl WasiHttpViewExt for T { + async fn handle_async( + &mut self, + request_id: OutgoingRequest, + options: Option, + ) -> wasmtime::Result { let opts = options.unwrap_or( // TODO: Configurable defaults here? RequestOptions { @@ -64,12 +73,13 @@ impl WasiHttp { let between_bytes_timeout = Duration::from_millis(opts.between_bytes_timeout_ms.unwrap_or(600 * 1000).into()); - let request = match self.requests.get(&request_id) { - Some(r) => r, - None => bail!("not found!"), - }; + let request = self + .table() + .get_request(request_id) + .context("[handle_async] getting request")? + .clone(); - let method = match &request.method { + let method = match request.method() { crate::wasi::http::types::Method::Get => Method::GET, crate::wasi::http::types::Method::Head => Method::HEAD, crate::wasi::http::types::Method::Post => Method::POST, @@ -79,24 +89,36 @@ impl WasiHttp { crate::wasi::http::types::Method::Options => Method::OPTIONS, crate::wasi::http::types::Method::Trace => Method::TRACE, crate::wasi::http::types::Method::Patch => Method::PATCH, - crate::wasi::http::types::Method::Other(s) => bail!("unknown method {}", s), + crate::wasi::http::types::Method::Other(s) => { + return Err(crate::wasi::http::types::Error::InvalidUrl(format!( + "unknown method {}", + s + )) + .into()); + } }; - let scheme = match request.scheme.as_ref().unwrap_or(&Scheme::Https) { + let scheme = match request.scheme().as_ref().unwrap_or(&Scheme::Https) { Scheme::Http => "http://", Scheme::Https => "https://", - Scheme::Other(s) => bail!("unsupported scheme {}", s), + Scheme::Other(s) => { + return Err(crate::wasi::http::types::Error::InvalidUrl(format!( + "unsupported scheme {}", + s + )) + .into()); + } }; // Largely adapted from https://hyper.rs/guides/1/client/basic/ - let authority = match request.authority.find(":") { - Some(_) => request.authority.clone(), - None => request.authority.clone() + port_for_scheme(&request.scheme), + let authority = match request.authority().find(":") { + Some(_) => request.authority().to_owned(), + None => request.authority().to_owned() + port_for_scheme(request.scheme()), }; + let tcp_stream = TcpStream::connect(authority.clone()).await?; let mut sender = if scheme == "https://" { #[cfg(not(any(target_arch = "riscv64", target_arch = "s390x")))] { - let stream = TcpStream::connect(authority.clone()).await?; //TODO: uncomment this code and make the tls implementation a feature decision. //let connector = tokio_native_tls::native_tls::TlsConnector::builder().build()?; //let connector = tokio_native_tls::TlsConnector::from(connector); @@ -121,9 +143,11 @@ impl WasiHttp { let connector = tokio_rustls::TlsConnector::from(Arc::new(config)); let mut parts = authority.split(":"); let host = parts.next().unwrap_or(&authority); - let domain = - rustls::ServerName::try_from(host).map_err(|_| anyhow!("invalid dnsname"))?; - let stream = connector.connect(domain, stream).await?; + let domain = rustls::ServerName::try_from(host)?; + let stream = connector + .connect(domain, tcp_stream) + .await + .map_err(|e| crate::wasi::http::types::Error::ProtocolError(e.to_string()))?; let t = timeout( connect_timeout, @@ -139,10 +163,15 @@ impl WasiHttp { s } #[cfg(any(target_arch = "riscv64", target_arch = "s390x"))] - bail!("unsupported architecture for SSL") + return Err(crate::wasi::http::types::Error::UnexpectedError( + "unsupported architecture for SSL".to_string(), + )); } else { - let tcp = TcpStream::connect(authority).await?; - let t = timeout(connect_timeout, hyper::client::conn::http1::handshake(tcp)).await?; + let t = timeout( + connect_timeout, + hyper::client::conn::http1::handshake(tcp_stream), + ) + .await?; let (s, conn) = t?; tokio::task::spawn(async move { if let Err(err) = conn.await { @@ -152,50 +181,75 @@ impl WasiHttp { s }; - let url = scheme.to_owned() + &request.authority + &request.path_with_query; + let url = scheme.to_owned() + &request.authority() + &request.path_with_query(); let mut call = Request::builder() .method(method) .uri(url) - .header(hyper::header::HOST, request.authority.as_str()); + .header(hyper::header::HOST, request.authority()); - for (key, val) in request.headers.iter() { - for item in val { - call = call.header(key, item.clone()); + if let Some(headers) = request.headers() { + for (key, val) in self + .table() + .get_fields(headers) + .context("[handle_async] getting request headers")? + .iter() + { + for item in val { + call = call.header(key, item.clone()); + } } } - let response_id = self.response_id_base; - self.response_id_base = self.response_id_base + 1; - let mut response = ActiveResponse::new(response_id); - let body = Full::::new( - self.streams - .get(&request.body) - .unwrap_or(&Stream::default()) - .data - .clone() - .freeze(), - ); + let mut response = ActiveResponse::new(); + let body = match request.body() { + Some(id) => { + let table = self.table_mut(); + let stream = table + .get_stream(id) + .context("[handle_async] getting stream")?; + let input_stream = table + .get_input_stream_mut(stream.incoming()) + .context("[handle_async] getting mutable input stream")?; + let mut bytes = BytesMut::new(); + let mut eof = StreamState::Open; + while eof != StreamState::Closed { + let (chunk, state) = input_stream.read(4096)?; + eof = if chunk.is_empty() { + StreamState::Closed + } else { + state + }; + bytes.extend_from_slice(&chunk[..]); + } + Full::::new(bytes.freeze()).boxed() + } + None => Empty::::new().boxed(), + }; let t = timeout(first_bytes_timeout, sender.send_request(call.body(body)?)).await?; let mut res = t?; - response.status = res.status().try_into()?; + response.status = res.status().as_u16(); + + let mut map = ActiveFields::new(); for (key, value) in res.headers().iter() { - let mut vec = std::vec::Vec::new(); + let mut vec = Vec::new(); vec.push(value.as_bytes().to_vec()); - response - .response_headers - .insert(key.as_str().to_string(), vec); + map.insert(key.as_str().to_string(), vec); } - let mut buf = BytesMut::new(); + let headers = self + .table_mut() + .push_fields(Box::new(map)) + .context("[handle_async] pushing response headers")?; + response.set_headers(headers); + + let mut buf: Vec = Vec::new(); while let Some(next) = timeout(between_bytes_timeout, res.frame()).await? { let frame = next?; if let Some(chunk) = frame.data_ref() { - buf.put(chunk.clone()); + buf.extend_from_slice(chunk); } if let Some(trailers) = frame.trailers_ref() { - response.trailers = self.fields_id_base; - self.fields_id_base += 1; - let mut map: HashMap>> = HashMap::new(); + let mut map = ActiveFields::new(); for (name, value) in trailers.iter() { let key = name.to_string(); match map.get_mut(&key) { @@ -207,13 +261,30 @@ impl WasiHttp { } }; } - self.fields.insert(response.trailers, map); + let trailers = self + .table_mut() + .push_fields(Box::new(map)) + .context("[handle_async] pushing response trailers")?; + response.set_trailers(trailers); } } - response.body = self.streams_id_base; - self.streams_id_base = self.streams_id_base + 1; - self.streams.insert(response.body, buf.freeze().into()); - self.responses.insert(response_id, response); + + let response_id = self + .table_mut() + .push_response(Box::new(response)) + .context("[handle_async] pushing response")?; + let (stream_id, stream) = self + .table_mut() + .push_stream(Bytes::from(buf), response_id) + .context("[handle_async] pushing stream")?; + let response = self + .table_mut() + .get_response_mut(response_id) + .context("[handle_async] getting mutable response")?; + response.set_body(stream_id); + + self.http_ctx_mut().streams.insert(stream_id, stream); + Ok(response_id) } } diff --git a/crates/wasi-http/src/lib.rs b/crates/wasi-http/src/lib.rs index ce5211ef8c..95983787bd 100644 --- a/crates/wasi-http/src/lib.rs +++ b/crates/wasi-http/src/lib.rs @@ -1,27 +1,132 @@ use crate::component_impl::add_component_to_linker; -pub use crate::r#struct::WasiHttp; +pub use crate::http_impl::WasiHttpViewExt; +pub use crate::r#struct::{WasiHttpCtx, WasiHttpView}; +use core::fmt::Formatter; +use std::fmt::{self, Display}; -wasmtime::component::bindgen!({ path: "wasi-http/wit", world: "proxy"}); +wasmtime::component::bindgen!({ + world: "wasi:http/proxy", + with: { + "wasi:io/streams": wasmtime_wasi::preview2::bindings::io::streams, + "wasi:poll/poll": wasmtime_wasi::preview2::bindings::poll::poll, + }, + async: true, +}); pub mod component_impl; pub mod http_impl; -pub mod streams_impl; pub mod r#struct; pub mod types_impl; -pub fn add_to_component_linker( - linker: &mut wasmtime::component::Linker, - get_cx: impl Fn(&mut T) -> &mut WasiHttp + Send + Sync + Copy + 'static, -) -> anyhow::Result<()> { - crate::wasi::http::outgoing_handler::add_to_linker(linker, get_cx)?; - crate::wasi::http::types::add_to_linker(linker, get_cx)?; - crate::wasi::io::streams::add_to_linker(linker, get_cx)?; +pub fn add_to_component_linker(linker: &mut wasmtime::component::Linker) -> anyhow::Result<()> +where + T: WasiHttpView + + WasiHttpViewExt + + crate::wasi::http::outgoing_handler::Host + + crate::wasi::http::types::Host, +{ + crate::wasi::http::outgoing_handler::add_to_linker(linker, |t| t)?; + crate::wasi::http::types::add_to_linker(linker, |t| t)?; Ok(()) } -pub fn add_to_linker( - linker: &mut wasmtime::Linker, - get_cx: impl Fn(&mut T) -> &mut WasiHttp + Send + Sync + Copy + 'static, -) -> anyhow::Result<()> { - add_component_to_linker(linker, get_cx) +pub fn add_to_linker(linker: &mut wasmtime::Linker) -> anyhow::Result<()> +where + T: WasiHttpView + + WasiHttpViewExt + + crate::wasi::http::outgoing_handler::Host + + crate::wasi::http::types::Host + + wasmtime_wasi::preview2::bindings::io::streams::Host + + wasmtime_wasi::preview2::bindings::poll::poll::Host, +{ + add_component_to_linker::(linker, |t| t) +} + +impl std::error::Error for crate::wasi::http::types::Error {} + +impl Display for crate::wasi::http::types::Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + crate::wasi::http::types::Error::InvalidUrl(m) => { + write!(f, "[InvalidUrl] {}", m) + } + crate::wasi::http::types::Error::ProtocolError(m) => { + write!(f, "[ProtocolError] {}", m) + } + crate::wasi::http::types::Error::TimeoutError(m) => { + write!(f, "[TimeoutError] {}", m) + } + crate::wasi::http::types::Error::UnexpectedError(m) => { + write!(f, "[UnexpectedError] {}", m) + } + } + } +} + +impl From for crate::wasi::http::types::Error { + fn from(err: wasmtime_wasi::preview2::TableError) -> Self { + Self::UnexpectedError(err.to_string()) + } +} + +impl From for crate::wasi::http::types::Error { + fn from(err: anyhow::Error) -> Self { + Self::UnexpectedError(err.to_string()) + } +} + +impl From for crate::wasi::http::types::Error { + fn from(err: std::io::Error) -> Self { + let message = err.to_string(); + match err.kind() { + std::io::ErrorKind::InvalidInput => Self::InvalidUrl(message), + std::io::ErrorKind::AddrNotAvailable => Self::InvalidUrl(message), + _ => { + if message.starts_with("failed to lookup address information") { + Self::InvalidUrl("invalid dnsname".to_string()) + } else { + Self::ProtocolError(message) + } + } + } + } +} + +impl From for crate::wasi::http::types::Error { + fn from(err: http::Error) -> Self { + Self::InvalidUrl(err.to_string()) + } +} + +impl From for crate::wasi::http::types::Error { + fn from(err: hyper::Error) -> Self { + let message = err.message().to_string(); + if err.is_timeout() { + Self::TimeoutError(message) + } else if err.is_parse_status() || err.is_user() { + Self::InvalidUrl(message) + } else if err.is_body_write_aborted() + || err.is_canceled() + || err.is_closed() + || err.is_incomplete_message() + || err.is_parse() + { + Self::ProtocolError(message) + } else { + Self::UnexpectedError(message) + } + } +} + +impl From for crate::wasi::http::types::Error { + fn from(err: tokio::time::error::Elapsed) -> Self { + Self::TimeoutError(err.to_string()) + } +} + +#[cfg(not(any(target_arch = "riscv64", target_arch = "s390x")))] +impl From for crate::wasi::http::types::Error { + fn from(_err: rustls::client::InvalidDnsNameError) -> Self { + Self::InvalidUrl("invalid dnsname".to_string()) + } } diff --git a/crates/wasi-http/src/streams_impl.rs b/crates/wasi-http/src/streams_impl.rs deleted file mode 100644 index 1df36d4b11..0000000000 --- a/crates/wasi-http/src/streams_impl.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::wasi::io::streams::{Host, InputStream, OutputStream, Pollable, StreamError}; -use crate::WasiHttp; -use anyhow::{anyhow, bail}; -use std::vec::Vec; - -impl Host for WasiHttp { - fn read( - &mut self, - stream: InputStream, - len: u64, - ) -> wasmtime::Result, bool), StreamError>> { - let st = self - .streams - .get_mut(&stream) - .ok_or_else(|| anyhow!("stream not found: {stream}"))?; - if st.closed { - bail!("stream is dropped!"); - } - let s = &mut st.data; - if len == 0 { - Ok(Ok((bytes::Bytes::new().to_vec(), s.len() > 0))) - } else if s.len() > len.try_into()? { - let result = s.split_to(len.try_into()?); - Ok(Ok((result.to_vec(), false))) - } else { - s.truncate(s.len()); - Ok(Ok((s.clone().to_vec(), true))) - } - } - - fn skip( - &mut self, - stream: InputStream, - len: u64, - ) -> wasmtime::Result> { - let st = self - .streams - .get_mut(&stream) - .ok_or_else(|| anyhow!("stream not found: {stream}"))?; - if st.closed { - bail!("stream is dropped!"); - } - let s = &mut st.data; - if len == 0 { - Ok(Ok((0, s.len() > 0))) - } else if s.len() > len.try_into()? { - s.truncate(len.try_into()?); - Ok(Ok((len, false))) - } else { - let bytes = s.len(); - s.truncate(s.len()); - Ok(Ok((bytes.try_into()?, true))) - } - } - - fn subscribe_to_input_stream(&mut self, _this: InputStream) -> wasmtime::Result { - bail!("unimplemented: subscribe_to_input_stream"); - } - - fn drop_input_stream(&mut self, stream: InputStream) -> wasmtime::Result<()> { - let st = self - .streams - .get_mut(&stream) - .ok_or_else(|| anyhow!("stream not found: {stream}"))?; - st.closed = true; - Ok(()) - } - - fn write( - &mut self, - this: OutputStream, - buf: Vec, - ) -> wasmtime::Result> { - let len = buf.len(); - let st = self.streams.entry(this).or_default(); - if st.closed { - bail!("cannot write to closed stream"); - } - st.data.extend_from_slice(buf.as_slice()); - Ok(Ok(len.try_into()?)) - } - - fn write_zeroes( - &mut self, - this: OutputStream, - len: u64, - ) -> wasmtime::Result> { - let mut data = Vec::with_capacity(len.try_into()?); - let mut i = 0; - while i < len { - data.push(0); - i = i + 1; - } - self.write(this, data) - } - - fn splice( - &mut self, - _this: OutputStream, - _src: InputStream, - _len: u64, - ) -> wasmtime::Result> { - bail!("unimplemented: splice"); - } - - fn forward( - &mut self, - _this: OutputStream, - _src: InputStream, - ) -> wasmtime::Result> { - bail!("unimplemented: forward"); - } - - fn subscribe_to_output_stream(&mut self, _this: OutputStream) -> wasmtime::Result { - bail!("unimplemented: subscribe_to_output_stream"); - } - - fn drop_output_stream(&mut self, stream: OutputStream) -> wasmtime::Result<()> { - let st = self - .streams - .get_mut(&stream) - .ok_or_else(|| anyhow!("stream not found: {stream}"))?; - st.closed = true; - Ok(()) - } - - fn blocking_read( - &mut self, - _: InputStream, - _: u64, - ) -> wasmtime::Result, bool), StreamError>> { - bail!("unimplemented") - } - - fn blocking_skip( - &mut self, - _: InputStream, - _: u64, - ) -> wasmtime::Result> { - bail!("unimplemented") - } - - fn blocking_write( - &mut self, - _: OutputStream, - _: Vec, - ) -> wasmtime::Result> { - bail!("unimplemented") - } - - fn blocking_write_zeroes( - &mut self, - _: OutputStream, - _: u64, - ) -> wasmtime::Result> { - bail!("unimplemented") - } - - fn blocking_splice( - &mut self, - _: OutputStream, - _: InputStream, - _: u64, - ) -> wasmtime::Result> { - bail!("unimplemented") - } -} diff --git a/crates/wasi-http/src/struct.rs b/crates/wasi-http/src/struct.rs index d73fd7c6d5..35503b7a6a 100644 --- a/crates/wasi-http/src/struct.rs +++ b/crates/wasi-http/src/struct.rs @@ -1,124 +1,419 @@ -use crate::wasi::http::types::{Method, RequestOptions, Scheme}; -use bytes::{BufMut, Bytes, BytesMut}; +//! Implements the base structure (i.e. [WasiHttpCtx]) that will provide the +//! implementation of the wasi-http API. + +use crate::wasi::http::types::{ + IncomingStream, Method, OutgoingRequest, OutgoingStream, RequestOptions, Scheme, +}; +use bytes::Bytes; +use std::any::Any; use std::collections::HashMap; +use std::ops::{Deref, DerefMut}; +use wasmtime_wasi::preview2::{ + pipe::{AsyncReadStream, AsyncWriteStream}, + HostInputStream, HostOutputStream, Table, TableError, TableStreamExt, WasiView, +}; -#[derive(Clone, Default)] -pub struct Stream { - pub closed: bool, - pub data: BytesMut, -} +const MAX_BUF_SIZE: usize = 65_536; -#[derive(Clone)] -pub struct WasiHttp { - pub request_id_base: u32, - pub response_id_base: u32, - pub fields_id_base: u32, - pub streams_id_base: u32, - pub future_id_base: u32, - pub requests: HashMap, - pub responses: HashMap, - pub fields: HashMap>>>, +/// Capture the state necessary for use in the wasi-http API implementation. +pub struct WasiHttpCtx { pub streams: HashMap, - pub futures: HashMap, } -#[derive(Clone)] +impl WasiHttpCtx { + /// Make a new context from the default state. + pub fn new() -> Self { + Self { + streams: HashMap::new(), + } + } +} + +pub trait WasiHttpView: WasiView { + fn http_ctx(&self) -> &WasiHttpCtx; + fn http_ctx_mut(&mut self) -> &mut WasiHttpCtx; +} + +pub type FieldsMap = HashMap>>; + +#[derive(Clone, Debug)] pub struct ActiveRequest { - pub id: u32, - pub active_request: bool, + pub active: bool, pub method: Method, pub scheme: Option, pub path_with_query: String, pub authority: String, - pub headers: HashMap>>, - pub body: u32, + pub headers: Option, + pub body: Option, } -#[derive(Clone)] -pub struct ActiveResponse { - pub id: u32, - pub active_response: bool, - pub status: u16, - pub body: u32, - pub response_headers: HashMap>>, - pub trailers: u32, -} +pub trait HttpRequest: Send + Sync { + fn new() -> Self + where + Self: Sized; -#[derive(Clone)] -pub struct ActiveFuture { - pub id: u32, - pub request_id: u32, - pub options: Option, + fn as_any(&self) -> &dyn Any; + + fn method(&self) -> &Method; + fn scheme(&self) -> &Option; + fn path_with_query(&self) -> &str; + fn authority(&self) -> &str; + fn headers(&self) -> Option; + fn set_headers(&mut self, headers: u32); + fn body(&self) -> Option; + fn set_body(&mut self, body: u32); } -impl ActiveRequest { - pub fn new(id: u32) -> Self { +impl HttpRequest for ActiveRequest { + fn new() -> Self { Self { - id, - active_request: false, + active: false, method: Method::Get, scheme: Some(Scheme::Http), path_with_query: "".to_string(), authority: "".to_string(), - headers: HashMap::new(), - body: 0, + headers: None, + body: None, } } + + fn as_any(&self) -> &dyn Any { + self + } + + fn method(&self) -> &Method { + &self.method + } + + fn scheme(&self) -> &Option { + &self.scheme + } + + fn path_with_query(&self) -> &str { + &self.path_with_query + } + + fn authority(&self) -> &str { + &self.authority + } + + fn headers(&self) -> Option { + self.headers + } + + fn set_headers(&mut self, headers: u32) { + self.headers = Some(headers); + } + + fn body(&self) -> Option { + self.body + } + + fn set_body(&mut self, body: u32) { + self.body = Some(body); + } +} + +#[derive(Clone, Debug)] +pub struct ActiveResponse { + pub active: bool, + pub status: u16, + pub headers: Option, + pub body: Option, + pub trailers: Option, +} + +pub trait HttpResponse: Send + Sync { + fn new() -> Self + where + Self: Sized; + + fn as_any(&self) -> &dyn Any; + + fn status(&self) -> u16; + fn headers(&self) -> Option; + fn set_headers(&mut self, headers: u32); + fn body(&self) -> Option; + fn set_body(&mut self, body: u32); + fn trailers(&self) -> Option; + fn set_trailers(&mut self, trailers: u32); } -impl ActiveResponse { - pub fn new(id: u32) -> Self { +impl HttpResponse for ActiveResponse { + fn new() -> Self { Self { - id, - active_response: false, + active: false, status: 0, - body: 0, - response_headers: HashMap::new(), - trailers: 0, + headers: None, + body: None, + trailers: None, } } + + fn as_any(&self) -> &dyn Any { + self + } + + fn status(&self) -> u16 { + self.status + } + + fn headers(&self) -> Option { + self.headers + } + + fn set_headers(&mut self, headers: u32) { + self.headers = Some(headers); + } + + fn body(&self) -> Option { + self.body + } + + fn set_body(&mut self, body: u32) { + self.body = Some(body); + } + + fn trailers(&self) -> Option { + self.trailers + } + + fn set_trailers(&mut self, trailers: u32) { + self.trailers = Some(trailers); + } +} + +#[derive(Clone)] +pub struct ActiveFuture { + request_id: OutgoingRequest, + options: Option, + response_id: Option, + pollable_id: Option, } impl ActiveFuture { - pub fn new(id: u32, request_id: u32, options: Option) -> Self { + pub fn new(request_id: OutgoingRequest, options: Option) -> Self { Self { - id, request_id, options, + response_id: None, + pollable_id: None, } } + + pub fn request_id(&self) -> u32 { + self.request_id + } + + pub fn options(&self) -> Option { + self.options + } + + pub fn response_id(&self) -> Option { + self.response_id + } + + pub fn set_response_id(&mut self, response_id: u32) { + self.response_id = Some(response_id); + } + + pub fn pollable_id(&self) -> Option { + self.pollable_id + } + + pub fn set_pollable_id(&mut self, pollable_id: u32) { + self.pollable_id = Some(pollable_id); + } } -impl Stream { +#[derive(Clone, Debug)] +pub struct ActiveFields(HashMap>>); + +impl ActiveFields { pub fn new() -> Self { - Self::default() + Self(FieldsMap::new()) } } -impl From for Stream { - fn from(bytes: Bytes) -> Self { - let mut buf = BytesMut::with_capacity(bytes.len()); - buf.put(bytes); +pub trait HttpFields: Send + Sync { + fn as_any(&self) -> &dyn Any; +} + +impl HttpFields for ActiveFields { + fn as_any(&self) -> &dyn Any { + self + } +} + +impl Deref for ActiveFields { + type Target = FieldsMap; + fn deref(&self) -> &FieldsMap { + &self.0 + } +} + +impl DerefMut for ActiveFields { + fn deref_mut(&mut self) -> &mut FieldsMap { + &mut self.0 + } +} + +#[derive(Clone, Debug)] +pub struct Stream { + input_id: u32, + output_id: u32, + parent_id: u32, +} + +impl Stream { + pub fn new(input_id: u32, output_id: u32, parent_id: u32) -> Self { Self { - closed: false, - data: buf, + input_id, + output_id, + parent_id, } } + + pub fn incoming(&self) -> IncomingStream { + self.input_id + } + + pub fn outgoing(&self) -> OutgoingStream { + self.output_id + } + + pub fn parent_id(&self) -> u32 { + self.parent_id + } } -impl WasiHttp { - pub fn new() -> Self { - Self { - request_id_base: 1, - response_id_base: 1, - fields_id_base: 1, - streams_id_base: 1, - future_id_base: 1, - requests: HashMap::new(), - responses: HashMap::new(), - fields: HashMap::new(), - streams: HashMap::new(), - futures: HashMap::new(), +pub trait TableHttpExt { + fn push_request(&mut self, request: Box) -> Result; + fn get_request(&self, id: u32) -> Result<&(dyn HttpRequest), TableError>; + fn get_request_mut(&mut self, id: u32) -> Result<&mut Box, TableError>; + fn delete_request(&mut self, id: u32) -> Result<(), TableError>; + + fn push_response(&mut self, response: Box) -> Result; + fn get_response(&self, id: u32) -> Result<&dyn HttpResponse, TableError>; + fn get_response_mut(&mut self, id: u32) -> Result<&mut Box, TableError>; + fn delete_response(&mut self, id: u32) -> Result<(), TableError>; + + fn push_future(&mut self, future: Box) -> Result; + fn get_future(&self, id: u32) -> Result<&ActiveFuture, TableError>; + fn get_future_mut(&mut self, id: u32) -> Result<&mut Box, TableError>; + fn delete_future(&mut self, id: u32) -> Result<(), TableError>; + + fn push_fields(&mut self, fields: Box) -> Result; + fn get_fields(&self, id: u32) -> Result<&ActiveFields, TableError>; + fn get_fields_mut(&mut self, id: u32) -> Result<&mut Box, TableError>; + fn delete_fields(&mut self, id: u32) -> Result<(), TableError>; + + fn push_stream(&mut self, content: Bytes, parent: u32) -> Result<(u32, Stream), TableError>; + fn get_stream(&self, id: u32) -> Result<&Stream, TableError>; + fn get_stream_mut(&mut self, id: u32) -> Result<&mut Box, TableError>; + fn delete_stream(&mut self, id: u32) -> Result<(), TableError>; +} + +impl TableHttpExt for Table { + fn push_request(&mut self, request: Box) -> Result { + self.push(Box::new(request)) + } + fn get_request(&self, id: u32) -> Result<&dyn HttpRequest, TableError> { + self.get::>(id).map(|f| f.as_ref()) + } + fn get_request_mut(&mut self, id: u32) -> Result<&mut Box, TableError> { + self.get_mut::>(id) + } + fn delete_request(&mut self, id: u32) -> Result<(), TableError> { + self.delete::>(id).map(|_old| ()) + } + + fn push_response(&mut self, response: Box) -> Result { + self.push(Box::new(response)) + } + fn get_response(&self, id: u32) -> Result<&dyn HttpResponse, TableError> { + self.get::>(id).map(|f| f.as_ref()) + } + fn get_response_mut(&mut self, id: u32) -> Result<&mut Box, TableError> { + self.get_mut::>(id) + } + fn delete_response(&mut self, id: u32) -> Result<(), TableError> { + self.delete::>(id).map(|_old| ()) + } + + fn push_future(&mut self, future: Box) -> Result { + self.push(Box::new(future)) + } + fn get_future(&self, id: u32) -> Result<&ActiveFuture, TableError> { + self.get::>(id).map(|f| f.as_ref()) + } + fn get_future_mut(&mut self, id: u32) -> Result<&mut Box, TableError> { + self.get_mut::>(id) + } + fn delete_future(&mut self, id: u32) -> Result<(), TableError> { + self.delete::>(id).map(|_old| ()) + } + + fn push_fields(&mut self, fields: Box) -> Result { + self.push(Box::new(fields)) + } + fn get_fields(&self, id: u32) -> Result<&ActiveFields, TableError> { + self.get::>(id).map(|f| f.as_ref()) + } + fn get_fields_mut(&mut self, id: u32) -> Result<&mut Box, TableError> { + self.get_mut::>(id) + } + fn delete_fields(&mut self, id: u32) -> Result<(), TableError> { + self.delete::>(id).map(|_old| ()) + } + + fn push_stream(&mut self, content: Bytes, parent: u32) -> Result<(u32, Stream), TableError> { + let (a, b) = tokio::io::duplex(MAX_BUF_SIZE); + let (_, write_stream) = tokio::io::split(a); + let (read_stream, _) = tokio::io::split(b); + let input_stream = AsyncReadStream::new(read_stream); + let mut output_stream = AsyncWriteStream::new(write_stream); + + let mut cursor = 0; + while cursor < content.len() { + let (written, _) = output_stream + .write(content.slice(cursor..content.len())) + .map_err(|_| TableError::NotPresent)?; + cursor += written; } + + let input_stream = Box::new(input_stream); + let output_id = self.push_output_stream(Box::new(output_stream))?; + let input_id = self.push_input_stream(input_stream)?; + let stream = Stream::new(input_id, output_id, parent); + let cloned_stream = stream.clone(); + let stream_id = self.push(Box::new(Box::new(stream)))?; + Ok((stream_id, cloned_stream)) + } + fn get_stream(&self, id: u32) -> Result<&Stream, TableError> { + self.get::>(id).map(|f| f.as_ref()) + } + fn get_stream_mut(&mut self, id: u32) -> Result<&mut Box, TableError> { + self.get_mut::>(id) + } + fn delete_stream(&mut self, id: u32) -> Result<(), TableError> { + let stream = self.get_stream_mut(id)?; + let input_stream = stream.incoming(); + let output_stream = stream.outgoing(); + self.delete::>(id).map(|_old| ())?; + self.delete::>(input_stream) + .map(|_old| ())?; + self.delete::>(output_stream) + .map(|_old| ()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn instantiate() { + WasiHttpCtx::new().unwrap(); } } diff --git a/crates/wasi-http/src/types_impl.rs b/crates/wasi-http/src/types_impl.rs index 5757acebf6..027d1d4e9f 100644 --- a/crates/wasi-http/src/types_impl.rs +++ b/crates/wasi-http/src/types_impl.rs @@ -1,75 +1,76 @@ -use crate::r#struct::{ActiveRequest, Stream}; +use crate::http_impl::WasiHttpViewExt; +use crate::r#struct::{ActiveFields, ActiveRequest, HttpRequest, TableHttpExt}; use crate::wasi::http::types::{ - Error, Fields, FutureIncomingResponse, Headers, Host, IncomingRequest, IncomingResponse, + Error, Fields, FutureIncomingResponse, Headers, IncomingRequest, IncomingResponse, IncomingStream, Method, OutgoingRequest, OutgoingResponse, OutgoingStream, ResponseOutparam, Scheme, StatusCode, Trailers, }; -use crate::wasi::poll::poll::Pollable; -use crate::WasiHttp; -use anyhow::{anyhow, bail}; -use std::collections::{hash_map::Entry, HashMap}; -use tokio::runtime::{Handle, Runtime}; +use crate::WasiHttpView; +use anyhow::{anyhow, bail, Context}; +use bytes::Bytes; +use wasmtime_wasi::preview2::{bindings::poll::poll::Pollable, HostPollable, TablePollableExt}; -impl Host for WasiHttp { - fn drop_fields(&mut self, fields: Fields) -> wasmtime::Result<()> { - self.fields.remove(&fields); +#[async_trait::async_trait] +impl crate::wasi::http::types::Host for T { + async fn drop_fields(&mut self, fields: Fields) -> wasmtime::Result<()> { + self.table_mut() + .delete_fields(fields) + .context("[drop_fields] deleting fields")?; Ok(()) } - fn new_fields(&mut self, entries: Vec<(String, String)>) -> wasmtime::Result { - let mut map = HashMap::new(); - for item in entries.iter() { - let mut vec = std::vec::Vec::new(); - vec.push(item.1.clone().into_bytes()); - map.insert(item.0.clone(), vec); + async fn new_fields(&mut self, entries: Vec<(String, String)>) -> wasmtime::Result { + let mut map = ActiveFields::new(); + for (key, value) in entries { + map.insert(key, vec![value.clone().into_bytes()]); } - let id = self.fields_id_base; - self.fields_id_base = id + 1; - self.fields.insert(id, map); - + let id = self + .table_mut() + .push_fields(Box::new(map)) + .context("[new_fields] pushing fields")?; Ok(id) } - fn fields_get(&mut self, fields: Fields, name: String) -> wasmtime::Result>> { + async fn fields_get(&mut self, fields: Fields, name: String) -> wasmtime::Result>> { let res = self - .fields - .get(&fields) - .ok_or_else(|| anyhow!("fields not found: {fields}"))? + .table_mut() + .get_fields(fields) + .context("[fields_get] getting fields")? .get(&name) .ok_or_else(|| anyhow!("key not found: {name}"))? .clone(); Ok(res) } - fn fields_set( + async fn fields_set( &mut self, fields: Fields, name: String, value: Vec>, ) -> wasmtime::Result<()> { - match self.fields.get_mut(&fields) { - Some(m) => { + match self.table_mut().get_fields_mut(fields) { + Ok(m) => { m.insert(name, value.clone()); Ok(()) } - None => bail!("fields not found"), + Err(_) => bail!("fields not found"), } } - fn fields_delete(&mut self, fields: Fields, name: String) -> wasmtime::Result<()> { - match self.fields.get_mut(&fields) { - Some(m) => m.remove(&name), - None => None, + async fn fields_delete(&mut self, fields: Fields, name: String) -> wasmtime::Result<()> { + match self.table_mut().get_fields_mut(fields) { + Ok(m) => m.remove(&name), + Err(_) => None, }; Ok(()) } - fn fields_append( + async fn fields_append( &mut self, fields: Fields, name: String, value: Vec, ) -> wasmtime::Result<()> { let m = self - .fields - .get_mut(&fields) - .ok_or_else(|| anyhow!("unknown fields: {fields}"))?; + .table_mut() + .get_fields_mut(fields) + .context("[fields_append] getting mutable fields")?; match m.get_mut(&name) { Some(v) => v.push(value), None => { @@ -80,10 +81,10 @@ impl Host for WasiHttp { }; Ok(()) } - fn fields_entries(&mut self, fields: Fields) -> wasmtime::Result)>> { - let field_map = match self.fields.get(&fields) { - Some(m) => m, - None => bail!("fields not found."), + async fn fields_entries(&mut self, fields: Fields) -> wasmtime::Result)>> { + let field_map = match self.table().get_fields(fields) { + Ok(m) => m.iter(), + Err(_) => bail!("fields not found."), }; let mut result = Vec::new(); for (name, value) in field_map { @@ -91,76 +92,100 @@ impl Host for WasiHttp { } Ok(result) } - fn fields_clone(&mut self, fields: Fields) -> wasmtime::Result { - let id = self.fields_id_base; - self.fields_id_base = self.fields_id_base + 1; - - let m = self - .fields - .get(&fields) - .ok_or_else(|| anyhow!("fields not found: {fields}"))?; - self.fields.insert(id, m.clone()); + async fn fields_clone(&mut self, fields: Fields) -> wasmtime::Result { + let table = self.table_mut(); + let m = table + .get_fields(fields) + .context("[fields_clone] getting fields")?; + let id = table + .push_fields(Box::new(m.clone())) + .context("[fields_clone] pushing fields")?; Ok(id) } - fn finish_incoming_stream(&mut self, s: IncomingStream) -> wasmtime::Result> { - for (_, value) in self.responses.iter() { - if value.body == s { - return match value.trailers { - 0 => Ok(None), - _ => Ok(Some(value.trailers)), - }; + async fn finish_incoming_stream( + &mut self, + stream_id: IncomingStream, + ) -> wasmtime::Result> { + for (_, stream) in self.http_ctx().streams.iter() { + if stream_id == stream.incoming() { + let response = self + .table() + .get_response(stream.parent_id()) + .context("[finish_incoming_stream] get trailers from response")?; + return Ok(response.trailers()); } } bail!("unknown stream!") } - fn finish_outgoing_stream( + async fn finish_outgoing_stream( &mut self, _s: OutgoingStream, _trailers: Option, ) -> wasmtime::Result<()> { bail!("unimplemented: finish_outgoing_stream") } - fn drop_incoming_request(&mut self, _request: IncomingRequest) -> wasmtime::Result<()> { + async fn drop_incoming_request(&mut self, _request: IncomingRequest) -> wasmtime::Result<()> { bail!("unimplemented: drop_incoming_request") } - fn drop_outgoing_request(&mut self, request: OutgoingRequest) -> wasmtime::Result<()> { - if let Entry::Occupied(e) = self.requests.entry(request) { - let r = e.remove(); - self.streams.remove(&r.body); + async fn drop_outgoing_request(&mut self, request: OutgoingRequest) -> wasmtime::Result<()> { + let r = self + .table_mut() + .get_request(request) + .context("[drop_outgoing_request] getting fields")?; + + // Cleanup dependent resources + let body = r.body(); + let headers = r.headers(); + if let Some(b) = body { + self.table_mut().delete_stream(b).ok(); + } + if let Some(h) = headers { + self.table_mut().delete_fields(h).ok(); } + + self.table_mut() + .delete_request(request) + .context("[drop_outgoing_request] deleting request")?; + Ok(()) } - fn incoming_request_method(&mut self, _request: IncomingRequest) -> wasmtime::Result { + async fn incoming_request_method( + &mut self, + _request: IncomingRequest, + ) -> wasmtime::Result { bail!("unimplemented: incoming_request_method") } - fn incoming_request_path_with_query( + async fn incoming_request_path_with_query( &mut self, _request: IncomingRequest, ) -> wasmtime::Result> { bail!("unimplemented: incoming_request_path") } - fn incoming_request_scheme( + async fn incoming_request_scheme( &mut self, _request: IncomingRequest, ) -> wasmtime::Result> { bail!("unimplemented: incoming_request_scheme") } - fn incoming_request_authority( + async fn incoming_request_authority( &mut self, _request: IncomingRequest, ) -> wasmtime::Result> { bail!("unimplemented: incoming_request_authority") } - fn incoming_request_headers(&mut self, _request: IncomingRequest) -> wasmtime::Result { + async fn incoming_request_headers( + &mut self, + _request: IncomingRequest, + ) -> wasmtime::Result { bail!("unimplemented: incoming_request_headers") } - fn incoming_request_consume( + async fn incoming_request_consume( &mut self, _request: IncomingRequest, ) -> wasmtime::Result> { bail!("unimplemented: incoming_request_consume") } - fn new_outgoing_request( + async fn new_outgoing_request( &mut self, method: Method, path_with_query: Option, @@ -168,137 +193,209 @@ impl Host for WasiHttp { authority: Option, headers: Headers, ) -> wasmtime::Result { - let id = self.request_id_base; - self.request_id_base = self.request_id_base + 1; - - let mut req = ActiveRequest::new(id); + let mut req = ActiveRequest::new(); req.path_with_query = path_with_query.unwrap_or("".to_string()); req.authority = authority.unwrap_or("".to_string()); req.method = method; - req.headers = match self.fields.get(&headers) { - Some(h) => h.clone(), - None => bail!("headers not found."), - }; + req.headers = Some(headers); req.scheme = scheme; - self.requests.insert(id, req); + let id = self + .table_mut() + .push_request(Box::new(req)) + .context("[new_outgoing_request] pushing request")?; Ok(id) } - fn outgoing_request_write( + async fn outgoing_request_write( &mut self, request: OutgoingRequest, ) -> wasmtime::Result> { let req = self - .requests - .get_mut(&request) - .ok_or_else(|| anyhow!("unknown request: {request}"))?; - if req.body == 0 { - req.body = self.streams_id_base; - self.streams_id_base = self.streams_id_base + 1; - self.streams.insert(req.body, Stream::default()); - } - Ok(Ok(req.body)) - } - fn drop_response_outparam(&mut self, _response: ResponseOutparam) -> wasmtime::Result<()> { + .table() + .get_request(request) + .context("[outgoing_request_write] getting request")?; + let stream_id = req.body().unwrap_or_else(|| { + let (new, stream) = self + .table_mut() + .push_stream(Bytes::new(), request) + .expect("[outgoing_request_write] valid output stream"); + self.http_ctx_mut().streams.insert(new, stream); + let req = self + .table_mut() + .get_request_mut(request) + .expect("[outgoing_request_write] request to be found"); + req.set_body(new); + new + }); + let stream = self + .table() + .get_stream(stream_id) + .context("[outgoing_request_write] getting stream")?; + Ok(Ok(stream.outgoing())) + } + async fn drop_response_outparam( + &mut self, + _response: ResponseOutparam, + ) -> wasmtime::Result<()> { bail!("unimplemented: drop_response_outparam") } - fn set_response_outparam( + async fn set_response_outparam( &mut self, _outparam: ResponseOutparam, _response: Result, ) -> wasmtime::Result> { bail!("unimplemented: set_response_outparam") } - fn drop_incoming_response(&mut self, response: IncomingResponse) -> wasmtime::Result<()> { - if let Entry::Occupied(e) = self.responses.entry(response) { - let r = e.remove(); - self.streams.remove(&r.body); + async fn drop_incoming_response(&mut self, response: IncomingResponse) -> wasmtime::Result<()> { + let r = self + .table() + .get_response(response) + .context("[drop_incoming_response] getting response")?; + + // Cleanup dependent resources + let body = r.body(); + let headers = r.headers(); + if let Some(id) = body { + let stream = self + .table() + .get_stream(id) + .context("[drop_incoming_response] getting stream")?; + let incoming_id = stream.incoming(); + if let Some(trailers) = self.finish_incoming_stream(incoming_id).await? { + self.table_mut() + .delete_fields(trailers) + .context("[drop_incoming_response] deleting trailers") + .unwrap_or_else(|_| ()); + } + self.table_mut().delete_stream(id).ok(); } + if let Some(h) = headers { + self.table_mut().delete_fields(h).ok(); + } + + self.table_mut() + .delete_response(response) + .context("[drop_incoming_response] deleting response")?; Ok(()) } - fn drop_outgoing_response(&mut self, _response: OutgoingResponse) -> wasmtime::Result<()> { + async fn drop_outgoing_response( + &mut self, + _response: OutgoingResponse, + ) -> wasmtime::Result<()> { bail!("unimplemented: drop_outgoing_response") } - fn incoming_response_status( + async fn incoming_response_status( &mut self, response: IncomingResponse, ) -> wasmtime::Result { let r = self - .responses - .get(&response) - .ok_or_else(|| anyhow!("response not found: {response}"))?; - Ok(r.status) + .table() + .get_response(response) + .context("[incoming_response_status] getting response")?; + Ok(r.status()) } - fn incoming_response_headers( + async fn incoming_response_headers( &mut self, response: IncomingResponse, ) -> wasmtime::Result { let r = self - .responses - .get(&response) - .ok_or_else(|| anyhow!("response not found: {response}"))?; - let id = self.fields_id_base; - self.fields_id_base = self.fields_id_base + 1; - - self.fields.insert(id, r.response_headers.clone()); - Ok(id) + .table() + .get_response(response) + .context("[incoming_response_headers] getting response")?; + Ok(r.headers().unwrap_or(0 as Headers)) } - fn incoming_response_consume( + async fn incoming_response_consume( &mut self, response: IncomingResponse, ) -> wasmtime::Result> { - let r = self - .responses - .get(&response) - .ok_or_else(|| anyhow!("response not found: {response}"))?; - - Ok(Ok(r.body)) - } - fn new_outgoing_response( + let table = self.table_mut(); + let r = table + .get_response(response) + .context("[incoming_response_consume] getting response")?; + Ok(Ok(r + .body() + .map(|id| { + table + .get_stream(id) + .map(|stream| stream.incoming()) + .expect("[incoming_response_consume] response body stream") + }) + .unwrap_or(0 as IncomingStream))) + } + async fn new_outgoing_response( &mut self, _status_code: StatusCode, _headers: Headers, ) -> wasmtime::Result { bail!("unimplemented: new_outgoing_response") } - fn outgoing_response_write( + async fn outgoing_response_write( &mut self, _response: OutgoingResponse, ) -> wasmtime::Result> { bail!("unimplemented: outgoing_response_write") } - fn drop_future_incoming_response( + async fn drop_future_incoming_response( &mut self, future: FutureIncomingResponse, ) -> wasmtime::Result<()> { - self.futures.remove(&future); + self.table_mut() + .delete_future(future) + .context("[drop_future_incoming_response] deleting future")?; Ok(()) } - fn future_incoming_response_get( + async fn future_incoming_response_get( &mut self, future: FutureIncomingResponse, ) -> wasmtime::Result>> { let f = self - .futures - .get(&future) - .ok_or_else(|| anyhow!("future not found: {future}"))?; - - let (handle, _runtime) = match Handle::try_current() { - Ok(h) => (h, None), - Err(_) => { - let rt = Runtime::new().unwrap(); - let _enter = rt.enter(); - (rt.handle().clone(), Some(rt)) + .table() + .get_future(future) + .context("[future_incoming_response_get] getting future")?; + Ok(match f.pollable_id() { + Some(_) => { + let result = match f.response_id() { + Some(id) => Ok(id), + None => { + let response = self.handle_async(f.request_id(), f.options()).await; + match response { + Ok(id) => { + let future_mut = self.table_mut().get_future_mut(future)?; + future_mut.set_response_id(id); + } + _ => {} + } + response + } + }; + Some(result) } - }; - let response = handle - .block_on(self.handle_async(f.request_id, f.options)) - .map_err(|e| Error::UnexpectedError(e.to_string())); - Ok(Some(response)) + None => None, + }) } - fn listen_to_future_incoming_response( + async fn listen_to_future_incoming_response( &mut self, - _f: FutureIncomingResponse, + future: FutureIncomingResponse, ) -> wasmtime::Result { - bail!("unimplemented: listen_to_future_incoming_response") + let f = self + .table() + .get_future(future) + .context("[listen_to_future_incoming_response] getting future")?; + Ok(match f.pollable_id() { + Some(pollable_id) => pollable_id, + None => { + let pollable = + HostPollable::Closure(Box::new(|| Box::pin(futures::future::ready(Ok(()))))); + let pollable_id = self + .table_mut() + .push_host_pollable(pollable) + .context("[listen_to_future_incoming_response] pushing host pollable")?; + let f = self + .table_mut() + .get_future_mut(future) + .context("[listen_to_future_incoming_response] getting future")?; + f.set_pollable_id(pollable_id); + pollable_id + } + }) } } diff --git a/crates/wasi-http/wasi-http b/crates/wasi-http/wasi-http deleted file mode 160000 index 1c95bc21db..0000000000 --- a/crates/wasi-http/wasi-http +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1c95bc21dbd193b046e4232d063a82c8b5ba7994 diff --git a/crates/wasi-http/wit/command-extended.wit b/crates/wasi-http/wit/command-extended.wit new file mode 100644 index 0000000000..518fabdcac --- /dev/null +++ b/crates/wasi-http/wit/command-extended.wit @@ -0,0 +1,39 @@ +// All of the same imports and exports available in the wasi:cli/command world +// with addition of HTTP proxy related imports: +world command-extended { + import wasi:clocks/wall-clock + import wasi:clocks/monotonic-clock + import wasi:clocks/timezone + import wasi:filesystem/types + import wasi:filesystem/preopens + import wasi:sockets/instance-network + import wasi:sockets/ip-name-lookup + import wasi:sockets/network + import wasi:sockets/tcp-create-socket + import wasi:sockets/tcp + import wasi:sockets/udp-create-socket + import wasi:sockets/udp + import wasi:random/random + import wasi:random/insecure + import wasi:random/insecure-seed + import wasi:poll/poll + import wasi:io/streams + import wasi:cli/environment + import wasi:cli/exit + import wasi:cli/stdin + import wasi:cli/stdout + import wasi:cli/stderr + import wasi:cli/terminal-input + import wasi:cli/terminal-output + import wasi:cli/terminal-stdin + import wasi:cli/terminal-stdout + import wasi:cli/terminal-stderr + + export wasi:cli/run + + // We should replace all others with `include self.command` + // as soon as the unioning of worlds is available: + // https://github.com/WebAssembly/component-model/issues/169 + import wasi:logging/logging + import wasi:http/outgoing-handler +} diff --git a/crates/wasi-http/wit/deps/cli/command.wit b/crates/wasi-http/wit/deps/cli/command.wit new file mode 100644 index 0000000000..3cd17bea3f --- /dev/null +++ b/crates/wasi-http/wit/deps/cli/command.wit @@ -0,0 +1,33 @@ +package wasi:cli + +world command { + import wasi:clocks/wall-clock + import wasi:clocks/monotonic-clock + import wasi:clocks/timezone + import wasi:filesystem/types + import wasi:filesystem/preopens + import wasi:sockets/instance-network + import wasi:sockets/ip-name-lookup + import wasi:sockets/network + import wasi:sockets/tcp-create-socket + import wasi:sockets/tcp + import wasi:sockets/udp-create-socket + import wasi:sockets/udp + import wasi:random/random + import wasi:random/insecure + import wasi:random/insecure-seed + import wasi:poll/poll + import wasi:io/streams + + import environment + import exit + import stdin + import stdout + import stderr + import terminal-input + import terminal-output + import terminal-stdin + import terminal-stdout + import terminal-stderr + export run +} diff --git a/crates/wasi-http/wit/deps/cli/environment.wit b/crates/wasi-http/wit/deps/cli/environment.wit new file mode 100644 index 0000000000..36790fe714 --- /dev/null +++ b/crates/wasi-http/wit/deps/cli/environment.wit @@ -0,0 +1,18 @@ +interface environment { + /// Get the POSIX-style environment variables. + /// + /// Each environment variable is provided as a pair of string variable names + /// and string value. + /// + /// Morally, these are a value import, but until value imports are available + /// in the component model, this import function should return the same + /// values each time it is called. + get-environment: func() -> list> + + /// Get the POSIX-style arguments to the program. + get-arguments: func() -> list + + /// Return a path that programs should use as their initial current working + /// directory, interpreting `.` as shorthand for this. + initial-cwd: func() -> option +} diff --git a/crates/wasi-http/wit/deps/cli/exit.wit b/crates/wasi-http/wit/deps/cli/exit.wit new file mode 100644 index 0000000000..4831d50789 --- /dev/null +++ b/crates/wasi-http/wit/deps/cli/exit.wit @@ -0,0 +1,4 @@ +interface exit { + /// Exit the current instance and any linked instances. + exit: func(status: result) +} diff --git a/crates/wasi-http/wit/deps/cli/run.wit b/crates/wasi-http/wit/deps/cli/run.wit new file mode 100644 index 0000000000..45a1ca533f --- /dev/null +++ b/crates/wasi-http/wit/deps/cli/run.wit @@ -0,0 +1,4 @@ +interface run { + /// Run the program. + run: func() -> result +} diff --git a/crates/wasi-http/wit/deps/cli/stdio.wit b/crates/wasi-http/wit/deps/cli/stdio.wit new file mode 100644 index 0000000000..6c9d4a41a6 --- /dev/null +++ b/crates/wasi-http/wit/deps/cli/stdio.wit @@ -0,0 +1,17 @@ +interface stdin { + use wasi:io/streams.{input-stream} + + get-stdin: func() -> input-stream +} + +interface stdout { + use wasi:io/streams.{output-stream} + + get-stdout: func() -> output-stream +} + +interface stderr { + use wasi:io/streams.{output-stream} + + get-stderr: func() -> output-stream +} diff --git a/crates/wasi-http/wit/deps/cli/terminal.wit b/crates/wasi-http/wit/deps/cli/terminal.wit new file mode 100644 index 0000000000..f32e744374 --- /dev/null +++ b/crates/wasi-http/wit/deps/cli/terminal.wit @@ -0,0 +1,59 @@ +interface terminal-input { + /// The input side of a terminal. + /// + /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). + type terminal-input = u32 + + // In the future, this may include functions for disabling echoing, + // disabling input buffering so that keyboard events are sent through + // immediately, querying supported features, and so on. + + /// Dispose of the specified terminal-input after which it may no longer + /// be used. + drop-terminal-input: func(this: terminal-input) +} + +interface terminal-output { + /// The output side of a terminal. + /// + /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). + type terminal-output = u32 + + // In the future, this may include functions for querying the terminal + // size, being notified of terminal size changes, querying supported + // features, and so on. + + /// Dispose of the specified terminal-output, after which it may no longer + /// be used. + drop-terminal-output: func(this: terminal-output) +} + +/// An interface providing an optional `terminal-input` for stdin as a +/// link-time authority. +interface terminal-stdin { + use terminal-input.{terminal-input} + + /// If stdin is connected to a terminal, return a `terminal-input` handle + /// allowing further interaction with it. + get-terminal-stdin: func() -> option +} + +/// An interface providing an optional `terminal-output` for stdout as a +/// link-time authority. +interface terminal-stdout { + use terminal-output.{terminal-output} + + /// If stdout is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + get-terminal-stdout: func() -> option +} + +/// An interface providing an optional `terminal-output` for stderr as a +/// link-time authority. +interface terminal-stderr { + use terminal-output.{terminal-output} + + /// If stderr is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + get-terminal-stderr: func() -> option +} diff --git a/crates/wasi-http/wit/deps/clocks/monotonic-clock.wit b/crates/wasi-http/wit/deps/clocks/monotonic-clock.wit new file mode 100644 index 0000000000..50eb4de111 --- /dev/null +++ b/crates/wasi-http/wit/deps/clocks/monotonic-clock.wit @@ -0,0 +1,34 @@ +package wasi:clocks + +/// WASI Monotonic Clock is a clock API intended to let users measure elapsed +/// time. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A monotonic clock is a clock which has an unspecified initial value, and +/// successive reads of the clock will produce non-decreasing values. +/// +/// It is intended for measuring elapsed time. +interface monotonic-clock { + use wasi:poll/poll.{pollable} + + /// A timestamp in nanoseconds. + type instant = u64 + + /// Read the current value of the clock. + /// + /// The clock is monotonic, therefore calling this function repeatedly will + /// produce a sequence of non-decreasing values. + now: func() -> instant + + /// Query the resolution of the clock. + resolution: func() -> instant + + /// Create a `pollable` which will resolve once the specified time has been + /// reached. + subscribe: func( + when: instant, + absolute: bool + ) -> pollable +} diff --git a/crates/wasi-http/wit/deps/clocks/timezone.wit b/crates/wasi-http/wit/deps/clocks/timezone.wit new file mode 100644 index 0000000000..2b6855668e --- /dev/null +++ b/crates/wasi-http/wit/deps/clocks/timezone.wit @@ -0,0 +1,63 @@ +package wasi:clocks + +interface timezone { + use wall-clock.{datetime} + + /// A timezone. + /// + /// In timezones that recognize daylight saving time, also known as daylight + /// time and summer time, the information returned from the functions varies + /// over time to reflect these adjustments. + /// + /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). + type timezone = u32 + + /// Return information needed to display the given `datetime`. This includes + /// the UTC offset, the time zone name, and a flag indicating whether + /// daylight saving time is active. + /// + /// If the timezone cannot be determined for the given `datetime`, return a + /// `timezone-display` for `UTC` with a `utc-offset` of 0 and no daylight + /// saving time. + display: func(this: timezone, when: datetime) -> timezone-display + + /// The same as `display`, but only return the UTC offset. + utc-offset: func(this: timezone, when: datetime) -> s32 + + /// Dispose of the specified input-stream, after which it may no longer + /// be used. + drop-timezone: func(this: timezone) + + /// Information useful for displaying the timezone of a specific `datetime`. + /// + /// This information may vary within a single `timezone` to reflect daylight + /// saving time adjustments. + record timezone-display { + /// The number of seconds difference between UTC time and the local + /// time of the timezone. + /// + /// The returned value will always be less than 86400 which is the + /// number of seconds in a day (24*60*60). + /// + /// In implementations that do not expose an actual time zone, this + /// should return 0. + utc-offset: s32, + + /// The abbreviated name of the timezone to display to a user. The name + /// `UTC` indicates Coordinated Universal Time. Otherwise, this should + /// reference local standards for the name of the time zone. + /// + /// In implementations that do not expose an actual time zone, this + /// should be the string `UTC`. + /// + /// In time zones that do not have an applicable name, a formatted + /// representation of the UTC offset may be returned, such as `-04:00`. + name: string, + + /// Whether daylight saving time is active. + /// + /// In implementations that do not expose an actual time zone, this + /// should return false. + in-daylight-saving-time: bool, + } +} diff --git a/crates/wasi-http/wit/deps/clocks/wall-clock.wit b/crates/wasi-http/wit/deps/clocks/wall-clock.wit new file mode 100644 index 0000000000..6137724f60 --- /dev/null +++ b/crates/wasi-http/wit/deps/clocks/wall-clock.wit @@ -0,0 +1,43 @@ +package wasi:clocks + +/// WASI Wall Clock is a clock API intended to let users query the current +/// time. The name "wall" makes an analogy to a "clock on the wall", which +/// is not necessarily monotonic as it may be reset. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A wall clock is a clock which measures the date and time according to +/// some external reference. +/// +/// External references may be reset, so this clock is not necessarily +/// monotonic, making it unsuitable for measuring elapsed time. +/// +/// It is intended for reporting the current date and time for humans. +interface wall-clock { + /// A time and date in seconds plus nanoseconds. + record datetime { + seconds: u64, + nanoseconds: u32, + } + + /// Read the current value of the clock. + /// + /// This clock is not monotonic, therefore calling this function repeatedly + /// will not necessarily produce a sequence of non-decreasing values. + /// + /// The returned timestamps represent the number of seconds since + /// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch], + /// also known as [Unix Time]. + /// + /// The nanoseconds field of the output is always less than 1000000000. + /// + /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 + /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time + now: func() -> datetime + + /// Query the resolution of the clock. + /// + /// The nanoseconds field of the output is always less than 1000000000. + resolution: func() -> datetime +} diff --git a/crates/wasi-http/wit/deps/filesystem/preopens.wit b/crates/wasi-http/wit/deps/filesystem/preopens.wit new file mode 100644 index 0000000000..f45661b8a8 --- /dev/null +++ b/crates/wasi-http/wit/deps/filesystem/preopens.wit @@ -0,0 +1,6 @@ +interface preopens { + use types.{descriptor} + + /// Return the set of preopened directories, and their path. + get-directories: func() -> list> +} diff --git a/crates/wasi-http/wit/deps/filesystem/types.wit b/crates/wasi-http/wit/deps/filesystem/types.wit new file mode 100644 index 0000000000..e72a742de6 --- /dev/null +++ b/crates/wasi-http/wit/deps/filesystem/types.wit @@ -0,0 +1,824 @@ +/// WASI filesystem is a filesystem API primarily intended to let users run WASI +/// programs that access their files on their existing filesystems, without +/// significant overhead. +/// +/// It is intended to be roughly portable between Unix-family platforms and +/// Windows, though it does not hide many of the major differences. +/// +/// Paths are passed as interface-type `string`s, meaning they must consist of +/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain +/// paths which are not accessible by this API. +/// +/// The directory separator in WASI is always the forward-slash (`/`). +/// +/// All paths in WASI are relative paths, and are interpreted relative to a +/// `descriptor` referring to a base directory. If a `path` argument to any WASI +/// function starts with `/`, or if any step of resolving a `path`, including +/// `..` and symbolic link steps, reaches a directory outside of the base +/// directory, or reaches a symlink to an absolute or rooted path in the +/// underlying filesystem, the function fails with `error-code::not-permitted`. +interface types { + use wasi:io/streams.{input-stream, output-stream} + use wasi:clocks/wall-clock.{datetime} + + /// File size or length of a region within a file. + type filesize = u64 + + /// The type of a filesystem object referenced by a descriptor. + /// + /// Note: This was called `filetype` in earlier versions of WASI. + enum descriptor-type { + /// The type of the descriptor or file is unknown or is different from + /// any of the other types specified. + unknown, + /// The descriptor refers to a block device inode. + block-device, + /// The descriptor refers to a character device inode. + character-device, + /// The descriptor refers to a directory inode. + directory, + /// The descriptor refers to a named pipe. + fifo, + /// The file refers to a symbolic link inode. + symbolic-link, + /// The descriptor refers to a regular file inode. + regular-file, + /// The descriptor refers to a socket. + socket, + } + + /// Descriptor flags. + /// + /// Note: This was called `fdflags` in earlier versions of WASI. + flags descriptor-flags { + /// Read mode: Data can be read. + read, + /// Write mode: Data can be written to. + write, + /// Request that writes be performed according to synchronized I/O file + /// integrity completion. The data stored in the file and the file's + /// metadata are synchronized. This is similar to `O_SYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + file-integrity-sync, + /// Request that writes be performed according to synchronized I/O data + /// integrity completion. Only the data stored in the file is + /// synchronized. This is similar to `O_DSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + data-integrity-sync, + /// Requests that reads be performed at the same level of integrety + /// requested for writes. This is similar to `O_RSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + requested-write-sync, + /// Mutating directories mode: Directory contents may be mutated. + /// + /// When this flag is unset on a descriptor, operations using the + /// descriptor which would create, rename, delete, modify the data or + /// metadata of filesystem objects, or obtain another handle which + /// would permit any of those, shall fail with `error-code::read-only` if + /// they would otherwise succeed. + /// + /// This may only be set on directories. + mutate-directory, + } + + /// File attributes. + /// + /// Note: This was called `filestat` in earlier versions of WASI. + record descriptor-stat { + /// File type. + %type: descriptor-type, + /// Number of hard links to the file. + link-count: link-count, + /// For regular files, the file size in bytes. For symbolic links, the + /// length in bytes of the pathname contained in the symbolic link. + size: filesize, + /// Last data access timestamp. + data-access-timestamp: datetime, + /// Last data modification timestamp. + data-modification-timestamp: datetime, + /// Last file status change timestamp. + status-change-timestamp: datetime, + } + + /// Flags determining the method of how paths are resolved. + flags path-flags { + /// As long as the resolved path corresponds to a symbolic link, it is + /// expanded. + symlink-follow, + } + + /// Open flags used by `open-at`. + flags open-flags { + /// Create file if it does not exist, similar to `O_CREAT` in POSIX. + create, + /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. + directory, + /// Fail if file already exists, similar to `O_EXCL` in POSIX. + exclusive, + /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. + truncate, + } + + /// Permissions mode used by `open-at`, `change-file-permissions-at`, and + /// similar. + flags modes { + /// True if the resource is considered readable by the containing + /// filesystem. + readable, + /// True if the resource is considered writable by the containing + /// filesystem. + writable, + /// True if the resource is considered executable by the containing + /// filesystem. This does not apply to directories. + executable, + } + + /// Access type used by `access-at`. + variant access-type { + /// Test for readability, writeability, or executability. + access(modes), + + /// Test whether the path exists. + exists, + } + + /// Number of hard links to an inode. + type link-count = u64 + + /// When setting a timestamp, this gives the value to set it to. + variant new-timestamp { + /// Leave the timestamp set to its previous value. + no-change, + /// Set the timestamp to the current time of the system clock associated + /// with the filesystem. + now, + /// Set the timestamp to the given value. + timestamp(datetime), + } + + /// A directory entry. + record directory-entry { + /// The type of the file referred to by this directory entry. + %type: descriptor-type, + + /// The name of the object. + name: string, + } + + /// Error codes returned by functions, similar to `errno` in POSIX. + /// Not all of these error codes are returned by the functions provided by this + /// API; some are used in higher-level library layers, and others are provided + /// merely for alignment with POSIX. + enum error-code { + /// Permission denied, similar to `EACCES` in POSIX. + access, + /// Resource unavailable, or operation would block, similar to `EAGAIN` and `EWOULDBLOCK` in POSIX. + would-block, + /// Connection already in progress, similar to `EALREADY` in POSIX. + already, + /// Bad descriptor, similar to `EBADF` in POSIX. + bad-descriptor, + /// Device or resource busy, similar to `EBUSY` in POSIX. + busy, + /// Resource deadlock would occur, similar to `EDEADLK` in POSIX. + deadlock, + /// Storage quota exceeded, similar to `EDQUOT` in POSIX. + quota, + /// File exists, similar to `EEXIST` in POSIX. + exist, + /// File too large, similar to `EFBIG` in POSIX. + file-too-large, + /// Illegal byte sequence, similar to `EILSEQ` in POSIX. + illegal-byte-sequence, + /// Operation in progress, similar to `EINPROGRESS` in POSIX. + in-progress, + /// Interrupted function, similar to `EINTR` in POSIX. + interrupted, + /// Invalid argument, similar to `EINVAL` in POSIX. + invalid, + /// I/O error, similar to `EIO` in POSIX. + io, + /// Is a directory, similar to `EISDIR` in POSIX. + is-directory, + /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. + loop, + /// Too many links, similar to `EMLINK` in POSIX. + too-many-links, + /// Message too large, similar to `EMSGSIZE` in POSIX. + message-size, + /// Filename too long, similar to `ENAMETOOLONG` in POSIX. + name-too-long, + /// No such device, similar to `ENODEV` in POSIX. + no-device, + /// No such file or directory, similar to `ENOENT` in POSIX. + no-entry, + /// No locks available, similar to `ENOLCK` in POSIX. + no-lock, + /// Not enough space, similar to `ENOMEM` in POSIX. + insufficient-memory, + /// No space left on device, similar to `ENOSPC` in POSIX. + insufficient-space, + /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. + not-directory, + /// Directory not empty, similar to `ENOTEMPTY` in POSIX. + not-empty, + /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. + not-recoverable, + /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. + unsupported, + /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. + no-tty, + /// No such device or address, similar to `ENXIO` in POSIX. + no-such-device, + /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. + overflow, + /// Operation not permitted, similar to `EPERM` in POSIX. + not-permitted, + /// Broken pipe, similar to `EPIPE` in POSIX. + pipe, + /// Read-only file system, similar to `EROFS` in POSIX. + read-only, + /// Invalid seek, similar to `ESPIPE` in POSIX. + invalid-seek, + /// Text file busy, similar to `ETXTBSY` in POSIX. + text-file-busy, + /// Cross-device link, similar to `EXDEV` in POSIX. + cross-device, + } + + /// File or memory access pattern advisory information. + enum advice { + /// The application has no advice to give on its behavior with respect + /// to the specified data. + normal, + /// The application expects to access the specified data sequentially + /// from lower offsets to higher offsets. + sequential, + /// The application expects to access the specified data in a random + /// order. + random, + /// The application expects to access the specified data in the near + /// future. + will-need, + /// The application expects that it will not access the specified data + /// in the near future. + dont-need, + /// The application expects to access the specified data once and then + /// not reuse it thereafter. + no-reuse, + } + + /// A descriptor is a reference to a filesystem object, which may be a file, + /// directory, named pipe, special file, or other object on which filesystem + /// calls may be made. + /// + /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). + type descriptor = u32 + + /// A 128-bit hash value, split into parts because wasm doesn't have a + /// 128-bit integer type. + record metadata-hash-value { + /// 64 bits of a 128-bit hash value. + lower: u64, + /// Another 64 bits of a 128-bit hash value. + upper: u64, + } + + /// Return a stream for reading from a file, if available. + /// + /// May fail with an error-code describing why the file cannot be read. + /// + /// Multiple read, write, and append streams may be active on the same open + /// file and they do not interfere with each other. + /// + /// Note: This allows using `read-stream`, which is similar to `read` in POSIX. + read-via-stream: func( + this: descriptor, + /// The offset within the file at which to start reading. + offset: filesize, + ) -> result + + /// Return a stream for writing to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be written. + /// + /// Note: This allows using `write-stream`, which is similar to `write` in + /// POSIX. + write-via-stream: func( + this: descriptor, + /// The offset within the file at which to start writing. + offset: filesize, + ) -> result + + /// Return a stream for appending to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be appended. + /// + /// Note: This allows using `write-stream`, which is similar to `write` with + /// `O_APPEND` in in POSIX. + append-via-stream: func( + this: descriptor, + ) -> result + + /// Provide file advisory information on a descriptor. + /// + /// This is similar to `posix_fadvise` in POSIX. + advise: func( + this: descriptor, + /// The offset within the file to which the advisory applies. + offset: filesize, + /// The length of the region to which the advisory applies. + length: filesize, + /// The advice. + advice: advice + ) -> result<_, error-code> + + /// Synchronize the data of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fdatasync` in POSIX. + sync-data: func(this: descriptor) -> result<_, error-code> + + /// Get flags associated with a descriptor. + /// + /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX. + /// + /// Note: This returns the value that was the `fs_flags` value returned + /// from `fdstat_get` in earlier versions of WASI. + get-flags: func(this: descriptor) -> result + + /// Get the dynamic type of a descriptor. + /// + /// Note: This returns the same value as the `type` field of the `fd-stat` + /// returned by `stat`, `stat-at` and similar. + /// + /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided + /// by `fstat` in POSIX. + /// + /// Note: This returns the value that was the `fs_filetype` value returned + /// from `fdstat_get` in earlier versions of WASI. + get-type: func(this: descriptor) -> result + + /// Adjust the size of an open file. If this increases the file's size, the + /// extra bytes are filled with zeros. + /// + /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. + set-size: func(this: descriptor, size: filesize) -> result<_, error-code> + + /// Adjust the timestamps of an open file or directory. + /// + /// Note: This is similar to `futimens` in POSIX. + /// + /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. + set-times: func( + this: descriptor, + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code> + + /// Read from a descriptor, without using and updating the descriptor's offset. + /// + /// This function returns a list of bytes containing the data that was + /// read, along with a bool which, when true, indicates that the end of the + /// file was reached. The returned list will contain up to `length` bytes; it + /// may return fewer than requested, if the end of the file is reached or + /// if the I/O operation is interrupted. + /// + /// In the future, this may change to return a `stream`. + /// + /// Note: This is similar to `pread` in POSIX. + read: func( + this: descriptor, + /// The maximum number of bytes to read. + length: filesize, + /// The offset within the file at which to read. + offset: filesize, + ) -> result, bool>, error-code> + + /// Write to a descriptor, without using and updating the descriptor's offset. + /// + /// It is valid to write past the end of a file; the file is extended to the + /// extent of the write, with bytes between the previous end and the start of + /// the write set to zero. + /// + /// In the future, this may change to take a `stream`. + /// + /// Note: This is similar to `pwrite` in POSIX. + write: func( + this: descriptor, + /// Data to write + buffer: list, + /// The offset within the file at which to write. + offset: filesize, + ) -> result + + /// Read directory entries from a directory. + /// + /// On filesystems where directories contain entries referring to themselves + /// and their parents, often named `.` and `..` respectively, these entries + /// are omitted. + /// + /// This always returns a new stream which starts at the beginning of the + /// directory. Multiple streams may be active on the same directory, and they + /// do not interfere with each other. + read-directory: func( + this: descriptor + ) -> result + + /// Synchronize the data and metadata of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fsync` in POSIX. + sync: func(this: descriptor) -> result<_, error-code> + + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + create-directory-at: func( + this: descriptor, + /// The relative path at which to create the directory. + path: string, + ) -> result<_, error-code> + + /// Return the attributes of an open file or directory. + /// + /// Note: This is similar to `fstat` in POSIX, except that it does not return + /// device and inode information. For testing whether two descriptors refer to + /// the same underlying filesystem object, use `is-same-object`. To obtain + /// additional data that can be used do determine whether a file has been + /// modified, use `metadata-hash`. + /// + /// Note: This was called `fd_filestat_get` in earlier versions of WASI. + stat: func(this: descriptor) -> result + + /// Return the attributes of a file or directory. + /// + /// Note: This is similar to `fstatat` in POSIX, except that it does not + /// return device and inode information. See the `stat` description for a + /// discussion of alternatives. + /// + /// Note: This was called `path_filestat_get` in earlier versions of WASI. + stat-at: func( + this: descriptor, + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result + + /// Adjust the timestamps of a file or directory. + /// + /// Note: This is similar to `utimensat` in POSIX. + /// + /// Note: This was called `path_filestat_set_times` in earlier versions of + /// WASI. + set-times-at: func( + this: descriptor, + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to operate on. + path: string, + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code> + + /// Create a hard link. + /// + /// Note: This is similar to `linkat` in POSIX. + link-at: func( + this: descriptor, + /// Flags determining the method of how the path is resolved. + old-path-flags: path-flags, + /// The relative source path from which to link. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: descriptor, + /// The relative destination path at which to create the hard link. + new-path: string, + ) -> result<_, error-code> + + /// Open a file or directory. + /// + /// The returned descriptor is not guaranteed to be the lowest-numbered + /// descriptor not currently open/ it is randomized to prevent applications + /// from depending on making assumptions about indexes, since this is + /// error-prone in multi-threaded contexts. The returned descriptor is + /// guaranteed to be less than 2**31. + /// + /// If `flags` contains `descriptor-flags::mutate-directory`, and the base + /// descriptor doesn't have `descriptor-flags::mutate-directory` set, + /// `open-at` fails with `error-code::read-only`. + /// + /// If `flags` contains `write` or `mutate-directory`, or `open-flags` + /// contains `truncate` or `create`, and the base descriptor doesn't have + /// `descriptor-flags::mutate-directory` set, `open-at` fails with + /// `error-code::read-only`. + /// + /// Note: This is similar to `openat` in POSIX. + open-at: func( + this: descriptor, + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the object to open. + path: string, + /// The method by which to open the file. + open-flags: open-flags, + /// Flags to use for the resulting descriptor. + %flags: descriptor-flags, + /// Permissions to use when creating a new file. + modes: modes + ) -> result + + /// Read the contents of a symbolic link. + /// + /// If the contents contain an absolute or rooted path in the underlying + /// filesystem, this function fails with `error-code::not-permitted`. + /// + /// Note: This is similar to `readlinkat` in POSIX. + readlink-at: func( + this: descriptor, + /// The relative path of the symbolic link from which to read. + path: string, + ) -> result + + /// Remove a directory. + /// + /// Return `error-code::not-empty` if the directory is not empty. + /// + /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + remove-directory-at: func( + this: descriptor, + /// The relative path to a directory to remove. + path: string, + ) -> result<_, error-code> + + /// Rename a filesystem object. + /// + /// Note: This is similar to `renameat` in POSIX. + rename-at: func( + this: descriptor, + /// The relative source path of the file or directory to rename. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: descriptor, + /// The relative destination path to which to rename the file or directory. + new-path: string, + ) -> result<_, error-code> + + /// Create a symbolic link (also known as a "symlink"). + /// + /// If `old-path` starts with `/`, the function fails with + /// `error-code::not-permitted`. + /// + /// Note: This is similar to `symlinkat` in POSIX. + symlink-at: func( + this: descriptor, + /// The contents of the symbolic link. + old-path: string, + /// The relative destination path at which to create the symbolic link. + new-path: string, + ) -> result<_, error-code> + + /// Check accessibility of a filesystem path. + /// + /// Check whether the given filesystem path names an object which is + /// readable, writable, or executable, or whether it exists. + /// + /// This does not a guarantee that subsequent accesses will succeed, as + /// filesystem permissions may be modified asynchronously by external + /// entities. + /// + /// Note: This is similar to `faccessat` with the `AT_EACCESS` flag in POSIX. + access-at: func( + this: descriptor, + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path to check. + path: string, + /// The type of check to perform. + %type: access-type + ) -> result<_, error-code> + + /// Unlink a filesystem object that is not a directory. + /// + /// Return `error-code::is-directory` if the path refers to a directory. + /// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + unlink-file-at: func( + this: descriptor, + /// The relative path to a file to unlink. + path: string, + ) -> result<_, error-code> + + /// Change the permissions of a filesystem object that is not a directory. + /// + /// Note that the ultimate meanings of these permissions is + /// filesystem-specific. + /// + /// Note: This is similar to `fchmodat` in POSIX. + change-file-permissions-at: func( + this: descriptor, + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path to operate on. + path: string, + /// The new permissions for the filesystem object. + modes: modes, + ) -> result<_, error-code> + + /// Change the permissions of a directory. + /// + /// Note that the ultimate meanings of these permissions is + /// filesystem-specific. + /// + /// Unlike in POSIX, the `executable` flag is not reinterpreted as a "search" + /// flag. `read` on a directory implies readability and searchability, and + /// `execute` is not valid for directories. + /// + /// Note: This is similar to `fchmodat` in POSIX. + change-directory-permissions-at: func( + this: descriptor, + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path to operate on. + path: string, + /// The new permissions for the directory. + modes: modes, + ) -> result<_, error-code> + + /// Request a shared advisory lock for an open file. + /// + /// This requests a *shared* lock; more than one shared lock can be held for + /// a file at the same time. + /// + /// If the open file has an exclusive lock, this function downgrades the lock + /// to a shared lock. If it has a shared lock, this function has no effect. + /// + /// This requests an *advisory* lock, meaning that the file could be accessed + /// by other programs that don't hold the lock. + /// + /// It is unspecified how shared locks interact with locks acquired by + /// non-WASI programs. + /// + /// This function blocks until the lock can be acquired. + /// + /// Not all filesystems support locking; on filesystems which don't support + /// locking, this function returns `error-code::unsupported`. + /// + /// Note: This is similar to `flock(fd, LOCK_SH)` in Unix. + lock-shared: func(this: descriptor) -> result<_, error-code> + + /// Request an exclusive advisory lock for an open file. + /// + /// This requests an *exclusive* lock; no other locks may be held for the + /// file while an exclusive lock is held. + /// + /// If the open file has a shared lock and there are no exclusive locks held + /// for the file, this function upgrades the lock to an exclusive lock. If the + /// open file already has an exclusive lock, this function has no effect. + /// + /// This requests an *advisory* lock, meaning that the file could be accessed + /// by other programs that don't hold the lock. + /// + /// It is unspecified whether this function succeeds if the file descriptor + /// is not opened for writing. It is unspecified how exclusive locks interact + /// with locks acquired by non-WASI programs. + /// + /// This function blocks until the lock can be acquired. + /// + /// Not all filesystems support locking; on filesystems which don't support + /// locking, this function returns `error-code::unsupported`. + /// + /// Note: This is similar to `flock(fd, LOCK_EX)` in Unix. + lock-exclusive: func(this: descriptor) -> result<_, error-code> + + /// Request a shared advisory lock for an open file. + /// + /// This requests a *shared* lock; more than one shared lock can be held for + /// a file at the same time. + /// + /// If the open file has an exclusive lock, this function downgrades the lock + /// to a shared lock. If it has a shared lock, this function has no effect. + /// + /// This requests an *advisory* lock, meaning that the file could be accessed + /// by other programs that don't hold the lock. + /// + /// It is unspecified how shared locks interact with locks acquired by + /// non-WASI programs. + /// + /// This function returns `error-code::would-block` if the lock cannot be + /// acquired. + /// + /// Not all filesystems support locking; on filesystems which don't support + /// locking, this function returns `error-code::unsupported`. + /// + /// Note: This is similar to `flock(fd, LOCK_SH | LOCK_NB)` in Unix. + try-lock-shared: func(this: descriptor) -> result<_, error-code> + + /// Request an exclusive advisory lock for an open file. + /// + /// This requests an *exclusive* lock; no other locks may be held for the + /// file while an exclusive lock is held. + /// + /// If the open file has a shared lock and there are no exclusive locks held + /// for the file, this function upgrades the lock to an exclusive lock. If the + /// open file already has an exclusive lock, this function has no effect. + /// + /// This requests an *advisory* lock, meaning that the file could be accessed + /// by other programs that don't hold the lock. + /// + /// It is unspecified whether this function succeeds if the file descriptor + /// is not opened for writing. It is unspecified how exclusive locks interact + /// with locks acquired by non-WASI programs. + /// + /// This function returns `error-code::would-block` if the lock cannot be + /// acquired. + /// + /// Not all filesystems support locking; on filesystems which don't support + /// locking, this function returns `error-code::unsupported`. + /// + /// Note: This is similar to `flock(fd, LOCK_EX | LOCK_NB)` in Unix. + try-lock-exclusive: func(this: descriptor) -> result<_, error-code> + + /// Release a shared or exclusive lock on an open file. + /// + /// Note: This is similar to `flock(fd, LOCK_UN)` in Unix. + unlock: func(this: descriptor) -> result<_, error-code> + + /// Dispose of the specified `descriptor`, after which it may no longer + /// be used. + drop-descriptor: func(this: descriptor) + + /// A stream of directory entries. + /// + /// This [represents a stream of `dir-entry`](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Streams). + type directory-entry-stream = u32 + + /// Read a single directory entry from a `directory-entry-stream`. + read-directory-entry: func( + this: directory-entry-stream + ) -> result, error-code> + + /// Dispose of the specified `directory-entry-stream`, after which it may no longer + /// be used. + drop-directory-entry-stream: func(this: directory-entry-stream) + + /// Test whether two descriptors refer to the same filesystem object. + /// + /// In POSIX, this corresponds to testing whether the two descriptors have the + /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers. + /// wasi-filesystem does not expose device and inode numbers, so this function + /// may be used instead. + is-same-object: func(this: descriptor, other: descriptor) -> bool + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a descriptor. + /// + /// This returns a hash of the last-modification timestamp and file size, and + /// may also include the inode number, device number, birth timestamp, and + /// other metadata fields that may change when the file is modified or + /// replaced. It may also include a secret value chosen by the + /// implementation and not otherwise exposed. + /// + /// Implementations are encourated to provide the following properties: + /// + /// - If the file is not modified or replaced, the computed hash value should + /// usually not change. + /// - If the object is modified or replaced, the computed hash value should + /// usually change. + /// - The inputs to the hash should not be easily computable from the + /// computed hash. + /// + /// However, none of these is required. + metadata-hash: func( + this: descriptor, + ) -> result + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a directory descriptor and a relative path. + /// + /// This performs the same hash computation as `metadata-hash`. + metadata-hash-at: func( + this: descriptor, + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result +} diff --git a/crates/wasi-http/wit/deps/filesystem/world.wit b/crates/wasi-http/wit/deps/filesystem/world.wit new file mode 100644 index 0000000000..b51f484f83 --- /dev/null +++ b/crates/wasi-http/wit/deps/filesystem/world.wit @@ -0,0 +1,6 @@ +package wasi:filesystem + +world example-world { + import types + import preopens +} diff --git a/crates/wasi-http/wit/deps/http/incoming-handler.wit b/crates/wasi-http/wit/deps/http/incoming-handler.wit new file mode 100644 index 0000000000..d0e2704655 --- /dev/null +++ b/crates/wasi-http/wit/deps/http/incoming-handler.wit @@ -0,0 +1,24 @@ +// The `wasi:http/incoming-handler` interface is meant to be exported by +// components and called by the host in response to a new incoming HTTP +// response. +// +// NOTE: in Preview3, this interface will be merged with +// `wasi:http/outgoing-handler` into a single `wasi:http/handler` interface +// that takes a `request` parameter and returns a `response` result. +// +interface incoming-handler { + use types.{incoming-request, response-outparam} + + // The `handle` function takes an outparam instead of returning its response + // so that the component may stream its response while streaming any other + // request or response bodies. The callee MUST write a response to the + // `response-out` and then finish the response before returning. The `handle` + // function is allowed to continue execution after finishing the response's + // output stream. While this post-response execution is taken off the + // critical path, since there is no return value, there is no way to report + // its success or failure. + handle: func( + request: incoming-request, + response-out: response-outparam + ) +} diff --git a/crates/wasi-http/wit/deps/http/outgoing-handler.wit b/crates/wasi-http/wit/deps/http/outgoing-handler.wit new file mode 100644 index 0000000000..06c8e469f9 --- /dev/null +++ b/crates/wasi-http/wit/deps/http/outgoing-handler.wit @@ -0,0 +1,18 @@ +// The `wasi:http/outgoing-handler` interface is meant to be imported by +// components and implemented by the host. +// +// NOTE: in Preview3, this interface will be merged with +// `wasi:http/outgoing-handler` into a single `wasi:http/handler` interface +// that takes a `request` parameter and returns a `response` result. +// +interface outgoing-handler { + use types.{outgoing-request, request-options, future-incoming-response} + + // The parameter and result types of the `handle` function allow the caller + // to concurrently stream the bodies of the outgoing request and the incoming + // response. + handle: func( + request: outgoing-request, + options: option + ) -> future-incoming-response +} diff --git a/crates/wasi-http/wit/deps/http/proxy.wit b/crates/wasi-http/wit/deps/http/proxy.wit new file mode 100644 index 0000000000..162ab32b23 --- /dev/null +++ b/crates/wasi-http/wit/deps/http/proxy.wit @@ -0,0 +1,34 @@ +package wasi:http + +// The `wasi:http/proxy` world captures a widely-implementable intersection of +// hosts that includes HTTP forward and reverse proxies. Components targeting +// this world may concurrently stream in and out any number of incoming and +// outgoing HTTP requests. +world proxy { + // HTTP proxies have access to time and randomness. + import wasi:clocks/wall-clock + import wasi:clocks/monotonic-clock + import wasi:clocks/timezone + import wasi:random/random + + // Proxies have standard output and error streams which are expected to + // terminate in a developer-facing console provided by the host. + import wasi:cli/stdout + import wasi:cli/stderr + + // TODO: this is a temporary workaround until component tooling is able to + // gracefully handle the absence of stdin. Hosts must return an eof stream + // for this import, which is what wasi-libc + tooling will do automatically + // when this import is properly removed. + import wasi:cli/stdin + + // This is the default handler to use when user code simply wants to make an + // HTTP request (e.g., via `fetch()`). + import outgoing-handler + + // The host delivers incoming HTTP requests to a component by calling the + // `handle` function of this exported interface. A host may arbitrarily reuse + // or not reuse component instance when delivering incoming HTTP requests and + // thus a component must be able to handle 0..N calls to `handle`. + export incoming-handler +} diff --git a/crates/wasi-http/wit/deps/http/types.wit b/crates/wasi-http/wit/deps/http/types.wit new file mode 100644 index 0000000000..7b7b015529 --- /dev/null +++ b/crates/wasi-http/wit/deps/http/types.wit @@ -0,0 +1,155 @@ +// The `wasi:http/types` interface is meant to be imported by components to +// define the HTTP resource types and operations used by the component's +// imported and exported interfaces. +interface types { + use wasi:io/streams.{input-stream, output-stream} + use wasi:poll/poll.{pollable} + + // This type corresponds to HTTP standard Methods. + variant method { + get, + head, + post, + put, + delete, + connect, + options, + trace, + patch, + other(string) + } + + // This type corresponds to HTTP standard Related Schemes. + variant scheme { + HTTP, + HTTPS, + other(string) + } + + // TODO: perhaps better align with HTTP semantics? + // This type enumerates the different kinds of errors that may occur when + // initially returning a response. + variant error { + invalid-url(string), + timeout-error(string), + protocol-error(string), + unexpected-error(string) + } + + // This following block defines the `fields` resource which corresponds to + // HTTP standard Fields. Soon, when resource types are added, the `type + // fields = u32` type alias can be replaced by a proper `resource fields` + // definition containing all the functions using the method syntactic sugar. + type fields = u32 + drop-fields: func(fields: fields) + new-fields: func(entries: list>) -> fields + fields-get: func(fields: fields, name: string) -> list> + fields-set: func(fields: fields, name: string, value: list>) + fields-delete: func(fields: fields, name: string) + fields-append: func(fields: fields, name: string, value: list) + fields-entries: func(fields: fields) -> list>> + fields-clone: func(fields: fields) -> fields + + type headers = fields + type trailers = fields + + // The following block defines stream types which corresponds to the HTTP + // standard Contents and Trailers. With Preview3, all of these fields can be + // replaced by a stream>. In the interim, we need to + // build on separate resource types defined by `wasi:io/streams`. The + // `finish-` functions emulate the stream's result value and MUST be called + // exactly once after the final read/write from/to the stream before dropping + // the stream. + type incoming-stream = input-stream + type outgoing-stream = output-stream + finish-incoming-stream: func(s: incoming-stream) -> option + finish-outgoing-stream: func(s: outgoing-stream, trailers: option) + + // The following block defines the `incoming-request` and `outgoing-request` + // resource types that correspond to HTTP standard Requests. Soon, when + // resource types are added, the `u32` type aliases can be replaced by + // proper `resource` type definitions containing all the functions as + // methods. Later, Preview2 will allow both types to be merged together into + // a single `request` type (that uses the single `stream` type mentioned + // above). The `consume` and `write` methods may only be called once (and + // return failure thereafter). + type incoming-request = u32 + type outgoing-request = u32 + drop-incoming-request: func(request: incoming-request) + drop-outgoing-request: func(request: outgoing-request) + incoming-request-method: func(request: incoming-request) -> method + incoming-request-path-with-query: func(request: incoming-request) -> option + incoming-request-scheme: func(request: incoming-request) -> option + incoming-request-authority: func(request: incoming-request) -> option + incoming-request-headers: func(request: incoming-request) -> headers + incoming-request-consume: func(request: incoming-request) -> result + new-outgoing-request: func( + method: method, + path-with-query: option, + scheme: option, + authority: option, + headers: headers + ) -> outgoing-request + outgoing-request-write: func(request: outgoing-request) -> result + + // Additional optional parameters that can be set when making a request. + record request-options { + // The following timeouts are specific to the HTTP protocol and work + // independently of the overall timeouts passed to `io.poll.poll-oneoff`. + + // The timeout for the initial connect. + connect-timeout-ms: option, + + // The timeout for receiving the first byte of the response body. + first-byte-timeout-ms: option, + + // The timeout for receiving the next chunk of bytes in the response body + // stream. + between-bytes-timeout-ms: option + } + + // The following block defines a special resource type used by the + // `wasi:http/incoming-handler` interface. When resource types are added, this + // block can be replaced by a proper `resource response-outparam { ... }` + // definition. Later, with Preview3, the need for an outparam goes away entirely + // (the `wasi:http/handler` interface used for both incoming and outgoing can + // simply return a `stream`). + type response-outparam = u32 + drop-response-outparam: func(response: response-outparam) + set-response-outparam: func(param: response-outparam, response: result) -> result + + // This type corresponds to the HTTP standard Status Code. + type status-code = u16 + + // The following block defines the `incoming-response` and `outgoing-response` + // resource types that correspond to HTTP standard Responses. Soon, when + // resource types are added, the `u32` type aliases can be replaced by proper + // `resource` type definitions containing all the functions as methods. Later, + // Preview2 will allow both types to be merged together into a single `response` + // type (that uses the single `stream` type mentioned above). The `consume` and + // `write` methods may only be called once (and return failure thereafter). + type incoming-response = u32 + type outgoing-response = u32 + drop-incoming-response: func(response: incoming-response) + drop-outgoing-response: func(response: outgoing-response) + incoming-response-status: func(response: incoming-response) -> status-code + incoming-response-headers: func(response: incoming-response) -> headers + incoming-response-consume: func(response: incoming-response) -> result + new-outgoing-response: func( + status-code: status-code, + headers: headers + ) -> outgoing-response + outgoing-response-write: func(response: outgoing-response) -> result + + // The following block defines a special resource type used by the + // `wasi:http/outgoing-handler` interface to emulate + // `future>` in advance of Preview3. Given a + // `future-incoming-response`, the client can call the non-blocking `get` + // method to get the result if it is available. If the result is not available, + // the client can call `listen` to get a `pollable` that can be passed to + // `io.poll.poll-oneoff`. + type future-incoming-response = u32 + drop-future-incoming-response: func(f: future-incoming-response) + future-incoming-response-get: func(f: future-incoming-response) -> option> + listen-to-future-incoming-response: func(f: future-incoming-response) -> pollable +} diff --git a/crates/wasi-http/wit/deps/io/streams.wit b/crates/wasi-http/wit/deps/io/streams.wit new file mode 100644 index 0000000000..98df181c1e --- /dev/null +++ b/crates/wasi-http/wit/deps/io/streams.wit @@ -0,0 +1,253 @@ +package wasi:io + +/// WASI I/O is an I/O abstraction API which is currently focused on providing +/// stream types. +/// +/// In the future, the component model is expected to add built-in stream types; +/// when it does, they are expected to subsume this API. +interface streams { + use wasi:poll/poll.{pollable} + + /// Streams provide a sequence of data and then end; once they end, they + /// no longer provide any further data. + /// + /// For example, a stream reading from a file ends when the stream reaches + /// the end of the file. For another example, a stream reading from a + /// socket ends when the socket is closed. + enum stream-status { + /// The stream is open and may produce further data. + open, + /// When reading, this indicates that the stream will not produce + /// further data. + /// When writing, this indicates that the stream will no longer be read. + /// Further writes are still permitted. + ended, + } + + /// An input bytestream. In the future, this will be replaced by handle + /// types. + /// + /// `input-stream`s are *non-blocking* to the extent practical on underlying + /// platforms. I/O operations always return promptly; if fewer bytes are + /// promptly available than requested, they return the number of bytes promptly + /// available, which could even be zero. To wait for data to be available, + /// use the `subscribe-to-input-stream` function to obtain a `pollable` which + /// can be polled for using `wasi:poll/poll.poll_oneoff`. + /// + /// And at present, it is a `u32` instead of being an actual handle, until + /// the wit-bindgen implementation of handles and resources is ready. + /// + /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). + type input-stream = u32 + + /// Perform a non-blocking read from the stream. + /// + /// This function returns a list of bytes containing the data that was + /// read, along with a `stream-status` which, indicates whether further + /// reads are expected to produce data. The returned list will contain up to + /// `len` bytes; it may return fewer than requested, but not more. An + /// empty list and `stream-status:open` indicates no more data is + /// available at this time, and that the pollable given by + /// `subscribe-to-input-stream` will be ready when more data is available. + /// + /// Once a stream has reached the end, subsequent calls to `read` or + /// `skip` will always report `stream-status:ended` rather than producing more + /// data. + /// + /// When the caller gives a `len` of 0, it represents a request to read 0 + /// bytes. This read should always succeed and return an empty list and + /// the current `stream-status`. + /// + /// The `len` parameter is a `u64`, which could represent a list of u8 which + /// is not possible to allocate in wasm32, or not desirable to allocate as + /// as a return value by the callee. The callee may return a list of bytes + /// less than `len` in size while more bytes are available for reading. + read: func( + this: input-stream, + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-status>> + + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, identical to `read`. + blocking-read: func( + this: input-stream, + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-status>> + + /// Skip bytes from a stream. + /// + /// This is similar to the `read` function, but avoids copying the + /// bytes into the instance. + /// + /// Once a stream has reached the end, subsequent calls to read or + /// `skip` will always report end-of-stream rather than producing more + /// data. + /// + /// This function returns the number of bytes skipped, along with a + /// `stream-status` indicating whether the end of the stream was + /// reached. The returned value will be at most `len`; it may be less. + skip: func( + this: input-stream, + /// The maximum number of bytes to skip. + len: u64, + ) -> result> + + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + blocking-skip: func( + this: input-stream, + /// The maximum number of bytes to skip. + len: u64, + ) -> result> + + /// Create a `pollable` which will resolve once either the specified stream + /// has bytes available to read or the other end of the stream has been + /// closed. + /// The created `pollable` is a child resource of the `input-stream`. + /// Implementations may trap if the `input-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + subscribe-to-input-stream: func(this: input-stream) -> pollable + + /// Dispose of the specified `input-stream`, after which it may no longer + /// be used. + /// Implementations may trap if this `input-stream` is dropped while child + /// `pollable` resources are still alive. + /// After this `input-stream` is dropped, implementations may report any + /// corresponding `output-stream` has `stream-state.closed`. + drop-input-stream: func(this: input-stream) + + /// An output bytestream. In the future, this will be replaced by handle + /// types. + /// + /// `output-stream`s are *non-blocking* to the extent practical on + /// underlying platforms. Except where specified otherwise, I/O operations also + /// always return promptly, after the number of bytes that can be written + /// promptly, which could even be zero. To wait for the stream to be ready to + /// accept data, the `subscribe-to-output-stream` function to obtain a + /// `pollable` which can be polled for using `wasi:poll`. + /// + /// And at present, it is a `u32` instead of being an actual handle, until + /// the wit-bindgen implementation of handles and resources is ready. + /// + /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). + type output-stream = u32 + + /// Perform a non-blocking write of bytes to a stream. + /// + /// This function returns a `u64` and a `stream-status`. The `u64` indicates + /// the number of bytes from `buf` that were written, which may be less than + /// the length of `buf`. The `stream-status` indicates if further writes to + /// the stream are expected to be read. + /// + /// When the returned `stream-status` is `open`, the `u64` return value may + /// be less than the length of `buf`. This indicates that no more bytes may + /// be written to the stream promptly. In that case the + /// `subscribe-to-output-stream` pollable will indicate when additional bytes + /// may be promptly written. + /// + /// Writing an empty list must return a non-error result with `0` for the + /// `u64` return value, and the current `stream-status`. + write: func( + this: output-stream, + /// Data to write + buf: list + ) -> result> + + /// Blocking write of bytes to a stream. + /// + /// This is similar to `write`, except that it blocks until at least one + /// byte can be written. + blocking-write: func( + this: output-stream, + /// Data to write + buf: list + ) -> result> + + /// Write multiple zero-bytes to a stream. + /// + /// This function returns a `u64` indicating the number of zero-bytes + /// that were written; it may be less than `len`. Equivelant to a call to + /// `write` with a list of zeroes of the given length. + write-zeroes: func( + this: output-stream, + /// The number of zero-bytes to write + len: u64 + ) -> result> + + /// Write multiple zero bytes to a stream, with blocking. + /// + /// This is similar to `write-zeroes`, except that it blocks until at least + /// one byte can be written. Equivelant to a call to `blocking-write` with + /// a list of zeroes of the given length. + blocking-write-zeroes: func( + this: output-stream, + /// The number of zero bytes to write + len: u64 + ) -> result> + + /// Read from one stream and write to another. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + /// + /// Unlike other I/O functions, this function blocks until all the data + /// read from the input stream has been written to the output stream. + splice: func( + this: output-stream, + /// The stream to read from + src: input-stream, + /// The number of bytes to splice + len: u64, + ) -> result> + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until at least + /// one byte can be read. + blocking-splice: func( + this: output-stream, + /// The stream to read from + src: input-stream, + /// The number of bytes to splice + len: u64, + ) -> result> + + /// Forward the entire contents of an input stream to an output stream. + /// + /// This function repeatedly reads from the input stream and writes + /// the data to the output stream, until the end of the input stream + /// is reached, or an error is encountered. + /// + /// Unlike other I/O functions, this function blocks until the end + /// of the input stream is seen and all the data has been written to + /// the output stream. + /// + /// This function returns the number of bytes transferred, and the status of + /// the output stream. + forward: func( + this: output-stream, + /// The stream to read from + src: input-stream + ) -> result> + + /// Create a `pollable` which will resolve once either the specified stream + /// is ready to accept bytes or the `stream-state` has become closed. + /// + /// Once the stream-state is closed, this pollable is always ready + /// immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + subscribe-to-output-stream: func(this: output-stream) -> pollable + + /// Dispose of the specified `output-stream`, after which it may no longer + /// be used. + /// Implementations may trap if this `output-stream` is dropped while + /// child `pollable` resources are still alive. + /// After this `output-stream` is dropped, implementations may report any + /// corresponding `input-stream` has `stream-state.closed`. + drop-output-stream: func(this: output-stream) +} diff --git a/crates/wasi/wit/deps/logging/handler.wit b/crates/wasi-http/wit/deps/logging/logging.wit similarity index 91% rename from crates/wasi/wit/deps/logging/handler.wit rename to crates/wasi-http/wit/deps/logging/logging.wit index e6b077be8a..b0cc4514dc 100644 --- a/crates/wasi/wit/deps/logging/handler.wit +++ b/crates/wasi-http/wit/deps/logging/logging.wit @@ -2,7 +2,7 @@ package wasi:logging /// WASI Logging is a logging API intended to let users emit log messages with /// simple priority levels and context values. -interface handler { +interface logging { /// A log level, describing a kind of message. enum level { /// Describes messages about the values of variables and the flow of @@ -22,6 +22,9 @@ interface handler { /// Describes messages indicating serious errors. error, + + /// Describes messages indicating fatal errors. + critical, } /// Emit a log message. diff --git a/crates/wasi-http/wit/deps/poll/poll.wit b/crates/wasi-http/wit/deps/poll/poll.wit new file mode 100644 index 0000000000..a6334c5570 --- /dev/null +++ b/crates/wasi-http/wit/deps/poll/poll.wit @@ -0,0 +1,39 @@ +package wasi:poll + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +interface poll { + /// A "pollable" handle. + /// + /// This is conceptually represents a `stream<_, _>`, or in other words, + /// a stream that one can wait on, repeatedly, but which does not itself + /// produce any data. It's temporary scaffolding until component-model's + /// async features are ready. + /// + /// And at present, it is a `u32` instead of being an actual handle, until + /// the wit-bindgen implementation of handles and resources is ready. + /// + /// `pollable` lifetimes are not automatically managed. Users must ensure + /// that they do not outlive the resource they reference. + /// + /// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources). + type pollable = u32 + + /// Dispose of the specified `pollable`, after which it may no longer + /// be used. + drop-pollable: func(this: pollable) + + /// Poll for completion on a set of pollables. + /// + /// The "oneoff" in the name refers to the fact that this function must do a + /// linear scan through the entire list of subscriptions, which may be + /// inefficient if the number is large and the same subscriptions are used + /// many times. In the future, this is expected to be obsoleted by the + /// component model async proposal, which will include a scalable waiting + /// facility. + /// + /// The result list is the same length as the argument + /// list, and indicates the readiness of each corresponding + /// element in that / list, with true indicating ready. + poll-oneoff: func(in: list) -> list +} diff --git a/crates/wasi-http/wit/deps/random/insecure-seed.wit b/crates/wasi-http/wit/deps/random/insecure-seed.wit new file mode 100644 index 0000000000..ff2ff65d07 --- /dev/null +++ b/crates/wasi-http/wit/deps/random/insecure-seed.wit @@ -0,0 +1,24 @@ +/// The insecure-seed interface for seeding hash-map DoS resistance. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +interface insecure-seed { + /// Return a 128-bit value that may contain a pseudo-random value. + /// + /// The returned value is not required to be computed from a CSPRNG, and may + /// even be entirely deterministic. Host implementations are encouraged to + /// provide pseudo-random values to any program exposed to + /// attacker-controlled content, to enable DoS protection built into many + /// languages' hash-map implementations. + /// + /// This function is intended to only be called once, by a source language + /// to initialize Denial Of Service (DoS) protection in its hash-map + /// implementation. + /// + /// # Expected future evolution + /// + /// This will likely be changed to a value import, to prevent it from being + /// called multiple times and potentially used for purposes other than DoS + /// protection. + insecure-seed: func() -> tuple +} diff --git a/crates/wasi-http/wit/deps/random/insecure.wit b/crates/wasi-http/wit/deps/random/insecure.wit new file mode 100644 index 0000000000..ff0826822d --- /dev/null +++ b/crates/wasi-http/wit/deps/random/insecure.wit @@ -0,0 +1,21 @@ +/// The insecure interface for insecure pseudo-random numbers. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +interface insecure { + /// Return `len` insecure pseudo-random bytes. + /// + /// This function is not cryptographically secure. Do not use it for + /// anything related to security. + /// + /// There are no requirements on the values of the returned bytes, however + /// implementations are encouraged to return evenly distributed values with + /// a long period. + get-insecure-random-bytes: func(len: u64) -> list + + /// Return an insecure pseudo-random `u64` value. + /// + /// This function returns the same type of pseudo-random data as + /// `get-insecure-random-bytes`, represented as a `u64`. + get-insecure-random-u64: func() -> u64 +} diff --git a/crates/wasi-http/wit/deps/random/random.wit b/crates/wasi-http/wit/deps/random/random.wit new file mode 100644 index 0000000000..f2bd6358c1 --- /dev/null +++ b/crates/wasi-http/wit/deps/random/random.wit @@ -0,0 +1,25 @@ +package wasi:random + +/// WASI Random is a random data API. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +interface random { + /// Return `len` cryptographically-secure pseudo-random bytes. + /// + /// This function must produce data from an adequately seeded + /// cryptographically-secure pseudo-random number generator (CSPRNG), so it + /// must not block, from the perspective of the calling program, and the + /// returned data is always unpredictable. + /// + /// This function must always return fresh pseudo-random data. Deterministic + /// environments must omit this function, rather than implementing it with + /// deterministic data. + get-random-bytes: func(len: u64) -> list + + /// Return a cryptographically-secure pseudo-random `u64` value. + /// + /// This function returns the same type of pseudo-random data as + /// `get-random-bytes`, represented as a `u64`. + get-random-u64: func() -> u64 +} diff --git a/crates/wasi-http/wit/deps/sockets/instance-network.wit b/crates/wasi-http/wit/deps/sockets/instance-network.wit new file mode 100644 index 0000000000..d911a29cc8 --- /dev/null +++ b/crates/wasi-http/wit/deps/sockets/instance-network.wit @@ -0,0 +1,9 @@ + +/// This interface provides a value-export of the default network handle.. +interface instance-network { + use network.{network} + + /// Get a handle to the default network. + instance-network: func() -> network + +} diff --git a/crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit b/crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit new file mode 100644 index 0000000000..6c64b4617b --- /dev/null +++ b/crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit @@ -0,0 +1,69 @@ + +interface ip-name-lookup { + use wasi:poll/poll.{pollable} + use network.{network, error-code, ip-address, ip-address-family} + + + /// Resolve an internet host name to a list of IP addresses. + /// + /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. + /// + /// # Parameters + /// - `name`: The name to look up. IP addresses are not allowed. Unicode domain names are automatically converted + /// to ASCII using IDNA encoding. + /// - `address-family`: If provided, limit the results to addresses of this specific address family. + /// - `include-unavailable`: When set to true, this function will also return addresses of which the runtime + /// thinks (or knows) can't be connected to at the moment. For example, this will return IPv6 addresses on + /// systems without an active IPv6 interface. Notes: + /// - Even when no public IPv6 interfaces are present or active, names like "localhost" can still resolve to an IPv6 address. + /// - Whatever is "available" or "unavailable" is volatile and can change everytime a network cable is unplugged. + /// + /// This function never blocks. It either immediately fails or immediately returns successfully with a `resolve-address-stream` + /// that can be used to (asynchronously) fetch the results. + /// + /// At the moment, the stream never completes successfully with 0 items. Ie. the first call + /// to `resolve-next-address` never returns `ok(none)`. This may change in the future. + /// + /// # Typical errors + /// - `invalid-name`: `name` is a syntactically invalid domain name. + /// - `invalid-name`: `name` is an IP address. + /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAI_FAMILY) + /// + /// # References: + /// - + /// - + /// - + /// - + resolve-addresses: func(network: network, name: string, address-family: option, include-unavailable: bool) -> result + + + + type resolve-address-stream = u32 + + /// Returns the next address from the resolver. + /// + /// This function should be called multiple times. On each call, it will + /// return the next address in connection order preference. If all + /// addresses have been exhausted, this function returns `none`. + /// After which, you should release the stream with `drop-resolve-address-stream`. + /// + /// This function never returns IPv4-mapped IPv6 addresses. + /// + /// # Typical errors + /// - `name-unresolvable`: Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY) + /// - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN) + /// - `permanent-resolver-failure`: A permanent failure in name resolution occurred. (EAI_FAIL) + /// - `would-block`: A result is not available yet. (EWOULDBLOCK, EAGAIN) + resolve-next-address: func(this: resolve-address-stream) -> result, error-code> + + /// Dispose of the specified `resolve-address-stream`, after which it may no longer be used. + /// + /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. + drop-resolve-address-stream: func(this: resolve-address-stream) + + /// Create a `pollable` which will resolve once the stream is ready for I/O. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func(this: resolve-address-stream) -> pollable +} diff --git a/crates/wasi-http/wit/deps/sockets/network.wit b/crates/wasi-http/wit/deps/sockets/network.wit new file mode 100644 index 0000000000..c370214ce1 --- /dev/null +++ b/crates/wasi-http/wit/deps/sockets/network.wit @@ -0,0 +1,187 @@ +package wasi:sockets + +interface network { + /// An opaque resource that represents access to (a subset of) the network. + /// This enables context-based security for networking. + /// There is no need for this to map 1:1 to a physical network interface. + /// + /// FYI, In the future this will be replaced by handle types. + type network = u32 + + /// Dispose of the specified `network`, after which it may no longer be used. + /// + /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. + drop-network: func(this: network) + + + /// Error codes. + /// + /// In theory, every API can return any error code. + /// In practice, API's typically only return the errors documented per API + /// combined with a couple of errors that are always possible: + /// - `unknown` + /// - `access-denied` + /// - `not-supported` + /// - `out-of-memory` + /// + /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. + enum error-code { + // ### GENERAL ERRORS ### + + /// Unknown error + unknown, + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + + /// The operation is not supported. + /// + /// POSIX equivalent: EOPNOTSUPP + not-supported, + + /// Not enough memory to complete the operation. + /// + /// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY + out-of-memory, + + /// The operation timed out before it could finish completely. + timeout, + + /// This operation is incompatible with another asynchronous operation that is already in progress. + concurrency-conflict, + + /// Trying to finish an asynchronous operation that: + /// - has not been started yet, or: + /// - was already finished by a previous `finish-*` call. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + not-in-progress, + + /// The operation has been aborted because it could not be completed immediately. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + would-block, + + + // ### IP ERRORS ### + + /// The specified address-family is not supported. + address-family-not-supported, + + /// An IPv4 address was passed to an IPv6 resource, or vice versa. + address-family-mismatch, + + /// The socket address is not a valid remote address. E.g. the IP address is set to INADDR_ANY, or the port is set to 0. + invalid-remote-address, + + /// The operation is only supported on IPv4 resources. + ipv4-only-operation, + + /// The operation is only supported on IPv6 resources. + ipv6-only-operation, + + + + // ### TCP & UDP SOCKET ERRORS ### + + /// A new socket resource could not be created because of a system limit. + new-socket-limit, + + /// The socket is already attached to another network. + already-attached, + + /// The socket is already bound. + already-bound, + + /// The socket is already in the Connection state. + already-connected, + + /// The socket is not bound to any local address. + not-bound, + + /// The socket is not in the Connection state. + not-connected, + + /// A bind operation failed because the provided address is not an address that the `network` can bind to. + address-not-bindable, + + /// A bind operation failed because the provided address is already in use. + address-in-use, + + /// A bind operation failed because there are no ephemeral ports available. + ephemeral-ports-exhausted, + + /// The remote address is not reachable + remote-unreachable, + + + // ### TCP SOCKET ERRORS ### + + /// The socket is already in the Listener state. + already-listening, + + /// The socket is already in the Listener state. + not-listening, + + /// The connection was forcefully rejected + connection-refused, + + /// The connection was reset. + connection-reset, + + + // ### UDP SOCKET ERRORS ### + datagram-too-large, + + + // ### NAME LOOKUP ERRORS ### + + /// The provided name is a syntactically invalid domain name. + invalid-name, + + /// Name does not exist or has no suitable associated IP addresses. + name-unresolvable, + + /// A temporary failure in name resolution occurred. + temporary-resolver-failure, + + /// A permanent failure in name resolution occurred. + permanent-resolver-failure, + } + + enum ip-address-family { + /// Similar to `AF_INET` in POSIX. + ipv4, + + /// Similar to `AF_INET6` in POSIX. + ipv6, + } + + type ipv4-address = tuple + type ipv6-address = tuple + + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + record ipv4-socket-address { + port: u16, // sin_port + address: ipv4-address, // sin_addr + } + + record ipv6-socket-address { + port: u16, // sin6_port + flow-info: u32, // sin6_flowinfo + address: ipv6-address, // sin6_addr + scope-id: u32, // sin6_scope_id + } + + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } + +} diff --git a/crates/wasi-http/wit/deps/sockets/tcp-create-socket.wit b/crates/wasi-http/wit/deps/sockets/tcp-create-socket.wit new file mode 100644 index 0000000000..f467d28569 --- /dev/null +++ b/crates/wasi-http/wit/deps/sockets/tcp-create-socket.wit @@ -0,0 +1,27 @@ + +interface tcp-create-socket { + use network.{network, error-code, ip-address-family} + use tcp.{tcp-socket} + + /// Create a new TCP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`listen`/`connect` + /// is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The host does not support TCP sockets. (EOPNOTSUPP) + /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References + /// - + /// - + /// - + /// - + create-tcp-socket: func(address-family: ip-address-family) -> result +} diff --git a/crates/wasi-http/wit/deps/sockets/tcp.wit b/crates/wasi-http/wit/deps/sockets/tcp.wit new file mode 100644 index 0000000000..7ed46a6904 --- /dev/null +++ b/crates/wasi-http/wit/deps/sockets/tcp.wit @@ -0,0 +1,255 @@ + +interface tcp { + use wasi:io/streams.{input-stream, output-stream} + use wasi:poll/poll.{pollable} + use network.{network, error-code, ip-socket-address, ip-address-family} + + /// A TCP socket handle. + type tcp-socket = u32 + + + enum shutdown-type { + /// Similar to `SHUT_RD` in POSIX. + receive, + + /// Similar to `SHUT_WR` in POSIX. + send, + + /// Similar to `SHUT_RDWR` in POSIX. + both, + } + + + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the TCP/UDP port is zero, the socket will be bound to a random free port. + /// + /// When a socket is not explicitly bound, the first invocation to a listen or connect operation will + /// implicitly bind the socket. + /// + /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. + /// + /// # Typical `start` errors + /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) + /// - `already-bound`: The socket is already bound. (EINVAL) + /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + /// + /// # Typical `finish` errors + /// - `ephemeral-ports-exhausted`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # References + /// - + /// - + /// - + /// - + start-bind: func(this: tcp-socket, network: network, local-address: ip-socket-address) -> result<_, error-code> + finish-bind: func(this: tcp-socket) -> result<_, error-code> + + /// Connect to a remote endpoint. + /// + /// On success: + /// - the socket is transitioned into the Connection state + /// - a pair of streams is returned that can be used to read & write to the connection + /// + /// # Typical `start` errors + /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + /// - `already-attached`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. + /// - `already-connected`: The socket is already in the Connection state. (EISCONN) + /// - `already-listening`: The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) + /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + /// + /// # Typical `finish` errors + /// - `timeout`: Connection timed out. (ETIMEDOUT) + /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) + /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `not-in-progress`: A `connect` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # References + /// - + /// - + /// - + /// - + start-connect: func(this: tcp-socket, network: network, remote-address: ip-socket-address) -> result<_, error-code> + finish-connect: func(this: tcp-socket) -> result, error-code> + + /// Start listening for new connections. + /// + /// Transitions the socket into the Listener state. + /// + /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. + /// + /// # Typical `start` errors + /// - `already-attached`: The socket is already attached to a different network. The `network` passed to `listen` must be identical to the one passed to `bind`. + /// - `already-connected`: The socket is already in the Connection state. (EISCONN, EINVAL on BSD) + /// - `already-listening`: The socket is already in the Listener state. + /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EINVAL on BSD) + /// + /// # Typical `finish` errors + /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// - `not-in-progress`: A `listen` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # References + /// - + /// - + /// - + /// - + start-listen: func(this: tcp-socket, network: network) -> result<_, error-code> + finish-listen: func(this: tcp-socket) -> result<_, error-code> + + /// Accept a new client socket. + /// + /// The returned socket is bound and in the Connection state. + /// + /// On success, this function returns the newly accepted client socket along with + /// a pair of streams that can be used to read & write to the connection. + /// + /// # Typical errors + /// - `not-listening`: Socket is not in the Listener state. (EINVAL) + /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) + /// + /// Host implementations must skip over transient errors returned by the native accept syscall. + /// + /// # References + /// - + /// - + /// - + /// - + accept: func(this: tcp-socket) -> result, error-code> + + /// Get the bound local address. + /// + /// # Typical errors + /// - `not-bound`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + local-address: func(this: tcp-socket) -> result + + /// Get the bound remote address. + /// + /// # Typical errors + /// - `not-connected`: The socket is not connected to a remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + remote-address: func(this: tcp-socket) -> result + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + address-family: func(this: tcp-socket) -> ip-address-family + + /// Whether IPv4 compatibility (dual-stack) mode is disabled or not. + /// + /// Equivalent to the IPV6_V6ONLY socket option. + /// + /// # Typical errors + /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. + /// - `already-bound`: (set) The socket is already bound. + /// - `not-supported`: (set) Host does not support dual-stack sockets. (Implementations are not required to.) + /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + ipv6-only: func(this: tcp-socket) -> result + set-ipv6-only: func(this: tcp-socket, value: bool) -> result<_, error-code> + + /// Hints the desired listen queue size. Implementations are free to ignore this. + /// + /// # Typical errors + /// - `already-connected`: (set) The socket is already in the Connection state. + /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + set-listen-backlog-size: func(this: tcp-socket, value: u64) -> result<_, error-code> + + /// Equivalent to the SO_KEEPALIVE socket option. + /// + /// # Typical errors + /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + keep-alive: func(this: tcp-socket) -> result + set-keep-alive: func(this: tcp-socket, value: bool) -> result<_, error-code> + + /// Equivalent to the TCP_NODELAY socket option. + /// + /// # Typical errors + /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + no-delay: func(this: tcp-socket) -> result + set-no-delay: func(this: tcp-socket, value: bool) -> result<_, error-code> + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// # Typical errors + /// - `already-connected`: (set) The socket is already in the Connection state. + /// - `already-listening`: (set) The socket is already in the Listener state. + /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + unicast-hop-limit: func(this: tcp-socket) -> result + set-unicast-hop-limit: func(this: tcp-socket, value: u8) -> result<_, error-code> + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// Note #1: an implementation may choose to cap or round the buffer size when setting the value. + /// In other words, after setting a value, reading the same setting back may return a different value. + /// + /// Note #2: there is not necessarily a direct relationship between the kernel buffer size and the bytes of + /// actual data to be sent/received by the application, because the kernel might also use the buffer space + /// for internal metadata structures. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `already-connected`: (set) The socket is already in the Connection state. + /// - `already-listening`: (set) The socket is already in the Listener state. + /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + receive-buffer-size: func(this: tcp-socket) -> result + set-receive-buffer-size: func(this: tcp-socket, value: u64) -> result<_, error-code> + send-buffer-size: func(this: tcp-socket) -> result + set-send-buffer-size: func(this: tcp-socket, value: u64) -> result<_, error-code> + + /// Create a `pollable` which will resolve once the socket is ready for I/O. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func(this: tcp-socket) -> pollable + + /// Initiate a graceful shutdown. + /// + /// - receive: the socket is not expecting to receive any more data from the peer. All subsequent read + /// operations on the `input-stream` associated with this socket will return an End Of Stream indication. + /// Any data still in the receive queue at time of calling `shutdown` will be discarded. + /// - send: the socket is not expecting to send any more data to the peer. All subsequent write + /// operations on the `output-stream` associated with this socket will return an error. + /// - both: same effect as receive & send combined. + /// + /// The shutdown function does not close (drop) the socket. + /// + /// # Typical errors + /// - `not-connected`: The socket is not in the Connection state. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + shutdown: func(this: tcp-socket, shutdown-type: shutdown-type) -> result<_, error-code> + + /// Dispose of the specified `tcp-socket`, after which it may no longer be used. + /// + /// Similar to the POSIX `close` function. + /// + /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. + drop-tcp-socket: func(this: tcp-socket) +} diff --git a/crates/wasi-http/wit/deps/sockets/udp-create-socket.wit b/crates/wasi-http/wit/deps/sockets/udp-create-socket.wit new file mode 100644 index 0000000000..1cfbd7f0bd --- /dev/null +++ b/crates/wasi-http/wit/deps/sockets/udp-create-socket.wit @@ -0,0 +1,27 @@ + +interface udp-create-socket { + use network.{network, error-code, ip-address-family} + use udp.{udp-socket} + + /// Create a new UDP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`connect` is called, + /// the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The host does not support UDP sockets. (EOPNOTSUPP) + /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References: + /// - + /// - + /// - + /// - + create-udp-socket: func(address-family: ip-address-family) -> result +} diff --git a/crates/wasi-http/wit/deps/sockets/udp.wit b/crates/wasi-http/wit/deps/sockets/udp.wit new file mode 100644 index 0000000000..9dd4573bd1 --- /dev/null +++ b/crates/wasi-http/wit/deps/sockets/udp.wit @@ -0,0 +1,211 @@ + +interface udp { + use wasi:poll/poll.{pollable} + use network.{network, error-code, ip-socket-address, ip-address-family} + + + /// A UDP socket handle. + type udp-socket = u32 + + + record datagram { + data: list, // Theoretical max size: ~64 KiB. In practice, typically less than 1500 bytes. + remote-address: ip-socket-address, + + /// Possible future additions: + /// local-address: ip-socket-address, // IP_PKTINFO / IP_RECVDSTADDR / IPV6_PKTINFO + /// local-interface: u32, // IP_PKTINFO / IP_RECVIF + /// ttl: u8, // IP_RECVTTL + /// dscp: u6, // IP_RECVTOS + /// ecn: u2, // IP_RECVTOS + } + + + + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the TCP/UDP port is zero, the socket will be bound to a random free port. + /// + /// When a socket is not explicitly bound, the first invocation to connect will implicitly bind the socket. + /// + /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. + /// + /// # Typical `start` errors + /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) + /// - `already-bound`: The socket is already bound. (EINVAL) + /// - `concurrency-conflict`: Another `bind` or `connect` operation is already in progress. (EALREADY) + /// + /// # Typical `finish` errors + /// - `ephemeral-ports-exhausted`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # References + /// - + /// - + /// - + /// - + start-bind: func(this: udp-socket, network: network, local-address: ip-socket-address) -> result<_, error-code> + finish-bind: func(this: udp-socket) -> result<_, error-code> + + /// Set the destination address. + /// + /// The local-address is updated based on the best network path to `remote-address`. + /// + /// When a destination address is set: + /// - all receive operations will only return datagrams sent from the provided `remote-address`. + /// - the `send` function can only be used to send to this destination. + /// + /// Note that this function does not generate any network traffic and the peer is not aware of this "connection". + /// + /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. + /// + /// # Typical `start` errors + /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `already-attached`: The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. + /// - `concurrency-conflict`: Another `bind` or `connect` operation is already in progress. (EALREADY) + /// + /// # Typical `finish` errors + /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `not-in-progress`: A `connect` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # References + /// - + /// - + /// - + /// - + start-connect: func(this: udp-socket, network: network, remote-address: ip-socket-address) -> result<_, error-code> + finish-connect: func(this: udp-socket) -> result<_, error-code> + + /// Receive a message. + /// + /// Returns: + /// - The sender address of the datagram + /// - The number of bytes read. + /// + /// # Typical errors + /// - `not-bound`: The socket is not bound to any local address. (EINVAL) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) + /// - `would-block`: There is no pending data available to be read at the moment. (EWOULDBLOCK, EAGAIN) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + receive: func(this: udp-socket) -> result + + /// Send a message to a specific destination address. + /// + /// The remote address option is required. To send a message to the "connected" peer, + /// call `remote-address` to get their address. + /// + /// # Typical errors + /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `already-connected`: The socket is in "connected" mode and the `datagram.remote-address` does not match the address passed to `connect`. (EISCONN) + /// - `not-bound`: The socket is not bound to any local address. Unlike POSIX, this function does not perform an implicit bind. + /// - `remote-unreachable`: The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) + /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) + /// - `would-block`: The send buffer is currently full. (EWOULDBLOCK, EAGAIN) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + send: func(this: udp-socket, datagram: datagram) -> result<_, error-code> + + /// Get the current bound address. + /// + /// # Typical errors + /// - `not-bound`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + local-address: func(this: udp-socket) -> result + + /// Get the address set with `connect`. + /// + /// # Typical errors + /// - `not-connected`: The socket is not connected to a remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + remote-address: func(this: udp-socket) -> result + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + address-family: func(this: udp-socket) -> ip-address-family + + /// Whether IPv4 compatibility (dual-stack) mode is disabled or not. + /// + /// Equivalent to the IPV6_V6ONLY socket option. + /// + /// # Typical errors + /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. + /// - `already-bound`: (set) The socket is already bound. + /// - `not-supported`: (set) Host does not support dual-stack sockets. (Implementations are not required to.) + /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) + ipv6-only: func(this: udp-socket) -> result + set-ipv6-only: func(this: udp-socket, value: bool) -> result<_, error-code> + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// # Typical errors + /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) + unicast-hop-limit: func(this: udp-socket) -> result + set-unicast-hop-limit: func(this: udp-socket, value: u8) -> result<_, error-code> + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// Note #1: an implementation may choose to cap or round the buffer size when setting the value. + /// In other words, after setting a value, reading the same setting back may return a different value. + /// + /// Note #2: there is not necessarily a direct relationship between the kernel buffer size and the bytes of + /// actual data to be sent/received by the application, because the kernel might also use the buffer space + /// for internal metadata structures. + /// + /// Fails when this socket is in the Listening state. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) + receive-buffer-size: func(this: udp-socket) -> result + set-receive-buffer-size: func(this: udp-socket, value: u64) -> result<_, error-code> + send-buffer-size: func(this: udp-socket) -> result + set-send-buffer-size: func(this: udp-socket, value: u64) -> result<_, error-code> + + /// Create a `pollable` which will resolve once the socket is ready for I/O. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func(this: udp-socket) -> pollable + + /// Dispose of the specified `udp-socket`, after which it may no longer be used. + /// + /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. + drop-udp-socket: func(this: udp-socket) +} diff --git a/crates/wasi-http/wit/main.wit b/crates/wasi-http/wit/main.wit new file mode 100644 index 0000000000..753770ad22 --- /dev/null +++ b/crates/wasi-http/wit/main.wit @@ -0,0 +1,33 @@ +package wasmtime:wasi + +// All of the same imports available in the wasi:cli/command world, but no +// export required: +world preview1-adapter-reactor { + import wasi:clocks/wall-clock + import wasi:clocks/monotonic-clock + import wasi:clocks/timezone + import wasi:filesystem/types + import wasi:filesystem/preopens + import wasi:sockets/instance-network + import wasi:sockets/ip-name-lookup + import wasi:sockets/network + import wasi:sockets/tcp-create-socket + import wasi:sockets/tcp + import wasi:sockets/udp-create-socket + import wasi:sockets/udp + import wasi:random/random + import wasi:random/insecure + import wasi:random/insecure-seed + import wasi:poll/poll + import wasi:io/streams + import wasi:cli/environment + import wasi:cli/exit + import wasi:cli/stdin + import wasi:cli/stdout + import wasi:cli/stderr + import wasi:cli/terminal-input + import wasi:cli/terminal-output + import wasi:cli/terminal-stdin + import wasi:cli/terminal-stdout + import wasi:cli/terminal-stderr +} diff --git a/crates/wasi-http/wit/test.wit b/crates/wasi-http/wit/test.wit new file mode 100644 index 0000000000..447304cba3 --- /dev/null +++ b/crates/wasi-http/wit/test.wit @@ -0,0 +1,28 @@ +// only used as part of `test-programs` +world test-reactor { + + import wasi:cli/environment + import wasi:io/streams + import wasi:filesystem/types + import wasi:filesystem/preopens + import wasi:cli/exit + + export add-strings: func(s: list) -> u32 + export get-strings: func() -> list + + use wasi:io/streams.{output-stream} + + export write-strings-to: func(o: output-stream) -> result + + use wasi:filesystem/types.{descriptor-stat} + export pass-an-imported-record: func(d: descriptor-stat) -> string +} + +world test-command { + import wasi:poll/poll + import wasi:io/streams + import wasi:cli/environment + import wasi:cli/stdin + import wasi:cli/stdout + import wasi:cli/stderr +} diff --git a/crates/wasi/wit/command-extended.wit b/crates/wasi/wit/command-extended.wit new file mode 100644 index 0000000000..518fabdcac --- /dev/null +++ b/crates/wasi/wit/command-extended.wit @@ -0,0 +1,39 @@ +// All of the same imports and exports available in the wasi:cli/command world +// with addition of HTTP proxy related imports: +world command-extended { + import wasi:clocks/wall-clock + import wasi:clocks/monotonic-clock + import wasi:clocks/timezone + import wasi:filesystem/types + import wasi:filesystem/preopens + import wasi:sockets/instance-network + import wasi:sockets/ip-name-lookup + import wasi:sockets/network + import wasi:sockets/tcp-create-socket + import wasi:sockets/tcp + import wasi:sockets/udp-create-socket + import wasi:sockets/udp + import wasi:random/random + import wasi:random/insecure + import wasi:random/insecure-seed + import wasi:poll/poll + import wasi:io/streams + import wasi:cli/environment + import wasi:cli/exit + import wasi:cli/stdin + import wasi:cli/stdout + import wasi:cli/stderr + import wasi:cli/terminal-input + import wasi:cli/terminal-output + import wasi:cli/terminal-stdin + import wasi:cli/terminal-stdout + import wasi:cli/terminal-stderr + + export wasi:cli/run + + // We should replace all others with `include self.command` + // as soon as the unioning of worlds is available: + // https://github.com/WebAssembly/component-model/issues/169 + import wasi:logging/logging + import wasi:http/outgoing-handler +} diff --git a/crates/wasi/wit/deps/cli/command.wit b/crates/wasi/wit/deps/cli/command.wit index c29b4a61f8..3cd17bea3f 100644 --- a/crates/wasi/wit/deps/cli/command.wit +++ b/crates/wasi/wit/deps/cli/command.wit @@ -18,6 +18,7 @@ world command { import wasi:random/insecure-seed import wasi:poll/poll import wasi:io/streams + import environment import exit import stdin diff --git a/crates/wasi/wit/deps/http/proxy.wit b/crates/wasi/wit/deps/http/proxy.wit new file mode 100644 index 0000000000..162ab32b23 --- /dev/null +++ b/crates/wasi/wit/deps/http/proxy.wit @@ -0,0 +1,34 @@ +package wasi:http + +// The `wasi:http/proxy` world captures a widely-implementable intersection of +// hosts that includes HTTP forward and reverse proxies. Components targeting +// this world may concurrently stream in and out any number of incoming and +// outgoing HTTP requests. +world proxy { + // HTTP proxies have access to time and randomness. + import wasi:clocks/wall-clock + import wasi:clocks/monotonic-clock + import wasi:clocks/timezone + import wasi:random/random + + // Proxies have standard output and error streams which are expected to + // terminate in a developer-facing console provided by the host. + import wasi:cli/stdout + import wasi:cli/stderr + + // TODO: this is a temporary workaround until component tooling is able to + // gracefully handle the absence of stdin. Hosts must return an eof stream + // for this import, which is what wasi-libc + tooling will do automatically + // when this import is properly removed. + import wasi:cli/stdin + + // This is the default handler to use when user code simply wants to make an + // HTTP request (e.g., via `fetch()`). + import outgoing-handler + + // The host delivers incoming HTTP requests to a component by calling the + // `handle` function of this exported interface. A host may arbitrarily reuse + // or not reuse component instance when delivering incoming HTTP requests and + // thus a component must be able to handle 0..N calls to `handle`. + export incoming-handler +} diff --git a/crates/wasi/wit/deps/http/types.wit b/crates/wasi/wit/deps/http/types.wit index ee4227f423..7b7b015529 100644 --- a/crates/wasi/wit/deps/http/types.wit +++ b/crates/wasi/wit/deps/http/types.wit @@ -1,5 +1,3 @@ -package wasi:http - // The `wasi:http/types` interface is meant to be imported by components to // define the HTTP resource types and operations used by the component's // imported and exported interfaces. @@ -45,11 +43,11 @@ interface types { type fields = u32 drop-fields: func(fields: fields) new-fields: func(entries: list>) -> fields - fields-get: func(fields: fields, name: string) -> list - fields-set: func(fields: fields, name: string, value: list) + fields-get: func(fields: fields, name: string) -> list> + fields-set: func(fields: fields, name: string, value: list>) fields-delete: func(fields: fields, name: string) - fields-append: func(fields: fields, name: string, value: string) - fields-entries: func(fields: fields) -> list> + fields-append: func(fields: fields, name: string, value: list) + fields-entries: func(fields: fields) -> list>> fields-clone: func(fields: fields) -> fields type headers = fields @@ -80,18 +78,16 @@ interface types { drop-incoming-request: func(request: incoming-request) drop-outgoing-request: func(request: outgoing-request) incoming-request-method: func(request: incoming-request) -> method - incoming-request-path: func(request: incoming-request) -> string - incoming-request-query: func(request: incoming-request) -> string + incoming-request-path-with-query: func(request: incoming-request) -> option incoming-request-scheme: func(request: incoming-request) -> option - incoming-request-authority: func(request: incoming-request) -> string + incoming-request-authority: func(request: incoming-request) -> option incoming-request-headers: func(request: incoming-request) -> headers incoming-request-consume: func(request: incoming-request) -> result new-outgoing-request: func( method: method, - path: string, - query: string, + path-with-query: option, scheme: option, - authority: string, + authority: option, headers: headers ) -> outgoing-request outgoing-request-write: func(request: outgoing-request) -> result @@ -120,7 +116,7 @@ interface types { // simply return a `stream`). type response-outparam = u32 drop-response-outparam: func(response: response-outparam) - set-response-outparam: func(response: result) -> result + set-response-outparam: func(param: response-outparam, response: result) -> result // This type corresponds to the HTTP standard Status Code. type status-code = u16 diff --git a/crates/wasi/wit/deps/logging/logging.wit b/crates/wasi/wit/deps/logging/logging.wit new file mode 100644 index 0000000000..b0cc4514dc --- /dev/null +++ b/crates/wasi/wit/deps/logging/logging.wit @@ -0,0 +1,37 @@ +package wasi:logging + +/// WASI Logging is a logging API intended to let users emit log messages with +/// simple priority levels and context values. +interface logging { + /// A log level, describing a kind of message. + enum level { + /// Describes messages about the values of variables and the flow of + /// control within a program. + trace, + + /// Describes messages likely to be of interest to someone debugging a + /// program. + debug, + + /// Describes messages likely to be of interest to someone monitoring a + /// program. + info, + + /// Describes messages indicating hazardous situations. + warn, + + /// Describes messages indicating serious errors. + error, + + /// Describes messages indicating fatal errors. + critical, + } + + /// Emit a log message. + /// + /// A log message has a `level` describing what kind of message is being + /// sent, a context, which is an uninterpreted string meant to help + /// consumers group similar messages, and a string containing the message + /// text. + log: func(level: level, context: string, message: string) +} diff --git a/src/commands/run.rs b/src/commands/run.rs index 2c9485d048..8b88f6b5af 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -23,8 +23,8 @@ use wasmtime_wasi_nn::WasiNnCtx; #[cfg(feature = "wasi-threads")] use wasmtime_wasi_threads::WasiThreadsCtx; -#[cfg(feature = "wasi-http")] -use wasmtime_wasi_http::WasiHttp; +// #[cfg(feature = "wasi-http")] +// use wasmtime_wasi_http::WasiHttpCtx; fn parse_env_var(s: &str) -> Result<(String, Option)> { let mut parts = s.splitn(2, '='); @@ -672,8 +672,8 @@ struct Host { wasi_nn: Option>, #[cfg(feature = "wasi-threads")] wasi_threads: Option>>, - #[cfg(feature = "wasi-http")] - wasi_http: Option, + // #[cfg(feature = "wasi-http")] + // wasi_http: Option>, limits: StoreLimits, guest_profiler: Option>, } @@ -773,11 +773,7 @@ fn populate_with_wasi( } #[cfg(feature = "wasi-http")] { - let w_http = WasiHttp::new(); - wasmtime_wasi_http::add_to_linker(linker, |host: &mut Host| { - host.wasi_http.as_mut().unwrap() - })?; - store.data_mut().wasi_http = Some(w_http); + bail!("wasi-http support will be swapped over to component CLI support soon"); } }