Browse Source

feat: align wasi-http with component linker (#6195)

* feat: align wasi-http with component linker

* feat(wasi-http): allow bidirectional stream

* feat(wasi-http): clean up children when dropping resource

* chore: update based on feedback

* chore: replace wasi http context references

* chore: fix logical issue with outgoing body stream

* chore: use thread-safe reference-counting pointer

* chore: cleanup resources using table

* fix(wasi-preview1-component-adapter): sync command extended wit

* fix(wasi-preview1-component-adapter): sync command extended wit

* fix(wasmtime-wasi): sync wit for http types

* chore: refactor using wasmtime-wasi crate

fix(wasi-http): misconfiguration in wasmtime linkers

chore: keep streams details

chore: fix wasi http tests

* chore: use pollable from wasmtime-wasi

* chore: update wasi http linker for module

* chore: update test programs for wasi http

* fix(wasi-http): ensure proper errors are surfaced

* chore: split wasi http tests into individual files

* chore: ensure protocol error is mapped correctly

* chore: disable temporarily wasi http in wasmtime cli

* chore: comment out wasi http in wasmtime cli

* chore(ci): ensure wit definitions in sync

* feat(wasi-http): generate async host binding

* chore: make wasi http tests async

* chore: update ci workflow based on suggestion

Co-authored-by: Pat Hickey <pat@moreproductive.org>

* feat(wasmtime-wasi): update logging world to latest

* feat(wasmtime): update proxy world to latest

* feat(wasmtime-wasi): add back command extended world

* fix(wasi-http): sync wit definitions

* chore: update tests with latest wit definitions

* Update src/commands/run.rs

* Update src/commands/run.rs

* Update src/commands/run.rs

* Update src/commands/run.rs

* Update src/commands/run.rs

* Update src/commands/run.rs

* Update src/commands/run.rs

* Update src/commands/run.rs

* Update src/commands/run.rs

* chore: fix formatting

* Ignore flaky test

* chore: fix compilation error for riscv64 arch

* Avoid `cp -T` on macos

Adding prtest:full to ensure that we've seen a successful build before
queuing.

* Don't build the wasi-http test programs for the native target

* Debug the wit consistency check

* Update streams.wit in wasi-http

* Mark the component outbound_request_post test flaky

* Disable flaky wasi-http-tests on windows only

* Use diff instead of rm/cp/git diff

* Disable more tests on windows

---------

Co-authored-by: Eduardo Rodrigues <eduardomourar@users.noreply.github.com>
Co-authored-by: Pat Hickey <pat@moreproductive.org>
Co-authored-by: Trevor Elliott <telliott@fastly.com>
pull/6858/head
Eduardo de Moura Rodrigues 1 year ago
committed by GitHub
parent
commit
e250334b8e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .github/workflows/main.yml
  2. 20
      Cargo.lock
  3. 1
      ci/run-tests.sh
  4. 18
      crates/test-programs/Cargo.toml
  5. 7
      crates/test-programs/build.rs
  6. 96
      crates/test-programs/src/http_server.rs
  7. 3
      crates/test-programs/src/lib.rs
  8. 140
      crates/test-programs/tests/wasi-http-components.rs
  9. 153
      crates/test-programs/tests/wasi-http-modules.rs
  10. 97
      crates/test-programs/tests/wasi-http.rs
  11. 5
      crates/test-programs/wasi-http-tests/Cargo.toml
  12. 188
      crates/test-programs/wasi-http-tests/src/bin/outbound_request.rs
  13. 41
      crates/test-programs/wasi-http-tests/src/bin/outbound_request_get.rs
  14. 31
      crates/test-programs/wasi-http-tests/src/bin/outbound_request_invalid_dnsname.rs
  15. 34
      crates/test-programs/wasi-http-tests/src/bin/outbound_request_invalid_port.rs
  16. 40
      crates/test-programs/wasi-http-tests/src/bin/outbound_request_invalid_version.rs
  17. 36
      crates/test-programs/wasi-http-tests/src/bin/outbound_request_post.rs
  18. 36
      crates/test-programs/wasi-http-tests/src/bin/outbound_request_put.rs
  19. 34
      crates/test-programs/wasi-http-tests/src/bin/outbound_request_unknown_method.rs
  20. 34
      crates/test-programs/wasi-http-tests/src/bin/outbound_request_unsupported_scheme.rs
  21. 178
      crates/test-programs/wasi-http-tests/src/lib.rs
  22. 11
      crates/wasi-http/Cargo.toml
  23. 714
      crates/wasi-http/src/component_impl.rs
  24. 207
      crates/wasi-http/src/http_impl.rs
  25. 135
      crates/wasi-http/src/lib.rs
  26. 167
      crates/wasi-http/src/streams_impl.rs
  27. 441
      crates/wasi-http/src/struct.rs
  28. 379
      crates/wasi-http/src/types_impl.rs
  29. 1
      crates/wasi-http/wasi-http
  30. 39
      crates/wasi-http/wit/command-extended.wit
  31. 33
      crates/wasi-http/wit/deps/cli/command.wit
  32. 18
      crates/wasi-http/wit/deps/cli/environment.wit
  33. 4
      crates/wasi-http/wit/deps/cli/exit.wit
  34. 4
      crates/wasi-http/wit/deps/cli/run.wit
  35. 17
      crates/wasi-http/wit/deps/cli/stdio.wit
  36. 59
      crates/wasi-http/wit/deps/cli/terminal.wit
  37. 34
      crates/wasi-http/wit/deps/clocks/monotonic-clock.wit
  38. 63
      crates/wasi-http/wit/deps/clocks/timezone.wit
  39. 43
      crates/wasi-http/wit/deps/clocks/wall-clock.wit
  40. 6
      crates/wasi-http/wit/deps/filesystem/preopens.wit
  41. 824
      crates/wasi-http/wit/deps/filesystem/types.wit
  42. 6
      crates/wasi-http/wit/deps/filesystem/world.wit
  43. 24
      crates/wasi-http/wit/deps/http/incoming-handler.wit
  44. 18
      crates/wasi-http/wit/deps/http/outgoing-handler.wit
  45. 34
      crates/wasi-http/wit/deps/http/proxy.wit
  46. 155
      crates/wasi-http/wit/deps/http/types.wit
  47. 253
      crates/wasi-http/wit/deps/io/streams.wit
  48. 5
      crates/wasi-http/wit/deps/logging/logging.wit
  49. 39
      crates/wasi-http/wit/deps/poll/poll.wit
  50. 24
      crates/wasi-http/wit/deps/random/insecure-seed.wit
  51. 21
      crates/wasi-http/wit/deps/random/insecure.wit
  52. 25
      crates/wasi-http/wit/deps/random/random.wit
  53. 9
      crates/wasi-http/wit/deps/sockets/instance-network.wit
  54. 69
      crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit
  55. 187
      crates/wasi-http/wit/deps/sockets/network.wit
  56. 27
      crates/wasi-http/wit/deps/sockets/tcp-create-socket.wit
  57. 255
      crates/wasi-http/wit/deps/sockets/tcp.wit
  58. 27
      crates/wasi-http/wit/deps/sockets/udp-create-socket.wit
  59. 211
      crates/wasi-http/wit/deps/sockets/udp.wit
  60. 33
      crates/wasi-http/wit/main.wit
  61. 28
      crates/wasi-http/wit/test.wit
  62. 39
      crates/wasi/wit/command-extended.wit
  63. 1
      crates/wasi/wit/deps/cli/command.wit
  64. 34
      crates/wasi/wit/deps/http/proxy.wit
  65. 22
      crates/wasi/wit/deps/http/types.wit
  66. 37
      crates/wasi/wit/deps/logging/logging.wit
  67. 14
      src/commands/run.rs

6
.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:

20
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",
]

1
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 \
$@

18
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" ]

7
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)

96
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<hyper::body::Incoming>,
) -> http::Result<Response<BoxBody<Bytes, std::convert::Infallible>>> {
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::<Bytes>::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<F> hyper::rt::Executor<F> 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<Output = anyhow::Result<()>>,
) -> Result<(), anyhow::Error> {
static CELL_HTTP1: OnceLock<TcpListener> = 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<Output = anyhow::Result<()>>,
) -> anyhow::Result<()> {
static CELL_HTTP2: OnceLock<TcpListener> = 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(())
}

3
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.

140
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<Ctx>, 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();
}

153
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<Ctx>, 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();
}

97
crates/test-programs/tests/wasi-http.rs

@ -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<hyper::body::Incoming>,
) -> http::Result<Response<BoxBody<Bytes, hyper::Error>>> {
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<dyn Error + Send + Sync>> {
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<dyn Error + Send + Sync>> {
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()
}

5
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",
] }

188
crates/test-programs/wasi-http-tests/src/bin/outbound_request.rs

@ -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<u8>)>,
body: Vec<u8>,
}
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<u8>> {
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<Response> {
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(())
}

41
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);

31
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);

34
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);

40
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);

36
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);

36
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);

34
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);

34
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);

178
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<u8>)>,
pub body: Vec<u8>,
}
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<u8>> {
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<Response> {
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<tokio::runtime::Runtime> = OnceLock::new();
pub fn in_tokio<F: std::future::Future>(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)
}
}
}

11
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

714
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<T>(caller: &mut Caller<'_, T>, size: u32) -> anyhow::Result<u32> {
async fn allocate_guest_pointer<T: Send>(
caller: &mut Caller<'_, T>,
size: u32,
) -> anyhow::Result<u32> {
let realloc = caller
.get_export("cabi_realloc")
.ok_or_else(|| anyhow!("missing required export cabi_realloc"))?;
@ -89,7 +91,9 @@ fn allocate_guest_pointer<T>(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<u8> {
@ -105,9 +109,17 @@ fn u32_array_to_u8(arr: &[u32]) -> Vec<u8> {
pub fn add_component_to_linker<T>(
linker: &mut wasmtime::Linker<T>,
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<T>(
has_first_byte_timeout: i32,
first_byte_timeout_ms: u32,
has_between_bytes_timeout: i32,
between_bytes_timeout_ms: u32|
-> anyhow::Result<u32> {
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<T>(
authority_is_some: i32,
authority_ptr: u32,
authority_len: u32,
headers: u32|
-> anyhow::Result<u32> {
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<u32> {
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<u32> {
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<u32> {
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(())

207
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<T: WasiHttpView> crate::wasi::http::outgoing_handler::Host for T {
async fn handle(
&mut self,
request_id: OutgoingRequest,
options: Option<RequestOptions>,
) -> wasmtime::Result<FutureIncomingResponse> {
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<Scheme>) -> &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<RequestOptions>,
) -> wasmtime::Result<FutureIncomingResponse> {
) -> wasmtime::Result<FutureIncomingResponse, crate::wasi::http::types::Error>;
}
#[async_trait::async_trait]
impl<T: WasiHttpView> WasiHttpViewExt for T {
async fn handle_async(
&mut self,
request_id: OutgoingRequest,
options: Option<RequestOptions>,
) -> wasmtime::Result<FutureIncomingResponse, crate::wasi::http::types::Error> {
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::<Bytes>::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::<Bytes>::new(bytes.freeze()).boxed()
}
None => Empty::<Bytes>::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<u8> = 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<String, Vec<Vec<u8>>> = 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)
}
}

135
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<T>(
linker: &mut wasmtime::component::Linker<T>,
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<T>(linker: &mut wasmtime::component::Linker<T>) -> 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<T>(
linker: &mut wasmtime::Linker<T>,
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<T>(linker: &mut wasmtime::Linker<T>) -> 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::<T>(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<wasmtime_wasi::preview2::TableError> for crate::wasi::http::types::Error {
fn from(err: wasmtime_wasi::preview2::TableError) -> Self {
Self::UnexpectedError(err.to_string())
}
}
impl From<anyhow::Error> for crate::wasi::http::types::Error {
fn from(err: anyhow::Error) -> Self {
Self::UnexpectedError(err.to_string())
}
}
impl From<std::io::Error> 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<http::Error> for crate::wasi::http::types::Error {
fn from(err: http::Error) -> Self {
Self::InvalidUrl(err.to_string())
}
}
impl From<hyper::Error> 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<tokio::time::error::Elapsed> 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<rustls::client::InvalidDnsNameError> for crate::wasi::http::types::Error {
fn from(_err: rustls::client::InvalidDnsNameError) -> Self {
Self::InvalidUrl("invalid dnsname".to_string())
}
}

167
crates/wasi-http/src/streams_impl.rs

@ -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<Result<(Vec<u8>, 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<Result<(u64, 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((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<Pollable> {
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<u8>,
) -> wasmtime::Result<Result<u64, StreamError>> {
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<Result<u64, StreamError>> {
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<Result<(u64, bool), StreamError>> {
bail!("unimplemented: splice");
}
fn forward(
&mut self,
_this: OutputStream,
_src: InputStream,
) -> wasmtime::Result<Result<u64, StreamError>> {
bail!("unimplemented: forward");
}
fn subscribe_to_output_stream(&mut self, _this: OutputStream) -> wasmtime::Result<Pollable> {
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<Result<(Vec<u8>, bool), StreamError>> {
bail!("unimplemented")
}
fn blocking_skip(
&mut self,
_: InputStream,
_: u64,
) -> wasmtime::Result<Result<(u64, bool), StreamError>> {
bail!("unimplemented")
}
fn blocking_write(
&mut self,
_: OutputStream,
_: Vec<u8>,
) -> wasmtime::Result<Result<u64, StreamError>> {
bail!("unimplemented")
}
fn blocking_write_zeroes(
&mut self,
_: OutputStream,
_: u64,
) -> wasmtime::Result<Result<u64, StreamError>> {
bail!("unimplemented")
}
fn blocking_splice(
&mut self,
_: OutputStream,
_: InputStream,
_: u64,
) -> wasmtime::Result<Result<(u64, bool), StreamError>> {
bail!("unimplemented")
}
}

441
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<u32, ActiveRequest>,
pub responses: HashMap<u32, ActiveResponse>,
pub fields: HashMap<u32, HashMap<String, Vec<Vec<u8>>>>,
/// Capture the state necessary for use in the wasi-http API implementation.
pub struct WasiHttpCtx {
pub streams: HashMap<u32, Stream>,
pub futures: HashMap<u32, ActiveFuture>,
}
#[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<String, Vec<Vec<u8>>>;
#[derive(Clone, Debug)]
pub struct ActiveRequest {
pub id: u32,
pub active_request: bool,
pub active: bool,
pub method: Method,
pub scheme: Option<Scheme>,
pub path_with_query: String,
pub authority: String,
pub headers: HashMap<String, Vec<Vec<u8>>>,
pub body: u32,
pub headers: Option<u32>,
pub body: Option<u32>,
}
#[derive(Clone)]
pub struct ActiveResponse {
pub id: u32,
pub active_response: bool,
pub status: u16,
pub body: u32,
pub response_headers: HashMap<String, Vec<Vec<u8>>>,
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<RequestOptions>,
fn as_any(&self) -> &dyn Any;
fn method(&self) -> &Method;
fn scheme(&self) -> &Option<Scheme>;
fn path_with_query(&self) -> &str;
fn authority(&self) -> &str;
fn headers(&self) -> Option<u32>;
fn set_headers(&mut self, headers: u32);
fn body(&self) -> Option<u32>;
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<Scheme> {
&self.scheme
}
fn path_with_query(&self) -> &str {
&self.path_with_query
}
fn authority(&self) -> &str {
&self.authority
}
fn headers(&self) -> Option<u32> {
self.headers
}
fn set_headers(&mut self, headers: u32) {
self.headers = Some(headers);
}
fn body(&self) -> Option<u32> {
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<u32>,
pub body: Option<u32>,
pub trailers: Option<u32>,
}
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<u32>;
fn set_headers(&mut self, headers: u32);
fn body(&self) -> Option<u32>;
fn set_body(&mut self, body: u32);
fn trailers(&self) -> Option<u32>;
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<u32> {
self.headers
}
fn set_headers(&mut self, headers: u32) {
self.headers = Some(headers);
}
fn body(&self) -> Option<u32> {
self.body
}
fn set_body(&mut self, body: u32) {
self.body = Some(body);
}
fn trailers(&self) -> Option<u32> {
self.trailers
}
fn set_trailers(&mut self, trailers: u32) {
self.trailers = Some(trailers);
}
}
#[derive(Clone)]
pub struct ActiveFuture {
request_id: OutgoingRequest,
options: Option<RequestOptions>,
response_id: Option<u32>,
pollable_id: Option<u32>,
}
impl ActiveFuture {
pub fn new(id: u32, request_id: u32, options: Option<RequestOptions>) -> Self {
pub fn new(request_id: OutgoingRequest, options: Option<RequestOptions>) -> 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<RequestOptions> {
self.options
}
pub fn response_id(&self) -> Option<u32> {
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<u32> {
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<String, Vec<Vec<u8>>>);
impl ActiveFields {
pub fn new() -> Self {
Self::default()
Self(FieldsMap::new())
}
}
impl From<Bytes> 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<dyn HttpRequest>) -> Result<u32, TableError>;
fn get_request(&self, id: u32) -> Result<&(dyn HttpRequest), TableError>;
fn get_request_mut(&mut self, id: u32) -> Result<&mut Box<dyn HttpRequest>, TableError>;
fn delete_request(&mut self, id: u32) -> Result<(), TableError>;
fn push_response(&mut self, response: Box<dyn HttpResponse>) -> Result<u32, TableError>;
fn get_response(&self, id: u32) -> Result<&dyn HttpResponse, TableError>;
fn get_response_mut(&mut self, id: u32) -> Result<&mut Box<dyn HttpResponse>, TableError>;
fn delete_response(&mut self, id: u32) -> Result<(), TableError>;
fn push_future(&mut self, future: Box<ActiveFuture>) -> Result<u32, TableError>;
fn get_future(&self, id: u32) -> Result<&ActiveFuture, TableError>;
fn get_future_mut(&mut self, id: u32) -> Result<&mut Box<ActiveFuture>, TableError>;
fn delete_future(&mut self, id: u32) -> Result<(), TableError>;
fn push_fields(&mut self, fields: Box<ActiveFields>) -> Result<u32, TableError>;
fn get_fields(&self, id: u32) -> Result<&ActiveFields, TableError>;
fn get_fields_mut(&mut self, id: u32) -> Result<&mut Box<ActiveFields>, 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<Stream>, TableError>;
fn delete_stream(&mut self, id: u32) -> Result<(), TableError>;
}
impl TableHttpExt for Table {
fn push_request(&mut self, request: Box<dyn HttpRequest>) -> Result<u32, TableError> {
self.push(Box::new(request))
}
fn get_request(&self, id: u32) -> Result<&dyn HttpRequest, TableError> {
self.get::<Box<dyn HttpRequest>>(id).map(|f| f.as_ref())
}
fn get_request_mut(&mut self, id: u32) -> Result<&mut Box<dyn HttpRequest>, TableError> {
self.get_mut::<Box<dyn HttpRequest>>(id)
}
fn delete_request(&mut self, id: u32) -> Result<(), TableError> {
self.delete::<Box<dyn HttpRequest>>(id).map(|_old| ())
}
fn push_response(&mut self, response: Box<dyn HttpResponse>) -> Result<u32, TableError> {
self.push(Box::new(response))
}
fn get_response(&self, id: u32) -> Result<&dyn HttpResponse, TableError> {
self.get::<Box<dyn HttpResponse>>(id).map(|f| f.as_ref())
}
fn get_response_mut(&mut self, id: u32) -> Result<&mut Box<dyn HttpResponse>, TableError> {
self.get_mut::<Box<dyn HttpResponse>>(id)
}
fn delete_response(&mut self, id: u32) -> Result<(), TableError> {
self.delete::<Box<dyn HttpResponse>>(id).map(|_old| ())
}
fn push_future(&mut self, future: Box<ActiveFuture>) -> Result<u32, TableError> {
self.push(Box::new(future))
}
fn get_future(&self, id: u32) -> Result<&ActiveFuture, TableError> {
self.get::<Box<ActiveFuture>>(id).map(|f| f.as_ref())
}
fn get_future_mut(&mut self, id: u32) -> Result<&mut Box<ActiveFuture>, TableError> {
self.get_mut::<Box<ActiveFuture>>(id)
}
fn delete_future(&mut self, id: u32) -> Result<(), TableError> {
self.delete::<Box<ActiveFuture>>(id).map(|_old| ())
}
fn push_fields(&mut self, fields: Box<ActiveFields>) -> Result<u32, TableError> {
self.push(Box::new(fields))
}
fn get_fields(&self, id: u32) -> Result<&ActiveFields, TableError> {
self.get::<Box<ActiveFields>>(id).map(|f| f.as_ref())
}
fn get_fields_mut(&mut self, id: u32) -> Result<&mut Box<ActiveFields>, TableError> {
self.get_mut::<Box<ActiveFields>>(id)
}
fn delete_fields(&mut self, id: u32) -> Result<(), TableError> {
self.delete::<Box<ActiveFields>>(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::<Box<Stream>>(id).map(|f| f.as_ref())
}
fn get_stream_mut(&mut self, id: u32) -> Result<&mut Box<Stream>, TableError> {
self.get_mut::<Box<Stream>>(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::<Box<Stream>>(id).map(|_old| ())?;
self.delete::<Box<dyn HostInputStream>>(input_stream)
.map(|_old| ())?;
self.delete::<Box<dyn HostOutputStream>>(output_stream)
.map(|_old| ())
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn instantiate() {
WasiHttpCtx::new().unwrap();
}
}

379
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<T: WasiHttpView + WasiHttpViewExt> 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<Fields> {
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<Fields> {
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<Vec<Vec<u8>>> {
async fn fields_get(&mut self, fields: Fields, name: String) -> wasmtime::Result<Vec<Vec<u8>>> {
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<Vec<u8>>,
) -> 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<u8>,
) -> 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<Vec<(String, Vec<u8>)>> {
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<Vec<(String, Vec<u8>)>> {
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<Fields> {
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<Fields> {
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<Option<Trailers>> {
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<Option<Trailers>> {
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<Trailers>,
) -> 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<Method> {
async fn incoming_request_method(
&mut self,
_request: IncomingRequest,
) -> wasmtime::Result<Method> {
bail!("unimplemented: incoming_request_method")
}
fn incoming_request_path_with_query(
async fn incoming_request_path_with_query(
&mut self,
_request: IncomingRequest,
) -> wasmtime::Result<Option<String>> {
bail!("unimplemented: incoming_request_path")
}
fn incoming_request_scheme(
async fn incoming_request_scheme(
&mut self,
_request: IncomingRequest,
) -> wasmtime::Result<Option<Scheme>> {
bail!("unimplemented: incoming_request_scheme")
}
fn incoming_request_authority(
async fn incoming_request_authority(
&mut self,
_request: IncomingRequest,
) -> wasmtime::Result<Option<String>> {
bail!("unimplemented: incoming_request_authority")
}
fn incoming_request_headers(&mut self, _request: IncomingRequest) -> wasmtime::Result<Headers> {
async fn incoming_request_headers(
&mut self,
_request: IncomingRequest,
) -> wasmtime::Result<Headers> {
bail!("unimplemented: incoming_request_headers")
}
fn incoming_request_consume(
async fn incoming_request_consume(
&mut self,
_request: IncomingRequest,
) -> wasmtime::Result<Result<IncomingStream, ()>> {
bail!("unimplemented: incoming_request_consume")
}
fn new_outgoing_request(
async fn new_outgoing_request(
&mut self,
method: Method,
path_with_query: Option<String>,
@ -168,137 +193,209 @@ impl Host for WasiHttp {
authority: Option<String>,
headers: Headers,
) -> wasmtime::Result<OutgoingRequest> {
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<Result<OutgoingStream, ()>> {
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<OutgoingResponse, Error>,
) -> wasmtime::Result<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<StatusCode> {
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<Headers> {
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<Result<IncomingStream, ()>> {
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<OutgoingResponse> {
bail!("unimplemented: new_outgoing_response")
}
fn outgoing_response_write(
async fn outgoing_response_write(
&mut self,
_response: OutgoingResponse,
) -> wasmtime::Result<Result<OutgoingStream, ()>> {
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<Option<Result<IncomingResponse, Error>>> {
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<Pollable> {
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
}
})
}
}

1
crates/wasi-http/wasi-http

@ -1 +0,0 @@
Subproject commit 1c95bc21dbd193b046e4232d063a82c8b5ba7994

39
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
}

33
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
}

18
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<tuple<string, string>>
/// Get the POSIX-style arguments to the program.
get-arguments: func() -> list<string>
/// Return a path that programs should use as their initial current working
/// directory, interpreting `.` as shorthand for this.
initial-cwd: func() -> option<string>
}

4
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)
}

4
crates/wasi-http/wit/deps/cli/run.wit

@ -0,0 +1,4 @@
interface run {
/// Run the program.
run: func() -> result
}

17
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
}

59
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<terminal-input>
}
/// 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<terminal-output>
}
/// 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<terminal-output>
}

34
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
}

63
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,
}
}

43
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
}

6
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<tuple<descriptor, string>>
}

824
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<input-stream, error-code>
/// 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<output-stream, error-code>
/// 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<output-stream, error-code>
/// 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<descriptor-flags, error-code>
/// 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<descriptor-type, error-code>
/// 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<u8, error-code>`.
///
/// 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<tuple<list<u8>, 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<u8, error-code>`.
///
/// Note: This is similar to `pwrite` in POSIX.
write: func(
this: descriptor,
/// Data to write
buffer: list<u8>,
/// The offset within the file at which to write.
offset: filesize,
) -> result<filesize, error-code>
/// 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<directory-entry-stream, error-code>
/// 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<descriptor-stat, error-code>
/// 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<descriptor-stat, error-code>
/// 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<descriptor, error-code>
/// 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<string, error-code>
/// 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<option<directory-entry>, 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<metadata-hash-value, error-code>
/// 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<metadata-hash-value, error-code>
}

6
crates/wasi-http/wit/deps/filesystem/world.wit

@ -0,0 +1,6 @@
package wasi:filesystem
world example-world {
import types
import preopens
}

24
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
)
}

18
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<request-options>
) -> future-incoming-response
}

34
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
}

155
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<tuple<string,string>>) -> fields
fields-get: func(fields: fields, name: string) -> list<list<u8>>
fields-set: func(fields: fields, name: string, value: list<list<u8>>)
fields-delete: func(fields: fields, name: string)
fields-append: func(fields: fields, name: string, value: list<u8>)
fields-entries: func(fields: fields) -> list<tuple<string,list<u8>>>
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<u8, option<trailers>>. 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<trailers>
finish-outgoing-stream: func(s: outgoing-stream, trailers: option<trailers>)
// 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<string>
incoming-request-scheme: func(request: incoming-request) -> option<scheme>
incoming-request-authority: func(request: incoming-request) -> option<string>
incoming-request-headers: func(request: incoming-request) -> headers
incoming-request-consume: func(request: incoming-request) -> result<incoming-stream>
new-outgoing-request: func(
method: method,
path-with-query: option<string>,
scheme: option<scheme>,
authority: option<string>,
headers: headers
) -> outgoing-request
outgoing-request-write: func(request: outgoing-request) -> result<outgoing-stream>
// 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<u32>,
// The timeout for receiving the first byte of the response body.
first-byte-timeout-ms: option<u32>,
// The timeout for receiving the next chunk of bytes in the response body
// stream.
between-bytes-timeout-ms: option<u32>
}
// 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<outgoing-response, error>) -> 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<incoming-stream>
new-outgoing-response: func(
status-code: status-code,
headers: headers
) -> outgoing-response
outgoing-response-write: func(response: outgoing-response) -> result<outgoing-stream>
// The following block defines a special resource type used by the
// `wasi:http/outgoing-handler` interface to emulate
// `future<result<response, error>>` 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<result<incoming-response, error>>
listen-to-future-incoming-response: func(f: future-incoming-response) -> pollable
}

253
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<tuple<list<u8>, 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<tuple<list<u8>, 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<tuple<u64, stream-status>>
/// 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<tuple<u64, stream-status>>
/// 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<u8>
) -> result<tuple<u64, stream-status>>
/// 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<u8>
) -> result<tuple<u64, stream-status>>
/// 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<tuple<u64, stream-status>>
/// 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<tuple<u64, stream-status>>
/// 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<tuple<u64, stream-status>>
/// 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<tuple<u64, stream-status>>
/// 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<tuple<u64, stream-status>>
/// 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)
}

5
crates/wasi/wit/deps/logging/handler.wit → 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.

39
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<bool> is the same length as the argument
/// list<pollable>, and indicates the readiness of each corresponding
/// element in that / list, with true indicating ready.
poll-oneoff: func(in: list<pollable>) -> list<bool>
}

24
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<u64, u64>
}

21
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<u8>
/// 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
}

25
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<u8>
/// 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
}

9
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
}

69
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:
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getaddrinfo.html>
/// - <https://man7.org/linux/man-pages/man3/getaddrinfo.3.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfo>
/// - <https://man.freebsd.org/cgi/man.cgi?query=getaddrinfo&sektion=3>
resolve-addresses: func(network: network, name: string, address-family: option<ip-address-family>, include-unavailable: bool) -> result<resolve-address-stream, error-code>
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<option<ip-address>, 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
}

187
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<u8, u8, u8, u8>
type ipv6-address = tuple<u16, u16, u16, u16, u16, u16, u16, u16>
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),
}
}

27
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
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/socket.html>
/// - <https://man7.org/linux/man-pages/man2/socket.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketw>
/// - <https://man.freebsd.org/cgi/man.cgi?query=socket&sektion=2>
create-tcp-socket: func(address-family: ip-address-family) -> result<tcp-socket, error-code>
}

255
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
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/bind.html>
/// - <https://man7.org/linux/man-pages/man2/bind.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-bind>
/// - <https://man.freebsd.org/cgi/man.cgi?query=bind&sektion=2&format=html>
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
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/connect.html>
/// - <https://man7.org/linux/man-pages/man2/connect.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-connect>
/// - <https://man.freebsd.org/cgi/man.cgi?connect>
start-connect: func(this: tcp-socket, network: network, remote-address: ip-socket-address) -> result<_, error-code>
finish-connect: func(this: tcp-socket) -> result<tuple<input-stream, output-stream>, 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
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/listen.html>
/// - <https://man7.org/linux/man-pages/man2/listen.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-listen>
/// - <https://man.freebsd.org/cgi/man.cgi?query=listen&sektion=2>
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
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/accept.html>
/// - <https://man7.org/linux/man-pages/man2/accept.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-accept>
/// - <https://man.freebsd.org/cgi/man.cgi?query=accept&sektion=2>
accept: func(this: tcp-socket) -> result<tuple<tcp-socket, input-stream, output-stream>, error-code>
/// Get the bound local address.
///
/// # Typical errors
/// - `not-bound`: The socket is not bound to any local address.
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getsockname.html>
/// - <https://man7.org/linux/man-pages/man2/getsockname.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-getsockname>
/// - <https://man.freebsd.org/cgi/man.cgi?getsockname>
local-address: func(this: tcp-socket) -> result<ip-socket-address, error-code>
/// Get the bound remote address.
///
/// # Typical errors
/// - `not-connected`: The socket is not connected to a remote address. (ENOTCONN)
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getpeername.html>
/// - <https://man7.org/linux/man-pages/man2/getpeername.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-getpeername>
/// - <https://man.freebsd.org/cgi/man.cgi?query=getpeername&sektion=2&n=1>
remote-address: func(this: tcp-socket) -> result<ip-socket-address, error-code>
/// 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<bool, error-code>
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<bool, error-code>
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<bool, error-code>
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<u8, error-code>
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<u64, error-code>
set-receive-buffer-size: func(this: tcp-socket, value: u64) -> result<_, error-code>
send-buffer-size: func(this: tcp-socket) -> result<u64, error-code>
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
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/shutdown.html>
/// - <https://man7.org/linux/man-pages/man2/shutdown.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-shutdown>
/// - <https://man.freebsd.org/cgi/man.cgi?query=shutdown&sektion=2>
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)
}

27
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:
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/socket.html>
/// - <https://man7.org/linux/man-pages/man2/socket.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketw>
/// - <https://man.freebsd.org/cgi/man.cgi?query=socket&sektion=2>
create-udp-socket: func(address-family: ip-address-family) -> result<udp-socket, error-code>
}

211
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<u8>, // 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
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/bind.html>
/// - <https://man7.org/linux/man-pages/man2/bind.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-bind>
/// - <https://man.freebsd.org/cgi/man.cgi?query=bind&sektion=2&format=html>
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
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/connect.html>
/// - <https://man7.org/linux/man-pages/man2/connect.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-connect>
/// - <https://man.freebsd.org/cgi/man.cgi?connect>
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
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/recvfrom.html>
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/recvmsg.html>
/// - <https://man7.org/linux/man-pages/man2/recv.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-recv>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-recvfrom>
/// - <https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms741687(v=vs.85)>
/// - <https://man.freebsd.org/cgi/man.cgi?query=recv&sektion=2>
receive: func(this: udp-socket) -> result<datagram, error-code>
/// 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
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/sendto.html>
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/sendmsg.html>
/// - <https://man7.org/linux/man-pages/man2/send.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-send>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-sendto>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasendmsg>
/// - <https://man.freebsd.org/cgi/man.cgi?query=send&sektion=2>
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
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getsockname.html>
/// - <https://man7.org/linux/man-pages/man2/getsockname.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-getsockname>
/// - <https://man.freebsd.org/cgi/man.cgi?getsockname>
local-address: func(this: udp-socket) -> result<ip-socket-address, error-code>
/// Get the address set with `connect`.
///
/// # Typical errors
/// - `not-connected`: The socket is not connected to a remote address. (ENOTCONN)
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getpeername.html>
/// - <https://man7.org/linux/man-pages/man2/getpeername.2.html>
/// - <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-getpeername>
/// - <https://man.freebsd.org/cgi/man.cgi?query=getpeername&sektion=2&n=1>
remote-address: func(this: udp-socket) -> result<ip-socket-address, error-code>
/// 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<bool, error-code>
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<u8, error-code>
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<u64, error-code>
set-receive-buffer-size: func(this: udp-socket, value: u64) -> result<_, error-code>
send-buffer-size: func(this: udp-socket) -> result<u64, error-code>
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)
}

33
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
}

28
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<string>) -> u32
export get-strings: func() -> list<string>
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
}

39
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
}

1
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

34
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
}

22
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<tuple<string,string>>) -> fields
fields-get: func(fields: fields, name: string) -> list<string>
fields-set: func(fields: fields, name: string, value: list<string>)
fields-get: func(fields: fields, name: string) -> list<list<u8>>
fields-set: func(fields: fields, name: string, value: list<list<u8>>)
fields-delete: func(fields: fields, name: string)
fields-append: func(fields: fields, name: string, value: string)
fields-entries: func(fields: fields) -> list<tuple<string,string>>
fields-append: func(fields: fields, name: string, value: list<u8>)
fields-entries: func(fields: fields) -> list<tuple<string,list<u8>>>
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<string>
incoming-request-scheme: func(request: incoming-request) -> option<scheme>
incoming-request-authority: func(request: incoming-request) -> string
incoming-request-authority: func(request: incoming-request) -> option<string>
incoming-request-headers: func(request: incoming-request) -> headers
incoming-request-consume: func(request: incoming-request) -> result<incoming-stream>
new-outgoing-request: func(
method: method,
path: string,
query: string,
path-with-query: option<string>,
scheme: option<scheme>,
authority: string,
authority: option<string>,
headers: headers
) -> outgoing-request
outgoing-request-write: func(request: outgoing-request) -> result<outgoing-stream>
@ -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<outgoing-response, error>) -> result
set-response-outparam: func(param: response-outparam, response: result<outgoing-response, error>) -> result
// This type corresponds to the HTTP standard Status Code.
type status-code = u16

37
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)
}

14
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<String>)> {
let mut parts = s.splitn(2, '=');
@ -672,8 +672,8 @@ struct Host {
wasi_nn: Option<Arc<WasiNnCtx>>,
#[cfg(feature = "wasi-threads")]
wasi_threads: Option<Arc<WasiThreadsCtx<Host>>>,
#[cfg(feature = "wasi-http")]
wasi_http: Option<WasiHttp>,
// #[cfg(feature = "wasi-http")]
// wasi_http: Option<Arc<WasiHttpCtx>>,
limits: StoreLimits,
guest_profiler: Option<Arc<GuestProfiler>>,
}
@ -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");
}
}

Loading…
Cancel
Save