From 46ec55125bce5a3e02437d4c6dd08fc94b85462a Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Mon, 3 Mar 2025 18:20:22 +0100 Subject: [PATCH 01/19] feat(p3): implement `wasi:http` Signed-off-by: Roman Volosatovs --- Cargo.lock | 19 +- Cargo.toml | 2 +- ci/vendor-wit.sh | 9 + crates/test-programs/artifacts/build.rs | 4 + crates/test-programs/src/bin/api_0_3_proxy.rs | 74 ++ ...ttp_0_3_outbound_request_content_length.rs | 110 ++ .../src/bin/http_0_3_outbound_request_get.rs | 43 + ...tp_0_3_outbound_request_invalid_dnsname.rs | 35 + ...ttp_0_3_outbound_request_invalid_header.rs | 88 ++ .../http_0_3_outbound_request_invalid_port.rs | 27 + ...tp_0_3_outbound_request_invalid_version.rs | 36 + .../http_0_3_outbound_request_large_post.rs | 46 + ...outbound_request_missing_path_and_query.rs | 27 + .../src/bin/http_0_3_outbound_request_post.rs | 40 + .../src/bin/http_0_3_outbound_request_put.rs | 40 + ...ttp_0_3_outbound_request_response_build.rs | 67 ++ .../bin/http_0_3_outbound_request_timeout.rs | 43 + ...ttp_0_3_outbound_request_unknown_method.rs | 28 + ...0_3_outbound_request_unsupported_scheme.rs | 32 + crates/test-programs/src/p3/http.rs | 135 +++ crates/test-programs/src/p3/mod.rs | 50 +- crates/wasi-http/Cargo.toml | 9 +- crates/wasi-http/src/lib.rs | 3 + .../src/p2/wit/deps/cli@v0.2.3/command.wit | 10 - .../src/p2/wit/deps/cli@v0.2.3/imports.wit | 36 - .../src/p2/wit/deps/cli@v0.2.3/stdio.wit | 26 - .../p2/wit/deps/filesystem@v0.2.3/world.wit | 9 - .../src/p2/wit/deps/http@v0.2.3/handler.wit | 49 - .../src/p2/wit/deps/http@v0.2.3/types.wit | 673 ----------- .../src/p2/wit/deps/io@v0.2.3/error.wit | 34 - .../src/p2/wit/deps/io@v0.2.3/poll.wit | 47 - .../src/p2/wit/deps/io@v0.2.3/streams.wit | 290 ----- .../src/p2/wit/deps/io@v0.2.3/world.wit | 10 - .../src/p2/wit/deps/random@v0.2.3/world.wit | 13 - .../deps/sockets@v0.2.3/instance-network.wit | 11 - .../deps/sockets@v0.2.3/ip-name-lookup.wit | 56 - .../p2/wit/deps/sockets@v0.2.3/network.wit | 169 --- .../deps/sockets@v0.2.3/tcp-create-socket.wit | 30 - .../src/p2/wit/deps/sockets@v0.2.3/tcp.wit | 387 ------ .../deps/sockets@v0.2.3/udp-create-socket.wit | 30 - .../src/p2/wit/deps/sockets@v0.2.3/udp.wit | 288 ----- .../src/p2/wit/deps/sockets@v0.2.3/world.wit | 19 - crates/wasi-http/src/p3/bindings.rs | 44 + crates/wasi-http/src/p3/body.rs | 243 ++++ crates/wasi-http/src/p3/client.rs | 342 ++++++ crates/wasi-http/src/p3/conv.rs | 152 +++ crates/wasi-http/src/p3/host/handle.rs | 409 +++++++ crates/wasi-http/src/p3/host/mod.rs | 126 ++ crates/wasi-http/src/p3/host/types.rs | 1047 +++++++++++++++++ crates/wasi-http/src/p3/mod.rs | 399 +++++++ crates/wasi-http/src/p3/proxy.rs | 61 + crates/wasi-http/src/p3/request.rs | 64 + crates/wasi-http/src/p3/response.rs | 196 +++ .../cli@82b86d9@wit-0.3.0-draft/command.wit | 10 + .../environment.wit | 8 +- .../cli@82b86d9@wit-0.3.0-draft}/exit.wit | 4 +- .../cli@82b86d9@wit-0.3.0-draft/imports.wit | 34 + .../deps/cli@82b86d9@wit-0.3.0-draft}/run.wit | 4 +- .../cli@82b86d9@wit-0.3.0-draft/stdio.wit | 17 + .../cli@82b86d9@wit-0.3.0-draft}/terminal.wit | 26 +- .../monotonic-clock.wit | 35 +- .../timezone.wit | 2 +- .../wall-clock.wit | 10 +- .../clocks@646092f@wit-0.3.0-draft}/world.wit | 8 +- .../preopens.wit | 8 +- .../types.wit | 183 ++- .../world.wit | 9 + .../http@ae89575@wit-0.3.0-draft/handler.wit | 17 + .../http@ae89575@wit-0.3.0-draft}/proxy.wit | 34 +- .../http@ae89575@wit-0.3.0-draft/types.wit | 431 +++++++ .../insecure-seed.wit | 6 +- .../insecure.wit | 8 +- .../random.wit | 8 +- .../random@9499404@wit-0.3.0-draft/world.wit | 13 + .../ip-name-lookup.wit | 62 + .../sockets@41d7079@wit-0.3.0-draft/types.wit | 725 ++++++++++++ .../sockets@41d7079@wit-0.3.0-draft/world.wit | 9 + crates/wasi-http/src/p3/wit/world.wit | 6 + crates/wasi-http/tests/all/http_server.rs | 143 ++- crates/wasi-http/tests/all/main.rs | 545 +-------- crates/wasi-http/tests/all/{ => p2}/async_.rs | 29 +- crates/wasi-http/tests/all/p2/mod.rs | 540 +++++++++ crates/wasi-http/tests/all/{ => p2}/sync.rs | 29 +- crates/wasi-http/tests/all/p3/mod.rs | 562 +++++++++ crates/wasi-http/tests/all/p3/outgoing.rs | 132 +++ crates/wasi/src/p3/mod.rs | 87 ++ src/commands/serve.rs | 296 ++++- 87 files changed, 7202 insertions(+), 3075 deletions(-) create mode 100644 crates/test-programs/src/bin/api_0_3_proxy.rs create mode 100644 crates/test-programs/src/bin/http_0_3_outbound_request_content_length.rs create mode 100644 crates/test-programs/src/bin/http_0_3_outbound_request_get.rs create mode 100644 crates/test-programs/src/bin/http_0_3_outbound_request_invalid_dnsname.rs create mode 100644 crates/test-programs/src/bin/http_0_3_outbound_request_invalid_header.rs create mode 100644 crates/test-programs/src/bin/http_0_3_outbound_request_invalid_port.rs create mode 100644 crates/test-programs/src/bin/http_0_3_outbound_request_invalid_version.rs create mode 100644 crates/test-programs/src/bin/http_0_3_outbound_request_large_post.rs create mode 100644 crates/test-programs/src/bin/http_0_3_outbound_request_missing_path_and_query.rs create mode 100644 crates/test-programs/src/bin/http_0_3_outbound_request_post.rs create mode 100644 crates/test-programs/src/bin/http_0_3_outbound_request_put.rs create mode 100644 crates/test-programs/src/bin/http_0_3_outbound_request_response_build.rs create mode 100644 crates/test-programs/src/bin/http_0_3_outbound_request_timeout.rs create mode 100644 crates/test-programs/src/bin/http_0_3_outbound_request_unknown_method.rs create mode 100644 crates/test-programs/src/bin/http_0_3_outbound_request_unsupported_scheme.rs create mode 100644 crates/test-programs/src/p3/http.rs delete mode 100644 crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/command.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/imports.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/stdio.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/filesystem@v0.2.3/world.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/http@v0.2.3/handler.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/http@v0.2.3/types.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/io@v0.2.3/error.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/io@v0.2.3/poll.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/io@v0.2.3/streams.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/io@v0.2.3/world.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/random@v0.2.3/world.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/instance-network.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/ip-name-lookup.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/network.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/tcp-create-socket.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/tcp.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/udp-create-socket.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/udp.wit delete mode 100644 crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/world.wit create mode 100644 crates/wasi-http/src/p3/bindings.rs create mode 100644 crates/wasi-http/src/p3/body.rs create mode 100644 crates/wasi-http/src/p3/client.rs create mode 100644 crates/wasi-http/src/p3/conv.rs create mode 100644 crates/wasi-http/src/p3/host/handle.rs create mode 100644 crates/wasi-http/src/p3/host/mod.rs create mode 100644 crates/wasi-http/src/p3/host/types.rs create mode 100644 crates/wasi-http/src/p3/mod.rs create mode 100644 crates/wasi-http/src/p3/proxy.rs create mode 100644 crates/wasi-http/src/p3/request.rs create mode 100644 crates/wasi-http/src/p3/response.rs create mode 100644 crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/command.wit rename crates/wasi-http/src/{p2/wit/deps/cli@v0.2.3 => p3/wit/deps/cli@82b86d9@wit-0.3.0-draft}/environment.wit (87%) rename crates/wasi-http/src/{p2/wit/deps/cli@v0.2.3 => p3/wit/deps/cli@82b86d9@wit-0.3.0-draft}/exit.wit (92%) create mode 100644 crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/imports.wit rename crates/wasi-http/src/{p2/wit/deps/cli@v0.2.3 => p3/wit/deps/cli@82b86d9@wit-0.3.0-draft}/run.wit (56%) create mode 100644 crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/stdio.wit rename crates/wasi-http/src/{p2/wit/deps/cli@v0.2.3 => p3/wit/deps/cli@82b86d9@wit-0.3.0-draft}/terminal.wit (82%) rename crates/wasi-http/src/{p2/wit/deps/clocks@v0.2.3 => p3/wit/deps/clocks@646092f@wit-0.3.0-draft}/monotonic-clock.wit (61%) rename crates/wasi-http/src/{p2/wit/deps/clocks@v0.2.3 => p3/wit/deps/clocks@646092f@wit-0.3.0-draft}/timezone.wit (98%) rename crates/wasi-http/src/{p2/wit/deps/clocks@v0.2.3 => p3/wit/deps/clocks@646092f@wit-0.3.0-draft}/wall-clock.wit (92%) rename crates/wasi-http/src/{p2/wit/deps/clocks@v0.2.3 => p3/wit/deps/clocks@646092f@wit-0.3.0-draft}/world.wit (55%) rename crates/wasi-http/src/{p2/wit/deps/filesystem@v0.2.3 => p3/wit/deps/filesystem@740cd76@wit-0.3.0-draft}/preopens.wit (62%) rename crates/wasi-http/src/{p2/wit/deps/filesystem@v0.2.3 => p3/wit/deps/filesystem@740cd76@wit-0.3.0-draft}/types.wit (84%) create mode 100644 crates/wasi-http/src/p3/wit/deps/filesystem@740cd76@wit-0.3.0-draft/world.wit create mode 100644 crates/wasi-http/src/p3/wit/deps/http@ae89575@wit-0.3.0-draft/handler.wit rename crates/wasi-http/src/{p2/wit/deps/http@v0.2.3 => p3/wit/deps/http@ae89575@wit-0.3.0-draft}/proxy.wit (70%) create mode 100644 crates/wasi-http/src/p3/wit/deps/http@ae89575@wit-0.3.0-draft/types.wit rename crates/wasi-http/src/{p2/wit/deps/random@v0.2.3 => p3/wit/deps/random@9499404@wit-0.3.0-draft}/insecure-seed.wit (93%) rename crates/wasi-http/src/{p2/wit/deps/random@v0.2.3 => p3/wit/deps/random@9499404@wit-0.3.0-draft}/insecure.wit (88%) rename crates/wasi-http/src/{p2/wit/deps/random@v0.2.3 => p3/wit/deps/random@9499404@wit-0.3.0-draft}/random.wit (91%) create mode 100644 crates/wasi-http/src/p3/wit/deps/random@9499404@wit-0.3.0-draft/world.wit create mode 100644 crates/wasi-http/src/p3/wit/deps/sockets@41d7079@wit-0.3.0-draft/ip-name-lookup.wit create mode 100644 crates/wasi-http/src/p3/wit/deps/sockets@41d7079@wit-0.3.0-draft/types.wit create mode 100644 crates/wasi-http/src/p3/wit/deps/sockets@41d7079@wit-0.3.0-draft/world.wit create mode 100644 crates/wasi-http/src/p3/wit/world.wit rename crates/wasi-http/tests/all/{ => p2}/async_.rs (87%) create mode 100644 crates/wasi-http/tests/all/p2/mod.rs rename crates/wasi-http/tests/all/{ => p2}/sync.rs (84%) create mode 100644 crates/wasi-http/tests/all/p3/mod.rs create mode 100644 crates/wasi-http/tests/all/p3/outgoing.rs diff --git a/Cargo.lock b/Cargo.lock index 16f4afcb95..c70d5cf2cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -296,9 +296,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.5.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bytesize" @@ -1705,12 +1705,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "pin-project-lite", @@ -1718,9 +1718,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -1736,9 +1736,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "1.0.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f9214f3e703236b221f1a9cd88ec8b4adfa5296de01ab96216361f4692f56" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", @@ -1750,6 +1750,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "smallvec", "tokio", "want", ] diff --git a/Cargo.toml b/Cargo.toml index 77d55fb719..ccbed2c141 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -366,7 +366,7 @@ tokio = { version = "1.43.0", features = [ "rt", "time" ] } hyper = "1.0.1" http = "1.0.0" http-body = "1.0.0" -http-body-util = "0.1.0" +http-body-util = "0.1.1" bytes = { version = "1.4", default-features = false } futures = { version = "0.3.27", default-features = false } indexmap = { version = "2.0.0", default-features = false } diff --git a/ci/vendor-wit.sh b/ci/vendor-wit.sh index d8e4645db2..419e4a0d1e 100755 --- a/ci/vendor-wit.sh +++ b/ci/vendor-wit.sh @@ -75,6 +75,15 @@ make_vendor "wasi/src/p3" " random@9499404@wit-0.3.0-draft sockets@41d7079@wit-0.3.0-draft " +# `wasi:http` from https://github.com/WebAssembly/wasi-http/pull/158 +make_vendor "wasi-http/src/p3" " + cli@82b86d9@wit-0.3.0-draft + clocks@646092f@wit-0.3.0-draft + filesystem@740cd76@wit-0.3.0-draft + random@9499404@wit-0.3.0-draft + sockets@41d7079@wit-0.3.0-draft + http@ae89575@wit-0.3.0-draft +" rm -rf $cache_dir diff --git a/crates/test-programs/artifacts/build.rs b/crates/test-programs/artifacts/build.rs index 2b4ee70148..f33cb00fe9 100644 --- a/crates/test-programs/artifacts/build.rs +++ b/crates/test-programs/artifacts/build.rs @@ -74,10 +74,12 @@ fn build_and_generate_tests() { s if s.starts_with("filesystem_0_3") => "filesystem_0_3", s if s.starts_with("random_0_3") => "random_0_3", s if s.starts_with("sockets_0_3") => "sockets_0_3", + s if s.starts_with("http_0_3") => "http_0_3", s if s.starts_with("http_") => "http", s if s.starts_with("preview1_") => "preview1", s if s.starts_with("preview2_") => "preview2", s if s.starts_with("cli_") => "cli", + s if s.starts_with("api_0_3") => "api_0_3", s if s.starts_with("api_") => "api", s if s.starts_with("nn_") => "nn", s if s.starts_with("piped_") => "piped", @@ -113,7 +115,9 @@ fn build_and_generate_tests() { s if s.starts_with("filesystem_0_3") => &reactor_adapter, s if s.starts_with("random_0_3") => &reactor_adapter, s if s.starts_with("sockets_0_3") => &reactor_adapter, + s if s.starts_with("http_0_3") => &reactor_adapter, s if s.starts_with("async_") => &reactor_adapter, + s if s.starts_with("api_0_3_proxy") => &proxy_adapter, s if s.starts_with("api_proxy") => &proxy_adapter, _ => &command_adapter, }; diff --git a/crates/test-programs/src/bin/api_0_3_proxy.rs b/crates/test-programs/src/bin/api_0_3_proxy.rs new file mode 100644 index 0000000000..86fb80e2b4 --- /dev/null +++ b/crates/test-programs/src/bin/api_0_3_proxy.rs @@ -0,0 +1,74 @@ +use futures::{join, SinkExt as _}; +use test_programs::p3::wasi::http::types::{ErrorCode, Headers, Request, Response}; +use test_programs::p3::{wit_future, wit_stream}; +use wit_bindgen_rt::async_support::spawn; + +struct T; + +test_programs::p3::proxy::export!(T); + +impl test_programs::p3::proxy::exports::wasi::http::handler::Guest for T { + async fn handle(request: Request) -> Result { + assert!(request.scheme().is_some()); + assert!(request.authority().is_some()); + assert!(request.path_with_query().is_some()); + + // TODO: adapt below + //test_filesystem(); + + let header = String::from("custom-forbidden-header"); + let req_hdrs = request.headers(); + + assert!( + !req_hdrs.has(&header), + "forbidden `custom-forbidden-header` found in request" + ); + + assert!(req_hdrs.delete(&header).is_err()); + assert!(req_hdrs.append(&header, b"no".as_ref()).is_err()); + + assert!( + !req_hdrs.has(&header), + "append of forbidden header succeeded" + ); + + assert!( + !req_hdrs.has("host"), + "forbidden host header present in incoming request" + ); + + let hdrs = Headers::new(); + let (mut contents_tx, contents_rx) = wit_stream::new(); + let (trailers_tx, trailers_rx) = wit_future::new(); + let (resp, transmit) = Response::new(hdrs, Some(contents_rx), trailers_rx); + spawn(async { + join!( + async { + contents_tx + .send(b"hello, world!".to_vec()) + .await + .expect("writing response"); + drop(contents_tx); + trailers_tx.write(Ok(None)); + }, + async { + transmit + .await + .expect("failed to transmit response") + .unwrap() + .unwrap() + } + ); + }); + Ok(resp) + } +} + +// Technically this should not be here for a proxy, but given the current +// framework for tests it's required since this file is built as a `bin` +fn main() {} + +// TODO: adapt below +//fn test_filesystem() { +// assert!(std::fs::File::open(".").is_err()); +//} diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_content_length.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_content_length.rs new file mode 100644 index 0000000000..12e44f1c01 --- /dev/null +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_content_length.rs @@ -0,0 +1,110 @@ +use futures::SinkExt as _; +use test_programs::p3::wasi::http::types::{ErrorCode, Headers, Method, Request, Scheme, Trailers}; +use test_programs::p3::{wit_future, wit_stream}; +use wit_bindgen_rt::async_support::{FutureWriter, StreamWriter}; + +struct Component; + +test_programs::p3::export!(Component); + +fn make_request() -> ( + Request, + StreamWriter, + FutureWriter, ErrorCode>>, +) { + let (contents_tx, contents_rx) = wit_stream::new(); + let (trailers_tx, trailers_rx) = wit_future::new(); + let (request, _) = Request::new( + Headers::from_list(&[("Content-Length".to_string(), b"11".to_vec())]).unwrap(), + Some(contents_rx), + trailers_rx, + None, + ); + + request.set_method(&Method::Post).expect("setting method"); + request + .set_scheme(Some(&Scheme::Http)) + .expect("setting scheme"); + let addr = test_programs::p3::wasi::cli::environment::get_environment() + .into_iter() + .find_map(|(k, v)| k.eq("HTTP_SERVER").then_some(v)) + .unwrap(); + request + .set_authority(Some(&addr)) + .expect("setting authority"); + request + .set_path_with_query(Some("/")) + .expect("setting path with query"); + + (request, contents_tx, trailers_tx) +} + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + { + println!("writing enough"); + let (_, mut contents_tx, trailers_tx) = make_request(); + contents_tx.send(b"long enough".to_vec()).await.unwrap(); + drop(contents_tx); + trailers_tx.write(Ok(None)).await; + } + + { + println!("writing too little"); + let (_, mut contents_tx, trailers_tx) = make_request(); + contents_tx.send(b"msg".to_vec()).await.unwrap(); + drop(contents_tx); + trailers_tx.write(Ok(None)).await; + + // handle() + + // TODO: Figure out how/if to represent this in wasip3 + //let e = OutgoingBody::finish(outgoing_body, None) + // .expect_err("finish should fail"); + + //assert!( + // matches!(&e, ErrorCode::HttpRequestBodySize(Some(3))), + // "unexpected error: {e:#?}" + //); + } + + { + println!("writing too much"); + let (_, mut contents_tx, trailers_tx) = make_request(); + contents_tx + .send(b"more than 11 bytes".to_vec()) + .await + .unwrap(); + drop(contents_tx); + trailers_tx.write(Ok(None)).await; + + // TODO: Figure out how/if to represent this in wasip3 + //let e = request_body + // .blocking_write_and_flush("more than 11 bytes".as_bytes()) + // .expect_err("write should fail"); + //let e = match e { + // test_programs::wasi::io::streams::StreamError::LastOperationFailed(e) => { + // http_error_code(&e) + // } + // test_programs::wasi::io::streams::StreamError::Closed => panic!("request closed"), + //}; + //assert!( + // matches!( + // e, + // Some(ErrorCode::HttpRequestBodySize(Some(18))) + // ), + // "unexpected error {e:?}" + //); + //let e = OutgoingBody::finish(outgoing_body, None) + // .expect_err("finish should fail"); + + //assert!( + // matches!(&e, ErrorCode::HttpRequestBodySize(Some(18))), + // "unexpected error: {e:#?}" + //); + } + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_get.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_get.rs new file mode 100644 index 0000000000..85c580f67c --- /dev/null +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_get.rs @@ -0,0 +1,43 @@ +use anyhow::Context; +use test_programs::p3::wasi::http::types::{Method, Scheme}; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + let addr = test_programs::p3::wasi::cli::environment::get_environment() + .into_iter() + .find_map(|(k, v)| k.eq("HTTP_SERVER").then_some(v)) + .unwrap(); + let res = test_programs::p3::http::request( + Method::Get, + Scheme::Http, + &addr, + "/get?some=arg&goes=here", + None, + None, + None, + None, + None, + ) + .await + .context("/get") + .unwrap(); + + println!("{addr} /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(), + format!("/get?some=arg&goes=here") + ); + assert_eq!(res.body, b""); + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_invalid_dnsname.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_invalid_dnsname.rs new file mode 100644 index 0000000000..16a7d5baf0 --- /dev/null +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_invalid_dnsname.rs @@ -0,0 +1,35 @@ +use test_programs::p3::wasi::http::types::{ErrorCode, Method, Scheme}; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + let res = test_programs::p3::http::request( + Method::Get, + Scheme::Http, + "some.invalid.dnsname:3000", + "/", + None, + None, + None, + None, + None, + ) + .await; + + let e = res.unwrap_err(); + assert!( + matches!( + e.downcast_ref::() + .expect("expected a wasi-http ErrorCode"), + ErrorCode::DnsError(_) | ErrorCode::ConnectionRefused, + ), + "Unexpected error: {e:#?}" + ); + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_invalid_header.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_invalid_header.rs new file mode 100644 index 0000000000..10447d2fc8 --- /dev/null +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_invalid_header.rs @@ -0,0 +1,88 @@ +use test_programs::p3::wasi::http::types::{HeaderError, Headers, Request}; +use test_programs::p3::wit_future; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + let hdrs = Headers::new(); + assert!(matches!( + hdrs.append("malformed header name", b"ok value".as_ref()), + Err(HeaderError::InvalidSyntax) + )); + + assert!(matches!( + hdrs.append("ok-header-name", b"ok value".as_ref()), + Ok(()) + )); + + assert!(matches!( + hdrs.append("ok-header-name", b"bad\nvalue".as_ref()), + Err(HeaderError::InvalidSyntax) + )); + + assert!(matches!( + hdrs.append("Connection", b"keep-alive".as_ref()), + Err(HeaderError::Forbidden) + )); + + assert!(matches!( + hdrs.append("Keep-Alive", b"stuff".as_ref()), + Err(HeaderError::Forbidden) + )); + + assert!(matches!( + hdrs.append("Host", b"example.com".as_ref()), + Err(HeaderError::Forbidden) + )); + + assert!(matches!( + hdrs.append("custom-forbidden-header", b"keep-alive".as_ref()), + Err(HeaderError::Forbidden) + )); + + assert!(matches!( + hdrs.append("Custom-Forbidden-Header", b"keep-alive".as_ref()), + Err(HeaderError::Forbidden) + )); + + assert!(matches!( + Headers::from_list(&[("bad header".to_owned(), b"value".to_vec())]), + Err(HeaderError::InvalidSyntax) + )); + + assert!(matches!( + Headers::from_list(&[("custom-forbidden-header".to_owned(), b"value".to_vec())]), + Err(HeaderError::Forbidden) + )); + + assert!(matches!( + Headers::from_list(&[("ok-header-name".to_owned(), b"bad\nvalue".to_vec())]), + Err(HeaderError::InvalidSyntax) + )); + + let (_, rx) = wit_future::new(); + let (req, _) = Request::new(hdrs, None, rx, None); + let hdrs = req.headers(); + + assert!(matches!( + hdrs.set("Content-Length", &[b"10".to_vec()]), + Err(HeaderError::Immutable), + )); + + assert!(matches!( + hdrs.append("Content-Length", b"10".as_ref()), + Err(HeaderError::Immutable), + )); + + assert!(matches!( + hdrs.delete("Content-Length"), + Err(HeaderError::Immutable), + )); + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_invalid_port.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_invalid_port.rs new file mode 100644 index 0000000000..b93e7cf7e8 --- /dev/null +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_invalid_port.rs @@ -0,0 +1,27 @@ +use test_programs::p3::wasi::http::types::{Method, Scheme}; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + let res = test_programs::p3::http::request( + Method::Get, + Scheme::Http, + "localhost:99999", + "/", + None, + None, + None, + None, + None, + ) + .await; + + assert!(res.is_err()); + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_invalid_version.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_invalid_version.rs new file mode 100644 index 0000000000..8562d52912 --- /dev/null +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_invalid_version.rs @@ -0,0 +1,36 @@ +use test_programs::p3::wasi::http::types::{ErrorCode, Method, Scheme}; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + let addr = test_programs::p3::wasi::cli::environment::get_environment() + .into_iter() + .find_map(|(k, v)| k.eq("HTTP_SERVER").then_some(v)) + .unwrap(); + let res = test_programs::p3::http::request( + Method::Connect, + Scheme::Http, + &addr, + "/", + None, + Some(&[]), + None, + None, + None, + ) + .await; + + assert!(matches!( + res.unwrap_err() + .downcast::() + .expect("expected a wasi-http ErrorCode"), + ErrorCode::HttpProtocolError, + )); + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_large_post.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_large_post.rs new file mode 100644 index 0000000000..23bfeaabee --- /dev/null +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_large_post.rs @@ -0,0 +1,46 @@ +use anyhow::Context; +use std::io::{self, Read}; +use test_programs::p3::wasi::http::types::{Method, Scheme}; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + // Make sure the final body is larger than 1024*1024, but we cannot allocate + // so much memory directly in the wasm program, so we use the `repeat` + // method to increase the body size. + const LEN: usize = 1024; + const REPEAT: usize = 1025; + let mut buffer = [0; LEN]; + let addr = test_programs::p3::wasi::cli::environment::get_environment() + .into_iter() + .find_map(|(k, v)| k.eq("HTTP_SERVER").then_some(v)) + .unwrap(); + io::repeat(0b001).read_exact(&mut buffer).unwrap(); + let res = test_programs::p3::http::request( + Method::Post, + Scheme::Http, + &addr, + "/post", + Some(&buffer.repeat(REPEAT)), + None, + None, + None, + None, + ) + .await + .context("/post large") + .unwrap(); + + println!("/post large: {}", res.status); + 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.len(), LEN * REPEAT); + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_missing_path_and_query.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_missing_path_and_query.rs new file mode 100644 index 0000000000..4d00350cd8 --- /dev/null +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_missing_path_and_query.rs @@ -0,0 +1,27 @@ +use test_programs::p3::wasi::http::handler::handle; +use test_programs::p3::wasi::http::types::{Fields, Method, Request, Scheme}; +use test_programs::p3::wit_future; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + let fields = Fields::new(); + let (_, rx) = wit_future::new(); + let (req, _) = Request::new(fields, None, rx, None); + req.set_method(&Method::Get).unwrap(); + req.set_scheme(Some(&Scheme::Https)).unwrap(); + req.set_authority(Some("example.com")).unwrap(); + + // Don't set path/query + // req.set_path_with_query(Some("/")).unwrap(); + + let res = handle(req).await; + assert!(res.is_err()); + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_post.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_post.rs new file mode 100644 index 0000000000..67b437c5a2 --- /dev/null +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_post.rs @@ -0,0 +1,40 @@ +use anyhow::Context; +use test_programs::p3::wasi::http::types::{Method, Scheme}; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + let addr = test_programs::p3::wasi::cli::environment::get_environment() + .into_iter() + .find_map(|(k, v)| k.eq("HTTP_SERVER").then_some(v)) + .unwrap(); + let res = test_programs::p3::http::request( + Method::Post, + Scheme::Http, + &addr, + "/post", + Some(b"{\"foo\": \"bar\"}"), + None, + None, + None, + None, + ) + .await + .context("/post") + .unwrap(); + + println!("/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"); + let uri = res.header("x-wasmtime-test-uri").unwrap(); + assert_eq!(std::str::from_utf8(uri).unwrap(), format!("/post")); + assert_eq!(res.body, b"{\"foo\": \"bar\"}", "invalid body returned"); + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_put.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_put.rs new file mode 100644 index 0000000000..5c1db0ee6c --- /dev/null +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_put.rs @@ -0,0 +1,40 @@ +use anyhow::Context; +use test_programs::p3::wasi::http::types::{Method, Scheme}; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + let addr = test_programs::p3::wasi::cli::environment::get_environment() + .into_iter() + .find_map(|(k, v)| k.eq("HTTP_SERVER").then_some(v)) + .unwrap(); + let res = test_programs::p3::http::request( + Method::Put, + Scheme::Http, + &addr, + "/put", + Some(&[]), + None, + None, + None, + None, + ) + .await + .context("/put") + .unwrap(); + + println!("/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"); + let uri = res.header("x-wasmtime-test-uri").unwrap(); + assert_eq!(std::str::from_utf8(uri).unwrap(), format!("/put")); + assert_eq!(res.body, b""); + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_response_build.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_response_build.rs new file mode 100644 index 0000000000..0f72912891 --- /dev/null +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_response_build.rs @@ -0,0 +1,67 @@ +use futures::SinkExt as _; +use test_programs::p3::wasi::http::types::{Fields, Headers, Method, Request, Response, Scheme}; +use test_programs::p3::{wit_future, wit_stream}; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + println!("Called _start"); + { + let headers = Headers::from_list(&[( + "Content-Type".to_string(), + "application/json".to_string().into_bytes(), + )]) + .unwrap(); + let (mut contents_tx, contents_rx) = wit_stream::new(); + let (_, trailers_rx) = wit_future::new(); + let (request, _) = Request::new(headers, Some(contents_rx), trailers_rx, None); + + request.set_method(&Method::Get).expect("setting method"); + request + .set_scheme(Some(&Scheme::Https)) + .expect("setting scheme"); + request + .set_authority(Some("www.example.com")) + .expect("setting authority"); + contents_tx.send(b"request-body".to_vec()).await.unwrap(); + } + { + let headers = Headers::from_list(&[( + "Content-Type".to_string(), + "application/text".to_string().into_bytes(), + )]) + .unwrap(); + let (mut contents_tx, contents_rx) = wit_stream::new(); + let (_, trailers_rx) = wit_future::new(); + let _ = Response::new(headers, Some(contents_rx), trailers_rx); + contents_tx.send(b"response-body".to_vec()).await.unwrap(); + } + + { + let (_, trailers_rx) = wit_future::new(); + let (req, _) = Request::new(Fields::new(), None, trailers_rx, None); + + assert!(req + .set_method(&Method::Other("invalid method".to_string())) + .is_err()); + + assert!(req.set_authority(Some("bad-port:99999")).is_err()); + assert!(req.set_authority(Some("bad-\nhost")).is_err()); + assert!(req.set_authority(Some("too-many-ports:80:80:80")).is_err()); + + assert!(req + .set_scheme(Some(&Scheme::Other("bad\nscheme".to_string()))) + .is_err()); + + assert!(req.set_path_with_query(Some("/bad\npath")).is_err()); + } + + println!("Done"); + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_timeout.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_timeout.rs new file mode 100644 index 0000000000..c3f969c11f --- /dev/null +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_timeout.rs @@ -0,0 +1,43 @@ +use anyhow::Context; +use std::net::SocketAddr; +use std::time::Duration; +use test_programs::p3::wasi::http::types::{ErrorCode, Method, Scheme}; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + // This address inside the TEST-NET-3 address block is expected to time out. + let addr = SocketAddr::from(([203, 0, 113, 12], 80)).to_string(); + let timeout = Duration::from_millis(200); + let connect_timeout: Option = Some(timeout.as_nanos() as u64); + let res = test_programs::p3::http::request( + Method::Get, + Scheme::Http, + &addr, + "/get?some=arg&goes=here", + None, + None, + connect_timeout, + None, + None, + ) + .await + .context("/get"); + + assert!(res.is_err()); + let err = res.unwrap_err(); + assert!( + matches!( + err.downcast_ref::(), + Some(ErrorCode::ConnectionTimeout | ErrorCode::ConnectionRefused) + ), + "expected connection timeout: {err:?}" + ); + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_unknown_method.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_unknown_method.rs new file mode 100644 index 0000000000..9d926026cb --- /dev/null +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_unknown_method.rs @@ -0,0 +1,28 @@ +use test_programs::p3::wasi::http::types::{Method, Scheme}; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + let res = test_programs::p3::http::request( + Method::Other("bad\nmethod".to_owned()), + Scheme::Http, + "localhost:3000", + "/", + None, + None, + None, + None, + None, + ) + .await; + + // This error arises from input validation in the `set_method` function on `OutgoingRequest`. + assert_eq!(res.unwrap_err().to_string(), "failed to set method"); + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_unsupported_scheme.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_unsupported_scheme.rs new file mode 100644 index 0000000000..20f01700f1 --- /dev/null +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_unsupported_scheme.rs @@ -0,0 +1,32 @@ +use test_programs::p3::wasi::http::types::{ErrorCode, Method, Scheme}; + +struct Component; + +test_programs::p3::export!(Component); + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + let res = test_programs::p3::http::request( + Method::Get, + Scheme::Other("WS".to_owned()), + "localhost:3000", + "/", + None, + None, + None, + None, + None, + ) + .await; + + assert!(matches!( + res.unwrap_err() + .downcast::() + .expect("expected a wasi-http ErrorCode"), + ErrorCode::HttpProtocolError, + )); + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/p3/http.rs b/crates/test-programs/src/p3/http.rs new file mode 100644 index 0000000000..c30e7b787b --- /dev/null +++ b/crates/test-programs/src/p3/http.rs @@ -0,0 +1,135 @@ +use core::fmt; + +use anyhow::{anyhow, Context as _, Result}; +use futures::{try_join, SinkExt as _, TryStreamExt as _}; + +use crate::p3::wasi::http::{handler, types}; +use crate::p3::{wit_future, wit_stream}; + +pub struct Response { + pub status: types::StatusCode, + pub headers: Vec<(String, Vec)>, + pub body: Vec, + pub trailers: Option)>>, +} +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.field("trailers", &self.trailers); + out.finish() + } +} + +impl Response { + pub fn header(&self, name: &str) -> Option<&Vec> { + self.headers + .iter() + .find_map(|(k, v)| if k == name { Some(v) } else { None }) + } +} + +pub async fn request( + method: types::Method, + scheme: types::Scheme, + authority: &str, + path_with_query: &str, + body: Option<&[u8]>, + additional_headers: Option<&[(String, Vec)]>, + connect_timeout: Option, + first_by_timeout: Option, + between_bytes_timeout: Option, +) -> Result { + fn header_val(v: &str) -> Vec { + v.to_string().into_bytes() + } + let headers = types::Headers::from_list( + &[ + &[ + ("User-agent".to_string(), header_val("WASI-HTTP/0.0.1")), + ("Content-type".to_string(), header_val("application/json")), + ], + additional_headers.unwrap_or(&[]), + ] + .concat(), + )?; + + let options = types::RequestOptions::new(); + options + .set_connect_timeout(connect_timeout) + .map_err(|_err| anyhow!("failed to set connect_timeout"))?; + options + .set_first_byte_timeout(first_by_timeout) + .map_err(|_err| anyhow!("failed to set first_byte_timeout"))?; + options + .set_between_bytes_timeout(between_bytes_timeout) + .map_err(|_err| anyhow!("failed to set between_bytes_timeout"))?; + + let (mut contents_tx, contents_rx) = wit_stream::new(); + let (trailers_tx, trailers_rx) = wit_future::new(); + let (request, transmit) = + types::Request::new(headers, Some(contents_rx), trailers_rx, Some(options)); + + request + .set_method(&method) + .map_err(|()| anyhow!("failed to set method"))?; + request + .set_scheme(Some(&scheme)) + .map_err(|()| anyhow!("failed to set scheme"))?; + request + .set_authority(Some(authority)) + .map_err(|()| anyhow!("failed to set authority"))?; + request + .set_path_with_query(Some(&path_with_query)) + .map_err(|()| anyhow!("failed to set path_with_query"))?; + + let ((), (), response) = try_join!( + async { + if let Some(buf) = body { + contents_tx + .send(buf.into()) + .await + .expect("failed to send body content chunk"); + } + drop(contents_tx); + trailers_tx.write(Ok(None)).await; + anyhow::Ok(()) + }, + async { + transmit + .await + .expect("transmit sender dropped") + .expect("failed to receive request transmit result") + .context("failed to transmit request")?; + Ok(()) + }, + async { + let response = handler::handle(request).await?; + let status = response.status_code(); + let headers = response.headers().entries(); + + let (body, trailers) = response.body().expect("failed to get response body"); + let body = body.try_collect::>().await?; + let body = body.concat(); + let trailers = trailers + .await + .expect("trailers sender dropped") + .expect("failed to receive response trailers result") + .context("failed to read body")?; + let trailers = trailers.map(|trailers| trailers.entries()); + Ok(Response { + status, + headers, + body, + trailers, + }) + }, + )?; + Ok(response) +} diff --git a/crates/test-programs/src/p3/mod.rs b/crates/test-programs/src/p3/mod.rs index 9a79f7788f..1514ac276d 100644 --- a/crates/test-programs/src/p3/mod.rs +++ b/crates/test-programs/src/p3/mod.rs @@ -1,8 +1,19 @@ +pub mod http; pub mod sockets; wit_bindgen::generate!({ - path: "../wasi/src/p3/wit", - world: "wasi:cli/command", + inline: " + package wasmtime:test; + + world testp3 { + include wasi:cli/imports@0.3.0; + include wasi:http/imports@0.3.0-draft; + + export wasi:cli/run@0.3.0; + } + ", + path: "../wasi-http/src/p3/wit", + world: "wasmtime:test/testp3", default_bindings_module: "test_programs::p3", pub_export_macro: true, async: { @@ -10,6 +21,7 @@ wit_bindgen::generate!({ "wasi:clocks/monotonic-clock@0.3.0#wait-for", "wasi:clocks/monotonic-clock@0.3.0#wait-until", "wasi:filesystem/types@0.3.0#[method]descriptor.write-via-stream", + "wasi:http/handler@0.3.0-draft#handle", "wasi:sockets/ip-name-lookup@0.3.0#resolve-addresses", "wasi:sockets/types@0.3.0#[method]tcp-socket.connect", "wasi:sockets/types@0.3.0#[method]tcp-socket.send", @@ -22,3 +34,37 @@ wit_bindgen::generate!({ }, generate_all }); + +pub mod proxy { + wit_bindgen::generate!({ + inline: " + package wasmtime:test; + + world proxyp3 { + include wasi:http/proxy@0.3.0-draft; + } + ", + path: "../wasi-http/src/p3/wit", + world: "wasmtime:test/proxyp3", + default_bindings_module: "test_programs::p3::proxy", + pub_export_macro: true, + async: { + imports: [ + "wasi:http/handler@0.3.0-draft#handle", + ], + exports: [ + "wasi:http/handler@0.3.0-draft#handle", + ], + }, + with: { + "wasi:http/handler@0.3.0-draft": generate, + "wasi:http/types@0.3.0-draft": crate::p3::wasi::http::types, + "wasi:random/random@0.3.0": crate::p3::wasi::random::random, + "wasi:cli/stdout@0.3.0": crate::p3::wasi::cli::stdout, + "wasi:cli/stderr@0.3.0": crate::p3::wasi::cli::stderr, + "wasi:cli/stdin@0.3.0": crate::p3::wasi::cli::stdin, + "wasi:clocks/monotonic-clock@0.3.0": crate::p3::wasi::clocks::monotonic_clock, + "wasi:clocks/wall-clock@0.3.0": crate::p3::wasi::clocks::wall_clock, + }, + }); +} diff --git a/crates/wasi-http/Cargo.toml b/crates/wasi-http/Cargo.toml index 624e69773b..18ba36a2a4 100644 --- a/crates/wasi-http/Cargo.toml +++ b/crates/wasi-http/Cargo.toml @@ -15,7 +15,7 @@ workspace = true anyhow = { workspace = true } async-trait = { workspace = true } bytes = { workspace = true } -futures = { workspace = true, default-features = false } +futures = { workspace = true, default-features = false, features = ["async-await"] } hyper = { workspace = true, features = ["full"] } tokio = { workspace = true, features = [ "net", @@ -41,3 +41,10 @@ tokio = { workspace = true, features = ['macros'] } futures = { workspace = true, default-features = false, features = ['alloc'] } sha2 = "0.10.2" base64 = { workspace = true } + +[features] +default = ["p3"] +p3 = [ + "wasmtime-wasi/p3", + "wasmtime/component-model-async", +] diff --git a/crates/wasi-http/src/lib.rs b/crates/wasi-http/src/lib.rs index c9bd946c2e..96d129a32f 100644 --- a/crates/wasi-http/src/lib.rs +++ b/crates/wasi-http/src/lib.rs @@ -229,6 +229,9 @@ pub mod types; pub mod bindings; +#[cfg(feature = "p3")] +pub mod p3; + pub use crate::error::{ http_request_error, hyper_request_error, hyper_response_error, HttpError, HttpResult, }; diff --git a/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/command.wit b/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/command.wit deleted file mode 100644 index 3a81766d64..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/command.wit +++ /dev/null @@ -1,10 +0,0 @@ -package wasi:cli@0.2.3; - -@since(version = 0.2.0) -world command { - @since(version = 0.2.0) - include imports; - - @since(version = 0.2.0) - export run; -} diff --git a/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/imports.wit b/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/imports.wit deleted file mode 100644 index 8b4e3975ec..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/imports.wit +++ /dev/null @@ -1,36 +0,0 @@ -package wasi:cli@0.2.3; - -@since(version = 0.2.0) -world imports { - @since(version = 0.2.0) - include wasi:clocks/imports@0.2.3; - @since(version = 0.2.0) - include wasi:filesystem/imports@0.2.3; - @since(version = 0.2.0) - include wasi:sockets/imports@0.2.3; - @since(version = 0.2.0) - include wasi:random/imports@0.2.3; - @since(version = 0.2.0) - include wasi:io/imports@0.2.3; - - @since(version = 0.2.0) - import environment; - @since(version = 0.2.0) - import exit; - @since(version = 0.2.0) - import stdin; - @since(version = 0.2.0) - import stdout; - @since(version = 0.2.0) - import stderr; - @since(version = 0.2.0) - import terminal-input; - @since(version = 0.2.0) - import terminal-output; - @since(version = 0.2.0) - import terminal-stdin; - @since(version = 0.2.0) - import terminal-stdout; - @since(version = 0.2.0) - import terminal-stderr; -} diff --git a/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/stdio.wit b/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/stdio.wit deleted file mode 100644 index 1b54f5318a..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/stdio.wit +++ /dev/null @@ -1,26 +0,0 @@ -@since(version = 0.2.0) -interface stdin { - @since(version = 0.2.0) - use wasi:io/streams@0.2.3.{input-stream}; - - @since(version = 0.2.0) - get-stdin: func() -> input-stream; -} - -@since(version = 0.2.0) -interface stdout { - @since(version = 0.2.0) - use wasi:io/streams@0.2.3.{output-stream}; - - @since(version = 0.2.0) - get-stdout: func() -> output-stream; -} - -@since(version = 0.2.0) -interface stderr { - @since(version = 0.2.0) - use wasi:io/streams@0.2.3.{output-stream}; - - @since(version = 0.2.0) - get-stderr: func() -> output-stream; -} diff --git a/crates/wasi-http/src/p2/wit/deps/filesystem@v0.2.3/world.wit b/crates/wasi-http/src/p2/wit/deps/filesystem@v0.2.3/world.wit deleted file mode 100644 index 29405bc2cc..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/filesystem@v0.2.3/world.wit +++ /dev/null @@ -1,9 +0,0 @@ -package wasi:filesystem@0.2.3; - -@since(version = 0.2.0) -world imports { - @since(version = 0.2.0) - import types; - @since(version = 0.2.0) - import preopens; -} diff --git a/crates/wasi-http/src/p2/wit/deps/http@v0.2.3/handler.wit b/crates/wasi-http/src/p2/wit/deps/http@v0.2.3/handler.wit deleted file mode 100644 index 6a6c62966f..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/http@v0.2.3/handler.wit +++ /dev/null @@ -1,49 +0,0 @@ -/// This interface defines a handler of incoming HTTP Requests. It should -/// be exported by components which can respond to HTTP Requests. -@since(version = 0.2.0) -interface incoming-handler { - @since(version = 0.2.0) - use types.{incoming-request, response-outparam}; - - /// This function is invoked with an incoming HTTP Request, and a resource - /// `response-outparam` which provides the capability to reply with an HTTP - /// Response. The response is sent by calling the `response-outparam.set` - /// method, which allows execution to continue after the response has been - /// sent. This enables both streaming to the response body, and performing other - /// work. - /// - /// The implementor of this function must write a response to the - /// `response-outparam` before returning, or else the caller will respond - /// with an error on its behalf. - @since(version = 0.2.0) - handle: func( - request: incoming-request, - response-out: response-outparam - ); -} - -/// This interface defines a handler of outgoing HTTP Requests. It should be -/// imported by components which wish to make HTTP Requests. -@since(version = 0.2.0) -interface outgoing-handler { - @since(version = 0.2.0) - use types.{ - outgoing-request, request-options, future-incoming-response, error-code - }; - - /// This function is invoked with an outgoing HTTP Request, and it returns - /// a resource `future-incoming-response` which represents an HTTP Response - /// which may arrive in the future. - /// - /// The `options` argument accepts optional parameters for the HTTP - /// protocol's transport layer. - /// - /// This function may return an error if the `outgoing-request` is invalid - /// or not allowed to be made. Otherwise, protocol errors are reported - /// through the `future-incoming-response`. - @since(version = 0.2.0) - handle: func( - request: outgoing-request, - options: option - ) -> result; -} diff --git a/crates/wasi-http/src/p2/wit/deps/http@v0.2.3/types.wit b/crates/wasi-http/src/p2/wit/deps/http@v0.2.3/types.wit deleted file mode 100644 index 2498f180ad..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/http@v0.2.3/types.wit +++ /dev/null @@ -1,673 +0,0 @@ -/// This interface defines all of the types and methods for implementing -/// HTTP Requests and Responses, both incoming and outgoing, as well as -/// their headers, trailers, and bodies. -@since(version = 0.2.0) -interface types { - @since(version = 0.2.0) - use wasi:clocks/monotonic-clock@0.2.3.{duration}; - @since(version = 0.2.0) - use wasi:io/streams@0.2.3.{input-stream, output-stream}; - @since(version = 0.2.0) - use wasi:io/error@0.2.3.{error as io-error}; - @since(version = 0.2.0) - use wasi:io/poll@0.2.3.{pollable}; - - /// This type corresponds to HTTP standard Methods. - @since(version = 0.2.0) - variant method { - get, - head, - post, - put, - delete, - connect, - options, - trace, - patch, - other(string) - } - - /// This type corresponds to HTTP standard Related Schemes. - @since(version = 0.2.0) - variant scheme { - HTTP, - HTTPS, - other(string) - } - - /// These cases are inspired by the IANA HTTP Proxy Error Types: - /// - @since(version = 0.2.0) - variant error-code { - DNS-timeout, - DNS-error(DNS-error-payload), - destination-not-found, - destination-unavailable, - destination-IP-prohibited, - destination-IP-unroutable, - connection-refused, - connection-terminated, - connection-timeout, - connection-read-timeout, - connection-write-timeout, - connection-limit-reached, - TLS-protocol-error, - TLS-certificate-error, - TLS-alert-received(TLS-alert-received-payload), - HTTP-request-denied, - HTTP-request-length-required, - HTTP-request-body-size(option), - HTTP-request-method-invalid, - HTTP-request-URI-invalid, - HTTP-request-URI-too-long, - HTTP-request-header-section-size(option), - HTTP-request-header-size(option), - HTTP-request-trailer-section-size(option), - HTTP-request-trailer-size(field-size-payload), - HTTP-response-incomplete, - HTTP-response-header-section-size(option), - HTTP-response-header-size(field-size-payload), - HTTP-response-body-size(option), - HTTP-response-trailer-section-size(option), - HTTP-response-trailer-size(field-size-payload), - HTTP-response-transfer-coding(option), - HTTP-response-content-coding(option), - HTTP-response-timeout, - HTTP-upgrade-failed, - HTTP-protocol-error, - loop-detected, - configuration-error, - /// This is a catch-all error for anything that doesn't fit cleanly into a - /// more specific case. It also includes an optional string for an - /// unstructured description of the error. Users should not depend on the - /// string for diagnosing errors, as it's not required to be consistent - /// between implementations. - internal-error(option) - } - - /// Defines the case payload type for `DNS-error` above: - @since(version = 0.2.0) - record DNS-error-payload { - rcode: option, - info-code: option - } - - /// Defines the case payload type for `TLS-alert-received` above: - @since(version = 0.2.0) - record TLS-alert-received-payload { - alert-id: option, - alert-message: option - } - - /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: - @since(version = 0.2.0) - record field-size-payload { - field-name: option, - field-size: option - } - - /// Attempts to extract a http-related `error` from the wasi:io `error` - /// provided. - /// - /// Stream operations which return - /// `wasi:io/stream/stream-error::last-operation-failed` have a payload of - /// type `wasi:io/error/error` with more information about the operation - /// that failed. This payload can be passed through to this function to see - /// if there's http-related information about the error to return. - /// - /// Note that this function is fallible because not all io-errors are - /// http-related errors. - @since(version = 0.2.0) - http-error-code: func(err: borrow) -> option; - - /// This type enumerates the different kinds of errors that may occur when - /// setting or appending to a `fields` resource. - @since(version = 0.2.0) - variant header-error { - /// This error indicates that a `field-name` or `field-value` was - /// syntactically invalid when used with an operation that sets headers in a - /// `fields`. - invalid-syntax, - - /// This error indicates that a forbidden `field-name` was used when trying - /// to set a header in a `fields`. - forbidden, - - /// This error indicates that the operation on the `fields` was not - /// permitted because the fields are immutable. - immutable, - } - - /// Field names are always strings. - /// - /// Field names should always be treated as case insensitive by the `fields` - /// resource for the purposes of equality checking. - @since(version = 0.2.1) - type field-name = field-key; - - /// Field keys are always strings. - /// - /// Field keys should always be treated as case insensitive by the `fields` - /// resource for the purposes of equality checking. - /// - /// # Deprecation - /// - /// This type has been deprecated in favor of the `field-name` type. - @since(version = 0.2.0) - @deprecated(version = 0.2.2) - type field-key = string; - - /// Field values should always be ASCII strings. However, in - /// reality, HTTP implementations often have to interpret malformed values, - /// so they are provided as a list of bytes. - @since(version = 0.2.0) - type field-value = list; - - /// This following block defines the `fields` resource which corresponds to - /// HTTP standard Fields. Fields are a common representation used for both - /// Headers and Trailers. - /// - /// A `fields` may be mutable or immutable. A `fields` created using the - /// constructor, `from-list`, or `clone` will be mutable, but a `fields` - /// resource given by other means (including, but not limited to, - /// `incoming-request.headers`, `outgoing-request.headers`) might be be - /// immutable. In an immutable fields, the `set`, `append`, and `delete` - /// operations will fail with `header-error.immutable`. - @since(version = 0.2.0) - resource fields { - - /// Construct an empty HTTP Fields. - /// - /// The resulting `fields` is mutable. - @since(version = 0.2.0) - constructor(); - - /// Construct an HTTP Fields. - /// - /// The resulting `fields` is mutable. - /// - /// The list represents each name-value pair in the Fields. Names - /// which have multiple values are represented by multiple entries in this - /// list with the same name. - /// - /// The tuple is a pair of the field name, represented as a string, and - /// Value, represented as a list of bytes. - /// - /// An error result will be returned if any `field-name` or `field-value` is - /// syntactically invalid, or if a field is forbidden. - @since(version = 0.2.0) - from-list: static func( - entries: list> - ) -> result; - - /// Get all of the values corresponding to a name. If the name is not present - /// in this `fields` or is syntactically invalid, an empty list is returned. - /// However, if the name is present but empty, this is represented by a list - /// with one or more empty field-values present. - @since(version = 0.2.0) - get: func(name: field-name) -> list; - - /// Returns `true` when the name is present in this `fields`. If the name is - /// syntactically invalid, `false` is returned. - @since(version = 0.2.0) - has: func(name: field-name) -> bool; - - /// Set all of the values for a name. Clears any existing values for that - /// name, if they have been set. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - /// - /// Fails with `header-error.invalid-syntax` if the `field-name` or any of - /// the `field-value`s are syntactically invalid. - @since(version = 0.2.0) - set: func(name: field-name, value: list) -> result<_, header-error>; - - /// Delete all values for a name. Does nothing if no values for the name - /// exist. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - /// - /// Fails with `header-error.invalid-syntax` if the `field-name` is - /// syntactically invalid. - @since(version = 0.2.0) - delete: func(name: field-name) -> result<_, header-error>; - - /// Append a value for a name. Does not change or delete any existing - /// values for that name. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - /// - /// Fails with `header-error.invalid-syntax` if the `field-name` or - /// `field-value` are syntactically invalid. - @since(version = 0.2.0) - append: func(name: field-name, value: field-value) -> result<_, header-error>; - - /// Retrieve the full set of names and values in the Fields. Like the - /// constructor, the list represents each name-value pair. - /// - /// The outer list represents each name-value pair in the Fields. Names - /// which have multiple values are represented by multiple entries in this - /// list with the same name. - /// - /// The names and values are always returned in the original casing and in - /// the order in which they will be serialized for transport. - @since(version = 0.2.0) - entries: func() -> list>; - - /// Make a deep copy of the Fields. Equivalent in behavior to calling the - /// `fields` constructor on the return value of `entries`. The resulting - /// `fields` is mutable. - @since(version = 0.2.0) - clone: func() -> fields; - } - - /// Headers is an alias for Fields. - @since(version = 0.2.0) - type headers = fields; - - /// Trailers is an alias for Fields. - @since(version = 0.2.0) - type trailers = fields; - - /// Represents an incoming HTTP Request. - @since(version = 0.2.0) - resource incoming-request { - - /// Returns the method of the incoming request. - @since(version = 0.2.0) - method: func() -> method; - - /// Returns the path with query parameters from the request, as a string. - @since(version = 0.2.0) - path-with-query: func() -> option; - - /// Returns the protocol scheme from the request. - @since(version = 0.2.0) - scheme: func() -> option; - - /// Returns the authority of the Request's target URI, if present. - @since(version = 0.2.0) - authority: func() -> option; - - /// Get the `headers` associated with the request. - /// - /// The returned `headers` resource is immutable: `set`, `append`, and - /// `delete` operations will fail with `header-error.immutable`. - /// - /// The `headers` returned are a child resource: it must be dropped before - /// the parent `incoming-request` is dropped. Dropping this - /// `incoming-request` before all children are dropped will trap. - @since(version = 0.2.0) - headers: func() -> headers; - - /// Gives the `incoming-body` associated with this request. Will only - /// return success at most once, and subsequent calls will return error. - @since(version = 0.2.0) - consume: func() -> result; - } - - /// Represents an outgoing HTTP Request. - @since(version = 0.2.0) - resource outgoing-request { - - /// Construct a new `outgoing-request` with a default `method` of `GET`, and - /// `none` values for `path-with-query`, `scheme`, and `authority`. - /// - /// * `headers` is the HTTP Headers for the Request. - /// - /// It is possible to construct, or manipulate with the accessor functions - /// below, an `outgoing-request` with an invalid combination of `scheme` - /// and `authority`, or `headers` which are not permitted to be sent. - /// It is the obligation of the `outgoing-handler.handle` implementation - /// to reject invalid constructions of `outgoing-request`. - @since(version = 0.2.0) - constructor( - headers: headers - ); - - /// Returns the resource corresponding to the outgoing Body for this - /// Request. - /// - /// Returns success on the first call: the `outgoing-body` resource for - /// this `outgoing-request` can be retrieved at most once. Subsequent - /// calls will return error. - @since(version = 0.2.0) - body: func() -> result; - - /// Get the Method for the Request. - @since(version = 0.2.0) - method: func() -> method; - /// Set the Method for the Request. Fails if the string present in a - /// `method.other` argument is not a syntactically valid method. - @since(version = 0.2.0) - set-method: func(method: method) -> result; - - /// Get the combination of the HTTP Path and Query for the Request. - /// When `none`, this represents an empty Path and empty Query. - @since(version = 0.2.0) - path-with-query: func() -> option; - /// Set the combination of the HTTP Path and Query for the Request. - /// When `none`, this represents an empty Path and empty Query. Fails is the - /// string given is not a syntactically valid path and query uri component. - @since(version = 0.2.0) - set-path-with-query: func(path-with-query: option) -> result; - - /// Get the HTTP Related Scheme for the Request. When `none`, the - /// implementation may choose an appropriate default scheme. - @since(version = 0.2.0) - scheme: func() -> option; - /// Set the HTTP Related Scheme for the Request. When `none`, the - /// implementation may choose an appropriate default scheme. Fails if the - /// string given is not a syntactically valid uri scheme. - @since(version = 0.2.0) - set-scheme: func(scheme: option) -> result; - - /// Get the authority of the Request's target URI. A value of `none` may be used - /// with Related Schemes which do not require an authority. The HTTP and - /// HTTPS schemes always require an authority. - @since(version = 0.2.0) - authority: func() -> option; - /// Set the authority of the Request's target URI. A value of `none` may be used - /// with Related Schemes which do not require an authority. The HTTP and - /// HTTPS schemes always require an authority. Fails if the string given is - /// not a syntactically valid URI authority. - @since(version = 0.2.0) - set-authority: func(authority: option) -> result; - - /// Get the headers associated with the Request. - /// - /// The returned `headers` resource is immutable: `set`, `append`, and - /// `delete` operations will fail with `header-error.immutable`. - /// - /// This headers resource is a child: it must be dropped before the parent - /// `outgoing-request` is dropped, or its ownership is transferred to - /// another component by e.g. `outgoing-handler.handle`. - @since(version = 0.2.0) - headers: func() -> headers; - } - - /// Parameters for making an HTTP Request. Each of these parameters is - /// currently an optional timeout applicable to the transport layer of the - /// HTTP protocol. - /// - /// These timeouts are separate from any the user may use to bound a - /// blocking call to `wasi:io/poll.poll`. - @since(version = 0.2.0) - resource request-options { - /// Construct a default `request-options` value. - @since(version = 0.2.0) - constructor(); - - /// The timeout for the initial connect to the HTTP Server. - @since(version = 0.2.0) - connect-timeout: func() -> option; - - /// Set the timeout for the initial connect to the HTTP Server. An error - /// return value indicates that this timeout is not supported. - @since(version = 0.2.0) - set-connect-timeout: func(duration: option) -> result; - - /// The timeout for receiving the first byte of the Response body. - @since(version = 0.2.0) - first-byte-timeout: func() -> option; - - /// Set the timeout for receiving the first byte of the Response body. An - /// error return value indicates that this timeout is not supported. - @since(version = 0.2.0) - set-first-byte-timeout: func(duration: option) -> result; - - /// The timeout for receiving subsequent chunks of bytes in the Response - /// body stream. - @since(version = 0.2.0) - between-bytes-timeout: func() -> option; - - /// Set the timeout for receiving subsequent chunks of bytes in the Response - /// body stream. An error return value indicates that this timeout is not - /// supported. - @since(version = 0.2.0) - set-between-bytes-timeout: func(duration: option) -> result; - } - - /// Represents the ability to send an HTTP Response. - /// - /// This resource is used by the `wasi:http/incoming-handler` interface to - /// allow a Response to be sent corresponding to the Request provided as the - /// other argument to `incoming-handler.handle`. - @since(version = 0.2.0) - resource response-outparam { - - /// Set the value of the `response-outparam` to either send a response, - /// or indicate an error. - /// - /// This method consumes the `response-outparam` to ensure that it is - /// called at most once. If it is never called, the implementation - /// will respond with an error. - /// - /// The user may provide an `error` to `response` to allow the - /// implementation determine how to respond with an HTTP error response. - @since(version = 0.2.0) - set: static func( - param: response-outparam, - response: result, - ); - } - - /// This type corresponds to the HTTP standard Status Code. - @since(version = 0.2.0) - type status-code = u16; - - /// Represents an incoming HTTP Response. - @since(version = 0.2.0) - resource incoming-response { - - /// Returns the status code from the incoming response. - @since(version = 0.2.0) - status: func() -> status-code; - - /// Returns the headers from the incoming response. - /// - /// The returned `headers` resource is immutable: `set`, `append`, and - /// `delete` operations will fail with `header-error.immutable`. - /// - /// This headers resource is a child: it must be dropped before the parent - /// `incoming-response` is dropped. - @since(version = 0.2.0) - headers: func() -> headers; - - /// Returns the incoming body. May be called at most once. Returns error - /// if called additional times. - @since(version = 0.2.0) - consume: func() -> result; - } - - /// Represents an incoming HTTP Request or Response's Body. - /// - /// A body has both its contents - a stream of bytes - and a (possibly - /// empty) set of trailers, indicating that the full contents of the - /// body have been received. This resource represents the contents as - /// an `input-stream` and the delivery of trailers as a `future-trailers`, - /// and ensures that the user of this interface may only be consuming either - /// the body contents or waiting on trailers at any given time. - @since(version = 0.2.0) - resource incoming-body { - - /// Returns the contents of the body, as a stream of bytes. - /// - /// Returns success on first call: the stream representing the contents - /// can be retrieved at most once. Subsequent calls will return error. - /// - /// The returned `input-stream` resource is a child: it must be dropped - /// before the parent `incoming-body` is dropped, or consumed by - /// `incoming-body.finish`. - /// - /// This invariant ensures that the implementation can determine whether - /// the user is consuming the contents of the body, waiting on the - /// `future-trailers` to be ready, or neither. This allows for network - /// backpressure is to be applied when the user is consuming the body, - /// and for that backpressure to not inhibit delivery of the trailers if - /// the user does not read the entire body. - @since(version = 0.2.0) - %stream: func() -> result; - - /// Takes ownership of `incoming-body`, and returns a `future-trailers`. - /// This function will trap if the `input-stream` child is still alive. - @since(version = 0.2.0) - finish: static func(this: incoming-body) -> future-trailers; - } - - /// Represents a future which may eventually return trailers, or an error. - /// - /// In the case that the incoming HTTP Request or Response did not have any - /// trailers, this future will resolve to the empty set of trailers once the - /// complete Request or Response body has been received. - @since(version = 0.2.0) - resource future-trailers { - - /// Returns a pollable which becomes ready when either the trailers have - /// been received, or an error has occurred. When this pollable is ready, - /// the `get` method will return `some`. - @since(version = 0.2.0) - subscribe: func() -> pollable; - - /// Returns the contents of the trailers, or an error which occurred, - /// once the future is ready. - /// - /// The outer `option` represents future readiness. Users can wait on this - /// `option` to become `some` using the `subscribe` method. - /// - /// The outer `result` is used to retrieve the trailers or error at most - /// once. It will be success on the first call in which the outer option - /// is `some`, and error on subsequent calls. - /// - /// The inner `result` represents that either the HTTP Request or Response - /// body, as well as any trailers, were received successfully, or that an - /// error occurred receiving them. The optional `trailers` indicates whether - /// or not trailers were present in the body. - /// - /// When some `trailers` are returned by this method, the `trailers` - /// resource is immutable, and a child. Use of the `set`, `append`, or - /// `delete` methods will return an error, and the resource must be - /// dropped before the parent `future-trailers` is dropped. - @since(version = 0.2.0) - get: func() -> option, error-code>>>; - } - - /// Represents an outgoing HTTP Response. - @since(version = 0.2.0) - resource outgoing-response { - - /// Construct an `outgoing-response`, with a default `status-code` of `200`. - /// If a different `status-code` is needed, it must be set via the - /// `set-status-code` method. - /// - /// * `headers` is the HTTP Headers for the Response. - @since(version = 0.2.0) - constructor(headers: headers); - - /// Get the HTTP Status Code for the Response. - @since(version = 0.2.0) - status-code: func() -> status-code; - - /// Set the HTTP Status Code for the Response. Fails if the status-code - /// given is not a valid http status code. - @since(version = 0.2.0) - set-status-code: func(status-code: status-code) -> result; - - /// Get the headers associated with the Request. - /// - /// The returned `headers` resource is immutable: `set`, `append`, and - /// `delete` operations will fail with `header-error.immutable`. - /// - /// This headers resource is a child: it must be dropped before the parent - /// `outgoing-request` is dropped, or its ownership is transferred to - /// another component by e.g. `outgoing-handler.handle`. - @since(version = 0.2.0) - headers: func() -> headers; - - /// Returns the resource corresponding to the outgoing Body for this Response. - /// - /// Returns success on the first call: the `outgoing-body` resource for - /// this `outgoing-response` can be retrieved at most once. Subsequent - /// calls will return error. - @since(version = 0.2.0) - body: func() -> result; - } - - /// Represents an outgoing HTTP Request or Response's Body. - /// - /// A body has both its contents - a stream of bytes - and a (possibly - /// empty) set of trailers, inducating the full contents of the body - /// have been sent. This resource represents the contents as an - /// `output-stream` child resource, and the completion of the body (with - /// optional trailers) with a static function that consumes the - /// `outgoing-body` resource, and ensures that the user of this interface - /// may not write to the body contents after the body has been finished. - /// - /// If the user code drops this resource, as opposed to calling the static - /// method `finish`, the implementation should treat the body as incomplete, - /// and that an error has occurred. The implementation should propagate this - /// error to the HTTP protocol by whatever means it has available, - /// including: corrupting the body on the wire, aborting the associated - /// Request, or sending a late status code for the Response. - @since(version = 0.2.0) - resource outgoing-body { - - /// Returns a stream for writing the body contents. - /// - /// The returned `output-stream` is a child resource: it must be dropped - /// before the parent `outgoing-body` resource is dropped (or finished), - /// otherwise the `outgoing-body` drop or `finish` will trap. - /// - /// Returns success on the first call: the `output-stream` resource for - /// this `outgoing-body` may be retrieved at most once. Subsequent calls - /// will return error. - @since(version = 0.2.0) - write: func() -> result; - - /// Finalize an outgoing body, optionally providing trailers. This must be - /// called to signal that the response is complete. If the `outgoing-body` - /// is dropped without calling `outgoing-body.finalize`, the implementation - /// should treat the body as corrupted. - /// - /// Fails if the body's `outgoing-request` or `outgoing-response` was - /// constructed with a Content-Length header, and the contents written - /// to the body (via `write`) does not match the value given in the - /// Content-Length. - @since(version = 0.2.0) - finish: static func( - this: outgoing-body, - trailers: option - ) -> result<_, error-code>; - } - - /// Represents a future which may eventually return an incoming HTTP - /// Response, or an error. - /// - /// This resource is returned by the `wasi:http/outgoing-handler` interface to - /// provide the HTTP Response corresponding to the sent Request. - @since(version = 0.2.0) - resource future-incoming-response { - /// Returns a pollable which becomes ready when either the Response has - /// been received, or an error has occurred. When this pollable is ready, - /// the `get` method will return `some`. - @since(version = 0.2.0) - subscribe: func() -> pollable; - - /// Returns the incoming HTTP Response, or an error, once one is ready. - /// - /// The outer `option` represents future readiness. Users can wait on this - /// `option` to become `some` using the `subscribe` method. - /// - /// The outer `result` is used to retrieve the response or error at most - /// once. It will be success on the first call in which the outer option - /// is `some`, and error on subsequent calls. - /// - /// The inner `result` represents that either the incoming HTTP Response - /// status and headers have received successfully, or that an error - /// occurred. Errors may also occur while consuming the response body, - /// but those will be reported by the `incoming-body` and its - /// `output-stream` child. - @since(version = 0.2.0) - get: func() -> option>>; - } -} diff --git a/crates/wasi-http/src/p2/wit/deps/io@v0.2.3/error.wit b/crates/wasi-http/src/p2/wit/deps/io@v0.2.3/error.wit deleted file mode 100644 index 97c6068779..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/io@v0.2.3/error.wit +++ /dev/null @@ -1,34 +0,0 @@ -package wasi:io@0.2.3; - -@since(version = 0.2.0) -interface error { - /// A resource which represents some error information. - /// - /// The only method provided by this resource is `to-debug-string`, - /// which provides some human-readable information about the error. - /// - /// In the `wasi:io` package, this resource is returned through the - /// `wasi:io/streams/stream-error` type. - /// - /// To provide more specific error information, other interfaces may - /// offer functions to "downcast" this error into more specific types. For example, - /// errors returned from streams derived from filesystem types can be described using - /// the filesystem's own error-code type. This is done using the function - /// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow` - /// parameter and returns an `option`. - /// - /// The set of functions which can "downcast" an `error` into a more - /// concrete type is open. - @since(version = 0.2.0) - resource error { - /// Returns a string that is suitable to assist humans in debugging - /// this error. - /// - /// WARNING: The returned string should not be consumed mechanically! - /// It may change across platforms, hosts, or other implementation - /// details. Parsing this string is a major platform-compatibility - /// hazard. - @since(version = 0.2.0) - to-debug-string: func() -> string; - } -} diff --git a/crates/wasi-http/src/p2/wit/deps/io@v0.2.3/poll.wit b/crates/wasi-http/src/p2/wit/deps/io@v0.2.3/poll.wit deleted file mode 100644 index 9bcbe8e036..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/io@v0.2.3/poll.wit +++ /dev/null @@ -1,47 +0,0 @@ -package wasi:io@0.2.3; - -/// A poll API intended to let users wait for I/O events on multiple handles -/// at once. -@since(version = 0.2.0) -interface poll { - /// `pollable` represents a single I/O event which may be ready, or not. - @since(version = 0.2.0) - resource pollable { - - /// Return the readiness of a pollable. This function never blocks. - /// - /// Returns `true` when the pollable is ready, and `false` otherwise. - @since(version = 0.2.0) - ready: func() -> bool; - - /// `block` returns immediately if the pollable is ready, and otherwise - /// blocks until ready. - /// - /// This function is equivalent to calling `poll.poll` on a list - /// containing only this pollable. - @since(version = 0.2.0) - block: func(); - } - - /// Poll for completion on a set of pollables. - /// - /// This function takes a list of pollables, which identify I/O sources of - /// interest, and waits until one or more of the events is ready for I/O. - /// - /// The result `list` contains one or more indices of handles in the - /// argument list that is ready for I/O. - /// - /// This function traps if either: - /// - the list is empty, or: - /// - the list contains more elements than can be indexed with a `u32` value. - /// - /// A timeout can be implemented by adding a pollable from the - /// wasi-clocks API to the list. - /// - /// This function does not return a `result`; polling in itself does not - /// do any I/O so it doesn't fail. If any of the I/O sources identified by - /// the pollables has an error, it is indicated by marking the source as - /// being ready for I/O. - @since(version = 0.2.0) - poll: func(in: list>) -> list; -} diff --git a/crates/wasi-http/src/p2/wit/deps/io@v0.2.3/streams.wit b/crates/wasi-http/src/p2/wit/deps/io@v0.2.3/streams.wit deleted file mode 100644 index 0de0846293..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/io@v0.2.3/streams.wit +++ /dev/null @@ -1,290 +0,0 @@ -package wasi:io@0.2.3; - -/// 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. -@since(version = 0.2.0) -interface streams { - @since(version = 0.2.0) - use error.{error}; - @since(version = 0.2.0) - use poll.{pollable}; - - /// An error for input-stream and output-stream operations. - @since(version = 0.2.0) - variant stream-error { - /// The last operation (a write or flush) failed before completion. - /// - /// More information is available in the `error` payload. - /// - /// After this, the stream will be closed. All future operations return - /// `stream-error::closed`. - last-operation-failed(error), - /// The stream is closed: no more input will be accepted by the - /// stream. A closed output-stream will return this error on all - /// future operations. - closed - } - - /// An input bytestream. - /// - /// `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` function to obtain a `pollable` which can be polled - /// for using `wasi:io/poll`. - @since(version = 0.2.0) - resource input-stream { - /// Perform a non-blocking read from the stream. - /// - /// When the source of a `read` is binary data, the bytes from the source - /// are returned verbatim. When the source of a `read` is known to the - /// implementation to be text, bytes containing the UTF-8 encoding of the - /// text are returned. - /// - /// This function returns a list of bytes containing the read data, - /// when successful. The returned list will contain up to `len` bytes; - /// it may return fewer than requested, but not more. The list is - /// empty when no bytes are available for reading at this time. The - /// pollable given by `subscribe` will be ready when more bytes are - /// available. - /// - /// This function fails with a `stream-error` when the operation - /// encounters an error, giving `last-operation-failed`, or when the - /// stream is closed, giving `closed`. - /// - /// When the caller gives a `len` of 0, it represents a request to - /// read 0 bytes. If the stream is still open, this call should - /// succeed and return an empty list, or otherwise fail with `closed`. - /// - /// 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. - @since(version = 0.2.0) - read: func( - /// The maximum number of bytes to read - len: u64 - ) -> result, stream-error>; - - /// Read bytes from a stream, after blocking until at least one byte can - /// be read. Except for blocking, behavior is identical to `read`. - @since(version = 0.2.0) - blocking-read: func( - /// The maximum number of bytes to read - len: u64 - ) -> result, stream-error>; - - /// Skip bytes from a stream. Returns number of bytes skipped. - /// - /// Behaves identical to `read`, except instead of returning a list - /// of bytes, returns the number of bytes consumed from the stream. - @since(version = 0.2.0) - skip: func( - /// The maximum number of bytes to skip. - len: u64, - ) -> result; - - /// Skip bytes from a stream, after blocking until at least one byte - /// can be skipped. Except for blocking behavior, identical to `skip`. - @since(version = 0.2.0) - blocking-skip: func( - /// The maximum number of bytes to skip. - len: u64, - ) -> result; - - /// Create a `pollable` which will resolve once either the specified stream - /// has bytes available to read or the other end of the stream has been - /// closed. - /// The created `pollable` is a child resource of the `input-stream`. - /// Implementations may trap if the `input-stream` is dropped before - /// all derived `pollable`s created with this function are dropped. - @since(version = 0.2.0) - subscribe: func() -> pollable; - } - - - /// An output bytestream. - /// - /// `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` function to obtain a `pollable` which can be - /// polled for using `wasi:io/poll`. - /// - /// Dropping an `output-stream` while there's still an active write in - /// progress may result in the data being lost. Before dropping the stream, - /// be sure to fully flush your writes. - @since(version = 0.2.0) - resource output-stream { - /// Check readiness for writing. This function never blocks. - /// - /// Returns the number of bytes permitted for the next call to `write`, - /// or an error. Calling `write` with more bytes than this function has - /// permitted will trap. - /// - /// When this function returns 0 bytes, the `subscribe` pollable will - /// become ready when this function will report at least 1 byte, or an - /// error. - @since(version = 0.2.0) - check-write: func() -> result; - - /// Perform a write. This function never blocks. - /// - /// When the destination of a `write` is binary data, the bytes from - /// `contents` are written verbatim. When the destination of a `write` is - /// known to the implementation to be text, the bytes of `contents` are - /// transcoded from UTF-8 into the encoding of the destination and then - /// written. - /// - /// Precondition: check-write gave permit of Ok(n) and contents has a - /// length of less than or equal to n. Otherwise, this function will trap. - /// - /// returns Err(closed) without writing if the stream has closed since - /// the last call to check-write provided a permit. - @since(version = 0.2.0) - write: func( - contents: list - ) -> result<_, stream-error>; - - /// Perform a write of up to 4096 bytes, and then flush the stream. Block - /// until all of these operations are complete, or an error occurs. - /// - /// This is a convenience wrapper around the use of `check-write`, - /// `subscribe`, `write`, and `flush`, and is implemented with the - /// following pseudo-code: - /// - /// ```text - /// let pollable = this.subscribe(); - /// while !contents.is_empty() { - /// // Wait for the stream to become writable - /// pollable.block(); - /// let Ok(n) = this.check-write(); // eliding error handling - /// let len = min(n, contents.len()); - /// let (chunk, rest) = contents.split_at(len); - /// this.write(chunk ); // eliding error handling - /// contents = rest; - /// } - /// this.flush(); - /// // Wait for completion of `flush` - /// pollable.block(); - /// // Check for any errors that arose during `flush` - /// let _ = this.check-write(); // eliding error handling - /// ``` - @since(version = 0.2.0) - blocking-write-and-flush: func( - contents: list - ) -> result<_, stream-error>; - - /// Request to flush buffered output. This function never blocks. - /// - /// This tells the output-stream that the caller intends any buffered - /// output to be flushed. the output which is expected to be flushed - /// is all that has been passed to `write` prior to this call. - /// - /// Upon calling this function, the `output-stream` will not accept any - /// writes (`check-write` will return `ok(0)`) until the flush has - /// completed. The `subscribe` pollable will become ready when the - /// flush has completed and the stream can accept more writes. - @since(version = 0.2.0) - flush: func() -> result<_, stream-error>; - - /// Request to flush buffered output, and block until flush completes - /// and stream is ready for writing again. - @since(version = 0.2.0) - blocking-flush: func() -> result<_, stream-error>; - - /// Create a `pollable` which will resolve once the output-stream - /// is ready for more writing, or an error has occurred. When this - /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an - /// error. - /// - /// If the stream 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. - @since(version = 0.2.0) - subscribe: func() -> pollable; - - /// Write zeroes to a stream. - /// - /// This should be used precisely like `write` with the exact same - /// preconditions (must use check-write first), but instead of - /// passing a list of bytes, you simply pass the number of zero-bytes - /// that should be written. - @since(version = 0.2.0) - write-zeroes: func( - /// The number of zero-bytes to write - len: u64 - ) -> result<_, stream-error>; - - /// Perform a write of up to 4096 zeroes, and then flush the stream. - /// Block until all of these operations are complete, or an error - /// occurs. - /// - /// This is a convenience wrapper around the use of `check-write`, - /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with - /// the following pseudo-code: - /// - /// ```text - /// let pollable = this.subscribe(); - /// while num_zeroes != 0 { - /// // Wait for the stream to become writable - /// pollable.block(); - /// let Ok(n) = this.check-write(); // eliding error handling - /// let len = min(n, num_zeroes); - /// this.write-zeroes(len); // eliding error handling - /// num_zeroes -= len; - /// } - /// this.flush(); - /// // Wait for completion of `flush` - /// pollable.block(); - /// // Check for any errors that arose during `flush` - /// let _ = this.check-write(); // eliding error handling - /// ``` - @since(version = 0.2.0) - blocking-write-zeroes-and-flush: func( - /// The number of zero-bytes to write - len: u64 - ) -> result<_, stream-error>; - - /// Read from one stream and write to another. - /// - /// The behavior of splice is equivalent to: - /// 1. calling `check-write` on the `output-stream` - /// 2. calling `read` on the `input-stream` with the smaller of the - /// `check-write` permitted length and the `len` provided to `splice` - /// 3. calling `write` on the `output-stream` with that read data. - /// - /// Any error reported by the call to `check-write`, `read`, or - /// `write` ends the splice and reports that error. - /// - /// This function returns the number of bytes transferred; it may be less - /// than `len`. - @since(version = 0.2.0) - splice: func( - /// The stream to read from - src: borrow, - /// The number of bytes to splice - len: u64, - ) -> result; - - /// Read from one stream and write to another, with blocking. - /// - /// This is similar to `splice`, except that it blocks until the - /// `output-stream` is ready for writing, and the `input-stream` - /// is ready for reading, before performing the `splice`. - @since(version = 0.2.0) - blocking-splice: func( - /// The stream to read from - src: borrow, - /// The number of bytes to splice - len: u64, - ) -> result; - } -} diff --git a/crates/wasi-http/src/p2/wit/deps/io@v0.2.3/world.wit b/crates/wasi-http/src/p2/wit/deps/io@v0.2.3/world.wit deleted file mode 100644 index f1d2102dca..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/io@v0.2.3/world.wit +++ /dev/null @@ -1,10 +0,0 @@ -package wasi:io@0.2.3; - -@since(version = 0.2.0) -world imports { - @since(version = 0.2.0) - import streams; - - @since(version = 0.2.0) - import poll; -} diff --git a/crates/wasi-http/src/p2/wit/deps/random@v0.2.3/world.wit b/crates/wasi-http/src/p2/wit/deps/random@v0.2.3/world.wit deleted file mode 100644 index 0c1218f36e..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/random@v0.2.3/world.wit +++ /dev/null @@ -1,13 +0,0 @@ -package wasi:random@0.2.3; - -@since(version = 0.2.0) -world imports { - @since(version = 0.2.0) - import random; - - @since(version = 0.2.0) - import insecure; - - @since(version = 0.2.0) - import insecure-seed; -} diff --git a/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/instance-network.wit b/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/instance-network.wit deleted file mode 100644 index 5f6e6c1cc9..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/instance-network.wit +++ /dev/null @@ -1,11 +0,0 @@ - -/// This interface provides a value-export of the default network handle.. -@since(version = 0.2.0) -interface instance-network { - @since(version = 0.2.0) - use network.{network}; - - /// Get a handle to the default network. - @since(version = 0.2.0) - instance-network: func() -> network; -} diff --git a/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/ip-name-lookup.wit b/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/ip-name-lookup.wit deleted file mode 100644 index c1d8a47c16..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/ip-name-lookup.wit +++ /dev/null @@ -1,56 +0,0 @@ -@since(version = 0.2.0) -interface ip-name-lookup { - @since(version = 0.2.0) - use wasi:io/poll@0.2.3.{pollable}; - @since(version = 0.2.0) - use network.{network, error-code, ip-address}; - - /// Resolve an internet host name to a list of IP addresses. - /// - /// Unicode domain names are automatically converted to ASCII using IDNA encoding. - /// If the input is an IP address string, the address is parsed and returned - /// as-is without making any external requests. - /// - /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. - /// - /// 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. - /// - /// # Typical errors - /// - `invalid-argument`: `name` is a syntactically invalid domain name or IP address. - /// - /// # References: - /// - - /// - - /// - - /// - - @since(version = 0.2.0) - resolve-addresses: func(network: borrow, name: string) -> result; - - @since(version = 0.2.0) - resource resolve-address-stream { - /// 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`. - /// - /// 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) - @since(version = 0.2.0) - resolve-next-address: func() -> result, error-code>; - - /// Create a `pollable` which will resolve once the stream is ready for I/O. - /// - /// Note: this function is here for WASI 0.2 only. - /// It's planned to be removed when `future` is natively supported in Preview3. - @since(version = 0.2.0) - subscribe: func() -> pollable; - } -} diff --git a/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/network.wit b/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/network.wit deleted file mode 100644 index f3f60a3709..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/network.wit +++ /dev/null @@ -1,169 +0,0 @@ -@since(version = 0.2.0) -interface network { - @unstable(feature = network-error-code) - use wasi:io/error@0.2.3.{error}; - - /// 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. - @since(version = 0.2.0) - resource 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` - /// - `concurrency-conflict` - /// - /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. - @since(version = 0.2.0) - enum error-code { - /// Unknown error - unknown, - - /// Access denied. - /// - /// POSIX equivalent: EACCES, EPERM - access-denied, - - /// The operation is not supported. - /// - /// POSIX equivalent: EOPNOTSUPP - not-supported, - - /// One of the arguments is invalid. - /// - /// POSIX equivalent: EINVAL - invalid-argument, - - /// 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. - /// - /// POSIX equivalent: EALREADY - 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, - - - /// The operation is not valid in the socket's current state. - invalid-state, - - /// A new socket resource could not be created because of a system limit. - new-socket-limit, - - /// 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 or because there are no ephemeral ports available. - address-in-use, - - /// The remote address is not reachable - remote-unreachable, - - - /// The TCP connection was forcefully rejected - connection-refused, - - /// The TCP connection was reset. - connection-reset, - - /// A TCP connection was aborted. - connection-aborted, - - - /// The size of a datagram sent to a UDP socket exceeded the maximum - /// supported size. - datagram-too-large, - - - /// 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, - } - - /// Attempts to extract a network-related `error-code` from the stream - /// `error` provided. - /// - /// Stream operations which return `stream-error::last-operation-failed` - /// have a payload with more information about the operation that failed. - /// This payload can be passed through to this function to see if there's - /// network-related information about the error to return. - /// - /// Note that this function is fallible because not all stream-related - /// errors are network-related errors. - @unstable(feature = network-error-code) - network-error-code: func(err: borrow) -> option; - - @since(version = 0.2.0) - enum ip-address-family { - /// Similar to `AF_INET` in POSIX. - ipv4, - - /// Similar to `AF_INET6` in POSIX. - ipv6, - } - - @since(version = 0.2.0) - type ipv4-address = tuple; - @since(version = 0.2.0) - type ipv6-address = tuple; - - @since(version = 0.2.0) - variant ip-address { - ipv4(ipv4-address), - ipv6(ipv6-address), - } - - @since(version = 0.2.0) - record ipv4-socket-address { - /// sin_port - port: u16, - /// sin_addr - address: ipv4-address, - } - - @since(version = 0.2.0) - record ipv6-socket-address { - /// sin6_port - port: u16, - /// sin6_flowinfo - flow-info: u32, - /// sin6_addr - address: ipv6-address, - /// sin6_scope_id - scope-id: u32, - } - - @since(version = 0.2.0) - variant ip-socket-address { - ipv4(ipv4-socket-address), - ipv6(ipv6-socket-address), - } -} diff --git a/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/tcp-create-socket.wit b/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/tcp-create-socket.wit deleted file mode 100644 index eedbd30768..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/tcp-create-socket.wit +++ /dev/null @@ -1,30 +0,0 @@ -@since(version = 0.2.0) -interface tcp-create-socket { - @since(version = 0.2.0) - use network.{network, error-code, ip-address-family}; - @since(version = 0.2.0) - use tcp.{tcp-socket}; - - /// Create a new TCP socket. - /// - /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. - /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. - /// - /// 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 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 - /// - - /// - - /// - - /// - - @since(version = 0.2.0) - create-tcp-socket: func(address-family: ip-address-family) -> result; -} diff --git a/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/tcp.wit b/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/tcp.wit deleted file mode 100644 index b4cd87fcef..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/tcp.wit +++ /dev/null @@ -1,387 +0,0 @@ -@since(version = 0.2.0) -interface tcp { - @since(version = 0.2.0) - use wasi:io/streams@0.2.3.{input-stream, output-stream}; - @since(version = 0.2.0) - use wasi:io/poll@0.2.3.{pollable}; - @since(version = 0.2.0) - use wasi:clocks/monotonic-clock@0.2.3.{duration}; - @since(version = 0.2.0) - use network.{network, error-code, ip-socket-address, ip-address-family}; - - @since(version = 0.2.0) - enum shutdown-type { - /// Similar to `SHUT_RD` in POSIX. - receive, - - /// Similar to `SHUT_WR` in POSIX. - send, - - /// Similar to `SHUT_RDWR` in POSIX. - both, - } - - /// A TCP socket resource. - /// - /// The socket can be in one of the following states: - /// - `unbound` - /// - `bind-in-progress` - /// - `bound` (See note below) - /// - `listen-in-progress` - /// - `listening` - /// - `connect-in-progress` - /// - `connected` - /// - `closed` - /// See - /// for more information. - /// - /// Note: Except where explicitly mentioned, whenever this documentation uses - /// the term "bound" without backticks it actually means: in the `bound` state *or higher*. - /// (i.e. `bound`, `listen-in-progress`, `listening`, `connect-in-progress` or `connected`) - /// - /// In addition to the general error codes documented on the - /// `network::error-code` type, TCP socket methods may always return - /// `error(invalid-state)` when in the `closed` state. - @since(version = 0.2.0) - resource tcp-socket { - /// 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. - /// - /// Bind can be attempted multiple times on the same socket, even with - /// different arguments on each iteration. But never concurrently and - /// only as long as the previous bind failed. Once a bind succeeds, the - /// binding can't be changed anymore. - /// - /// # Typical errors - /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) - /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) - /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL) - /// - `invalid-state`: The socket is already bound. (EINVAL) - /// - `address-in-use`: 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) - /// - /// # Implementors note - /// When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT - /// state of a recently closed socket on the same local address. In practice this means that the SO_REUSEADDR - /// socket option should be set implicitly on all platforms, except on Windows where this is the default behavior - /// and SO_REUSEADDR performs something different entirely. - /// - /// Unlike in POSIX, in WASI the bind operation is async. This enables - /// interactive WASI hosts to inject permission prompts. Runtimes that - /// don't want to make use of this ability can simply call the native - /// `bind` as part of either `start-bind` or `finish-bind`. - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.2.0) - start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; - @since(version = 0.2.0) - finish-bind: func() -> result<_, error-code>; - - /// Connect to a remote endpoint. - /// - /// On success: - /// - the socket is transitioned into the `connected` state. - /// - a pair of streams is returned that can be used to read & write to the connection - /// - /// After a failed connection attempt, the socket will be in the `closed` - /// state and the only valid action left is to `drop` the socket. A single - /// socket can not be used to connect more than once. - /// - /// # Typical errors - /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) - /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) - /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos) - /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) - /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) - /// - `invalid-argument`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. - /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN) - /// - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows) - /// - `timeout`: Connection timed out. (ETIMEDOUT) - /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) - /// - `connection-reset`: The connection was reset. (ECONNRESET) - /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) - /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) - /// - `address-in-use`: 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) - /// - /// # Implementors note - /// The POSIX equivalent of `start-connect` is the regular `connect` syscall. - /// Because all WASI sockets are non-blocking this is expected to return - /// EINPROGRESS, which should be translated to `ok()` in WASI. - /// - /// The POSIX equivalent of `finish-connect` is a `poll` for event `POLLOUT` - /// with a timeout of 0 on the socket descriptor. Followed by a check for - /// the `SO_ERROR` socket option, in case the poll signaled readiness. - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.2.0) - start-connect: func(network: borrow, remote-address: ip-socket-address) -> result<_, error-code>; - @since(version = 0.2.0) - finish-connect: func() -> result, error-code>; - - /// Start listening for new connections. - /// - /// Transitions the socket into the `listening` state. - /// - /// Unlike POSIX, the socket must already be explicitly bound. - /// - /// # Typical errors - /// - `invalid-state`: The socket is not bound to any local address. (EDESTADDRREQ) - /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD) - /// - `invalid-state`: The socket is already in the `listening` state. - /// - `address-in-use`: 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) - /// - /// # Implementors note - /// Unlike in POSIX, in WASI the listen operation is async. This enables - /// interactive WASI hosts to inject permission prompts. Runtimes that - /// don't want to make use of this ability can simply call the native - /// `listen` as part of either `start-listen` or `finish-listen`. - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.2.0) - start-listen: func() -> result<_, error-code>; - @since(version = 0.2.0) - finish-listen: func() -> result<_, error-code>; - - /// Accept a new client socket. - /// - /// The returned socket is bound and in the `connected` state. The following properties are inherited from the listener socket: - /// - `address-family` - /// - `keep-alive-enabled` - /// - `keep-alive-idle-time` - /// - `keep-alive-interval` - /// - `keep-alive-count` - /// - `hop-limit` - /// - `receive-buffer-size` - /// - `send-buffer-size` - /// - /// 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 - /// - `invalid-state`: Socket is not in the `listening` state. (EINVAL) - /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) - /// - `connection-aborted`: An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED) - /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.2.0) - accept: func() -> result, error-code>; - - /// Get the bound local address. - /// - /// POSIX mentions: - /// > If the socket has not been bound to a local name, the value - /// > stored in the object pointed to by `address` is unspecified. - /// - /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. - /// - /// # Typical errors - /// - `invalid-state`: The socket is not bound to any local address. - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.2.0) - local-address: func() -> result; - - /// Get the remote address. - /// - /// # Typical errors - /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.2.0) - remote-address: func() -> result; - - /// Whether the socket is in the `listening` state. - /// - /// Equivalent to the SO_ACCEPTCONN socket option. - @since(version = 0.2.0) - is-listening: func() -> bool; - - /// Whether this is a IPv4 or IPv6 socket. - /// - /// Equivalent to the SO_DOMAIN socket option. - @since(version = 0.2.0) - address-family: func() -> ip-address-family; - - /// Hints the desired listen queue size. Implementations are free to ignore this. - /// - /// If the provided value is 0, an `invalid-argument` error is returned. - /// Any other value will never cause an error, but it might be silently clamped and/or rounded. - /// - /// # Typical errors - /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. - /// - `invalid-argument`: (set) The provided value was 0. - /// - `invalid-state`: (set) The socket is in the `connect-in-progress` or `connected` state. - @since(version = 0.2.0) - set-listen-backlog-size: func(value: u64) -> result<_, error-code>; - - /// Enables or disables keepalive. - /// - /// The keepalive behavior can be adjusted using: - /// - `keep-alive-idle-time` - /// - `keep-alive-interval` - /// - `keep-alive-count` - /// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true. - /// - /// Equivalent to the SO_KEEPALIVE socket option. - @since(version = 0.2.0) - keep-alive-enabled: func() -> result; - @since(version = 0.2.0) - set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; - - /// Amount of time the connection has to be idle before TCP starts sending keepalive packets. - /// - /// If the provided value is 0, an `invalid-argument` error is returned. - /// Any other value will never cause an error, but it might be silently clamped and/or rounded. - /// I.e. after setting a value, reading the same setting back may return a different value. - /// - /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS) - /// - /// # Typical errors - /// - `invalid-argument`: (set) The provided value was 0. - @since(version = 0.2.0) - keep-alive-idle-time: func() -> result; - @since(version = 0.2.0) - set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; - - /// The time between keepalive packets. - /// - /// If the provided value is 0, an `invalid-argument` error is returned. - /// Any other value will never cause an error, but it might be silently clamped and/or rounded. - /// I.e. after setting a value, reading the same setting back may return a different value. - /// - /// Equivalent to the TCP_KEEPINTVL socket option. - /// - /// # Typical errors - /// - `invalid-argument`: (set) The provided value was 0. - @since(version = 0.2.0) - keep-alive-interval: func() -> result; - @since(version = 0.2.0) - set-keep-alive-interval: func(value: duration) -> result<_, error-code>; - - /// The maximum amount of keepalive packets TCP should send before aborting the connection. - /// - /// If the provided value is 0, an `invalid-argument` error is returned. - /// Any other value will never cause an error, but it might be silently clamped and/or rounded. - /// I.e. after setting a value, reading the same setting back may return a different value. - /// - /// Equivalent to the TCP_KEEPCNT socket option. - /// - /// # Typical errors - /// - `invalid-argument`: (set) The provided value was 0. - @since(version = 0.2.0) - keep-alive-count: func() -> result; - @since(version = 0.2.0) - set-keep-alive-count: func(value: u32) -> result<_, error-code>; - - /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. - /// - /// If the provided value is 0, an `invalid-argument` error is returned. - /// - /// # Typical errors - /// - `invalid-argument`: (set) The TTL value must be 1 or higher. - @since(version = 0.2.0) - hop-limit: func() -> result; - @since(version = 0.2.0) - set-hop-limit: func(value: u8) -> result<_, error-code>; - - /// The kernel buffer space reserved for sends/receives on this socket. - /// - /// If the provided value is 0, an `invalid-argument` error is returned. - /// Any other value will never cause an error, but it might be silently clamped and/or rounded. - /// I.e. after setting a value, reading the same setting back may return a different value. - /// - /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. - /// - /// # Typical errors - /// - `invalid-argument`: (set) The provided value was 0. - @since(version = 0.2.0) - receive-buffer-size: func() -> result; - @since(version = 0.2.0) - set-receive-buffer-size: func(value: u64) -> result<_, error-code>; - @since(version = 0.2.0) - send-buffer-size: func() -> result; - @since(version = 0.2.0) - set-send-buffer-size: func(value: u64) -> result<_, error-code>; - - /// Create a `pollable` which can be used to poll for, or block on, - /// completion of any of the asynchronous operations of this socket. - /// - /// When `finish-bind`, `finish-listen`, `finish-connect` or `accept` - /// return `error(would-block)`, this pollable can be used to wait for - /// their success or failure, after which the method can be retried. - /// - /// The pollable is not limited to the async operation that happens to be - /// in progress at the time of calling `subscribe` (if any). Theoretically, - /// `subscribe` only has to be called once per socket and can then be - /// (re)used for the remainder of the socket's lifetime. - /// - /// See - /// for more information. - /// - /// Note: this function is here for WASI 0.2 only. - /// It's planned to be removed when `future` is natively supported in Preview3. - @since(version = 0.2.0) - subscribe: func() -> pollable; - - /// Initiate a graceful shutdown. - /// - /// - `receive`: The socket is not expecting to receive any data from - /// the peer. The `input-stream` associated with this socket will be - /// closed. Any data still in the receive queue at time of calling - /// this method will be discarded. - /// - `send`: The socket has no more data to send to the peer. The `output-stream` - /// associated with this socket will be closed and a FIN packet will be sent. - /// - `both`: Same effect as `receive` & `send` combined. - /// - /// This function is idempotent; shutting down a direction more than once - /// has no effect and returns `ok`. - /// - /// The shutdown function does not close (drop) the socket. - /// - /// # Typical errors - /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.2.0) - shutdown: func(shutdown-type: shutdown-type) -> result<_, error-code>; - } -} diff --git a/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/udp-create-socket.wit b/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/udp-create-socket.wit deleted file mode 100644 index e8eeacbfef..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/udp-create-socket.wit +++ /dev/null @@ -1,30 +0,0 @@ -@since(version = 0.2.0) -interface udp-create-socket { - @since(version = 0.2.0) - use network.{network, error-code, ip-address-family}; - @since(version = 0.2.0) - use udp.{udp-socket}; - - /// Create a new UDP socket. - /// - /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. - /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. - /// - /// 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` 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 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: - /// - - /// - - /// - - /// - - @since(version = 0.2.0) - create-udp-socket: func(address-family: ip-address-family) -> result; -} diff --git a/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/udp.wit b/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/udp.wit deleted file mode 100644 index 01901ca27f..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/udp.wit +++ /dev/null @@ -1,288 +0,0 @@ -@since(version = 0.2.0) -interface udp { - @since(version = 0.2.0) - use wasi:io/poll@0.2.3.{pollable}; - @since(version = 0.2.0) - use network.{network, error-code, ip-socket-address, ip-address-family}; - - /// A received datagram. - @since(version = 0.2.0) - record incoming-datagram { - /// The payload. - /// - /// Theoretical max size: ~64 KiB. In practice, typically less than 1500 bytes. - data: list, - - /// The source address. - /// - /// This field is guaranteed to match the remote address the stream was initialized with, if any. - /// - /// Equivalent to the `src_addr` out parameter of `recvfrom`. - remote-address: ip-socket-address, - } - - /// A datagram to be sent out. - @since(version = 0.2.0) - record outgoing-datagram { - /// The payload. - data: list, - - /// The destination address. - /// - /// The requirements on this field depend on how the stream was initialized: - /// - with a remote address: this field must be None or match the stream's remote address exactly. - /// - without a remote address: this field is required. - /// - /// If this value is None, the send operation is equivalent to `send` in POSIX. Otherwise it is equivalent to `sendto`. - remote-address: option, - } - - /// A UDP socket handle. - @since(version = 0.2.0) - resource udp-socket { - /// 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 port is zero, the socket will be bound to a random free port. - /// - /// # Typical errors - /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) - /// - `invalid-state`: The socket is already bound. (EINVAL) - /// - `address-in-use`: 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) - /// - /// # Implementors note - /// Unlike in POSIX, in WASI the bind operation is async. This enables - /// interactive WASI hosts to inject permission prompts. Runtimes that - /// don't want to make use of this ability can simply call the native - /// `bind` as part of either `start-bind` or `finish-bind`. - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.2.0) - start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; - @since(version = 0.2.0) - finish-bind: func() -> result<_, error-code>; - - /// Set up inbound & outbound communication channels, optionally to a specific peer. - /// - /// This function only changes the local socket configuration and does not generate any network traffic. - /// On success, the `remote-address` of the socket is updated. The `local-address` may be updated as well, - /// based on the best network path to `remote-address`. - /// - /// When a `remote-address` is provided, the returned streams are limited to communicating with that specific peer: - /// - `send` can only be used to send to this destination. - /// - `receive` will only return datagrams sent from the provided `remote-address`. - /// - /// This method may be called multiple times on the same socket to change its association, but - /// only the most recently returned pair of streams will be operational. Implementations may trap if - /// the streams returned by a previous invocation haven't been dropped yet before calling `stream` again. - /// - /// The POSIX equivalent in pseudo-code is: - /// ```text - /// if (was previously connected) { - /// connect(s, AF_UNSPEC) - /// } - /// if (remote_address is Some) { - /// connect(s, remote_address) - /// } - /// ``` - /// - /// Unlike in POSIX, the socket must already be explicitly bound. - /// - /// # Typical errors - /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) - /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) - /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) - /// - `invalid-state`: The socket is not bound. - /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) - /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) - /// - `connection-refused`: The connection was refused. (ECONNREFUSED) - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.2.0) - %stream: func(remote-address: option) -> result, error-code>; - - /// Get the current bound address. - /// - /// POSIX mentions: - /// > If the socket has not been bound to a local name, the value - /// > stored in the object pointed to by `address` is unspecified. - /// - /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. - /// - /// # Typical errors - /// - `invalid-state`: The socket is not bound to any local address. - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.2.0) - local-address: func() -> result; - - /// Get the address the socket is currently streaming to. - /// - /// # Typical errors - /// - `invalid-state`: The socket is not streaming to a specific remote address. (ENOTCONN) - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.2.0) - remote-address: func() -> result; - - /// Whether this is a IPv4 or IPv6 socket. - /// - /// Equivalent to the SO_DOMAIN socket option. - @since(version = 0.2.0) - address-family: func() -> ip-address-family; - - /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. - /// - /// If the provided value is 0, an `invalid-argument` error is returned. - /// - /// # Typical errors - /// - `invalid-argument`: (set) The TTL value must be 1 or higher. - @since(version = 0.2.0) - unicast-hop-limit: func() -> result; - @since(version = 0.2.0) - set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; - - /// The kernel buffer space reserved for sends/receives on this socket. - /// - /// If the provided value is 0, an `invalid-argument` error is returned. - /// Any other value will never cause an error, but it might be silently clamped and/or rounded. - /// I.e. after setting a value, reading the same setting back may return a different value. - /// - /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. - /// - /// # Typical errors - /// - `invalid-argument`: (set) The provided value was 0. - @since(version = 0.2.0) - receive-buffer-size: func() -> result; - @since(version = 0.2.0) - set-receive-buffer-size: func(value: u64) -> result<_, error-code>; - @since(version = 0.2.0) - send-buffer-size: func() -> result; - @since(version = 0.2.0) - set-send-buffer-size: func(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 0.2 only. - /// It's planned to be removed when `future` is natively supported in Preview3. - @since(version = 0.2.0) - subscribe: func() -> pollable; - } - - @since(version = 0.2.0) - resource incoming-datagram-stream { - /// Receive messages on the socket. - /// - /// This function attempts to receive up to `max-results` datagrams on the socket without blocking. - /// The returned list may contain fewer elements than requested, but never more. - /// - /// This function returns successfully with an empty list when either: - /// - `max-results` is 0, or: - /// - `max-results` is greater than 0, but no results are immediately available. - /// This function never returns `error(would-block)`. - /// - /// # Typical errors - /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) - /// - `connection-refused`: The connection was refused. (ECONNREFUSED) - /// - /// # References - /// - - /// - - /// - - /// - - /// - - /// - - /// - - /// - - @since(version = 0.2.0) - receive: func(max-results: u64) -> result, error-code>; - - /// Create a `pollable` which will resolve once the stream is ready to receive again. - /// - /// Note: this function is here for WASI 0.2 only. - /// It's planned to be removed when `future` is natively supported in Preview3. - @since(version = 0.2.0) - subscribe: func() -> pollable; - } - - @since(version = 0.2.0) - resource outgoing-datagram-stream { - /// Check readiness for sending. This function never blocks. - /// - /// Returns the number of datagrams permitted for the next call to `send`, - /// or an error. Calling `send` with more datagrams than this function has - /// permitted will trap. - /// - /// When this function returns ok(0), the `subscribe` pollable will - /// become ready when this function will report at least ok(1), or an - /// error. - /// - /// Never returns `would-block`. - check-send: func() -> result; - - /// Send messages on the socket. - /// - /// This function attempts to send all provided `datagrams` on the socket without blocking and - /// returns how many messages were actually sent (or queued for sending). This function never - /// returns `error(would-block)`. If none of the datagrams were able to be sent, `ok(0)` is returned. - /// - /// This function semantically behaves the same as iterating the `datagrams` list and sequentially - /// sending each individual datagram until either the end of the list has been reached or the first error occurred. - /// If at least one datagram has been sent successfully, this function never returns an error. - /// - /// If the input list is empty, the function returns `ok(0)`. - /// - /// Each call to `send` must be permitted by a preceding `check-send`. Implementations must trap if - /// either `check-send` was not called or `datagrams` contains more items than `check-send` permitted. - /// - /// # Typical errors - /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) - /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) - /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) - /// - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `stream`. (EISCONN) - /// - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) - /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) - /// - `connection-refused`: The connection was refused. (ECONNREFUSED) - /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) - /// - /// # References - /// - - /// - - /// - - /// - - /// - - /// - - /// - - /// - - @since(version = 0.2.0) - send: func(datagrams: list) -> result; - - /// Create a `pollable` which will resolve once the stream is ready to send again. - /// - /// Note: this function is here for WASI 0.2 only. - /// It's planned to be removed when `future` is natively supported in Preview3. - @since(version = 0.2.0) - subscribe: func() -> pollable; - } -} diff --git a/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/world.wit b/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/world.wit deleted file mode 100644 index 2f0ad0d7c9..0000000000 --- a/crates/wasi-http/src/p2/wit/deps/sockets@v0.2.3/world.wit +++ /dev/null @@ -1,19 +0,0 @@ -package wasi:sockets@0.2.3; - -@since(version = 0.2.0) -world imports { - @since(version = 0.2.0) - import instance-network; - @since(version = 0.2.0) - import network; - @since(version = 0.2.0) - import udp; - @since(version = 0.2.0) - import udp-create-socket; - @since(version = 0.2.0) - import tcp; - @since(version = 0.2.0) - import tcp-create-socket; - @since(version = 0.2.0) - import ip-name-lookup; -} diff --git a/crates/wasi-http/src/p3/bindings.rs b/crates/wasi-http/src/p3/bindings.rs new file mode 100644 index 0000000000..76cae7a519 --- /dev/null +++ b/crates/wasi-http/src/p3/bindings.rs @@ -0,0 +1,44 @@ +//! Raw bindings to the `wasi:http` package. + +#[allow(missing_docs)] +mod generated { + wasmtime::component::bindgen!({ + path: "src/p3/wit", + world: "wasi:http/proxy", + //tracing: true, // TODO: Reenable once fixed + trappable_imports: true, + concurrent_exports: true, + concurrent_imports: true, + async: { + only_imports: [ + "wasi:http/handler@0.3.0-draft#handle", + "wasi:http/types@0.3.0-draft#[method]request.body", + "wasi:http/types@0.3.0-draft#[method]response.body", + "wasi:http/types@0.3.0-draft#[static]request.new", + "wasi:http/types@0.3.0-draft#[static]response.new", + ], + }, + with: { + "wasi:http/types/fields": with::Fields, + "wasi:http/types/request": crate::p3::Request, + "wasi:http/types/request-options": with::RequestOptions, + "wasi:http/types/response": crate::p3::Response, + }, + }); + + mod with { + /// The concrete type behind a `wasi:http/types/fields` resource. + pub type Fields = wasmtime_wasi::p3::WithChildren; + + /// The concrete type behind a `wasi:http/types/request-options` resource. + pub type RequestOptions = wasmtime_wasi::p3::WithChildren; + } +} + +pub use self::generated::wasi::*; + +/// Raw bindings to the `wasi:http/proxy` exports. +pub use self::generated::exports; + +/// Bindings to the `wasi:http/proxy` world. +pub use self::generated::{Proxy, ProxyIndices, ProxyPre}; diff --git a/crates/wasi-http/src/p3/body.rs b/crates/wasi-http/src/p3/body.rs new file mode 100644 index 0000000000..bb23abcb3a --- /dev/null +++ b/crates/wasi-http/src/p3/body.rs @@ -0,0 +1,243 @@ +use core::future::{poll_fn, Future}; +use core::mem; +use core::pin::Pin; +use core::task::{ready, Context, Poll}; + +use anyhow::Context as _; +use bytes::Bytes; +use http::HeaderMap; +use http_body_util::{combinators::BoxBody, BodyExt as _}; +use tokio::sync::oneshot; +use wasmtime::{ + component::{AbortOnDropHandle, ErrorContext, FutureWriter, Resource, StreamReader}, + AsContextMut, +}; +use wasmtime_wasi::p3::{ResourceView, WithChildren}; + +use crate::p3::bindings::http::types::ErrorCode; + +pub(crate) type OutgoingContentsStreamFuture = Pin< + Box< + dyn Future, Bytes), Option>> + + Send + + Sync + + 'static, + >, +>; + +pub(crate) type OutgoingTrailerFuture = Pin< + Box< + dyn Future< + Output = Result< + Result>>, ErrorCode>, + Option, + >, + > + Send + + Sync + + 'static, + >, +>; + +pub(crate) type OutgoingTrailerFutureMut<'a> = Pin< + &'a mut (dyn Future< + Output = Result< + Result>>, ErrorCode>, + Option, + >, + > + Send + + Sync + + 'static), +>; + +pub(crate) fn empty_body() -> impl http_body::Body> { + http_body_util::Empty::new().map_err(|_| None) +} + +/// A body frame +pub enum BodyFrame { + /// Data frame + Data(Bytes), + /// Trailer frame, this is the last frame of the body and it includes the transmit/receipt result + Trailers(Result>>, ErrorCode>), +} + +/// The concrete type behind a `wasi:http/types/body` resource. +pub enum Body { + /// Body constructed by the guest + Guest { + /// The body stream + contents: Option, + /// Future, on which guest will write result and optional trailers + trailers: Option, + /// Buffered frame, if any + buffer: Option, + /// Future, on which transmission result will be written + tx: FutureWriter>, + }, + /// Body constructed by the host + Host { + /// Underlying body stream + stream: Option>, + /// Buffered frame, if any + buffer: Option, + }, + /// Body has been fully consumed + Consumed, +} + +impl Body { + /// Construct a new [Body] + pub fn new(body: T) -> Self + where + T: http_body::Body + Send + Sync + 'static, + T::Error: Into, + { + Self::Host { + stream: Some(body.map_err(Into::into).boxed()), + buffer: None, + } + } + + /// Construct a new empty [Body] + pub fn empty() -> Self { + Self::Host { + stream: Some(http_body_util::Empty::new().map_err(Into::into).boxed()), + buffer: None, + } + } +} + +pub(crate) struct GuestRequestTrailers { + pub trailers: Option, ErrorCode>>>, + #[allow(dead_code)] + pub trailer_task: AbortOnDropHandle, +} + +impl Future for GuestRequestTrailers { + type Output = Option>>; + + fn poll( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>>> { + let Some(trailers) = &mut self.trailers else { + return Poll::Ready(None); + }; + let trailers = ready!(Pin::new(trailers).poll(cx)); + self.trailers = None; + match trailers { + Ok(Ok(Some(trailers))) => Poll::Ready(Some(Ok(trailers))), + Ok(Ok(None)) => Poll::Ready(None), + Ok(Err(err)) => Poll::Ready(Some(Err(Some(err)))), + Err(..) => Poll::Ready(Some(Err(None))), // future was dropped without writing a result + } + } +} + +fn poll_guest_response_trailers( + cx: &mut Context<'_>, + mut store: impl AsContextMut, + trailers: OutgoingTrailerFutureMut<'_>, +) -> Poll>>> { + match ready!(trailers.poll(cx)) { + Ok(Ok(Some(trailers))) => { + let mut store = store.as_context_mut(); + let table = store.data_mut().table(); + match table + .delete(trailers) + .context("failed to delete trailers") + .map(WithChildren::unwrap_or_clone) + { + Ok(Ok(trailers)) => Poll::Ready(Some(Ok(trailers))), + Ok(Err(err)) => Poll::Ready(Some(Err(Some(ErrorCode::InternalError(Some( + format!("{err:#}"), + )))))), + Err(err) => Poll::Ready(Some(Err(Some(ErrorCode::InternalError(Some(format!( + "{err:#}" + ))))))), + } + } + Ok(Ok(None)) => Poll::Ready(None), + Ok(Err(err)) => Poll::Ready(Some(Err(Some(err)))), + Err(..) => Poll::Ready(Some(Err(None))), + } +} + +pub(crate) async fn guest_response_trailers( + mut store: impl AsContextMut, + mut trailers: OutgoingTrailerFuture, +) -> Option>> +where + T: ResourceView, +{ + poll_fn(move |cx| poll_guest_response_trailers(cx, &mut store, trailers.as_mut())).await +} + +pub(crate) struct GuestResponseTrailers { + pub store: T, + pub trailers: Option, +} + +impl Future for GuestResponseTrailers +where + T: AsContextMut + Unpin, + T::Data: ResourceView, +{ + type Output = Option>>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let &mut Self { + ref mut store, + trailers: Some(ref mut trailers), + } = &mut *self.as_mut() + else { + return Poll::Ready(None); + }; + let trailers = ready!(poll_guest_response_trailers(cx, store, trailers.as_mut())); + self.trailers = None; + Poll::Ready(trailers) + } +} + +/// Body constructed by the guest +pub(crate) struct GuestBody { + pub contents: Option, + pub buffer: Bytes, +} + +impl GuestBody { + pub fn new(contents: OutgoingContentsStreamFuture, buffer: Bytes) -> Self { + Self { + contents: Some(contents), + buffer, + } + } +} + +impl http_body::Body for GuestBody { + type Data = Bytes; + type Error = Option; + + fn poll_frame( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + if !self.buffer.is_empty() { + let buffer = mem::take(&mut self.buffer); + return Poll::Ready(Some(Ok(http_body::Frame::data(buffer)))); + } + let Some(stream) = &mut self.contents else { + return Poll::Ready(None); + }; + match ready!(Pin::new(stream).poll(cx)) { + Ok((tail, buf)) => { + self.contents = Some(tail.read().into_future()); + Poll::Ready(Some(Ok(http_body::Frame::data(buf)))) + } + Err(..) => { + self.contents = None; + Poll::Ready(None) + } + } + } +} diff --git a/crates/wasi-http/src/p3/client.rs b/crates/wasi-http/src/p3/client.rs new file mode 100644 index 0000000000..38b1d329f2 --- /dev/null +++ b/crates/wasi-http/src/p3/client.rs @@ -0,0 +1,342 @@ +use core::pin::Pin; +use core::task::{ready, Context, Poll}; +use core::time::Duration; +use core::{error::Error as _, future::Future}; + +use bytes::Bytes; +use http::uri::Scheme; +use http_body_util::BodyExt as _; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::net::TcpStream; +use tracing::warn; + +use crate::io::TokioIo; +use crate::p3::bindings::http::types::{DnsErrorPayload, ErrorCode}; +use crate::p3::RequestOptions; + +/// Translate a [`hyper::Error`] to a wasi-http `ErrorCode` in the context of a request. +pub fn hyper_request_error(err: hyper::Error) -> ErrorCode { + // If there's a source, we might be able to extract a wasi-http error from it. + if let Some(cause) = err.source() { + if let Some(err) = cause.downcast_ref::() { + return err.clone(); + } + } + + warn!("hyper request error: {err:?}"); + + ErrorCode::HttpProtocolError +} + +/// Translate a [`hyper::Error`] to a wasi-http `ErrorCode` in the context of a response. +pub fn hyper_response_error(err: hyper::Error) -> ErrorCode { + if err.is_timeout() { + return ErrorCode::HttpResponseTimeout; + } + + // If there's a source, we might be able to extract a wasi-http error from it. + if let Some(cause) = err.source() { + if let Some(err) = cause.downcast_ref::() { + return err.clone(); + } + } + + warn!("hyper response error: {err:?}"); + + ErrorCode::HttpProtocolError +} + +fn dns_error(rcode: String, info_code: u16) -> ErrorCode { + ErrorCode::DnsError(DnsErrorPayload { + rcode: Some(rcode), + info_code: Some(info_code), + }) +} + +/// HTTP client +pub trait Client: Clone + Send + Sync { + /// Error returned by `send_request` + type Error: Into; + + /// Whether to set `host` header in the request passed to `send_request`. + fn set_host_header(&mut self) -> bool { + true + } + + /// Scheme to default to, when not set by the guest. + /// + /// If [None], `handle` will return [ErrorCode::HttpProtocolError] + /// for requests missing a scheme. + fn default_scheme(&mut self) -> Option { + Some(Scheme::HTTPS) + } + + /// Whether a given scheme should be considered supported. + /// + /// `handle` will return [ErrorCode::HttpProtocolError] for unsupported schemes. + fn is_supported_scheme(&mut self, scheme: &http::uri::Scheme) -> bool { + *scheme == Scheme::HTTP || *scheme == Scheme::HTTPS + } + + /// Send an outgoing request. + fn send_request( + &mut self, + request: http::Request< + impl http_body::Body> + Send + Sync + 'static, + >, + options: Option, + ) -> impl Future< + Output = wasmtime::Result< + Result< + ( + impl Future< + Output = Result< + http::Response< + impl http_body::Body + + Send + + Sync + + 'static, + >, + ErrorCode, + >, + > + Send + + Sync, + impl Future> + Send + Sync + 'static, + ), + ErrorCode, + >, + >, + > + Send + + Sync; +} + +/// Default HTTP client +#[derive(Clone, Debug, Default)] +pub struct DefaultClient; + +impl Client for DefaultClient { + type Error = ErrorCode; + + async fn send_request( + &mut self, + request: http::Request< + impl http_body::Body> + Send + Sync + 'static, + >, + options: Option, + ) -> wasmtime::Result< + Result< + ( + impl Future< + Output = Result< + http::Response< + impl http_body::Body + 'static, + >, + ErrorCode, + >, + >, + impl Future> + 'static, + ), + ErrorCode, + >, + > { + Ok(default_send_request(request, options).await) + } +} + +struct IncomingBody { + incoming: hyper::body::Incoming, + timeout: tokio::time::Interval, +} + +impl http_body::Body for IncomingBody { + type Data = ::Data; + type Error = ErrorCode; + + fn poll_frame( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + match Pin::new(&mut self.as_mut().incoming).poll_frame(cx) { + Poll::Ready(None) => Poll::Ready(None), + Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(hyper_response_error(err)))), + Poll::Ready(Some(Ok(frame))) => { + self.timeout.reset(); + Poll::Ready(Some(Ok(frame))) + } + Poll::Pending => { + ready!(self.timeout.poll_tick(cx)); + Poll::Ready(Some(Err(ErrorCode::ConnectionReadTimeout))) + } + } + } +} + +/// The default implementation of how an outgoing request is sent. +/// +/// This implementation is used by the `wasi:http/outgoing-handler` interface +/// default implementation. +pub async fn default_send_request( + mut request: http::Request< + impl http_body::Body> + Send + Sync + 'static, + >, + options: Option, +) -> Result< + ( + impl Future< + Output = Result< + http::Response>, + ErrorCode, + >, + >, + impl Future>, + ), + ErrorCode, +> { + trait TokioStream: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static { + fn boxed(self) -> Box + where + Self: Sized, + { + Box::new(self) + } + } + impl TokioStream for T where T: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static {} + + eprintln!("[host] default send request"); + + let uri = request.uri(); + let authority = uri.authority().ok_or(ErrorCode::HttpRequestUriInvalid)?; + let use_tls = uri.scheme() == Some(&Scheme::HTTPS); + let authority = if authority.port().is_some() { + authority.to_string() + } else { + let port = if use_tls { 443 } else { 80 }; + format!("{authority}:{port}") + }; + + let connect_timeout = options + .as_ref() + .and_then( + |RequestOptions { + connect_timeout, .. + }| *connect_timeout, + ) + .unwrap_or(Duration::from_secs(600)); + + let first_byte_timeout = options + .as_ref() + .and_then( + |RequestOptions { + first_byte_timeout, .. + }| *first_byte_timeout, + ) + .unwrap_or(Duration::from_secs(600)); + + let between_bytes_timeout = options + .as_ref() + .and_then( + |RequestOptions { + between_bytes_timeout, + .. + }| *between_bytes_timeout, + ) + .unwrap_or(Duration::from_secs(600)); + + eprintln!("[host] connect..."); + let stream = match tokio::time::timeout(connect_timeout, TcpStream::connect(&authority)).await { + Ok(Ok(stream)) => stream, + Ok(Err(err)) if err.kind() == std::io::ErrorKind::AddrNotAvailable => { + return Err(dns_error("address not available".to_string(), 0)) + } + Ok(Err(err)) + if err + .to_string() + .starts_with("failed to lookup address information") => + { + return Err(dns_error("address not available".to_string(), 0)) + } + Ok(Err(..)) => return Err(ErrorCode::ConnectionRefused), + Err(..) => return Err(ErrorCode::ConnectionTimeout), + }; + let stream = if use_tls { + #[cfg(any(target_arch = "riscv64", target_arch = "s390x"))] + { + return Err(ErrorCode::InternalError(Some( + "unsupported architecture for SSL".to_string(), + ))); + } + + #[cfg(not(any(target_arch = "riscv64", target_arch = "s390x")))] + { + use rustls::pki_types::ServerName; + + // derived from https://github.com/rustls/rustls/blob/main/examples/src/bin/simpleclient.rs + let root_cert_store = rustls::RootCertStore { + roots: webpki_roots::TLS_SERVER_ROOTS.into(), + }; + let config = rustls::ClientConfig::builder() + .with_root_certificates(root_cert_store) + .with_no_client_auth(); + let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(config)); + let mut parts = authority.split(":"); + let host = parts.next().unwrap_or(&authority); + let domain = ServerName::try_from(host) + .map_err(|e| { + warn!("dns lookup error: {e:?}"); + dns_error("invalid dns name".to_string(), 0) + })? + .to_owned(); + let stream = connector.connect(domain, stream).await.map_err(|e| { + warn!("tls protocol error: {e:?}"); + ErrorCode::TlsProtocolError + })?; + stream.boxed() + } + } else { + stream.boxed() + }; + eprintln!("[host] handshake..."); + let (mut sender, conn) = tokio::time::timeout( + connect_timeout, + // TODO: we should plumb the builder through the http context, and use it here + hyper::client::conn::http1::Builder::new().handshake(TokioIo::new(stream)), + ) + .await + .map_err(|_| ErrorCode::ConnectionTimeout)? + .map_err(hyper_request_error)?; + + // at this point, the request contains the scheme and the authority, but + // the http packet should only include those if addressing a proxy, so + // remove them here, since SendRequest::send_request does not do it for us + *request.uri_mut() = http::Uri::builder() + .path_and_query( + request + .uri() + .path_and_query() + .map(|p| p.as_str()) + .unwrap_or("/"), + ) + .build() + .expect("comes from valid request"); + + let request = + request.map(|body| body.map_err(|err| err.unwrap_or(ErrorCode::InternalError(None)))); + Ok(( + async move { + eprintln!("[host] real send request..."); + let response = tokio::time::timeout(first_byte_timeout, sender.send_request(request)) + .await + .map_err(|_| ErrorCode::ConnectionReadTimeout)? + .map_err(hyper_request_error)?; + let mut timeout = tokio::time::interval(between_bytes_timeout); + timeout.reset(); + Ok(response.map(|incoming| IncomingBody { incoming, timeout })) + }, + async move { + eprintln!("[host] await conn..."); + conn.await.map_err(hyper_request_error)?; + eprintln!("[host] conn awaited"); + Ok(()) + }, + )) +} diff --git a/crates/wasi-http/src/p3/conv.rs b/crates/wasi-http/src/p3/conv.rs new file mode 100644 index 0000000000..a7cc596c7f --- /dev/null +++ b/crates/wasi-http/src/p3/conv.rs @@ -0,0 +1,152 @@ +use core::convert::Infallible; + +use bytes::Bytes; + +use crate::p3::bindings::http::types::{ErrorCode, Method, Scheme}; +use crate::p3::{Body, Request}; + +impl From for ErrorCode { + fn from(_: Infallible) -> Self { + unreachable!() + } +} + +impl From for Method { + fn from(method: http::Method) -> Self { + Self::from(&method) + } +} + +impl From<&http::Method> for Method { + fn from(method: &http::Method) -> Self { + if method == http::Method::GET { + Self::Get + } else if method == http::Method::HEAD { + Self::Head + } else if method == http::Method::POST { + Self::Post + } else if method == http::Method::PUT { + Self::Put + } else if method == http::Method::DELETE { + Self::Delete + } else if method == http::Method::CONNECT { + Self::Connect + } else if method == http::Method::OPTIONS { + Self::Options + } else if method == http::Method::TRACE { + Self::Trace + } else if method == http::Method::PATCH { + Self::Patch + } else { + Self::Other(method.as_str().into()) + } + } +} + +impl TryFrom for http::Method { + type Error = http::method::InvalidMethod; + + fn try_from(method: Method) -> Result { + Self::try_from(&method) + } +} + +impl TryFrom<&Method> for http::Method { + type Error = http::method::InvalidMethod; + + fn try_from(method: &Method) -> Result { + match method { + Method::Get => Ok(Self::GET), + Method::Head => Ok(Self::HEAD), + Method::Post => Ok(Self::POST), + Method::Put => Ok(Self::PUT), + Method::Delete => Ok(Self::DELETE), + Method::Connect => Ok(Self::CONNECT), + Method::Options => Ok(Self::OPTIONS), + Method::Trace => Ok(Self::TRACE), + Method::Patch => Ok(Self::PATCH), + Method::Other(s) => s.parse(), + } + } +} + +impl From for Scheme { + fn from(scheme: http::uri::Scheme) -> Self { + Self::from(&scheme) + } +} + +impl From<&http::uri::Scheme> for Scheme { + fn from(scheme: &http::uri::Scheme) -> Self { + if *scheme == http::uri::Scheme::HTTP { + Self::Http + } else if *scheme == http::uri::Scheme::HTTPS { + Self::Https + } else { + Self::Other(scheme.as_str().into()) + } + } +} + +impl TryFrom for http::uri::Scheme { + type Error = http::uri::InvalidUri; + + fn try_from(scheme: Scheme) -> Result { + Self::try_from(&scheme) + } +} + +impl TryFrom<&Scheme> for http::uri::Scheme { + type Error = http::uri::InvalidUri; + + fn try_from(scheme: &Scheme) -> Result { + match scheme { + Scheme::Http => Ok(Self::HTTP), + Scheme::Https => Ok(Self::HTTPS), + Scheme::Other(s) => s.parse(), + } + } +} + +impl From> for Request +where + T: http_body::Body + Send + Sync + 'static, + T::Error: Into, +{ + fn from(req: http::Request) -> Self { + let ( + http::request::Parts { + method, + uri, + headers, + .. + }, + body, + ) = req.into_parts(); + let http::uri::Parts { + scheme, + authority, + path_and_query, + .. + } = uri.into_parts(); + Self::new( + method, + scheme, + authority, + path_and_query, + headers, + body, + None, + ) + } +} + +impl From for Body +where + T: http_body::Body + Send + Sync + 'static, + T::Error: Into, +{ + fn from(body: T) -> Self { + Self::new(body) + } +} diff --git a/crates/wasi-http/src/p3/host/handle.rs b/crates/wasi-http/src/p3/host/handle.rs new file mode 100644 index 0000000000..374c0d5923 --- /dev/null +++ b/crates/wasi-http/src/p3/host/handle.rs @@ -0,0 +1,409 @@ +use core::iter; + +use std::sync::Arc; + +use anyhow::bail; +use bytes::Bytes; +use futures::StreamExt as _; +use http::header::HOST; +use http::{HeaderValue, Uri}; +use http_body_util::{BodyExt as _, BodyStream, StreamBody}; +use tokio::sync::oneshot; +use wasmtime::component::{Accessor, AccessorTask, Resource}; +use wasmtime_wasi::p3::{AccessorTaskFn, ResourceView as _}; + +use crate::p3::bindings::http::handler; +use crate::p3::bindings::http::types::ErrorCode; +use crate::p3::{ + empty_body, Body, BodyFrame, Client as _, GuestBody, GuestRequestTrailers, + OutgoingTrailerFuture, Request, Response, WasiHttpImpl, WasiHttpView, +}; + +use super::{delete_request, get_fields_inner, push_response}; + +struct TrailerTask { + rx: OutgoingTrailerFuture, + tx: oneshot::Sender, ErrorCode>>, +} + +impl AccessorTask> for TrailerTask { + async fn run(self, store: &mut Accessor) -> wasmtime::Result<()> { + match self.rx.await { + Ok(Ok(trailers)) => store.with(|mut view| { + let trailers = trailers + .map(|trailers| get_fields_inner(view.table(), &trailers)) + .transpose()?; + _ = self.tx.send(Ok(trailers.as_deref().cloned())); + Ok(()) + }), + Ok(Err(err)) => { + _ = self.tx.send(Err(err)); + Ok(()) + } + Err(..) => Ok(()), + } + } +} + +impl handler::Host for WasiHttpImpl<&mut T> +where + T: WasiHttpView + 'static, +{ + async fn handle( + store: &mut Accessor, + request: Resource, + ) -> wasmtime::Result, ErrorCode>> { + eprintln!("[host] call handle"); + let Request { + method, + scheme, + authority, + path_with_query, + headers, + body, + options, + .. + } = store.with(|mut view| delete_request(view.table(), request))?; + + let mut client = store.with(|view| view.http().client.clone()); + + let options = options + .map(|options| options.unwrap_or_clone()) + .transpose()?; + let mut headers = headers.unwrap_or_clone()?; + if client.set_host_header() { + let host = if let Some(authority) = authority.as_ref() { + match HeaderValue::try_from(authority.as_str()) { + Ok(host) => host, + Err(err) => return Ok(Err(ErrorCode::InternalError(Some(err.to_string())))), + } + } else { + HeaderValue::from_static("") + }; + headers.insert(HOST, host); + } + + let scheme = match scheme { + None => client + .default_scheme() + .ok_or(ErrorCode::HttpProtocolError)?, + Some(scheme) if client.is_supported_scheme(&scheme) => scheme, + Some(..) => return Ok(Err(ErrorCode::HttpProtocolError)), + }; + let mut uri = Uri::builder().scheme(scheme); + if let Some(authority) = authority { + uri = uri.authority(authority) + }; + if let Some(path_with_query) = path_with_query { + uri = uri.path_and_query(path_with_query) + }; + let Ok(uri) = uri.build() else { + return Ok(Err(ErrorCode::HttpRequestUriInvalid)); + }; + + let Some(body) = Arc::into_inner(body) else { + return Ok(Err(ErrorCode::InternalError(Some( + "body is borrowed".into(), + )))); + }; + let Ok(body) = body.into_inner() else { + bail!("lock poisoned"); + }; + + let mut request = http::Request::builder(); + *request.headers_mut().unwrap() = headers; + let request = match request.method(method).uri(uri).body(()) { + Ok(request) => request, + Err(err) => return Ok(Err(ErrorCode::InternalError(Some(err.to_string())))), + }; + eprintln!("[host] have built req {request:?}"); + let (response, task) = match body { + Body::Guest { + contents: None, + trailers: None, + buffer: Some(BodyFrame::Trailers(Ok(None))), + tx, + } => { + let body = empty_body(); + let request = request.map(|()| body); + match client.send_request(request, options).await? { + Ok((response, io)) => { + let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { + let res = io.await; + tx.write(res.map_err(Into::into)).into_future().await; + Ok(()) + })); + let task = task.abort_handle(); + match response.await { + Ok(response) => { + (response.map(|body| body.map_err(Into::into).boxed()), task) + } + Err(err) => return Ok(Err(err)), + } + } + Err(err) => return Ok(Err(err)), + } + } + Body::Guest { + contents: None, + trailers: None, + buffer: Some(BodyFrame::Trailers(Ok(Some(trailers)))), + tx, + } => { + let trailers = store.with(|mut view| { + let trailers = get_fields_inner(view.table(), &trailers)?; + anyhow::Ok(trailers.clone()) + })?; + let body = empty_body().with_trailers(async move { Some(Ok(trailers)) }); + let request = request.map(|()| body); + match client.send_request(request, options).await? { + Ok((response, io)) => { + let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { + let res = io.await; + tx.write(res.map_err(Into::into)).into_future().await; + Ok(()) + })); + let task = task.abort_handle(); + match response.await { + Ok(response) => { + (response.map(|body| body.map_err(Into::into).boxed()), task) + } + Err(err) => return Ok(Err(err)), + } + } + Err(err) => return Ok(Err(err)), + } + } + Body::Guest { + contents: None, + trailers: None, + buffer: Some(BodyFrame::Trailers(Err(err))), + tx: _, + } => return Ok(Err(err)), + Body::Guest { + contents: None, + trailers: Some(trailers), + buffer: None, + tx, + } => { + eprintln!("[host] no contents, only trailers"); + let (trailers_tx, trailers_rx) = oneshot::channel(); + let task = store.spawn(TrailerTask { + rx: trailers, + tx: trailers_tx, + }); + let body = empty_body().with_trailers(GuestRequestTrailers { + trailers: Some(trailers_rx), + trailer_task: task.abort_handle(), + }); + let request = request.map(|()| body); + eprintln!("[host] send request.."); + match client.send_request(request, options).await? { + Ok((response, io)) => { + let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { + eprintln!("[host] await Tx result.."); + let res = io.await; + eprintln!("[host] write Tx result.."); + tx.write(res.map_err(Into::into)).into_future().await; + eprintln!("[host] done writing Tx result"); + Ok(()) + })); + eprintln!("[host] return Ok response"); + let task = task.abort_handle(); + match response.await { + Ok(response) => { + (response.map(|body| body.map_err(Into::into).boxed()), task) + } + Err(err) => return Ok(Err(err)), + } + } + Err(err) => return Ok(Err(err)), + } + } + Body::Guest { + contents: Some(contents), + trailers: Some(trailers), + buffer, + tx, + } => { + eprintln!("[host] contents, trailers"); + let (trailers_tx, trailers_rx) = oneshot::channel(); + let task = store.spawn(TrailerTask { + rx: trailers, + tx: trailers_tx, + }); + let buffer = match buffer { + Some(BodyFrame::Data(buf)) => buf, + Some(BodyFrame::Trailers(..)) => bail!("guest body is corrupted"), + None => Bytes::default(), + }; + let body = GuestBody::new(contents, buffer).with_trailers(GuestRequestTrailers { + trailers: Some(trailers_rx), + trailer_task: task.abort_handle(), + }); + let request = request.map(|()| body); + eprintln!("[host] send request.."); + match client.send_request(request, options).await? { + Ok((response, io)) => { + let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { + eprintln!("[host] await Tx result.."); + let res = io.await; + eprintln!("[host] write Tx result.."); + tx.write(res.map_err(Into::into)).into_future().await; + eprintln!("[host] done writing Tx result"); + Ok(()) + })); + eprintln!("[host] return Ok response"); + let task = task.abort_handle(); + match response.await { + Ok(response) => { + (response.map(|body| body.map_err(Into::into).boxed()), task) + } + Err(err) => return Ok(Err(err)), + } + } + Err(err) => return Ok(Err(err)), + } + } + Body::Guest { .. } => bail!("guest body is corrupted"), + Body::Host { + stream: Some(stream), + buffer: None, + } => { + let body = stream.map_err(Some); + let request = request.map(|()| body); + match client.send_request(request, options).await? { + Ok((response, io)) => { + let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { + _ = io.await; + Ok(()) + })); + let task = task.abort_handle(); + match response.await { + Ok(response) => { + (response.map(|body| body.map_err(Into::into).boxed()), task) + } + Err(err) => return Ok(Err(err)), + } + } + Err(err) => return Ok(Err(err)), + } + } + Body::Host { + stream: Some(stream), + buffer: Some(BodyFrame::Data(buffer)), + } => { + let buffer = futures::stream::iter(iter::once(Ok(http_body::Frame::data(buffer)))); + let body = StreamBody::new(buffer.chain(BodyStream::new(stream.map_err(Some)))); + let request = request.map(|()| body); + match client.send_request(request, options).await? { + Ok((response, io)) => { + let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { + _ = io.await; + Ok(()) + })); + let task = task.abort_handle(); + match response.await { + Ok(response) => { + (response.map(|body| body.map_err(Into::into).boxed()), task) + } + Err(err) => return Ok(Err(err)), + } + } + Err(err) => return Ok(Err(err)), + } + } + Body::Host { + stream: None, + buffer: Some(BodyFrame::Trailers(Ok(Some(trailers)))), + } => { + let trailers = store.with(|mut view| { + let trailers = get_fields_inner(view.table(), &trailers)?; + anyhow::Ok(trailers.clone()) + })?; + let body = empty_body().with_trailers(async move { Some(Ok(trailers)) }); + let request = request.map(|()| body); + match client.send_request(request, options).await? { + Ok((response, io)) => { + let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { + _ = io.await; + Ok(()) + })); + let task = task.abort_handle(); + match response.await { + Ok(response) => { + (response.map(|body| body.map_err(Into::into).boxed()), task) + } + Err(err) => return Ok(Err(err)), + } + } + Err(err) => return Ok(Err(err)), + } + } + Body::Host { + stream: None, + buffer: Some(BodyFrame::Trailers(Ok(None))), + } => { + let body = empty_body(); + let request = request.map(|()| body); + match client.send_request(request, options).await? { + Ok((response, io)) => { + let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { + _ = io.await; + Ok(()) + })); + let task = task.abort_handle(); + match response.await { + Ok(response) => { + (response.map(|body| body.map_err(Into::into).boxed()), task) + } + Err(err) => return Ok(Err(err)), + } + } + Err(err) => return Ok(Err(err)), + } + } + Body::Host { + stream: None, + buffer: Some(BodyFrame::Trailers(Err(err))), + } => return Ok(Err(err)), + Body::Host { .. } => bail!("host body is corrupted"), + Body::Consumed => { + let body = empty_body(); + let request = request.map(|()| body); + match client.send_request(request, options).await? { + Ok((response, io)) => { + let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { + _ = io.await; + Ok(()) + })); + let task = task.abort_handle(); + match response.await { + Ok(response) => { + (response.map(|body| body.map_err(Into::into).boxed()), task) + } + Err(err) => return Ok(Err(err)), + } + } + Err(err) => return Ok(Err(err)), + } + } + }; + eprintln!("[host] handle return"); + let ( + http::response::Parts { + status, headers, .. + }, + body, + ) = response.into_parts(); + store.with(|mut view| { + let body = Body::Host { + stream: Some(body), + buffer: None, + }; + let response = Response::new_incoming(status, headers, body, task); + let response = push_response(view.table(), response)?; + Ok(Ok(response)) + }) + } +} diff --git a/crates/wasi-http/src/p3/host/mod.rs b/crates/wasi-http/src/p3/host/mod.rs new file mode 100644 index 0000000000..ffcdb362f2 --- /dev/null +++ b/crates/wasi-http/src/p3/host/mod.rs @@ -0,0 +1,126 @@ +use core::ops::{Deref, DerefMut}; + +use anyhow::Context as _; +use wasmtime::component::Resource; +use wasmtime_wasi::p3::WithChildren; +use wasmtime_wasi::ResourceTable; + +use crate::p3::{Request, Response}; + +mod handle; +mod types; + +fn get_fields<'a>( + table: &'a ResourceTable, + fields: &Resource>, +) -> wasmtime::Result<&'a WithChildren> { + table + .get(&fields) + .context("failed to get fields from table") +} + +fn get_fields_inner<'a>( + table: &'a ResourceTable, + fields: &Resource>, +) -> wasmtime::Result + use<'a>> { + let fields = get_fields(table, fields)?; + fields.get() +} + +fn get_fields_mut<'a>( + table: &'a mut ResourceTable, + fields: &Resource>, +) -> wasmtime::Result<&'a mut WithChildren> { + table + .get_mut(&fields) + .context("failed to get fields from table") +} + +fn get_fields_inner_mut<'a>( + table: &'a mut ResourceTable, + fields: &Resource>, +) -> wasmtime::Result + use<'a>>> { + let fields = get_fields_mut(table, fields)?; + fields.get_mut() +} + +fn push_fields( + table: &mut ResourceTable, + fields: WithChildren, +) -> wasmtime::Result>> { + table.push(fields).context("failed to push fields to table") +} + +fn push_fields_child( + table: &mut ResourceTable, + fields: WithChildren, + parent: &Resource, +) -> wasmtime::Result>> { + table + .push_child(fields, parent) + .context("failed to push fields to table") +} + +fn delete_fields( + table: &mut ResourceTable, + fields: Resource>, +) -> wasmtime::Result> { + table + .delete(fields) + .context("failed to delete fields from table") +} + +fn get_request<'a>( + table: &'a ResourceTable, + req: &Resource, +) -> wasmtime::Result<&'a Request> { + table.get(req).context("failed to get request from table") +} + +fn get_request_mut<'a>( + table: &'a mut ResourceTable, + req: &Resource, +) -> wasmtime::Result<&'a mut Request> { + table + .get_mut(req) + .context("failed to get request from table") +} + +fn push_request(table: &mut ResourceTable, req: Request) -> wasmtime::Result> { + table.push(req).context("failed to push request to table") +} + +fn delete_request(table: &mut ResourceTable, req: Resource) -> wasmtime::Result { + table + .delete(req) + .context("failed to delete request from table") +} + +fn get_response<'a>( + table: &'a ResourceTable, + res: &Resource, +) -> wasmtime::Result<&'a Response> { + table.get(res).context("failed to get response from table") +} + +fn get_response_mut<'a>( + table: &'a mut ResourceTable, + res: &Resource, +) -> wasmtime::Result<&'a mut Response> { + table + .get_mut(res) + .context("failed to get response from table") +} + +fn push_response(table: &mut ResourceTable, res: Response) -> wasmtime::Result> { + table.push(res).context("failed to push response to table") +} + +fn delete_response( + table: &mut ResourceTable, + res: Resource, +) -> wasmtime::Result { + table + .delete(res) + .context("failed to delete response from table") +} diff --git a/crates/wasi-http/src/p3/host/types.rs b/crates/wasi-http/src/p3/host/types.rs new file mode 100644 index 0000000000..a089573622 --- /dev/null +++ b/crates/wasi-http/src/p3/host/types.rs @@ -0,0 +1,1047 @@ +use core::future::{poll_fn, Future as _}; +use core::mem; +use core::ops::{Deref, DerefMut}; +use core::pin::Pin; +use core::task::Poll; + +use std::sync::Arc; + +use anyhow::{bail, Context as _}; +use bytes::Bytes; +use http_body::Body as _; +use wasmtime::component::{ + future, stream, Accessor, AccessorTask, FutureWriter, HostFuture, HostStream, Resource, + StreamWriter, +}; +use wasmtime_wasi::p3::bindings::clocks::monotonic_clock::Duration; +use wasmtime_wasi::p3::{ResourceView as _, WithChildren}; +use wasmtime_wasi::ResourceTable; + +use crate::p3::bindings::http::types::{ + ErrorCode, FieldName, FieldValue, HeaderError, Host, HostFields, HostRequest, + HostRequestOptions, HostResponse, Method, RequestOptionsError, Scheme, StatusCode, Trailers, +}; +use crate::p3::host::{ + delete_fields, delete_response, get_fields, get_fields_inner, get_fields_inner_mut, + get_request, get_request_mut, get_response, get_response_mut, push_fields, push_fields_child, + push_request, push_response, +}; +use crate::p3::{Body, BodyFrame, Request, RequestOptions, Response, WasiHttpImpl, WasiHttpView}; + +fn get_request_options<'a>( + table: &'a ResourceTable, + opts: &Resource>, +) -> wasmtime::Result<&'a WithChildren> { + table + .get(opts) + .context("failed to get request options from table") +} + +fn get_request_options_inner<'a>( + table: &'a ResourceTable, + opts: &Resource>, +) -> wasmtime::Result + use<'a>> { + let opts = get_request_options(table, opts)?; + opts.get() +} + +fn get_request_options_mut<'a>( + table: &'a mut ResourceTable, + opts: &Resource>, +) -> wasmtime::Result<&'a mut WithChildren> { + table + .get_mut(opts) + .context("failed to get request options from table") +} + +fn get_request_options_inner_mut<'a>( + table: &'a mut ResourceTable, + opts: &Resource>, +) -> wasmtime::Result + use<'a>>> { + let opts = get_request_options_mut(table, opts)?; + opts.get_mut() +} + +fn push_request_options( + table: &mut ResourceTable, + fields: WithChildren, +) -> wasmtime::Result>> { + table + .push(fields) + .context("failed to push request options to table") +} + +fn delete_request_options( + table: &mut ResourceTable, + opts: Resource>, +) -> wasmtime::Result> { + table + .delete(opts) + .context("failed to delete request options from table") +} + +/// Returns `true` when the header is forbidden according to this [`WasiHttpView`] implementation. +fn is_forbidden_header(view: &mut impl WasiHttpView, name: &http::header::HeaderName) -> bool { + static FORBIDDEN_HEADERS: [http::header::HeaderName; 10] = [ + http::header::CONNECTION, + http::header::HeaderName::from_static("keep-alive"), + http::header::PROXY_AUTHENTICATE, + http::header::PROXY_AUTHORIZATION, + http::header::HeaderName::from_static("proxy-connection"), + http::header::TE, + http::header::TRANSFER_ENCODING, + http::header::UPGRADE, + http::header::HOST, + http::header::HeaderName::from_static("http2-settings"), + ]; + + FORBIDDEN_HEADERS.contains(name) || view.is_forbidden_header(name) +} + +fn clone_trailer_result( + res: &Result>, ErrorCode>, +) -> Result>, ErrorCode> { + match res { + Ok(None) => Ok(None), + Ok(Some(trailers)) => Ok(Some(Resource::new_own(trailers.rep()))), + Err(err) => Err(err.clone()), + } +} + +type TrailerFuture = HostFuture>, ErrorCode>>; + +struct BodyTask { + body: Arc>, + contents_tx: StreamWriter, + trailers_tx: FutureWriter>, ErrorCode>>, +} + +impl AccessorTask> for BodyTask +where + U: WasiHttpView, +{ + async fn run(self, store: &mut Accessor) -> wasmtime::Result<()> { + let body = { + let Ok(mut body) = self.body.lock() else { + bail!("lock poisoned"); + }; + mem::replace(&mut *body, Body::Consumed) + }; + match body { + Body::Guest { + contents: None, + trailers: Some(mut trailers_rx), + buffer: None, + tx, + } => { + drop(self.contents_tx); + let mut trailers_tx = self.trailers_tx.watch_reader(); + let Some(Ok(res)) = poll_fn(|cx| match Pin::new(&mut trailers_tx).poll(cx) { + Poll::Ready(()) => return Poll::Ready(None), + Poll::Pending => trailers_rx.as_mut().poll(cx).map(Some), + }) + .await + else { + let Ok(mut body) = self.body.lock() else { + bail!("lock poisoned"); + }; + *body = Body::Guest { + contents: None, + trailers: Some(trailers_rx), + buffer: None, + tx, + }; + return Ok(()); + }; + let trailers_tx = trailers_tx.into_inner().await; + if !trailers_tx + .write(clone_trailer_result(&res)) + .into_future() + .await + { + let Ok(mut body) = self.body.lock() else { + bail!("lock poisoned"); + }; + *body = Body::Guest { + contents: None, + trailers: None, + buffer: Some(BodyFrame::Trailers(res)), + tx, + }; + return Ok(()); + } + tx.write(Ok(())).into_future().await; + Ok(()) + } + Body::Guest { + contents: None, + trailers: None, + buffer: Some(BodyFrame::Trailers(res)), + tx, + } => { + drop(self.contents_tx); + if !self + .trailers_tx + .write(clone_trailer_result(&res)) + .into_future() + .await + { + let Ok(mut body) = self.body.lock() else { + bail!("lock poisoned"); + }; + *body = Body::Guest { + contents: None, + trailers: None, + buffer: Some(BodyFrame::Trailers(res)), + tx, + }; + return Ok(()); + } + tx.write(Ok(())).into_future().await; + Ok(()) + } + Body::Guest { + contents: Some(mut contents_rx), + trailers: Some(mut trailers_rx), + buffer, + tx, + } => { + let mut contents_tx = self.contents_tx; + match buffer { + Some(BodyFrame::Data(buf)) => { + let Some(tx_tail) = contents_tx.write(buf.clone()).into_future().await + else { + let Ok(mut body) = self.body.lock() else { + bail!("lock poisoned"); + }; + *body = Body::Guest { + contents: Some(contents_rx), + trailers: Some(trailers_rx), + buffer: Some(BodyFrame::Data(buf)), + tx, + }; + return Ok(()); + }; + contents_tx = tx_tail; + } + Some(BodyFrame::Trailers(..)) => bail!("corrupted guest body state"), + None => {} + } + let mut contents_tx = contents_tx.watch_reader(); + loop { + let Some(rx) = poll_fn(|cx| match Pin::new(&mut contents_tx).poll(cx) { + Poll::Ready(()) => return Poll::Ready(None), + Poll::Pending => contents_rx.as_mut().poll(cx).map(Some), + }) + .await + else { + // read handle dropped + let Ok(mut body) = self.body.lock() else { + bail!("lock poisoned"); + }; + *body = Body::Guest { + contents: Some(contents_rx), + trailers: Some(trailers_rx), + buffer: None, + tx, + }; + return Ok(()); + }; + let Ok((rx_tail, buf)) = rx else { + break; + }; + contents_rx = rx_tail.read().into_future(); + let tx_tail = contents_tx.into_inner().await; + let Some(tx_tail) = tx_tail.write(buf.clone()).into_future().await else { + let Ok(mut body) = self.body.lock() else { + bail!("lock poisoned"); + }; + *body = Body::Guest { + contents: Some(contents_rx), + trailers: Some(trailers_rx), + buffer: Some(BodyFrame::Data(buf)), + tx, + }; + return Ok(()); + }; + contents_tx = tx_tail.watch_reader(); + } + drop(contents_tx); + + let mut trailers_tx = self.trailers_tx.watch_reader(); + let Some(Ok(res)) = poll_fn(|cx| match Pin::new(&mut trailers_tx).poll(cx) { + Poll::Ready(()) => return Poll::Ready(None), + Poll::Pending => trailers_rx.as_mut().poll(cx).map(Some), + }) + .await + else { + let Ok(mut body) = self.body.lock() else { + bail!("lock poisoned"); + }; + *body = Body::Guest { + contents: None, + trailers: Some(trailers_rx), + buffer: None, + tx, + }; + return Ok(()); + }; + let trailers_tx = trailers_tx.into_inner().await; + if !trailers_tx + .write(clone_trailer_result(&res)) + .into_future() + .await + { + let Ok(mut body) = self.body.lock() else { + bail!("lock poisoned"); + }; + *body = Body::Guest { + contents: None, + trailers: None, + buffer: Some(BodyFrame::Trailers(res)), + tx, + }; + return Ok(()); + } + tx.write(Ok(())).into_future().await; + Ok(()) + } + Body::Guest { .. } => bail!("corrupted guest body state"), + Body::Host { + stream: Some(mut stream), + buffer, + } => { + let mut contents_tx = self.contents_tx; + match buffer { + Some(BodyFrame::Data(buf)) => { + let Some(tx_tail) = contents_tx.write(buf.clone()).into_future().await + else { + let Ok(mut body) = self.body.lock() else { + bail!("lock poisoned"); + }; + *body = Body::Host { + stream: Some(stream), + buffer: Some(BodyFrame::Data(buf)), + }; + return Ok(()); + }; + contents_tx = tx_tail; + } + Some(BodyFrame::Trailers(..)) => bail!("corrupted guest body state"), + None => {} + } + let mut contents_tx = contents_tx.watch_reader(); + loop { + match poll_fn(|cx| match Pin::new(&mut contents_tx).poll(cx) { + Poll::Ready(()) => return Poll::Ready(None), + Poll::Pending => Pin::new(&mut stream).poll_frame(cx).map(Some), + }) + .await + { + None => { + // read handle dropped + let Ok(mut body) = self.body.lock() else { + bail!("lock poisoned"); + }; + *body = Body::Host { + stream: Some(stream), + buffer: None, + }; + return Ok(()); + } + Some(None) => { + drop(contents_tx); + if !self.trailers_tx.write(Ok(None)).into_future().await { + let Ok(mut body) = self.body.lock() else { + bail!("lock poisoned"); + }; + *body = Body::Host { + stream: None, + buffer: Some(BodyFrame::Trailers(Ok(None))), + }; + } + return Ok(()); + } + Some(Some(Ok(frame))) => { + match frame.into_data().map_err(http_body::Frame::into_trailers) { + Ok(buf) => { + let tx_tail = contents_tx.into_inner().await; + let Some(tx_tail) = + tx_tail.write(buf.clone()).into_future().await + else { + let Ok(mut body) = self.body.lock() else { + bail!("lock poisoned"); + }; + *body = Body::Host { + stream: Some(stream), + buffer: Some(BodyFrame::Data(buf)), + }; + return Ok(()); + }; + contents_tx = tx_tail.watch_reader(); + } + Err(Ok(trailers)) => { + drop(contents_tx); + let trailers = store.with(|mut view| { + push_fields(view.table(), WithChildren::new(trailers)) + })?; + if !self + .trailers_tx + .write(Ok(Some(Resource::new_own(trailers.rep())))) + .into_future() + .await + { + let Ok(mut body) = self.body.lock() else { + bail!("lock poisoned"); + }; + *body = Body::Host { + stream: None, + buffer: Some(BodyFrame::Trailers(Ok(Some(trailers)))), + }; + } + return Ok(()); + } + Err(Err(..)) => { + drop(contents_tx); + if !self + .trailers_tx + .write(Err(ErrorCode::HttpProtocolError)) + .into_future() + .await + { + let Ok(mut body) = self.body.lock() else { + bail!("lock poisoned"); + }; + *body = Body::Host { + stream: None, + buffer: Some(BodyFrame::Trailers(Err( + ErrorCode::HttpProtocolError, + ))), + }; + } + return Ok(()); + } + } + } + Some(Some(Err(err))) => { + drop(contents_tx); + if !self.trailers_tx.write(Err(err.clone())).into_future().await { + let Ok(mut body) = self.body.lock() else { + bail!("lock poisoned"); + }; + *body = Body::Host { + stream: None, + buffer: Some(BodyFrame::Trailers(Err(err))), + }; + } + return Ok(()); + } + } + } + } + Body::Host { + stream: None, + buffer: Some(BodyFrame::Trailers(res)), + } => { + drop(self.contents_tx); + if !self + .trailers_tx + .write(clone_trailer_result(&res)) + .into_future() + .await + { + let Ok(mut body) = self.body.lock() else { + bail!("lock poisoned"); + }; + *body = Body::Host { + stream: None, + buffer: Some(BodyFrame::Trailers(res)), + }; + } + return Ok(()); + } + Body::Host { .. } => bail!("corrupted host body state"), + Body::Consumed => bail!("body is consumed"), + } + } +} + +impl Host for WasiHttpImpl where T: WasiHttpView {} + +impl HostFields for WasiHttpImpl +where + T: WasiHttpView, +{ + fn new(&mut self) -> wasmtime::Result>> { + push_fields(self.table(), WithChildren::default()) + } + + fn from_list( + &mut self, + entries: Vec<(FieldName, FieldValue)>, + ) -> wasmtime::Result>, HeaderError>> { + let mut fields = http::HeaderMap::new(); + + for (header, value) in entries { + let Ok(header) = header.parse() else { + return Ok(Err(HeaderError::InvalidSyntax)); + }; + if is_forbidden_header(self, &header) { + return Ok(Err(HeaderError::Forbidden)); + } + let value = match http::header::HeaderValue::from_bytes(&value) { + Ok(value) => value, + Err(_) => return Ok(Err(HeaderError::InvalidSyntax)), + }; + fields.append(header, value); + } + let fields = push_fields(self.table(), WithChildren::new(fields))?; + Ok(Ok(fields)) + } + + fn get( + &mut self, + fields: Resource>, + name: FieldName, + ) -> wasmtime::Result> { + let fields = get_fields_inner(self.table(), &fields)?; + Ok(fields + .get_all(name) + .into_iter() + .map(|val| val.as_bytes().into()) + .collect()) + } + + fn has( + &mut self, + fields: Resource>, + name: FieldName, + ) -> wasmtime::Result { + let fields = get_fields_inner(self.table(), &fields)?; + Ok(fields.contains_key(name)) + } + + fn set( + &mut self, + fields: Resource>, + name: FieldName, + value: Vec, + ) -> wasmtime::Result> { + let Ok(name) = name.parse() else { + return Ok(Err(HeaderError::InvalidSyntax)); + }; + if is_forbidden_header(self, &name) { + return Ok(Err(HeaderError::Forbidden)); + } + let mut values = Vec::with_capacity(value.len()); + for value in value { + match http::header::HeaderValue::from_bytes(&value) { + Ok(value) => values.push(value), + Err(_) => return Ok(Err(HeaderError::InvalidSyntax)), + } + } + let Some(mut fields) = get_fields_inner_mut(self.table(), &fields)? else { + return Ok(Err(HeaderError::Immutable)); + }; + fields.remove(&name); + for value in values { + fields.append(&name, value); + } + Ok(Ok(())) + } + + fn delete( + &mut self, + fields: Resource>, + name: FieldName, + ) -> wasmtime::Result> { + let header = match http::header::HeaderName::from_bytes(name.as_bytes()) { + Ok(header) => header, + Err(_) => return Ok(Err(HeaderError::InvalidSyntax)), + }; + if is_forbidden_header(self, &header) { + return Ok(Err(HeaderError::Forbidden)); + } + let Some(mut fields) = get_fields_inner_mut(self.table(), &fields)? else { + return Ok(Err(HeaderError::Immutable)); + }; + fields.remove(&name); + Ok(Ok(())) + } + + fn get_and_delete( + &mut self, + fields: Resource>, + name: FieldName, + ) -> wasmtime::Result, HeaderError>> { + let Ok(header) = http::header::HeaderName::from_bytes(name.as_bytes()) else { + return Ok(Err(HeaderError::InvalidSyntax)); + }; + if is_forbidden_header(self, &header) { + return Ok(Err(HeaderError::Forbidden)); + } + let Some(mut fields) = get_fields_inner_mut(self.table(), &fields)? else { + return Ok(Err(HeaderError::Immutable)); + }; + let http::header::Entry::Occupied(entry) = fields.entry(header) else { + return Ok(Ok(vec![])); + }; + let (.., values) = entry.remove_entry_mult(); + Ok(Ok(values.map(|header| header.as_bytes().into()).collect())) + } + + fn append( + &mut self, + fields: Resource>, + name: FieldName, + value: FieldValue, + ) -> wasmtime::Result> { + let header = match http::header::HeaderName::from_bytes(name.as_bytes()) { + Ok(header) => header, + Err(_) => return Ok(Err(HeaderError::InvalidSyntax)), + }; + if is_forbidden_header(self, &header) { + return Ok(Err(HeaderError::Forbidden)); + } + let value = match http::header::HeaderValue::from_bytes(&value) { + Ok(value) => value, + Err(_) => return Ok(Err(HeaderError::InvalidSyntax)), + }; + let Some(mut fields) = get_fields_inner_mut(self.table(), &fields)? else { + return Ok(Err(HeaderError::Immutable)); + }; + fields.append(header, value); + Ok(Ok(())) + } + + fn entries( + &mut self, + fields: Resource>, + ) -> wasmtime::Result> { + let fields = get_fields_inner(self.table(), &fields)?; + let fields = fields + .iter() + .map(|(name, value)| (name.as_str().into(), value.as_bytes().into())) + .collect(); + Ok(fields) + } + + fn clone( + &mut self, + fields: Resource>, + ) -> wasmtime::Result>> { + let table = self.table(); + let fields = get_fields(table, &fields)?; + let fields = fields.clone()?; + push_fields(table, fields) + } + + fn drop(&mut self, fields: Resource>) -> wasmtime::Result<()> { + delete_fields(self.table(), fields)?; + Ok(()) + } +} + +impl HostRequest for WasiHttpImpl +where + T: WasiHttpView, +{ + async fn new( + store: &mut Accessor, + headers: Resource>, + contents: Option>, + trailers: TrailerFuture, + options: Option>>, + ) -> wasmtime::Result<(Resource, HostFuture>)> { + store.with(|mut view| { + let (res_tx, res_rx) = future(&mut view).context("failed to create future")?; + let contents = + contents.map(|contents| contents.into_reader(&mut view).read().into_future()); + let trailers = trailers.into_reader(&mut view).read().into_future(); + let table = view.table(); + let headers = delete_fields(table, headers)?; + let headers = headers.unwrap_or_clone()?; + let options = options + .map(|options| { + let options = delete_request_options(table, options)?; + options.unwrap_or_clone() + }) + .transpose()?; + let body = Body::Guest { + contents, + trailers: Some(trailers), + buffer: None, + tx: res_tx, + }; + let req = push_request( + table, + Request::new(http::Method::GET, None, None, None, headers, body, options), + )?; + Ok((req, res_rx.into())) + }) + } + + fn method(&mut self, req: Resource) -> wasmtime::Result { + let Request { method, .. } = get_request(self.table(), &req)?; + Ok(method.into()) + } + + fn set_method( + &mut self, + req: Resource, + method: Method, + ) -> wasmtime::Result> { + let req = get_request_mut(self.table(), &req)?; + let Ok(method) = method.try_into() else { + return Ok(Err(())); + }; + req.method = method; + Ok(Ok(())) + } + + fn path_with_query(&mut self, req: Resource) -> wasmtime::Result> { + let Request { + path_with_query, .. + } = get_request(self.table(), &req)?; + Ok(path_with_query.as_ref().map(|pq| pq.as_str().into())) + } + + fn set_path_with_query( + &mut self, + req: Resource, + path_with_query: Option, + ) -> wasmtime::Result> { + let req = get_request_mut(self.table(), &req)?; + let Some(path_with_query) = path_with_query else { + req.path_with_query = None; + return Ok(Ok(())); + }; + let Ok(path_with_query) = path_with_query.try_into() else { + return Ok(Err(())); + }; + req.path_with_query = Some(path_with_query); + Ok(Ok(())) + } + + fn scheme(&mut self, req: Resource) -> wasmtime::Result> { + let Request { scheme, .. } = get_request(self.table(), &req)?; + Ok(scheme.as_ref().map(Into::into)) + } + + fn set_scheme( + &mut self, + req: Resource, + scheme: Option, + ) -> wasmtime::Result> { + let req = get_request_mut(self.table(), &req)?; + let Some(scheme) = scheme else { + req.scheme = None; + return Ok(Ok(())); + }; + let Ok(scheme) = scheme.try_into() else { + return Ok(Err(())); + }; + req.scheme = Some(scheme); + Ok(Ok(())) + } + + fn authority(&mut self, req: Resource) -> wasmtime::Result> { + let Request { authority, .. } = get_request(self.table(), &req)?; + Ok(authority.as_ref().map(|auth| auth.as_str().into())) + } + + fn set_authority( + &mut self, + req: Resource, + authority: Option, + ) -> wasmtime::Result> { + let req = get_request_mut(self.table(), &req)?; + let Some(authority) = authority else { + req.authority = None; + return Ok(Ok(())); + }; + let has_port = authority.contains(':'); + let Ok(authority) = http::uri::Authority::try_from(authority) else { + return Ok(Err(())); + }; + if has_port && authority.port_u16().is_none() { + return Ok(Err(())); + } + req.authority = Some(authority); + Ok(Ok(())) + } + + fn options( + &mut self, + req: Resource, + ) -> wasmtime::Result>>> { + let table = self.table(); + let Request { options, .. } = get_request(table, &req)?; + if let Some(options) = options { + let options = push_request_options(table, options.child())?; + Ok(Some(options)) + } else { + Ok(None) + } + } + + fn headers( + &mut self, + req: Resource, + ) -> wasmtime::Result>> { + let table = self.table(); + let Request { headers, .. } = get_request(table, &req)?; + push_fields_child(table, headers.child(), &req) + } + + async fn body( + store: &mut Accessor, + req: Resource, + ) -> wasmtime::Result, TrailerFuture), ()>> { + store.with(|mut view| { + let (contents_tx, contents_rx) = + stream(&mut view).context("failed to create stream")?; + let (trailers_tx, trailers_rx) = + future(&mut view).context("failed to create future")?; + let Request { body, .. } = get_request_mut(view.table(), &req)?; + { + let Some(body) = Arc::get_mut(body) else { + return Ok(Err(())); + }; + let Ok(body) = body.get_mut() else { + bail!("lock poisoned"); + }; + if matches!(body, Body::Consumed) { + return Ok(Err(())); + } + } + let body = Arc::clone(&body); + let task = view.spawn(BodyTask { + body, + contents_tx, + trailers_tx, + }); + let req = get_request_mut(view.table(), &req)?; + req.task = Some(task.abort_handle()); + Ok(Ok((contents_rx.into(), trailers_rx.into()))) + }) + } + + fn drop(&mut self, req: Resource) -> wasmtime::Result<()> { + self.table() + .delete(req) + .context("failed to delete request from table")?; + Ok(()) + } +} + +impl HostRequestOptions for WasiHttpImpl +where + T: WasiHttpView, +{ + fn new(&mut self) -> wasmtime::Result>> { + push_request_options(self.table(), WithChildren::default()) + } + + fn connect_timeout( + &mut self, + opts: Resource>, + ) -> wasmtime::Result> { + let RequestOptions { + connect_timeout: Some(connect_timeout), + .. + } = *get_request_options_inner(self.table(), &opts)? + else { + return Ok(None); + }; + let ns = connect_timeout.as_nanos(); + let ns = ns + .try_into() + .context("connect timeout duration nanoseconds do not fit in u64")?; + Ok(Some(ns)) + } + + fn set_connect_timeout( + &mut self, + opts: Resource>, + duration: Option, + ) -> wasmtime::Result> { + let Some(mut opts) = get_request_options_inner_mut(self.table(), &opts)? else { + return Ok(Err(RequestOptionsError::Immutable)); + }; + opts.connect_timeout = duration.map(core::time::Duration::from_nanos); + Ok(Ok(())) + } + + fn first_byte_timeout( + &mut self, + opts: Resource>, + ) -> wasmtime::Result> { + let RequestOptions { + first_byte_timeout: Some(first_byte_timeout), + .. + } = *get_request_options_inner(self.table(), &opts)? + else { + return Ok(None); + }; + let ns = first_byte_timeout.as_nanos(); + let ns = ns + .try_into() + .context("first byte timeout duration nanoseconds do not fit in u64")?; + Ok(Some(ns)) + } + + fn set_first_byte_timeout( + &mut self, + opts: Resource>, + duration: Option, + ) -> wasmtime::Result> { + let Some(mut opts) = get_request_options_inner_mut(self.table(), &opts)? else { + return Ok(Err(RequestOptionsError::Immutable)); + }; + opts.first_byte_timeout = duration.map(core::time::Duration::from_nanos); + Ok(Ok(())) + } + + fn between_bytes_timeout( + &mut self, + opts: Resource>, + ) -> wasmtime::Result> { + let RequestOptions { + between_bytes_timeout: Some(between_bytes_timeout), + .. + } = *get_request_options_inner(self.table(), &opts)? + else { + return Ok(None); + }; + let ns = between_bytes_timeout.as_nanos(); + let ns = ns + .try_into() + .context("between bytes timeout duration nanoseconds do not fit in u64")?; + Ok(Some(ns)) + } + + fn set_between_bytes_timeout( + &mut self, + opts: Resource>, + duration: Option, + ) -> wasmtime::Result> { + let Some(mut opts) = get_request_options_inner_mut(self.table(), &opts)? else { + return Ok(Err(RequestOptionsError::Immutable)); + }; + opts.between_bytes_timeout = duration.map(core::time::Duration::from_nanos); + Ok(Ok(())) + } + + fn drop(&mut self, opts: Resource>) -> wasmtime::Result<()> { + delete_request_options(self.table(), opts)?; + Ok(()) + } + + fn clone( + &mut self, + opts: Resource>, + ) -> wasmtime::Result>> { + let table = self.table(); + let opts = get_request_options(table, &opts)?; + let opts = opts.clone()?; + push_request_options(table, opts) + } +} + +impl HostResponse for WasiHttpImpl +where + T: WasiHttpView, +{ + async fn new( + store: &mut Accessor, + headers: Resource>, + contents: Option>, + trailers: TrailerFuture, + ) -> wasmtime::Result<(Resource, HostFuture>)> { + store.with(|mut view| { + let (res_tx, res_rx) = future(&mut view).context("failed to create future")?; + let contents = + contents.map(|contents| contents.into_reader(&mut view).read().into_future()); + let trailers = trailers.into_reader(&mut view).read().into_future(); + let table = view.table(); + let headers = delete_fields(table, headers)?; + let headers = headers.unwrap_or_clone()?; + let body = Body::Guest { + contents, + trailers: Some(trailers), + buffer: None, + tx: res_tx, + }; + let res = push_response(table, Response::new(http::StatusCode::OK, headers, body))?; + Ok((res, res_rx.into())) + }) + } + + fn status_code(&mut self, res: Resource) -> wasmtime::Result { + let res = get_response(self.table(), &res)?; + Ok(res.status.into()) + } + + fn set_status_code( + &mut self, + res: Resource, + status_code: StatusCode, + ) -> wasmtime::Result> { + let res = get_response_mut(self.table(), &res)?; + let Ok(status) = http::StatusCode::from_u16(status_code) else { + return Ok(Err(())); + }; + res.status = status; + Ok(Ok(())) + } + + fn headers( + &mut self, + res: Resource, + ) -> wasmtime::Result>> { + let table = self.table(); + let Response { headers, .. } = get_response(table, &res)?; + push_fields_child(table, headers.child(), &res) + } + + async fn body( + store: &mut Accessor, + res: Resource, + ) -> wasmtime::Result, TrailerFuture), ()>> { + store.with(|mut view| { + let (contents_tx, contents_rx) = + stream(&mut view).context("failed to create stream")?; + let (trailers_tx, trailers_rx) = + future(&mut view).context("failed to create future")?; + let Response { body, .. } = get_response_mut(view.table(), &res)?; + { + let Some(body) = Arc::get_mut(body) else { + return Ok(Err(())); + }; + let Ok(body) = body.get_mut() else { + bail!("lock poisoned"); + }; + if matches!(body, Body::Consumed) { + return Ok(Err(())); + } + } + let body = Arc::clone(&body); + let task = view.spawn(BodyTask { + body, + contents_tx, + trailers_tx, + }); + let res = get_response_mut(view.table(), &res)?; + res.body_task = Some(task.abort_handle()); + Ok(Ok((contents_rx.into(), trailers_rx.into()))) + }) + } + + fn drop(&mut self, res: Resource) -> wasmtime::Result<()> { + eprintln!("[host] drop response"); + delete_response(self.table(), res)?; + eprintln!("[host] response dropped"); + Ok(()) + } +} diff --git a/crates/wasi-http/src/p3/mod.rs b/crates/wasi-http/src/p3/mod.rs new file mode 100644 index 0000000000..d96927ad31 --- /dev/null +++ b/crates/wasi-http/src/p3/mod.rs @@ -0,0 +1,399 @@ +//! # Wasmtime's WASI HTTP Implementation +//! +//! This crate is Wasmtime's host implementation of the `wasi:http` package as +//! part of WASIp2. This crate's implementation is primarily built on top of +//! [`hyper`] and [`tokio`]. +//! +//! # WASI HTTP Interfaces +//! +//! This crate contains implementations of the following interfaces: +//! +//! * [`wasi:http/incoming-handler`] +//! * [`wasi:http/outgoing-handler`] +//! * [`wasi:http/types`] +//! +//! The crate also contains an implementation of the [`wasi:http/proxy`] world. +//! +//! [`wasi:http/proxy`]: crate::bindings::Proxy +//! [`wasi:http/outgoing-handler`]: crate::bindings::http::outgoing_handler::Host +//! [`wasi:http/types`]: crate::bindings::http::types::Host +//! [`wasi:http/incoming-handler`]: crate::bindings::exports::wasi::http::incoming_handler::Guest +//! +//! This crate is very similar to [`wasmtime-wasi`] in the it uses the +//! `bindgen!` macro in Wasmtime to generate bindings to interfaces. Bindings +//! are located in the [`bindings`] module. +//! +//! # The `WasiHttpView` trait +//! +//! All `bindgen!`-generated `Host` traits are implemented in terms of a +//! [`WasiHttpView`] trait which provides basic access to [`WasiHttpCtx`], +//! configuration for WASI HTTP, and a [`wasmtime_wasi::ResourceTable`], the +//! state for all host-defined component model resources. +//! +//! The [`WasiHttpView`] trait additionally offers a few other configuration +//! methods such as [`WasiHttpView::send_request`] to customize how outgoing +//! HTTP requests are handled. +//! +//! # Async and Sync +//! +//! There are both asynchronous and synchronous bindings in this crate. For +//! example [`add_to_linker_async`] is for asynchronous embedders and +//! [`add_to_linker_sync`] is for synchronous embedders. Note that under the +//! hood both versions are implemented with `async` on top of [`tokio`]. +//! +//! # Examples +//! +//! Usage of this crate is done through a few steps to get everything hooked up: +//! +//! 1. First implement [`WasiHttpView`] for your type which is the `T` in +//! [`wasmtime::Store`]. +//! 2. Add WASI HTTP interfaces to a [`wasmtime::component::Linker`]. There +//! are a few options of how to do this: +//! * Use [`add_to_linker_async`] to bundle all interfaces in +//! `wasi:http/proxy` together +//! * Use [`add_only_http_to_linker_async`] to add only HTTP interfaces but +//! no others. This is useful when working with +//! [`wasmtime_wasi::add_to_linker_async`] for example. +//! * Add individual interfaces such as with the +//! [`bindings::http::outgoing_handler::add_to_linker_get_host`] function. +//! 3. Use [`ProxyPre`](bindings::ProxyPre) to pre-instantiate a component +//! before serving requests. +//! 4. When serving requests use +//! [`ProxyPre::instantiate_async`](bindings::ProxyPre::instantiate_async) +//! to create instances and handle HTTP requests. +//! +//! A standalone example of doing all this looks like: +//! +//! ```no_run +//! use anyhow::bail; +//! use hyper::server::conn::http1; +//! use std::sync::Arc; +//! use tokio::net::TcpListener; +//! use wasmtime::component::{Component, Linker, ResourceTable}; +//! use wasmtime::{Config, Engine, Result, Store}; +//! use wasmtime_wasi::{IoView, WasiCtx, WasiCtxBuilder, WasiView}; +//! use wasmtime_wasi_http::bindings::ProxyPre; +//! use wasmtime_wasi_http::bindings::http::types::Scheme; +//! use wasmtime_wasi_http::body::HyperOutgoingBody; +//! use wasmtime_wasi_http::io::TokioIo; +//! use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; +//! +//! #[tokio::main] +//! async fn main() -> Result<()> { +//! let component = std::env::args().nth(1).unwrap(); +//! +//! // Prepare the `Engine` for Wasmtime +//! let mut config = Config::new(); +//! config.async_support(true); +//! let engine = Engine::new(&config)?; +//! +//! // Compile the component on the command line to machine code +//! let component = Component::from_file(&engine, &component)?; +//! +//! // Prepare the `ProxyPre` which is a pre-instantiated version of the +//! // component that we have. This will make per-request instantiation +//! // much quicker. +//! let mut linker = Linker::new(&engine); +//! wasmtime_wasi_http::add_to_linker_async(&mut linker)?; +//! let pre = ProxyPre::new(linker.instantiate_pre(&component)?)?; +//! +//! // Prepare our server state and start listening for connections. +//! let server = Arc::new(MyServer { pre }); +//! let listener = TcpListener::bind("127.0.0.1:8000").await?; +//! println!("Listening on {}", listener.local_addr()?); +//! +//! loop { +//! // Accept a TCP connection and serve all of its requests in a separate +//! // tokio task. Note that for now this only works with HTTP/1.1. +//! let (client, addr) = listener.accept().await?; +//! println!("serving new client from {addr}"); +//! +//! let server = server.clone(); +//! tokio::task::spawn(async move { +//! if let Err(e) = http1::Builder::new() +//! .keep_alive(true) +//! .serve_connection( +//! TokioIo::new(client), +//! hyper::service::service_fn(move |req| { +//! let server = server.clone(); +//! async move { server.handle_request(req).await } +//! }), +//! ) +//! .await +//! { +//! eprintln!("error serving client[{addr}]: {e:?}"); +//! } +//! }); +//! } +//! } +//! +//! struct MyServer { +//! pre: ProxyPre, +//! } +//! +//! impl MyServer { +//! async fn handle_request( +//! &self, +//! req: hyper::Request, +//! ) -> Result> { +//! // Create per-http-request state within a `Store` and prepare the +//! // initial resources passed to the `handle` function. +//! let mut store = Store::new( +//! self.pre.engine(), +//! MyClientState { +//! table: ResourceTable::new(), +//! wasi: WasiCtxBuilder::new().inherit_stdio().build(), +//! http: WasiHttpCtx::new(), +//! }, +//! ); +//! let (sender, receiver) = tokio::sync::oneshot::channel(); +//! let req = store.data_mut().new_incoming_request(Scheme::Http, req)?; +//! let out = store.data_mut().new_response_outparam(sender)?; +//! let pre = self.pre.clone(); +//! +//! // Run the http request itself in a separate task so the task can +//! // optionally continue to execute beyond after the initial +//! // headers/response code are sent. +//! let task = tokio::task::spawn(async move { +//! let proxy = pre.instantiate_async(&mut store).await?; +//! +//! if let Err(e) = proxy +//! .wasi_http_incoming_handler() +//! .call_handle(store, req, out) +//! .await +//! { +//! return Err(e); +//! } +//! +//! Ok(()) +//! }); +//! +//! match receiver.await { +//! // If the client calls `response-outparam::set` then one of these +//! // methods will be called. +//! Ok(Ok(resp)) => Ok(resp), +//! Ok(Err(e)) => Err(e.into()), +//! +//! // Otherwise the `sender` will get dropped along with the `Store` +//! // meaning that the oneshot will get disconnected and here we can +//! // inspect the `task` result to see what happened +//! Err(_) => { +//! let e = match task.await { +//! Ok(r) => r.unwrap_err(), +//! Err(e) => e.into(), +//! }; +//! bail!("guest never invoked `response-outparam::set` method: {e:?}") +//! } +//! } +//! } +//! } +//! +//! struct MyClientState { +//! wasi: WasiCtx, +//! http: WasiHttpCtx, +//! table: ResourceTable, +//! } +//! impl IoView for MyClientState { +//! fn table(&mut self) -> &mut ResourceTable { +//! &mut self.table +//! } +//! } +//! impl WasiView for MyClientState { +//! fn ctx(&mut self) -> &mut WasiCtx { +//! &mut self.wasi +//! } +//! } +//! +//! impl WasiHttpView for MyClientState { +//! fn ctx(&mut self) -> &mut WasiHttpCtx { +//! &mut self.http +//! } +//! } +//! ``` + +pub mod bindings; +mod body; +mod client; +mod conv; +mod host; +mod proxy; +mod request; +mod response; + +pub use body::*; +pub use client::*; +pub use request::*; +pub use response::*; + +use wasmtime_wasi::p3::ResourceView; +use wasmtime_wasi::ResourceTable; + +/// Add all of the `wasi:http/proxy` world's interfaces to a [`wasmtime::component::Linker`]. +/// +/// This function will add the `async` variant of all interfaces into the +/// `Linker` provided. By `async` this means that this function is only +/// compatible with [`Config::async_support(true)`][async]. For embeddings with +/// async support disabled see [`add_to_linker_sync`] instead. +/// +/// [async]: wasmtime::Config::async_support +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Config}; +/// use wasmtime::component::{ResourceTable, Linker}; +/// use wasmtime_wasi::{IoView, WasiCtx, WasiView}; +/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.async_support(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi_http::add_to_linker_async(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// Ok(()) +/// } +/// +/// struct MyState { +/// ctx: WasiCtx, +/// http_ctx: WasiHttpCtx, +/// table: ResourceTable, +/// } +/// +/// impl IoView for MyState { +/// fn table(&mut self) -> &mut ResourceTable { &mut self.table } +/// } +/// impl WasiHttpView for MyState { +/// fn ctx(&mut self) -> &mut WasiHttpCtx { &mut self.http_ctx } +/// } +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> &mut WasiCtx { &mut self.ctx } +/// } +/// ``` +pub fn add_to_linker(l: &mut wasmtime::component::Linker) -> anyhow::Result<()> +where + T: WasiHttpView + + wasmtime_wasi::p3::clocks::WasiClocksView + + wasmtime_wasi::p3::random::WasiRandomView + + wasmtime_wasi::p3::cli::WasiCliView + + 'static, +{ + let cli_closure = type_annotate_wasi_cli::(|t| wasmtime_wasi::p3::cli::WasiCliImpl(t)); + let random_closure = + type_annotate_wasi_random::(|t| wasmtime_wasi::p3::random::WasiRandomImpl(t)); + wasmtime_wasi::p3::clocks::add_to_linker(l)?; + + wasmtime_wasi::p3::bindings::cli::stdin::add_to_linker_get_host(l, cli_closure)?; + wasmtime_wasi::p3::bindings::cli::stdout::add_to_linker_get_host(l, cli_closure)?; + wasmtime_wasi::p3::bindings::cli::stderr::add_to_linker_get_host(l, cli_closure)?; + wasmtime_wasi::p3::bindings::random::random::add_to_linker_get_host(l, random_closure)?; + + add_only_http_to_linker(l) +} + +// NB: workaround some rustc inference - a future refactoring may make this +// obsolete. +fn type_annotate_http(val: F) -> F +where + F: Fn(&mut T) -> WasiHttpImpl<&mut T>, +{ + val +} +fn type_annotate_wasi_cli(val: F) -> F +where + F: Fn(&mut T) -> wasmtime_wasi::p3::cli::WasiCliImpl<&mut T>, +{ + val +} +fn type_annotate_wasi_random(val: F) -> F +where + F: Fn(&mut T) -> wasmtime_wasi::p3::random::WasiRandomImpl<&mut T>, +{ + val +} + +/// A slimmed down version of [`add_to_linker_async`] which only adds +/// `wasi:http` interfaces to the linker. +/// +/// This is useful when using [`wasmtime_wasi::add_to_linker_async`] for +/// example to avoid re-adding the same interfaces twice. +pub fn add_only_http_to_linker(l: &mut wasmtime::component::Linker) -> anyhow::Result<()> +where + T: WasiHttpView + 'static, +{ + let closure = type_annotate_http::(|t| WasiHttpImpl(t)); + crate::p3::bindings::http::handler::add_to_linker_get_host(l, closure)?; + crate::p3::bindings::http::types::add_to_linker_get_host(l, closure)?; + + Ok(()) +} + +/// A concrete structure that all generated `Host` traits are implemented for. +/// +/// This type serves as a small newtype wrapper to implement all of the `Host` +/// traits for `wasi:http`. This type is internally used and is only needed if +/// you're interacting with `add_to_linker` functions generated by bindings +/// themselves (or `add_to_linker_get_host`). +/// +/// This type is automatically used when using +/// [`add_to_linker`](crate::p3::add_to_linker) +#[repr(transparent)] +pub struct WasiHttpImpl(pub T); + +impl WasiHttpView for &mut T { + type Client = T::Client; + + fn http(&self) -> &WasiHttpCtx { + (**self).http() + } + + fn is_forbidden_header(&mut self, name: &http::header::HeaderName) -> bool { + (**self).is_forbidden_header(name) + } +} + +impl WasiHttpView for WasiHttpImpl { + type Client = T::Client; + + fn http(&self) -> &WasiHttpCtx { + self.0.http() + } + + fn is_forbidden_header(&mut self, name: &http::header::HeaderName) -> bool { + self.0.is_forbidden_header(name) + } +} + +impl ResourceView for WasiHttpImpl { + fn table(&mut self) -> &mut ResourceTable { + self.0.table() + } +} + +/// A trait which provides internal WASI HTTP state. +pub trait WasiHttpView: ResourceView + Send { + /// HTTP client + type Client: Client; + + /// Returns a reference to [WasiHttpCtx] + fn http(&self) -> &WasiHttpCtx; + + /// Whether a given header should be considered forbidden and not allowed. + fn is_forbidden_header(&mut self, name: &http::header::HeaderName) -> bool { + _ = name; + false + } +} + +/// Capture the state necessary for use in the wasi-http API implementation. +#[derive(Debug, Default)] +pub struct WasiHttpCtx +where + C: Client, +{ + /// HTTP client + pub client: C, +} diff --git a/crates/wasi-http/src/p3/proxy.rs b/crates/wasi-http/src/p3/proxy.rs new file mode 100644 index 0000000000..40992bde68 --- /dev/null +++ b/crates/wasi-http/src/p3/proxy.rs @@ -0,0 +1,61 @@ +use anyhow::Context as _; +use bytes::Bytes; +use http_body_util::combinators::BoxBody; +use wasmtime::component::{FutureWriter, Promise, Resource}; +use wasmtime::AsContextMut; +use wasmtime_wasi::p3::ResourceView; + +use crate::p3::bindings::http::types::ErrorCode; +use crate::p3::bindings::Proxy; +use crate::p3::{Request, Response}; + +impl Proxy { + /// Call `handle` on [Proxy] getting a [Promise] back. + async fn handle_promise( + &self, + mut store: impl AsContextMut, + req: impl Into, + ) -> wasmtime::Result, ErrorCode>>> + where + T: ResourceView + Send, + { + let mut store = store.as_context_mut(); + let table = store.data_mut().table(); + let req = table + .push(req.into()) + .context("failed to push request to table")?; + self.wasi_http_handler().call_handle(&mut store, req).await + } + + /// Call `handle` on [Proxy]. + pub async fn handle( + &self, + mut store: impl AsContextMut + Send + 'static, + req: impl Into, + ) -> wasmtime::Result< + Result< + ( + http::Response>>, + Option>>, + ), + ErrorCode, + >, + > + where + T: ResourceView + Send + 'static, + { + let handle = self.handle_promise(&mut store, req).await?; + match handle.get(&mut store).await? { + Ok(res) => { + let res = store + .as_context_mut() + .data_mut() + .table() + .delete(res) + .context("failed to delete response from table")?; + res.into_http(store).map(Ok) + } + Err(err) => Ok(Err(err)), + } + } +} diff --git a/crates/wasi-http/src/p3/request.rs b/crates/wasi-http/src/p3/request.rs new file mode 100644 index 0000000000..9a8e4b7c72 --- /dev/null +++ b/crates/wasi-http/src/p3/request.rs @@ -0,0 +1,64 @@ +use core::time::Duration; + +use std::sync::Arc; + +use http::uri::{Authority, PathAndQuery, Scheme}; +use http::{HeaderMap, Method}; +use wasmtime::component::AbortOnDropHandle; +use wasmtime_wasi::p3::WithChildren; + +use crate::p3::Body; + +/// The concrete type behind a `wasi:http/types/request-options` resource. +#[derive(Clone, Debug, Default)] +pub struct RequestOptions { + /// How long to wait for a connection to be established. + pub connect_timeout: Option, + /// How long to wait for the first byte of the response body. + pub first_byte_timeout: Option, + /// How long to wait between frames of the response body. + pub between_bytes_timeout: Option, +} +/// The concrete type behind a `wasi:http/types/request` resource. +pub struct Request { + /// The method of the request. + pub method: Method, + /// The scheme of the request. + pub scheme: Option, + /// The authority of the request. + pub authority: Option, + /// The path and query of the request. + pub path_with_query: Option, + /// The request headers. + pub headers: WithChildren, + /// Request options. + pub options: Option>, + /// The request body. + pub(crate) body: Arc>, + /// Body stream task handle + pub(crate) task: Option, +} + +impl Request { + /// Construct a new [Request] + pub fn new( + method: Method, + scheme: Option, + authority: Option, + path_with_query: Option, + headers: HeaderMap, + body: impl Into, + options: Option, + ) -> Self { + Self { + method, + scheme, + authority, + path_with_query, + headers: WithChildren::new(headers), + body: Arc::new(std::sync::Mutex::new(body.into())), + options: options.map(WithChildren::new), + task: None, + } + } +} diff --git a/crates/wasi-http/src/p3/response.rs b/crates/wasi-http/src/p3/response.rs new file mode 100644 index 0000000000..10b76e90a6 --- /dev/null +++ b/crates/wasi-http/src/p3/response.rs @@ -0,0 +1,196 @@ +use core::iter; + +use std::sync::Arc; + +use anyhow::{bail, Context as _}; +use bytes::Bytes; +use futures::StreamExt as _; +use http::{HeaderMap, StatusCode}; +use http_body_util::combinators::BoxBody; +use http_body_util::{BodyExt, BodyStream, StreamBody}; +use wasmtime::component::{AbortOnDropHandle, FutureWriter}; +use wasmtime::AsContextMut; +use wasmtime_wasi::p3::{ResourceView, WithChildren}; + +use crate::p3::bindings::http::types::ErrorCode; +use crate::p3::{empty_body, guest_response_trailers, Body, BodyFrame, GuestBody}; + +/// The concrete type behind a `wasi:http/types/response` resource. +pub struct Response { + /// The status of the response. + pub status: StatusCode, + /// The headers of the response. + pub headers: WithChildren, + /// The body of the response. + pub(crate) body: Arc>, + /// Body stream task handle + pub(crate) body_task: Option, + /// I/O task handle + #[allow(dead_code)] + pub(crate) io_task: Option, +} + +impl Response { + /// Construct a new [Response] + pub fn new(status: StatusCode, headers: HeaderMap, body: Body) -> Self { + Self { + status, + headers: WithChildren::new(headers), + body: Arc::new(std::sync::Mutex::new(body)), + body_task: None, + io_task: None, + } + } + + /// Construct a new [Response] + pub(crate) fn new_incoming( + status: StatusCode, + headers: HeaderMap, + body: Body, + task: AbortOnDropHandle, + ) -> Self { + Self { + status, + headers: WithChildren::new(headers), + body: Arc::new(std::sync::Mutex::new(body)), + body_task: None, + io_task: Some(task), + } + } + + /// Convert [Response] into [http::Response]. + pub fn into_http( + self, + mut store: impl AsContextMut + Send + 'static, + ) -> anyhow::Result<( + http::Response>>, + Option>>, + )> { + let headers = self.headers.unwrap_or_clone()?; + let mut response = http::Response::builder().status(self.status); + *response.headers_mut().unwrap() = headers; + let response = response.body(()).context("failed to build response")?; + + let Some(body) = Arc::into_inner(self.body) else { + bail!("body is borrowed") + }; + let Ok(body) = body.into_inner() else { + bail!("lock poisoned"); + }; + let (body, tx) = match body { + Body::Guest { + contents: None, + trailers: None, + buffer: Some(BodyFrame::Trailers(Ok(None))), + tx, + } => (empty_body().boxed(), Some(tx)), + Body::Guest { + contents: None, + trailers: None, + buffer: Some(BodyFrame::Trailers(Ok(Some(trailers)))), + tx, + } => { + let mut store = store.as_context_mut(); + let table = store.data_mut().table(); + let trailers = table + .delete(trailers) + .context("failed to delete trailers")?; + let trailers = trailers.unwrap_or_clone()?; + ( + empty_body() + .with_trailers(async move { Some(Ok(trailers)) }) + .boxed(), + Some(tx), + ) + } + Body::Guest { + contents: None, + trailers: None, + buffer: Some(BodyFrame::Trailers(Err(err))), + tx, + } => ( + empty_body() + .with_trailers(async move { Some(Err(Some(err))) }) + .boxed(), + Some(tx), + ), + Body::Guest { + contents: None, + trailers: Some(trailers), + buffer: None, + tx, + } => { + let body = empty_body() + .with_trailers(guest_response_trailers(store, trailers)) + .boxed(); + (body, Some(tx)) + } + Body::Guest { + contents: Some(contents), + trailers: Some(trailers), + buffer, + tx, + } => { + let buffer = match buffer { + Some(BodyFrame::Data(buffer)) => buffer, + Some(BodyFrame::Trailers(..)) => bail!("guest body is corrupted"), + None => Bytes::default(), + }; + let body = GuestBody::new(contents, buffer) + .with_trailers(guest_response_trailers(store, trailers)) + .boxed(); + (body, Some(tx)) + } + Body::Guest { .. } => bail!("guest body is corrupted"), + Body::Consumed + | Body::Host { + stream: None, + buffer: Some(BodyFrame::Trailers(Ok(None))), + } => (empty_body().boxed(), None), + Body::Host { + stream: None, + buffer: Some(BodyFrame::Trailers(Ok(Some(trailers)))), + } => { + let mut store = store.as_context_mut(); + let table = store.data_mut().table(); + let trailers = table + .delete(trailers) + .context("failed to delete trailers")?; + let trailers = trailers.unwrap_or_clone()?; + ( + empty_body() + .with_trailers(async move { Some(Ok(trailers)) }) + .boxed(), + None, + ) + } + Body::Host { + stream: None, + buffer: Some(BodyFrame::Trailers(Err(err))), + } => ( + empty_body() + .with_trailers(async move { Some(Err(Some(err))) }) + .boxed(), + None, + ), + Body::Host { + stream: Some(stream), + buffer: None, + } => (stream.map_err(Some).boxed(), None), + Body::Host { + stream: Some(stream), + buffer: Some(BodyFrame::Data(buffer)), + } => { + let buffer = futures::stream::iter(iter::once(Ok(http_body::Frame::data(buffer)))); + ( + BodyExt::boxed(StreamBody::new( + buffer.chain(BodyStream::new(stream.map_err(Some))), + )), + None, + ) + } + Body::Host { .. } => bail!("host body is corrupted"), + }; + Ok((response.map(|()| body), tx)) + } +} diff --git a/crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/command.wit b/crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/command.wit new file mode 100644 index 0000000000..0310e51514 --- /dev/null +++ b/crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/command.wit @@ -0,0 +1,10 @@ +package wasi:cli@0.3.0; + +@since(version = 0.3.0) +world command { + @since(version = 0.3.0) + include imports; + + @since(version = 0.3.0) + export run; +} diff --git a/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/environment.wit b/crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/environment.wit similarity index 87% rename from crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/environment.wit rename to crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/environment.wit index 2f449bd7c1..d99dcc0ae3 100644 --- a/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/environment.wit +++ b/crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/environment.wit @@ -1,4 +1,4 @@ -@since(version = 0.2.0) +@since(version = 0.3.0) interface environment { /// Get the POSIX-style environment variables. /// @@ -8,15 +8,15 @@ interface environment { /// 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. - @since(version = 0.2.0) + @since(version = 0.3.0) get-environment: func() -> list>; /// Get the POSIX-style arguments to the program. - @since(version = 0.2.0) + @since(version = 0.3.0) get-arguments: func() -> list; /// Return a path that programs should use as their initial current working /// directory, interpreting `.` as shorthand for this. - @since(version = 0.2.0) + @since(version = 0.3.0) initial-cwd: func() -> option; } diff --git a/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/exit.wit b/crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/exit.wit similarity index 92% rename from crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/exit.wit rename to crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/exit.wit index 427935c8d0..e799a95a26 100644 --- a/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/exit.wit +++ b/crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/exit.wit @@ -1,7 +1,7 @@ -@since(version = 0.2.0) +@since(version = 0.3.0) interface exit { /// Exit the current instance and any linked instances. - @since(version = 0.2.0) + @since(version = 0.3.0) exit: func(status: result); /// Exit the current instance and any linked instances, reporting the diff --git a/crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/imports.wit b/crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/imports.wit new file mode 100644 index 0000000000..5dbc2ede8d --- /dev/null +++ b/crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/imports.wit @@ -0,0 +1,34 @@ +package wasi:cli@0.3.0; + +@since(version = 0.3.0) +world imports { + @since(version = 0.3.0) + include wasi:clocks/imports@0.3.0; + @since(version = 0.3.0) + include wasi:filesystem/imports@0.3.0; + @since(version = 0.3.0) + include wasi:sockets/imports@0.3.0; + @since(version = 0.3.0) + include wasi:random/imports@0.3.0; + + @since(version = 0.3.0) + import environment; + @since(version = 0.3.0) + import exit; + @since(version = 0.3.0) + import stdin; + @since(version = 0.3.0) + import stdout; + @since(version = 0.3.0) + import stderr; + @since(version = 0.3.0) + import terminal-input; + @since(version = 0.3.0) + import terminal-output; + @since(version = 0.3.0) + import terminal-stdin; + @since(version = 0.3.0) + import terminal-stdout; + @since(version = 0.3.0) + import terminal-stderr; +} diff --git a/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/run.wit b/crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/run.wit similarity index 56% rename from crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/run.wit rename to crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/run.wit index 655346efb6..6dd8b6879e 100644 --- a/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/run.wit +++ b/crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/run.wit @@ -1,6 +1,6 @@ -@since(version = 0.2.0) +@since(version = 0.3.0) interface run { /// Run the program. - @since(version = 0.2.0) + @since(version = 0.3.0) run: func() -> result; } diff --git a/crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/stdio.wit b/crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/stdio.wit new file mode 100644 index 0000000000..6a1208fad7 --- /dev/null +++ b/crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/stdio.wit @@ -0,0 +1,17 @@ +@since(version = 0.3.0) +interface stdin { + @since(version = 0.3.0) + get-stdin: func() -> stream; +} + +@since(version = 0.3.0) +interface stdout { + @since(version = 0.3.0) + set-stdout: func(data: stream); +} + +@since(version = 0.3.0) +interface stderr { + @since(version = 0.3.0) + set-stderr: func(data: stream); +} diff --git a/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/terminal.wit b/crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/terminal.wit similarity index 82% rename from crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/terminal.wit rename to crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/terminal.wit index d305498c64..c37184f4c7 100644 --- a/crates/wasi-http/src/p2/wit/deps/cli@v0.2.3/terminal.wit +++ b/crates/wasi-http/src/p3/wit/deps/cli@82b86d9@wit-0.3.0-draft/terminal.wit @@ -3,10 +3,10 @@ /// 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. -@since(version = 0.2.0) +@since(version = 0.3.0) interface terminal-input { /// The input side of a terminal. - @since(version = 0.2.0) + @since(version = 0.3.0) resource terminal-input; } @@ -15,48 +15,48 @@ interface terminal-input { /// In the future, this may include functions for querying the terminal /// size, being notified of terminal size changes, querying supported /// features, and so on. -@since(version = 0.2.0) +@since(version = 0.3.0) interface terminal-output { /// The output side of a terminal. - @since(version = 0.2.0) + @since(version = 0.3.0) resource terminal-output; } /// An interface providing an optional `terminal-input` for stdin as a /// link-time authority. -@since(version = 0.2.0) +@since(version = 0.3.0) interface terminal-stdin { - @since(version = 0.2.0) + @since(version = 0.3.0) use terminal-input.{terminal-input}; /// If stdin is connected to a terminal, return a `terminal-input` handle /// allowing further interaction with it. - @since(version = 0.2.0) + @since(version = 0.3.0) get-terminal-stdin: func() -> option; } /// An interface providing an optional `terminal-output` for stdout as a /// link-time authority. -@since(version = 0.2.0) +@since(version = 0.3.0) interface terminal-stdout { - @since(version = 0.2.0) + @since(version = 0.3.0) use terminal-output.{terminal-output}; /// If stdout is connected to a terminal, return a `terminal-output` handle /// allowing further interaction with it. - @since(version = 0.2.0) + @since(version = 0.3.0) get-terminal-stdout: func() -> option; } /// An interface providing an optional `terminal-output` for stderr as a /// link-time authority. -@since(version = 0.2.0) +@since(version = 0.3.0) interface terminal-stderr { - @since(version = 0.2.0) + @since(version = 0.3.0) use terminal-output.{terminal-output}; /// If stderr is connected to a terminal, return a `terminal-output` handle /// allowing further interaction with it. - @since(version = 0.2.0) + @since(version = 0.3.0) get-terminal-stderr: func() -> option; } diff --git a/crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/monotonic-clock.wit b/crates/wasi-http/src/p3/wit/deps/clocks@646092f@wit-0.3.0-draft/monotonic-clock.wit similarity index 61% rename from crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/monotonic-clock.wit rename to crates/wasi-http/src/p3/wit/deps/clocks@646092f@wit-0.3.0-draft/monotonic-clock.wit index c676fb84d8..87ebdaac51 100644 --- a/crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/monotonic-clock.wit +++ b/crates/wasi-http/src/p3/wit/deps/clocks@646092f@wit-0.3.0-draft/monotonic-clock.wit @@ -1,4 +1,4 @@ -package wasi:clocks@0.2.3; +package wasi:clocks@0.3.0; /// WASI Monotonic Clock is a clock API intended to let users measure elapsed /// time. /// @@ -7,44 +7,39 @@ package wasi:clocks@0.2.3; /// /// A monotonic clock is a clock which has an unspecified initial value, and /// successive reads of the clock will produce non-decreasing values. -@since(version = 0.2.0) +@since(version = 0.3.0) interface monotonic-clock { - @since(version = 0.2.0) - use wasi:io/poll@0.2.3.{pollable}; - /// An instant in time, in nanoseconds. An instant is relative to an /// unspecified initial value, and can only be compared to instances from /// the same monotonic-clock. - @since(version = 0.2.0) + @since(version = 0.3.0) type instant = u64; /// A duration of time, in nanoseconds. - @since(version = 0.2.0) + @since(version = 0.3.0) type duration = 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. - @since(version = 0.2.0) + @since(version = 0.3.0) now: func() -> instant; /// Query the resolution of the clock. Returns the duration of time /// corresponding to a clock tick. - @since(version = 0.2.0) + @since(version = 0.3.0) resolution: func() -> duration; - /// Create a `pollable` which will resolve once the specified instant - /// has occurred. - @since(version = 0.2.0) - subscribe-instant: func( + /// Wait until the specified instant has occurred. + @since(version = 0.3.0) + wait-until: func( when: instant, - ) -> pollable; + ); - /// Create a `pollable` that will resolve after the specified duration has - /// elapsed from the time this function is invoked. - @since(version = 0.2.0) - subscribe-duration: func( - when: duration, - ) -> pollable; + /// Wait for the specified duration has elapsed. + @since(version = 0.3.0) + wait-for: func( + how-long: duration, + ); } diff --git a/crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/timezone.wit b/crates/wasi-http/src/p3/wit/deps/clocks@646092f@wit-0.3.0-draft/timezone.wit similarity index 98% rename from crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/timezone.wit rename to crates/wasi-http/src/p3/wit/deps/clocks@646092f@wit-0.3.0-draft/timezone.wit index b43e93b233..ac9146834f 100644 --- a/crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/timezone.wit +++ b/crates/wasi-http/src/p3/wit/deps/clocks@646092f@wit-0.3.0-draft/timezone.wit @@ -1,4 +1,4 @@ -package wasi:clocks@0.2.3; +package wasi:clocks@0.3.0; @unstable(feature = clocks-timezone) interface timezone { diff --git a/crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/wall-clock.wit b/crates/wasi-http/src/p3/wit/deps/clocks@646092f@wit-0.3.0-draft/wall-clock.wit similarity index 92% rename from crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/wall-clock.wit rename to crates/wasi-http/src/p3/wit/deps/clocks@646092f@wit-0.3.0-draft/wall-clock.wit index e00ce08933..b7a85ab356 100644 --- a/crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/wall-clock.wit +++ b/crates/wasi-http/src/p3/wit/deps/clocks@646092f@wit-0.3.0-draft/wall-clock.wit @@ -1,4 +1,4 @@ -package wasi:clocks@0.2.3; +package wasi:clocks@0.3.0; /// 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. @@ -13,10 +13,10 @@ package wasi:clocks@0.2.3; /// monotonic, making it unsuitable for measuring elapsed time. /// /// It is intended for reporting the current date and time for humans. -@since(version = 0.2.0) +@since(version = 0.3.0) interface wall-clock { /// A time and date in seconds plus nanoseconds. - @since(version = 0.2.0) + @since(version = 0.3.0) record datetime { seconds: u64, nanoseconds: u32, @@ -35,12 +35,12 @@ interface wall-clock { /// /// [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 - @since(version = 0.2.0) + @since(version = 0.3.0) now: func() -> datetime; /// Query the resolution of the clock. /// /// The nanoseconds field of the output is always less than 1000000000. - @since(version = 0.2.0) + @since(version = 0.3.0) resolution: func() -> datetime; } diff --git a/crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/world.wit b/crates/wasi-http/src/p3/wit/deps/clocks@646092f@wit-0.3.0-draft/world.wit similarity index 55% rename from crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/world.wit rename to crates/wasi-http/src/p3/wit/deps/clocks@646092f@wit-0.3.0-draft/world.wit index 05f04f797d..f97bcfef13 100644 --- a/crates/wasi-http/src/p2/wit/deps/clocks@v0.2.3/world.wit +++ b/crates/wasi-http/src/p3/wit/deps/clocks@646092f@wit-0.3.0-draft/world.wit @@ -1,10 +1,10 @@ -package wasi:clocks@0.2.3; +package wasi:clocks@0.3.0; -@since(version = 0.2.0) +@since(version = 0.3.0) world imports { - @since(version = 0.2.0) + @since(version = 0.3.0) import monotonic-clock; - @since(version = 0.2.0) + @since(version = 0.3.0) import wall-clock; @unstable(feature = clocks-timezone) import timezone; diff --git a/crates/wasi-http/src/p2/wit/deps/filesystem@v0.2.3/preopens.wit b/crates/wasi-http/src/p3/wit/deps/filesystem@740cd76@wit-0.3.0-draft/preopens.wit similarity index 62% rename from crates/wasi-http/src/p2/wit/deps/filesystem@v0.2.3/preopens.wit rename to crates/wasi-http/src/p3/wit/deps/filesystem@740cd76@wit-0.3.0-draft/preopens.wit index cea97495b5..0b29aae334 100644 --- a/crates/wasi-http/src/p2/wit/deps/filesystem@v0.2.3/preopens.wit +++ b/crates/wasi-http/src/p3/wit/deps/filesystem@740cd76@wit-0.3.0-draft/preopens.wit @@ -1,11 +1,11 @@ -package wasi:filesystem@0.2.3; +package wasi:filesystem@0.3.0; -@since(version = 0.2.0) +@since(version = 0.3.0) interface preopens { - @since(version = 0.2.0) + @since(version = 0.3.0) use types.{descriptor}; /// Return the set of preopened directories, and their paths. - @since(version = 0.2.0) + @since(version = 0.3.0) get-directories: func() -> list>; } diff --git a/crates/wasi-http/src/p2/wit/deps/filesystem@v0.2.3/types.wit b/crates/wasi-http/src/p3/wit/deps/filesystem@740cd76@wit-0.3.0-draft/types.wit similarity index 84% rename from crates/wasi-http/src/p2/wit/deps/filesystem@v0.2.3/types.wit rename to crates/wasi-http/src/p3/wit/deps/filesystem@740cd76@wit-0.3.0-draft/types.wit index d229a21f48..af3cb254cc 100644 --- a/crates/wasi-http/src/p2/wit/deps/filesystem@v0.2.3/types.wit +++ b/crates/wasi-http/src/p3/wit/deps/filesystem@740cd76@wit-0.3.0-draft/types.wit @@ -1,4 +1,4 @@ -package wasi:filesystem@0.2.3; +package wasi:filesystem@0.3.0; /// 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. @@ -23,21 +23,19 @@ package wasi:filesystem@0.2.3; /// [WASI filesystem path resolution]. /// /// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md -@since(version = 0.2.0) +@since(version = 0.3.0) interface types { - @since(version = 0.2.0) - use wasi:io/streams@0.2.3.{input-stream, output-stream, error}; - @since(version = 0.2.0) - use wasi:clocks/wall-clock@0.2.3.{datetime}; + @since(version = 0.3.0) + use wasi:clocks/wall-clock@0.3.0.{datetime}; /// File size or length of a region within a file. - @since(version = 0.2.0) + @since(version = 0.3.0) type filesize = u64; /// The type of a filesystem object referenced by a descriptor. /// /// Note: This was called `filetype` in earlier versions of WASI. - @since(version = 0.2.0) + @since(version = 0.3.0) enum descriptor-type { /// The type of the descriptor or file is unknown or is different from /// any of the other types specified. @@ -61,7 +59,7 @@ interface types { /// Descriptor flags. /// /// Note: This was called `fdflags` in earlier versions of WASI. - @since(version = 0.2.0) + @since(version = 0.3.0) flags descriptor-flags { /// Read mode: Data can be read. read, @@ -105,7 +103,7 @@ interface types { /// File attributes. /// /// Note: This was called `filestat` in earlier versions of WASI. - @since(version = 0.2.0) + @since(version = 0.3.0) record descriptor-stat { /// File type. %type: descriptor-type, @@ -132,7 +130,7 @@ interface types { } /// Flags determining the method of how paths are resolved. - @since(version = 0.2.0) + @since(version = 0.3.0) flags path-flags { /// As long as the resolved path corresponds to a symbolic link, it is /// expanded. @@ -140,7 +138,7 @@ interface types { } /// Open flags used by `open-at`. - @since(version = 0.2.0) + @since(version = 0.3.0) flags open-flags { /// Create file if it does not exist, similar to `O_CREAT` in POSIX. create, @@ -153,11 +151,11 @@ interface types { } /// Number of hard links to an inode. - @since(version = 0.2.0) + @since(version = 0.3.0) type link-count = u64; /// When setting a timestamp, this gives the value to set it to. - @since(version = 0.2.0) + @since(version = 0.3.0) variant new-timestamp { /// Leave the timestamp set to its previous value. no-change, @@ -184,8 +182,6 @@ interface types { 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. @@ -259,7 +255,7 @@ interface types { } /// File or memory access pattern advisory information. - @since(version = 0.2.0) + @since(version = 0.3.0) enum advice { /// The application has no advice to give on its behavior with respect /// to the specified data. @@ -283,7 +279,7 @@ interface types { /// A 128-bit hash value, split into parts because wasm doesn't have a /// 128-bit integer type. - @since(version = 0.2.0) + @since(version = 0.3.0) record metadata-hash-value { /// 64 bits of a 128-bit hash value. lower: u64, @@ -294,47 +290,58 @@ interface types { /// 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. - @since(version = 0.2.0) + @since(version = 0.3.0) resource descriptor { - /// Return a stream for reading from a file, if available. - /// - /// May fail with an error-code describing why the file cannot be read. + /// Return a stream for reading from a file. /// /// 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. - @since(version = 0.2.0) + /// This function returns a future, which will resolve to an error code if + /// reading full contents of the file fails. + /// + /// Note: This is similar to `pread` in POSIX. + @since(version = 0.3.0) read-via-stream: func( /// The offset within the file at which to start reading. offset: filesize, - ) -> result; + ) -> tuple, future>>; /// 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. - @since(version = 0.2.0) + /// 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. + /// + /// This function returns once either full contents of the stream are + /// written or an error is encountered. + /// + /// Note: This is similar to `pwrite` in POSIX. + @since(version = 0.3.0) write-via-stream: func( + /// Data to write + data: stream, /// The offset within the file at which to start writing. offset: filesize, - ) -> result; + ) -> result<_, 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 POSIX. - @since(version = 0.2.0) - append-via-stream: func() -> result; + /// This function returns once either full contents of the stream are + /// written or an error is encountered. + /// + /// Note: This is similar to `write` with `O_APPEND` in POSIX. + @since(version = 0.3.0) + append-via-stream: func(data: stream) -> result<_, error-code>; /// Provide file advisory information on a descriptor. /// /// This is similar to `posix_fadvise` in POSIX. - @since(version = 0.2.0) + @since(version = 0.3.0) advise: func( /// The offset within the file to which the advisory applies. offset: filesize, @@ -350,7 +357,7 @@ interface types { /// opened for writing. /// /// Note: This is similar to `fdatasync` in POSIX. - @since(version = 0.2.0) + @since(version = 0.3.0) sync-data: func() -> result<_, error-code>; /// Get flags associated with a descriptor. @@ -359,7 +366,7 @@ interface types { /// /// Note: This returns the value that was the `fs_flags` value returned /// from `fdstat_get` in earlier versions of WASI. - @since(version = 0.2.0) + @since(version = 0.3.0) get-flags: func() -> result; /// Get the dynamic type of a descriptor. @@ -372,14 +379,14 @@ interface types { /// /// Note: This returns the value that was the `fs_filetype` value returned /// from `fdstat_get` in earlier versions of WASI. - @since(version = 0.2.0) + @since(version = 0.3.0) get-type: func() -> result; /// Adjust the size of an open file. If this increases the file's size, the /// extra bytes are filled with zeros. /// /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. - @since(version = 0.2.0) + @since(version = 0.3.0) set-size: func(size: filesize) -> result<_, error-code>; /// Adjust the timestamps of an open file or directory. @@ -387,7 +394,7 @@ interface types { /// Note: This is similar to `futimens` in POSIX. /// /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. - @since(version = 0.2.0) + @since(version = 0.3.0) set-times: func( /// The desired values of the data access timestamp. data-access-timestamp: new-timestamp, @@ -395,42 +402,6 @@ interface types { data-modification-timestamp: new-timestamp, ) -> result<_, error-code>; - /// Read from a descriptor, without using and updating the descriptor's offset. - /// - /// This function returns a list of bytes containing the data that was - /// read, along with a bool which, when true, indicates that the end of the - /// file was reached. The returned list will contain up to `length` bytes; it - /// may return fewer than requested, if the end of the file is reached or - /// if the I/O operation is interrupted. - /// - /// In the future, this may change to return a `stream`. - /// - /// Note: This is similar to `pread` in POSIX. - @since(version = 0.2.0) - read: func( - /// The maximum number of bytes to read. - length: filesize, - /// The offset within the file at which to read. - offset: filesize, - ) -> result, bool>, error-code>; - - /// Write to a descriptor, without using and updating the descriptor's offset. - /// - /// It is valid to write past the end of a file; the file is extended to the - /// extent of the write, with bytes between the previous end and the start of - /// the write set to zero. - /// - /// In the future, this may change to take a `stream`. - /// - /// Note: This is similar to `pwrite` in POSIX. - @since(version = 0.2.0) - write: func( - /// Data to write - buffer: list, - /// The offset within the file at which to write. - offset: filesize, - ) -> result; - /// Read directory entries from a directory. /// /// On filesystems where directories contain entries referring to themselves @@ -440,8 +411,11 @@ interface types { /// 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. - @since(version = 0.2.0) - read-directory: func() -> result; + /// + /// This function returns a future, which will resolve to an error code if + /// reading full contents of the directory fails. + @since(version = 0.3.0) + read-directory: func() -> tuple, future>>; /// Synchronize the data and metadata of a file to disk. /// @@ -449,13 +423,13 @@ interface types { /// opened for writing. /// /// Note: This is similar to `fsync` in POSIX. - @since(version = 0.2.0) + @since(version = 0.3.0) sync: func() -> result<_, error-code>; /// Create a directory. /// /// Note: This is similar to `mkdirat` in POSIX. - @since(version = 0.2.0) + @since(version = 0.3.0) create-directory-at: func( /// The relative path at which to create the directory. path: string, @@ -470,7 +444,7 @@ interface types { /// modified, use `metadata-hash`. /// /// Note: This was called `fd_filestat_get` in earlier versions of WASI. - @since(version = 0.2.0) + @since(version = 0.3.0) stat: func() -> result; /// Return the attributes of a file or directory. @@ -480,7 +454,7 @@ interface types { /// discussion of alternatives. /// /// Note: This was called `path_filestat_get` in earlier versions of WASI. - @since(version = 0.2.0) + @since(version = 0.3.0) stat-at: func( /// Flags determining the method of how the path is resolved. path-flags: path-flags, @@ -494,7 +468,7 @@ interface types { /// /// Note: This was called `path_filestat_set_times` in earlier versions of /// WASI. - @since(version = 0.2.0) + @since(version = 0.3.0) set-times-at: func( /// Flags determining the method of how the path is resolved. path-flags: path-flags, @@ -508,8 +482,12 @@ interface types { /// Create a hard link. /// + /// Fails with `error-code::no-entry` if the old path does not exist, + /// with `error-code::exist` if the new path already exists, and + /// `error-code::not-permitted` if the old path is not a file. + /// /// Note: This is similar to `linkat` in POSIX. - @since(version = 0.2.0) + @since(version = 0.3.0) link-at: func( /// Flags determining the method of how the path is resolved. old-path-flags: path-flags, @@ -533,7 +511,7 @@ interface types { /// `error-code::read-only`. /// /// Note: This is similar to `openat` in POSIX. - @since(version = 0.2.0) + @since(version = 0.3.0) open-at: func( /// Flags determining the method of how the path is resolved. path-flags: path-flags, @@ -551,7 +529,7 @@ interface types { /// filesystem, this function fails with `error-code::not-permitted`. /// /// Note: This is similar to `readlinkat` in POSIX. - @since(version = 0.2.0) + @since(version = 0.3.0) readlink-at: func( /// The relative path of the symbolic link from which to read. path: string, @@ -562,7 +540,7 @@ interface types { /// Return `error-code::not-empty` if the directory is not empty. /// /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. - @since(version = 0.2.0) + @since(version = 0.3.0) remove-directory-at: func( /// The relative path to a directory to remove. path: string, @@ -571,7 +549,7 @@ interface types { /// Rename a filesystem object. /// /// Note: This is similar to `renameat` in POSIX. - @since(version = 0.2.0) + @since(version = 0.3.0) rename-at: func( /// The relative source path of the file or directory to rename. old-path: string, @@ -587,7 +565,7 @@ interface types { /// `error-code::not-permitted`. /// /// Note: This is similar to `symlinkat` in POSIX. - @since(version = 0.2.0) + @since(version = 0.3.0) symlink-at: func( /// The contents of the symbolic link. old-path: string, @@ -599,7 +577,7 @@ interface types { /// /// Return `error-code::is-directory` if the path refers to a directory. /// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. - @since(version = 0.2.0) + @since(version = 0.3.0) unlink-file-at: func( /// The relative path to a file to unlink. path: string, @@ -611,7 +589,7 @@ interface types { /// 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. - @since(version = 0.2.0) + @since(version = 0.3.0) is-same-object: func(other: borrow) -> bool; /// Return a hash of the metadata associated with a filesystem object referred @@ -633,14 +611,14 @@ interface types { /// computed hash. /// /// However, none of these is required. - @since(version = 0.2.0) + @since(version = 0.3.0) metadata-hash: func() -> result; /// Return a hash of the metadata associated with a filesystem object referred /// to by a directory descriptor and a relative path. /// /// This performs the same hash computation as `metadata-hash`. - @since(version = 0.2.0) + @since(version = 0.3.0) metadata-hash-at: func( /// Flags determining the method of how the path is resolved. path-flags: path-flags, @@ -648,25 +626,4 @@ interface types { path: string, ) -> result; } - - /// A stream of directory entries. - @since(version = 0.2.0) - resource directory-entry-stream { - /// Read a single directory entry from a `directory-entry-stream`. - @since(version = 0.2.0) - read-directory-entry: func() -> result, error-code>; - } - - /// Attempts to extract a filesystem-related `error-code` from the stream - /// `error` provided. - /// - /// Stream operations which return `stream-error::last-operation-failed` - /// have a payload with more information about the operation that failed. - /// This payload can be passed through to this function to see if there's - /// filesystem-related information about the error to return. - /// - /// Note that this function is fallible because not all stream-related - /// errors are filesystem-related errors. - @since(version = 0.2.0) - filesystem-error-code: func(err: borrow) -> option; } diff --git a/crates/wasi-http/src/p3/wit/deps/filesystem@740cd76@wit-0.3.0-draft/world.wit b/crates/wasi-http/src/p3/wit/deps/filesystem@740cd76@wit-0.3.0-draft/world.wit new file mode 100644 index 0000000000..c0ab32afe2 --- /dev/null +++ b/crates/wasi-http/src/p3/wit/deps/filesystem@740cd76@wit-0.3.0-draft/world.wit @@ -0,0 +1,9 @@ +package wasi:filesystem@0.3.0; + +@since(version = 0.3.0) +world imports { + @since(version = 0.3.0) + import types; + @since(version = 0.3.0) + import preopens; +} diff --git a/crates/wasi-http/src/p3/wit/deps/http@ae89575@wit-0.3.0-draft/handler.wit b/crates/wasi-http/src/p3/wit/deps/http@ae89575@wit-0.3.0-draft/handler.wit new file mode 100644 index 0000000000..099d094c7f --- /dev/null +++ b/crates/wasi-http/src/p3/wit/deps/http@ae89575@wit-0.3.0-draft/handler.wit @@ -0,0 +1,17 @@ +/// This interface defines a handler of HTTP Requests. It may be imported by +/// components which wish to send HTTP Requests and also exported by components +/// which can respond to HTTP Requests. In addition, it may be used to pass +/// a request from one component to another without any use of a network. +interface handler { + use types.{request, response, error-code}; + + /// When exported, this function may be called with either an incoming + /// request read from the network or a request synthesized or forwarded by + /// another component. + /// + /// When imported, this function may be used to either send an outgoing + /// request over the network or pass it to another component. + handle: func( + request: request, + ) -> result; +} diff --git a/crates/wasi-http/src/p2/wit/deps/http@v0.2.3/proxy.wit b/crates/wasi-http/src/p3/wit/deps/http@ae89575@wit-0.3.0-draft/proxy.wit similarity index 70% rename from crates/wasi-http/src/p2/wit/deps/http@v0.2.3/proxy.wit rename to crates/wasi-http/src/p3/wit/deps/http@ae89575@wit-0.3.0-draft/proxy.wit index de3bbe8ae0..16688b96eb 100644 --- a/crates/wasi-http/src/p2/wit/deps/http@v0.2.3/proxy.wit +++ b/crates/wasi-http/src/p3/wit/deps/http@ae89575@wit-0.3.0-draft/proxy.wit @@ -1,50 +1,44 @@ -package wasi:http@0.2.3; +package wasi:http@0.3.0-draft; /// The `wasi:http/imports` world imports all the APIs for HTTP proxies. /// It is intended to be `include`d in other worlds. -@since(version = 0.2.0) world imports { /// HTTP proxies have access to time and randomness. - @since(version = 0.2.0) - import wasi:clocks/monotonic-clock@0.2.3; - @since(version = 0.2.0) - import wasi:clocks/wall-clock@0.2.3; - @since(version = 0.2.0) - import wasi:random/random@0.2.3; + include wasi:clocks/imports@0.3.0; + import wasi:random/random@0.3.0; /// Proxies have standard output and error streams which are expected to /// terminate in a developer-facing console provided by the host. - @since(version = 0.2.0) - import wasi:cli/stdout@0.2.3; - @since(version = 0.2.0) - import wasi:cli/stderr@0.2.3; + import wasi:cli/stdout@0.3.0; + import wasi:cli/stderr@0.3.0; /// 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. - @since(version = 0.2.0) - import wasi:cli/stdin@0.2.3; + import wasi:cli/stdin@0.3.0; /// This is the default handler to use when user code simply wants to make an /// HTTP request (e.g., via `fetch()`). - @since(version = 0.2.0) - import outgoing-handler; + /// + /// This may also be used to pass synthesized or forwarded requests to another + /// component. + import handler; } /// 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. -@since(version = 0.2.0) world proxy { - @since(version = 0.2.0) include imports; /// 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`. - @since(version = 0.2.0) - export incoming-handler; + /// + /// This may also be used to receive synthesized or forwarded requests from + /// another component. + export handler; } diff --git a/crates/wasi-http/src/p3/wit/deps/http@ae89575@wit-0.3.0-draft/types.wit b/crates/wasi-http/src/p3/wit/deps/http@ae89575@wit-0.3.0-draft/types.wit new file mode 100644 index 0000000000..a804ee9fcc --- /dev/null +++ b/crates/wasi-http/src/p3/wit/deps/http@ae89575@wit-0.3.0-draft/types.wit @@ -0,0 +1,431 @@ +/// This interface defines all of the types and methods for implementing HTTP +/// Requests and Responses, as well as their headers, trailers, and bodies. +interface types { + use wasi:clocks/monotonic-clock@0.3.0.{duration}; + + /// 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) + } + + /// These cases are inspired by the IANA HTTP Proxy Error Types: + /// + variant error-code { + DNS-timeout, + DNS-error(DNS-error-payload), + destination-not-found, + destination-unavailable, + destination-IP-prohibited, + destination-IP-unroutable, + connection-refused, + connection-terminated, + connection-timeout, + connection-read-timeout, + connection-write-timeout, + connection-limit-reached, + TLS-protocol-error, + TLS-certificate-error, + TLS-alert-received(TLS-alert-received-payload), + HTTP-request-denied, + HTTP-request-length-required, + HTTP-request-body-size(option), + HTTP-request-method-invalid, + HTTP-request-URI-invalid, + HTTP-request-URI-too-long, + HTTP-request-header-section-size(option), + HTTP-request-header-size(option), + HTTP-request-trailer-section-size(option), + HTTP-request-trailer-size(field-size-payload), + HTTP-response-incomplete, + HTTP-response-header-section-size(option), + HTTP-response-header-size(field-size-payload), + HTTP-response-body-size(option), + HTTP-response-trailer-section-size(option), + HTTP-response-trailer-size(field-size-payload), + HTTP-response-transfer-coding(option), + HTTP-response-content-coding(option), + HTTP-response-timeout, + HTTP-upgrade-failed, + HTTP-protocol-error, + loop-detected, + configuration-error, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. It also includes an optional string for an + /// unstructured description of the error. Users should not depend on the + /// string for diagnosing errors, as it's not required to be consistent + /// between implementations. + internal-error(option) + } + + /// Defines the case payload type for `DNS-error` above: + record DNS-error-payload { + rcode: option, + info-code: option + } + + /// Defines the case payload type for `TLS-alert-received` above: + record TLS-alert-received-payload { + alert-id: option, + alert-message: option + } + + /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: + record field-size-payload { + field-name: option, + field-size: option + } + + /// This type enumerates the different kinds of errors that may occur when + /// setting or appending to a `fields` resource. + variant header-error { + /// This error indicates that a `field-name` or `field-value` was + /// syntactically invalid when used with an operation that sets headers in a + /// `fields`. + invalid-syntax, + + /// This error indicates that a forbidden `field-name` was used when trying + /// to set a header in a `fields`. + forbidden, + + /// This error indicates that the operation on the `fields` was not + /// permitted because the fields are immutable. + immutable, + } + + /// This type enumerates the different kinds of errors that may occur when + /// setting fields of a `request-options` resource. + variant request-options-error { + /// Indicates the specified field is not supported by this implementation. + not-supported, + + /// Indicates that the operation on the `request-options` was not permitted + /// because it is immutable. + immutable, + } + + /// Field names are always strings. + /// + /// Field names should always be treated as case insensitive by the `fields` + /// resource for the purposes of equality checking. + type field-name = string; + + /// Field values should always be ASCII strings. However, in + /// reality, HTTP implementations often have to interpret malformed values, + /// so they are provided as a list of bytes. + type field-value = list; + + /// This following block defines the `fields` resource which corresponds to + /// HTTP standard Fields. Fields are a common representation used for both + /// Headers and Trailers. + /// + /// A `fields` may be mutable or immutable. A `fields` created using the + /// constructor, `from-list`, or `clone` will be mutable, but a `fields` + /// resource given by other means (including, but not limited to, + /// `request.headers`) might be be immutable. In an immutable fields, the + /// `set`, `append`, and `delete` operations will fail with + /// `header-error.immutable`. + /// + /// A `fields` resource should store `field-name`s and `field-value`s in their + /// original casing used to construct or mutate the `fields` resource. The `fields` + /// resource should use that original casing when serializing the fields for + /// transport or when returning them from a method. + resource fields { + + /// Construct an empty HTTP Fields. + /// + /// The resulting `fields` is mutable. + constructor(); + + /// Construct an HTTP Fields. + /// + /// The resulting `fields` is mutable. + /// + /// The list represents each name-value pair in the Fields. Names + /// which have multiple values are represented by multiple entries in this + /// list with the same name. + /// + /// The tuple is a pair of the field name, represented as a string, and + /// Value, represented as a list of bytes. In a valid Fields, all names + /// and values are valid UTF-8 strings. However, values are not always + /// well-formed, so they are represented as a raw list of bytes. + /// + /// An error result will be returned if any header or value was + /// syntactically invalid, or if a header was forbidden. + from-list: static func( + entries: list> + ) -> result; + + /// Get all of the values corresponding to a name. If the name is not present + /// in this `fields`, an empty list is returned. However, if the name is + /// present but empty, this is represented by a list with one or more + /// empty field-values present. + get: func(name: field-name) -> list; + + /// Returns `true` when the name is present in this `fields`. If the name is + /// syntactically invalid, `false` is returned. + has: func(name: field-name) -> bool; + + /// Set all of the values for a name. Clears any existing values for that + /// name, if they have been set. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + set: func(name: field-name, value: list) -> result<_, header-error>; + + /// Delete all values for a name. Does nothing if no values for the name + /// exist. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + delete: func(name: field-name) -> result<_, header-error>; + + /// Delete all values for a name. Does nothing if no values for the name + /// exist. + /// + /// Returns all values previously corresponding to the name, if any. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + get-and-delete: func(name: field-name) -> result, header-error>; + + /// Append a value for a name. Does not change or delete any existing + /// values for that name. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + append: func(name: field-name, value: field-value) -> result<_, header-error>; + + /// Retrieve the full set of names and values in the Fields. Like the + /// constructor, the list represents each name-value pair. + /// + /// The outer list represents each name-value pair in the Fields. Names + /// which have multiple values are represented by multiple entries in this + /// list with the same name. + /// + /// The names and values are always returned in the original casing and in + /// the order in which they will be serialized for transport. + entries: func() -> list>; + + /// Make a deep copy of the Fields. Equivalent in behavior to calling the + /// `fields` constructor on the return value of `entries`. The resulting + /// `fields` is mutable. + clone: func() -> fields; + } + + /// Headers is an alias for Fields. + type headers = fields; + + /// Trailers is an alias for Fields. + type trailers = fields; + + /// Represents an HTTP Request. + resource request { + + /// Construct a new `request` with a default `method` of `GET`, and + /// `none` values for `path-with-query`, `scheme`, and `authority`. + /// + /// `headers` is the HTTP Headers for the Request. + /// + /// `contents` is the optional body content stream. + /// Once it is closed, `trailers` future must resolve to a result. + /// If `trailers` resolves to an error, underlying connection + /// will be closed immediately. + /// + /// `options` is optional `request-options` resource to be used + /// if the request is sent over a network connection. + /// + /// It is possible to construct, or manipulate with the accessor functions + /// below, a `request` with an invalid combination of `scheme` + /// and `authority`, or `headers` which are not permitted to be sent. + /// It is the obligation of the `handler.handle` implementation + /// to reject invalid constructions of `request`. + /// + /// The returned future resolves to result of transmission of this request. + new: static func( + headers: headers, + contents: option>, + trailers: future, error-code>>, + options: option + ) -> tuple>>; + + /// Get the Method for the Request. + method: func() -> method; + /// Set the Method for the Request. Fails if the string present in a + /// `method.other` argument is not a syntactically valid method. + set-method: func(method: method) -> result; + + /// Get the combination of the HTTP Path and Query for the Request. When + /// `none`, this represents an empty Path and empty Query. + path-with-query: func() -> option; + /// Set the combination of the HTTP Path and Query for the Request. When + /// `none`, this represents an empty Path and empty Query. Fails is the + /// string given is not a syntactically valid path and query uri component. + set-path-with-query: func(path-with-query: option) -> result; + + /// Get the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. + scheme: func() -> option; + /// Set the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. Fails if the + /// string given is not a syntactically valid uri scheme. + set-scheme: func(scheme: option) -> result; + + /// Get the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and + /// HTTPS schemes always require an authority. + authority: func() -> option; + /// Set the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and + /// HTTPS schemes always require an authority. Fails if the string given is + /// not a syntactically valid URI authority. + set-authority: func(authority: option) -> result; + + /// Get the `request-options` to be associated with this request + /// + /// The returned `request-options` resource is immutable: `set-*` operations + /// will fail if invoked. + /// + /// This `request-options` resource is a child: it must be dropped before + /// the parent `request` is dropped, or its ownership is transferred to + /// another component by e.g. `handler.handle`. + options: func() -> option; + + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + headers: func() -> headers; + + /// Get body of the Request. + /// + /// Stream returned by this method represents the contents of the body. + /// Once the stream is reported as closed, callers should await the returned future + /// to determine whether the body was received successfully. + /// The future will only resolve after the stream is reported as closed. + /// + /// The stream and future returned by this method are children: + /// they should be closed or consumed before the parent `response` + /// is dropped, or its ownership is transferred to another component + /// by e.g. `handler.handle`. + /// + /// This method may be called multiple times. + /// + /// This method will return an error if it is called while either: + /// - a stream or future returned by a previous call to this method is still open + /// - a stream returned by a previous call to this method has reported itself as closed + /// Thus there will always be at most one readable stream open for a given body. + /// Each subsequent stream picks up where the last stream left off, up until it is finished. + body: func() -> result, future, error-code>>>>; + } + + /// Parameters for making an HTTP Request. Each of these parameters is + /// currently an optional timeout applicable to the transport layer of the + /// HTTP protocol. + /// + /// These timeouts are separate from any the user may use to bound an + /// asynchronous call. + resource request-options { + /// Construct a default `request-options` value. + constructor(); + + /// The timeout for the initial connect to the HTTP Server. + connect-timeout: func() -> option; + + /// Set the timeout for the initial connect to the HTTP Server. An error + /// return value indicates that this timeout is not supported or that this + /// handle is immutable. + set-connect-timeout: func(duration: option) -> result<_, request-options-error>; + + /// The timeout for receiving the first byte of the Response body. + first-byte-timeout: func() -> option; + + /// Set the timeout for receiving the first byte of the Response body. An + /// error return value indicates that this timeout is not supported or that + /// this handle is immutable. + set-first-byte-timeout: func(duration: option) -> result<_, request-options-error>; + + /// The timeout for receiving subsequent chunks of bytes in the Response + /// body stream. + between-bytes-timeout: func() -> option; + + /// Set the timeout for receiving subsequent chunks of bytes in the Response + /// body stream. An error return value indicates that this timeout is not + /// supported or that this handle is immutable. + set-between-bytes-timeout: func(duration: option) -> result<_, request-options-error>; + + /// Make a deep copy of the `request-options`. + /// The resulting `request-options` is mutable. + clone: func() -> request-options; + } + + /// This type corresponds to the HTTP standard Status Code. + type status-code = u16; + + /// Represents an HTTP Response. + resource response { + + /// Construct a new `response`, with a default `status-code` of `200`. + /// If a different `status-code` is needed, it must be set via the + /// `set-status-code` method. + /// + /// `headers` is the HTTP Headers for the Response. + /// + /// `contents` is the optional body content stream. + /// Once it is closed, `trailers` future must resolve to a result. + /// If `trailers` resolves to an error, underlying connection + /// will be closed immediately. + /// + /// The returned future resolves to result of transmission of this response. + new: static func( + headers: headers, + contents: option>, + trailers: future, error-code>>, + ) -> tuple>>; + + /// Get the HTTP Status Code for the Response. + status-code: func() -> status-code; + + /// Set the HTTP Status Code for the Response. Fails if the status-code + /// given is not a valid http status code. + set-status-code: func(status-code: status-code) -> result; + + /// Get the headers associated with the Response. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + headers: func() -> headers; + + /// Get body of the Response. + /// + /// Stream returned by this method represents the contents of the body. + /// Once the stream is reported as closed, callers should await the returned future + /// to determine whether the body was received successfully. + /// The future will only resolve after the stream is reported as closed. + /// + /// The stream and future returned by this method are children: + /// they should be closed or consumed before the parent `response` + /// is dropped, or its ownership is transferred to another component + /// by e.g. `handler.handle`. + /// + /// This method may be called multiple times. + /// + /// This method will return an error if it is called while either: + /// - a stream or future returned by a previous call to this method is still open + /// - a stream returned by a previous call to this method has reported itself as closed + /// Thus there will always be at most one readable stream open for a given body. + /// Each subsequent stream picks up where the last stream left off, up until it is finished. + body: func() -> result, future, error-code>>>>; + } +} diff --git a/crates/wasi-http/src/p2/wit/deps/random@v0.2.3/insecure-seed.wit b/crates/wasi-http/src/p3/wit/deps/random@9499404@wit-0.3.0-draft/insecure-seed.wit similarity index 93% rename from crates/wasi-http/src/p2/wit/deps/random@v0.2.3/insecure-seed.wit rename to crates/wasi-http/src/p3/wit/deps/random@9499404@wit-0.3.0-draft/insecure-seed.wit index 67d024d5bf..4708d90493 100644 --- a/crates/wasi-http/src/p2/wit/deps/random@v0.2.3/insecure-seed.wit +++ b/crates/wasi-http/src/p3/wit/deps/random@9499404@wit-0.3.0-draft/insecure-seed.wit @@ -1,9 +1,9 @@ -package wasi:random@0.2.3; +package wasi:random@0.3.0; /// The insecure-seed interface for seeding hash-map DoS resistance. /// /// It is intended to be portable at least between Unix-family platforms and /// Windows. -@since(version = 0.2.0) +@since(version = 0.3.0) interface insecure-seed { /// Return a 128-bit value that may contain a pseudo-random value. /// @@ -22,6 +22,6 @@ interface insecure-seed { /// 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. - @since(version = 0.2.0) + @since(version = 0.3.0) insecure-seed: func() -> tuple; } diff --git a/crates/wasi-http/src/p2/wit/deps/random@v0.2.3/insecure.wit b/crates/wasi-http/src/p3/wit/deps/random@9499404@wit-0.3.0-draft/insecure.wit similarity index 88% rename from crates/wasi-http/src/p2/wit/deps/random@v0.2.3/insecure.wit rename to crates/wasi-http/src/p3/wit/deps/random@9499404@wit-0.3.0-draft/insecure.wit index a07dfab327..4ea5e581fd 100644 --- a/crates/wasi-http/src/p2/wit/deps/random@v0.2.3/insecure.wit +++ b/crates/wasi-http/src/p3/wit/deps/random@9499404@wit-0.3.0-draft/insecure.wit @@ -1,9 +1,9 @@ -package wasi:random@0.2.3; +package wasi:random@0.3.0; /// The insecure interface for insecure pseudo-random numbers. /// /// It is intended to be portable at least between Unix-family platforms and /// Windows. -@since(version = 0.2.0) +@since(version = 0.3.0) interface insecure { /// Return `len` insecure pseudo-random bytes. /// @@ -13,13 +13,13 @@ interface insecure { /// There are no requirements on the values of the returned bytes, however /// implementations are encouraged to return evenly distributed values with /// a long period. - @since(version = 0.2.0) + @since(version = 0.3.0) get-insecure-random-bytes: func(len: u64) -> list; /// Return an insecure pseudo-random `u64` value. /// /// This function returns the same type of pseudo-random data as /// `get-insecure-random-bytes`, represented as a `u64`. - @since(version = 0.2.0) + @since(version = 0.3.0) get-insecure-random-u64: func() -> u64; } diff --git a/crates/wasi-http/src/p2/wit/deps/random@v0.2.3/random.wit b/crates/wasi-http/src/p3/wit/deps/random@9499404@wit-0.3.0-draft/random.wit similarity index 91% rename from crates/wasi-http/src/p2/wit/deps/random@v0.2.3/random.wit rename to crates/wasi-http/src/p3/wit/deps/random@9499404@wit-0.3.0-draft/random.wit index 91957e6330..786ef25f68 100644 --- a/crates/wasi-http/src/p2/wit/deps/random@v0.2.3/random.wit +++ b/crates/wasi-http/src/p3/wit/deps/random@9499404@wit-0.3.0-draft/random.wit @@ -1,9 +1,9 @@ -package wasi:random@0.2.3; +package wasi:random@0.3.0; /// WASI Random is a random data API. /// /// It is intended to be portable at least between Unix-family platforms and /// Windows. -@since(version = 0.2.0) +@since(version = 0.3.0) interface random { /// Return `len` cryptographically-secure random or pseudo-random bytes. /// @@ -17,13 +17,13 @@ interface random { /// This function must always return fresh data. Deterministic environments /// must omit this function, rather than implementing it with deterministic /// data. - @since(version = 0.2.0) + @since(version = 0.3.0) get-random-bytes: func(len: u64) -> list; /// Return a cryptographically-secure random or pseudo-random `u64` value. /// /// This function returns the same type of data as `get-random-bytes`, /// represented as a `u64`. - @since(version = 0.2.0) + @since(version = 0.3.0) get-random-u64: func() -> u64; } diff --git a/crates/wasi-http/src/p3/wit/deps/random@9499404@wit-0.3.0-draft/world.wit b/crates/wasi-http/src/p3/wit/deps/random@9499404@wit-0.3.0-draft/world.wit new file mode 100644 index 0000000000..838d38023c --- /dev/null +++ b/crates/wasi-http/src/p3/wit/deps/random@9499404@wit-0.3.0-draft/world.wit @@ -0,0 +1,13 @@ +package wasi:random@0.3.0; + +@since(version = 0.3.0) +world imports { + @since(version = 0.3.0) + import random; + + @since(version = 0.3.0) + import insecure; + + @since(version = 0.3.0) + import insecure-seed; +} diff --git a/crates/wasi-http/src/p3/wit/deps/sockets@41d7079@wit-0.3.0-draft/ip-name-lookup.wit b/crates/wasi-http/src/p3/wit/deps/sockets@41d7079@wit-0.3.0-draft/ip-name-lookup.wit new file mode 100644 index 0000000000..7cc8b03e35 --- /dev/null +++ b/crates/wasi-http/src/p3/wit/deps/sockets@41d7079@wit-0.3.0-draft/ip-name-lookup.wit @@ -0,0 +1,62 @@ +@since(version = 0.3.0) +interface ip-name-lookup { + @since(version = 0.3.0) + use types.{ip-address}; + + /// Lookup error codes. + @since(version = 0.3.0) + enum error-code { + /// Unknown error + unknown, + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + + /// `name` is a syntactically invalid domain name or IP address. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + + /// Name does not exist or has no suitable associated IP addresses. + /// + /// POSIX equivalent: EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY + name-unresolvable, + + /// A temporary failure in name resolution occurred. + /// + /// POSIX equivalent: EAI_AGAIN + temporary-resolver-failure, + + /// A permanent failure in name resolution occurred. + /// + /// POSIX equivalent: EAI_FAIL + permanent-resolver-failure, + } + + /// Resolve an internet host name to a list of IP addresses. + /// + /// Unicode domain names are automatically converted to ASCII using IDNA encoding. + /// If the input is an IP address string, the address is parsed and returned + /// as-is without making any external requests. + /// + /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. + /// + /// The results are returned in connection order preference. + /// + /// This function never succeeds with 0 results. It either fails or succeeds + /// with at least one address. Additionally, this function never returns + /// IPv4-mapped IPv6 addresses. + /// + /// The returned future will resolve to an error code in case of failure. + /// It will resolve to success once the returned stream is exhausted. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + resolve-addresses: func(name: string) -> result, error-code>; +} diff --git a/crates/wasi-http/src/p3/wit/deps/sockets@41d7079@wit-0.3.0-draft/types.wit b/crates/wasi-http/src/p3/wit/deps/sockets@41d7079@wit-0.3.0-draft/types.wit new file mode 100644 index 0000000000..456d4e5ccf --- /dev/null +++ b/crates/wasi-http/src/p3/wit/deps/sockets@41d7079@wit-0.3.0-draft/types.wit @@ -0,0 +1,725 @@ +@since(version = 0.3.0) +interface types { + @since(version = 0.3.0) + use wasi:clocks/monotonic-clock@0.3.0.{duration}; + + /// 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. + @since(version = 0.3.0) + enum error-code { + /// Unknown error + unknown, + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + + /// The operation is not supported. + /// + /// POSIX equivalent: EOPNOTSUPP + not-supported, + + /// One of the arguments is invalid. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + + /// 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, + + /// The operation is not valid in the socket's current state. + invalid-state, + + /// 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 or because there are no ephemeral ports available. + address-in-use, + + /// The remote address is not reachable + remote-unreachable, + + + /// The TCP connection was forcefully rejected + connection-refused, + + /// The TCP connection was reset. + connection-reset, + + /// A TCP connection was aborted. + connection-aborted, + + + /// The size of a datagram sent to a UDP socket exceeded the maximum + /// supported size. + datagram-too-large, + } + + @since(version = 0.3.0) + enum ip-address-family { + /// Similar to `AF_INET` in POSIX. + ipv4, + + /// Similar to `AF_INET6` in POSIX. + ipv6, + } + + @since(version = 0.3.0) + type ipv4-address = tuple; + @since(version = 0.3.0) + type ipv6-address = tuple; + + @since(version = 0.3.0) + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + @since(version = 0.3.0) + record ipv4-socket-address { + /// sin_port + port: u16, + /// sin_addr + address: ipv4-address, + } + + @since(version = 0.3.0) + record ipv6-socket-address { + /// sin6_port + port: u16, + /// sin6_flowinfo + flow-info: u32, + /// sin6_addr + address: ipv6-address, + /// sin6_scope_id + scope-id: u32, + } + + @since(version = 0.3.0) + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } + + /// A TCP socket resource. + /// + /// The socket can be in one of the following states: + /// - `unbound` + /// - `bound` (See note below) + /// - `listening` + /// - `connecting` + /// - `connected` + /// - `closed` + /// See + /// for more information. + /// + /// Note: Except where explicitly mentioned, whenever this documentation uses + /// the term "bound" without backticks it actually means: in the `bound` state *or higher*. + /// (i.e. `bound`, `listening`, `connecting` or `connected`) + /// + /// In addition to the general error codes documented on the + /// `types::error-code` type, TCP socket methods may always return + /// `error(invalid-state)` when in the `closed` state. + @since(version = 0.3.0) + resource tcp-socket { + + /// Create a new TCP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// Unlike POSIX, WASI sockets have no notion of a socket-level + /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's + /// async support. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + constructor(address-family: ip-address-family); + + /// Bind the socket to 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. + /// + /// Bind can be attempted multiple times on the same socket, even with + /// different arguments on each iteration. But never concurrently and + /// only as long as the previous bind failed. Once a bind succeeds, the + /// binding can't be changed anymore. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) + /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: 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 can be bound to. (EADDRNOTAVAIL) + /// + /// # Implementors note + /// When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT + /// state of a recently closed socket on the same local address. In practice this means that the SO_REUSEADDR + /// socket option should be set implicitly on all platforms, except on Windows where this is the default behavior + /// and SO_REUSEADDR performs something different entirely. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + bind: func(local-address: ip-socket-address) -> result<_, error-code>; + + /// Connect to a remote endpoint. + /// + /// On success, the socket is transitioned into the `connected` state and this function returns a connection resource. + /// + /// After a failed connection attempt, the socket will be in the `closed` + /// state and the only valid action left is to `drop` the socket. A single + /// socket can not be used to connect more than once. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) + /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + /// - `invalid-state`: The socket is already in the `connecting` state. (EALREADY) + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN) + /// - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows) + /// - `timeout`: Connection timed out. (ETIMEDOUT) + /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + connect: func(remote-address: ip-socket-address) -> result<_, error-code>; + + /// Start listening return a stream of new inbound connections. + /// + /// Transitions the socket into the `listening` state. This can be called + /// at most once per socket. + /// + /// If the socket is not already explicitly bound, this function will + /// implicitly bind the socket to a random free port. + /// + /// Normally, the returned sockets are bound, in the `connected` state + /// and immediately ready for I/O. Though, depending on exact timing and + /// circumstances, a newly accepted connection may already be `closed` + /// by the time the server attempts to perform its first I/O on it. This + /// is true regardless of whether the WASI implementation uses + /// "synthesized" sockets or not (see Implementors Notes below). + /// + /// The following properties are inherited from the listener socket: + /// - `address-family` + /// - `keep-alive-enabled` + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// - `hop-limit` + /// - `receive-buffer-size` + /// - `send-buffer-size` + /// + /// # Typical errors + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD) + /// - `invalid-state`: The socket is already in the `listening` state. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// + /// # Implementors note + /// This method returns a single perpetual stream that should only close + /// on fatal errors (if any). Yet, the POSIX' `accept` function may also + /// return transient errors (e.g. ECONNABORTED). The exact details differ + /// per operation system. For example, the Linux manual mentions: + /// + /// > Linux accept() passes already-pending network errors on the new + /// > socket as an error code from accept(). This behavior differs from + /// > other BSD socket implementations. For reliable operation the + /// > application should detect the network errors defined for the + /// > protocol after accept() and treat them like EAGAIN by retrying. + /// > In the case of TCP/IP, these are ENETDOWN, EPROTO, ENOPROTOOPT, + /// > EHOSTDOWN, ENONET, EHOSTUNREACH, EOPNOTSUPP, and ENETUNREACH. + /// Source: https://man7.org/linux/man-pages/man2/accept.2.html + /// + /// WASI implementations have two options to handle this: + /// - Optionally log it and then skip over non-fatal errors returned by + /// `accept`. Guest code never gets to see these failures. Or: + /// - Synthesize a `tcp-socket` resource that exposes the error when + /// attempting to send or receive on it. Guest code then sees these + /// failures as regular I/O errors. + /// + /// In either case, the stream returned by this `listen` method remains + /// operational. + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + listen: func() -> result, error-code>; + + /// Transmit data to peer. + /// + /// The caller should close the stream when it has no more data to send + /// to the peer. Under normal circumstances this will cause a FIN packet + /// to be sent out. Closing the stream is equivalent to calling + /// `shutdown(SHUT_WR)` in POSIX. + /// + /// This function may be called at most once and returns once the full + /// contents of the stream are transmitted or an error is encountered. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + send: func(data: stream) -> result<_, error-code>; + + /// Read data from peer. + /// + /// This function returns a `stream` which provides the data received from the + /// socket, and a `future` providing additional error information in case the + /// socket is closed abnormally. + /// + /// If the socket is closed normally, `stream.read` on the `stream` will return + /// `read-status::closed` with no `error-context` and the future resolves to + /// the value `ok`. If the socket is closed abnormally, `stream.read` on the + /// `stream` returns `read-status::closed` with an `error-context` and the future + /// resolves to `err` with an `error-code`. + /// + /// `receive` is meant to be called only once per socket. If it is called more + /// than once, the subsequent calls return a new `stream` that fails as if it + /// were closed abnormally. + /// + /// If the caller is not expecting to receive any data from the peer, + /// they may drop the stream. Any data still in the receive queue + /// will be discarded. This is equivalent to calling `shutdown(SHUT_RD)` + /// in POSIX. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + receive: func() -> tuple, future>>; + + /// Get the bound local address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + local-address: func() -> result; + + /// Get the remote address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + remote-address: func() -> result; + + /// Whether the socket is in the `listening` state. + /// + /// Equivalent to the SO_ACCEPTCONN socket option. + @since(version = 0.3.0) + is-listening: func() -> bool; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// This is the value passed to the constructor. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.3.0) + address-family: func() -> ip-address-family; + + /// Hints the desired listen queue size. Implementations are free to ignore this. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// + /// # Typical errors + /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. + /// - `invalid-argument`: (set) The provided value was 0. + /// - `invalid-state`: (set) The socket is in the `connecting` or `connected` state. + @since(version = 0.3.0) + set-listen-backlog-size: func(value: u64) -> result<_, error-code>; + + /// Enables or disables keepalive. + /// + /// The keepalive behavior can be adjusted using: + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true. + /// + /// Equivalent to the SO_KEEPALIVE socket option. + @since(version = 0.3.0) + keep-alive-enabled: func() -> result; + @since(version = 0.3.0) + set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; + + /// Amount of time the connection has to be idle before TCP starts sending keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS) + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0) + keep-alive-idle-time: func() -> result; + @since(version = 0.3.0) + set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; + + /// The time between keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPINTVL socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0) + keep-alive-interval: func() -> result; + @since(version = 0.3.0) + set-keep-alive-interval: func(value: duration) -> result<_, error-code>; + + /// The maximum amount of keepalive packets TCP should send before aborting the connection. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPCNT socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0) + keep-alive-count: func() -> result; + @since(version = 0.3.0) + set-keep-alive-count: func(value: u32) -> result<_, error-code>; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.3.0) + hop-limit: func() -> result; + @since(version = 0.3.0) + set-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0) + receive-buffer-size: func() -> result; + @since(version = 0.3.0) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.3.0) + send-buffer-size: func() -> result; + @since(version = 0.3.0) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + } + + /// A UDP socket handle. + @since(version = 0.3.0) + resource udp-socket { + + /// Create a new UDP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// Unlike POSIX, WASI sockets have no notion of a socket-level + /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's + /// async support. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + constructor(address-family: ip-address-family); + + /// Bind the socket to 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 port is zero, the socket will be bound to a random free port. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: 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 can be bound to. (EADDRNOTAVAIL) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + bind: func(local-address: ip-socket-address) -> result<_, error-code>; + + /// Associate this socket with a specific peer address. + /// + /// On success, the `remote-address` of the socket is updated. + /// The `local-address` may be updated as well, based on the best network + /// path to `remote-address`. If the socket was not already explicitly + /// bound, this function will implicitly bind the socket to a random + /// free port. + /// + /// When a UDP socket is "connected", the `send` and `receive` methods + /// are limited to communicating with that peer only: + /// - `send` can only be used to send to this destination. + /// - `receive` will only return datagrams sent from the provided `remote-address`. + /// + /// The name "connect" was kept to align with the existing POSIX + /// terminology. Other than that, this function only changes the local + /// socket configuration and does not generate any network traffic. + /// The peer is not aware of this "connection". + /// + /// This method may be called multiple times on the same socket to change + /// its association, but only the most recent one will be effective. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// + /// # Implementors note + /// If the socket is already connected, some platforms (e.g. Linux) + /// require a disconnect before connecting to a different peer address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + connect: func(remote-address: ip-socket-address) -> result<_, error-code>; + + /// Dissociate this socket from its peer address. + /// + /// After calling this method, `send` & `receive` are free to communicate + /// with any address again. + /// + /// The POSIX equivalent of this is calling `connect` with an `AF_UNSPEC` address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + disconnect: func() -> result<_, error-code>; + + /// Send a message on the socket to a particular peer. + /// + /// If the socket is connected, the peer address may be left empty. In + /// that case this is equivalent to `send` in POSIX. Otherwise it is + /// equivalent to `sendto`. + /// + /// Additionally, if the socket is connected, a `remote-address` argument + /// _may_ be provided but then it must be identical to the address + /// passed to `connect`. + /// + /// Implementations may trap if the `data` length exceeds 64 KiB. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `connect`. (EISCONN) + /// - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + send: func(data: list, remote-address: option) -> result<_, error-code>; + + /// Receive a message on the socket. + /// + /// On success, the return value contains a tuple of the received data + /// and the address of the sender. Theoretical maximum length of the + /// data is 64 KiB. Though in practice, it will typically be less than + /// 1500 bytes. + /// + /// If the socket is connected, the sender address is guaranteed to + /// match the remote address passed to `connect`. + /// + /// # Typical errors + /// - `invalid-state`: The socket has not been bound yet. + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + receive: func() -> result, ip-socket-address>, error-code>; + + /// Get the current bound address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + local-address: func() -> result; + + /// Get the address the socket is currently "connected" to. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not "connected" to a specific remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0) + remote-address: func() -> result; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// This is the value passed to the constructor. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.3.0) + address-family: func() -> ip-address-family; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.3.0) + unicast-hop-limit: func() -> result; + @since(version = 0.3.0) + set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.3.0) + receive-buffer-size: func() -> result; + @since(version = 0.3.0) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.3.0) + send-buffer-size: func() -> result; + @since(version = 0.3.0) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + } +} diff --git a/crates/wasi-http/src/p3/wit/deps/sockets@41d7079@wit-0.3.0-draft/world.wit b/crates/wasi-http/src/p3/wit/deps/sockets@41d7079@wit-0.3.0-draft/world.wit new file mode 100644 index 0000000000..6c9951d1c6 --- /dev/null +++ b/crates/wasi-http/src/p3/wit/deps/sockets@41d7079@wit-0.3.0-draft/world.wit @@ -0,0 +1,9 @@ +package wasi:sockets@0.3.0; + +@since(version = 0.3.0) +world imports { + @since(version = 0.3.0) + import types; + @since(version = 0.3.0) + import ip-name-lookup; +} diff --git a/crates/wasi-http/src/p3/wit/world.wit b/crates/wasi-http/src/p3/wit/world.wit new file mode 100644 index 0000000000..6ac1647965 --- /dev/null +++ b/crates/wasi-http/src/p3/wit/world.wit @@ -0,0 +1,6 @@ +// We actually don't use this; it's just to let bindgen! find the corresponding world in wit/deps. +package wasmtime:wasi-http; + +world bindings { + include wasi:http/proxy@0.3.0-draft; +} diff --git a/crates/wasi-http/tests/all/http_server.rs b/crates/wasi-http/tests/all/http_server.rs index f1a22abf7e..d7a089a23d 100644 --- a/crates/wasi-http/tests/all/http_server.rs +++ b/crates/wasi-http/tests/all/http_server.rs @@ -1,28 +1,33 @@ +use core::future::Future; +use core::net::SocketAddr; + +use std::net::TcpStream; +use std::thread::{self, JoinHandle}; + use anyhow::{Context, Result}; -use http_body_util::{combinators::BoxBody, BodyExt, Full}; -use hyper::{body::Bytes, service::service_fn, Request, Response}; -use std::{ - future::Future, - net::{SocketAddr, TcpStream}, - thread::JoinHandle, -}; +use http::header::CONTENT_LENGTH; +use hyper::service::service_fn; +use hyper::{Request, Response}; use tokio::net::TcpListener; +use tracing::{debug, trace, warn}; use wasmtime_wasi_http::io::TokioIo; async fn test( - mut req: Request, -) -> http::Result>> { - tracing::debug!("preparing mocked response",); + req: Request, +) -> http::Result> { + debug!(?req, "preparing mocked response for request"); let method = req.method().to_string(); - let body = req.body_mut().collect().await.unwrap(); - let buf = body.to_bytes(); - tracing::trace!("hyper request body size {:?}", buf.len()); - - Response::builder() - .status(http::StatusCode::OK) + let uri = req.uri().to_string(); + let resp = Response::builder() .header("x-wasmtime-test-method", method) - .header("x-wasmtime-test-uri", req.uri().to_string()) - .body(Full::::from(buf).boxed()) + .header("x-wasmtime-test-uri", uri); + let resp = if let Some(content_length) = req.headers().get(CONTENT_LENGTH) { + resp.header(CONTENT_LENGTH, content_length) + } else { + resp + }; + let body = req.into_body(); + resp.body(body) } pub struct Server { @@ -32,12 +37,13 @@ pub struct Server { impl Server { fn new( - run: impl FnOnce(TokioIo) -> F + Send + 'static, + run: impl Fn(TokioIo) -> F + Send + 'static, + conns: usize, ) -> Result where F: Future>, { - let thread = std::thread::spawn(|| -> Result<_> { + let thread = thread::spawn(|| -> Result<_> { let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() @@ -48,14 +54,24 @@ impl Server { })?; Ok((rt, listener)) }); - let (rt, listener) = thread.join().unwrap()?; + let (rt, listener) = thread.join().expect("failed to join thread")?; let addr = listener.local_addr().context("failed to get local addr")?; - let worker = std::thread::spawn(move || { - tracing::debug!("dedicated thread to start listening"); + let worker = thread::spawn(move || { + debug!("dedicated thread to start listening"); rt.block_on(async move { - tracing::debug!("preparing to accept connection"); - let (stream, _) = listener.accept().await.map_err(anyhow::Error::from)?; - run(TokioIo::new(stream)).await + for i in 0..conns { + debug!(i, "preparing to accept connection"); + let (stream, _) = listener.accept().await?; + if let Err(err) = run(TokioIo::new(stream)).await { + // If the worker fails with an error, report it here but don't panic. + // Some tests don't make a connection so the error will be that the tcp + // stream created above is closed immediately afterwards. Let the test + // independently decide if it failed or not, and this should be in the + // logs to assist with debugging if necessary. + warn!(i, ?err, "failed to serve connection") + } + } + Ok(()) }) }); Ok(Self { @@ -64,40 +80,46 @@ impl Server { }) } - pub fn http1() -> Result { - tracing::debug!("initializing http1 server"); - Self::new(|io| async move { - let mut builder = hyper::server::conn::http1::Builder::new(); - let http = builder.keep_alive(false).pipeline_flush(true); + pub fn http1(conns: usize) -> Result { + debug!("initializing http1 server"); + Self::new( + |io| async move { + let mut builder = hyper::server::conn::http1::Builder::new(); + let http = builder.keep_alive(false).pipeline_flush(true); - tracing::debug!("preparing to bind connection to service"); - let conn = http.serve_connection(io, service_fn(test)).await; - tracing::trace!("connection result {:?}", conn); - conn?; - Ok(()) - }) + debug!("preparing to bind connection to service"); + let conn = http.serve_connection(io, service_fn(test)).await; + debug!("connection result {:?}", conn); + conn?; + Ok(()) + }, + conns, + ) } - pub fn http2() -> Result { - tracing::debug!("initializing http2 server"); - Self::new(|io| async move { - let mut builder = hyper::server::conn::http2::Builder::new(TokioExecutor); - let http = builder.max_concurrent_streams(20); + pub fn http2(conns: usize) -> Result { + debug!("initializing http2 server"); + Self::new( + |io| async move { + let mut builder = hyper::server::conn::http2::Builder::new(TokioExecutor); + let http = builder.max_concurrent_streams(20); - tracing::debug!("preparing to bind connection to service"); - let conn = http.serve_connection(io, service_fn(test)).await; - tracing::trace!("connection result {:?}", conn); - if let Err(e) = &conn { - let message = e.to_string(); - if message.contains("connection closed before reading preface") - || message.contains("unspecific protocol error detected") - { - return Ok(()); + debug!("preparing to bind connection to service"); + let conn = http.serve_connection(io, service_fn(test)).await; + trace!("connection result {:?}", conn); + if let Err(e) = &conn { + let message = e.to_string(); + if message.contains("connection closed before reading preface") + || message.contains("unspecific protocol error detected") + { + return Ok(()); + } } - } - conn?; - Ok(()) - }) + conn?; + Ok(()) + }, + conns, + ) } pub fn addr(&self) -> String { @@ -107,19 +129,12 @@ impl Server { impl Drop for Server { fn drop(&mut self) { - tracing::debug!("shutting down http1 server"); + debug!("shutting down http1 server"); // Force a connection to happen in case one hasn't happened already. let _ = TcpStream::connect(&self.addr); - // If the worker fails with an error, report it here but don't panic. - // Some tests don't make a connection so the error will be that the tcp - // stream created above is closed immediately afterwards. Let the test - // independently decide if it failed or not, and this should be in the - // logs to assist with debugging if necessary. let worker = self.worker.take().unwrap(); - if let Err(e) = worker.join().unwrap() { - eprintln!("worker failed with error {e:?}"); - } + worker.join().unwrap().unwrap() } } diff --git a/crates/wasi-http/tests/all/main.rs b/crates/wasi-http/tests/all/main.rs index 395d67ab73..50285643f5 100644 --- a/crates/wasi-http/tests/all/main.rs +++ b/crates/wasi-http/tests/all/main.rs @@ -1,119 +1,10 @@ -use crate::http_server::Server; -use anyhow::{anyhow, Context, Result}; -use futures::{channel::oneshot, future, stream, FutureExt}; -use http_body::Frame; -use http_body_util::{combinators::BoxBody, BodyExt, Collected, Empty, StreamBody}; -use hyper::{body::Bytes, server::conn::http1, service::service_fn, Method, StatusCode}; -use sha2::{Digest, Sha256}; -use std::{collections::HashMap, iter, net::Ipv4Addr, str, sync::Arc}; -use tokio::task; -use wasmtime::{ - component::{Component, Linker, ResourceTable}, - Config, Engine, Store, -}; -use wasmtime_wasi::{self, pipe::MemoryOutputPipe, IoView, WasiCtx, WasiCtxBuilder, WasiView}; -use wasmtime_wasi_http::{ - bindings::http::types::{ErrorCode, Scheme}, - body::HyperOutgoingBody, - io::TokioIo, - types::{self, HostFutureIncomingResponse, IncomingResponse, OutgoingRequestConfig}, - HttpResult, WasiHttpCtx, WasiHttpView, -}; - mod http_server; +mod p2; +mod p3; -type RequestSender = Arc< - dyn Fn(hyper::Request, OutgoingRequestConfig) -> HostFutureIncomingResponse - + Send - + Sync, ->; - -struct Ctx { - table: ResourceTable, - wasi: WasiCtx, - http: WasiHttpCtx, - stdout: MemoryOutputPipe, - stderr: MemoryOutputPipe, - send_request: Option, - rejected_authority: Option, -} - -impl IoView for Ctx { - fn table(&mut self) -> &mut ResourceTable { - &mut self.table - } -} -impl WasiView for Ctx { - fn ctx(&mut self) -> &mut WasiCtx { - &mut self.wasi - } -} - -impl WasiHttpView for Ctx { - fn ctx(&mut self) -> &mut WasiHttpCtx { - &mut self.http - } - - fn send_request( - &mut self, - request: hyper::Request, - config: OutgoingRequestConfig, - ) -> HttpResult { - if let Some(rejected_authority) = &self.rejected_authority { - let authority = request.uri().authority().map(ToString::to_string).unwrap(); - if &authority == rejected_authority { - return Err(ErrorCode::HttpRequestDenied.into()); - } - } - if let Some(send_request) = self.send_request.clone() { - Ok(send_request(request, config)) - } else { - Ok(types::default_send_request(request, config)) - } - } - - fn is_forbidden_header(&mut self, name: &hyper::header::HeaderName) -> bool { - name.as_str() == "custom-forbidden-header" - } -} - -fn store(engine: &Engine, server: &Server) -> Store { - let stdout = MemoryOutputPipe::new(4096); - let stderr = MemoryOutputPipe::new(4096); - - // Create our wasi context. - let mut builder = WasiCtxBuilder::new(); - builder.stdout(stdout.clone()); - builder.stderr(stderr.clone()); - builder.env("HTTP_SERVER", &server.addr()); - let ctx = Ctx { - table: ResourceTable::new(), - wasi: builder.build(), - http: WasiHttpCtx::new(), - stderr, - stdout, - send_request: None, - rejected_authority: None, - }; - - Store::new(&engine, ctx) -} - -impl Drop for Ctx { - fn drop(&mut self) { - let stdout = self.stdout.contents(); - if !stdout.is_empty() { - println!("[guest] stdout:\n{}\n===", String::from_utf8_lossy(&stdout)); - } - let stderr = self.stderr.contents(); - if !stderr.is_empty() { - println!("[guest] stderr:\n{}\n===", String::from_utf8_lossy(&stderr)); - } - } -} - -// Assert that each of `sync` and `async` below are testing everything through +// Assert that each of all test scenarios are testing everything through // assertion of the existence of the test function itself. +#[macro_export] macro_rules! assert_test_exists { ($name:ident) => { #[expect(unused_imports, reason = "only here to ensure a name exists")] @@ -121,434 +12,6 @@ macro_rules! assert_test_exists { }; } -mod async_; -mod sync; - -async fn run_wasi_http( - component_filename: &str, - req: hyper::Request>, - send_request: Option, - rejected_authority: Option, -) -> anyhow::Result>, ErrorCode>> { - let stdout = MemoryOutputPipe::new(4096); - let stderr = MemoryOutputPipe::new(4096); - let table = ResourceTable::new(); - - 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)?; - let component = Component::from_file(&engine, component_filename)?; - - // Create our wasi context. - let mut builder = WasiCtxBuilder::new(); - builder.stdout(stdout.clone()); - builder.stderr(stderr.clone()); - let wasi = builder.build(); - let http = WasiHttpCtx::new(); - let ctx = Ctx { - table, - wasi, - http, - stderr, - stdout, - send_request, - rejected_authority, - }; - let mut store = Store::new(&engine, ctx); - - let mut linker = Linker::new(&engine); - wasmtime_wasi_http::add_to_linker_async(&mut linker)?; - let proxy = - wasmtime_wasi_http::bindings::Proxy::instantiate_async(&mut store, &component, &linker) - .await?; - - let req = store.data_mut().new_incoming_request(Scheme::Http, req)?; - - let (sender, receiver) = tokio::sync::oneshot::channel(); - let out = store.data_mut().new_response_outparam(sender)?; - - let handle = wasmtime_wasi::runtime::spawn(async move { - proxy - .wasi_http_incoming_handler() - .call_handle(&mut store, req, out) - .await?; - - Ok::<_, anyhow::Error>(()) - }); - - let resp = match receiver.await { - Ok(Ok(resp)) => { - let (parts, body) = resp.into_parts(); - let collected = BodyExt::collect(body).await?; - Some(Ok(hyper::Response::from_parts(parts, collected))) - } - Ok(Err(e)) => Some(Err(e)), - - // Fall through below to the `resp.expect(...)` which will hopefully - // return a more specific error from `handle.await`. - Err(_) => None, - }; - - // Now that the response has been processed, we can wait on the wasm to - // finish without deadlocking. - handle.await.context("Component execution")?; - - Ok(resp.expect("wasm never called set-response-outparam")) -} - -#[test_log::test(tokio::test)] -async fn wasi_http_proxy_tests() -> anyhow::Result<()> { - let req = hyper::Request::builder() - .header("custom-forbidden-header", "yes") - .uri("http://example.com:8080/test-path") - .method(http::Method::GET); - - let resp = run_wasi_http( - test_programs_artifacts::API_PROXY_COMPONENT, - req.body(body::empty())?, - None, - None, - ) - .await?; - - match resp { - Ok(resp) => println!("response: {resp:?}"), - Err(e) => panic!("Error given in response: {e:?}"), - }; - - Ok(()) -} - -#[test_log::test(tokio::test)] -async fn wasi_http_hash_all() -> Result<()> { - do_wasi_http_hash_all(false).await -} - -#[test_log::test(tokio::test)] -async fn wasi_http_hash_all_with_override() -> Result<()> { - do_wasi_http_hash_all(true).await -} - -async fn do_wasi_http_hash_all(override_send_request: bool) -> Result<()> { - let bodies = Arc::new( - [ - ("/a", "’Twas brillig, and the slithy toves"), - ("/b", "Did gyre and gimble in the wabe:"), - ("/c", "All mimsy were the borogoves,"), - ("/d", "And the mome raths outgrabe."), - ] - .into_iter() - .collect::>(), - ); - - let listener = tokio::net::TcpListener::bind((Ipv4Addr::new(127, 0, 0, 1), 0)).await?; - - let prefix = format!("http://{}", listener.local_addr()?); - - let (_tx, rx) = oneshot::channel::<()>(); - - let handle = { - let bodies = bodies.clone(); - - move |request: http::request::Parts| { - if let (Method::GET, Some(body)) = (request.method, bodies.get(request.uri.path())) { - Ok::<_, anyhow::Error>(hyper::Response::new(body::full(Bytes::copy_from_slice( - body.as_bytes(), - )))) - } else { - Ok(hyper::Response::builder() - .status(StatusCode::METHOD_NOT_ALLOWED) - .body(body::empty())?) - } - } - }; - - let send_request = if override_send_request { - Some(Arc::new( - move |request: hyper::Request, - OutgoingRequestConfig { - between_bytes_timeout, - .. - }| { - let response = handle(request.into_parts().0).map(|resp| { - Ok(IncomingResponse { - resp: resp.map(|body| { - body.map_err(wasmtime_wasi_http::hyper_response_error) - .boxed() - }), - worker: None, - between_bytes_timeout, - }) - }); - HostFutureIncomingResponse::ready(response) - }, - ) as RequestSender) - } else { - let server = async move { - loop { - let (stream, _) = listener.accept().await?; - let stream = TokioIo::new(stream); - let handle = handle.clone(); - task::spawn(async move { - if let Err(e) = http1::Builder::new() - .keep_alive(true) - .serve_connection( - stream, - service_fn(move |request| { - let handle = handle.clone(); - async move { handle(request.into_parts().0) } - }), - ) - .await - { - eprintln!("error serving connection: {e:?}"); - } - }); - - // Help rustc with type inference: - if false { - return Ok::<_, anyhow::Error>(()); - } - } - } - .then(|result| { - if let Err(e) = result { - eprintln!("error listening for connections: {e:?}"); - } - future::ready(()) - }) - .boxed(); - - task::spawn(async move { - drop(future::select(server, rx).await); - }); - - None - }; - - let mut request = hyper::Request::builder() - .method(http::Method::GET) - .uri("http://example.com:8080/hash-all"); - for path in bodies.keys() { - request = request.header("url", format!("{prefix}{path}")); - } - let request = request.body(body::empty())?; - - let response = run_wasi_http( - test_programs_artifacts::API_PROXY_STREAMING_COMPONENT, - request, - send_request, - None, - ) - .await??; - - assert_eq!(StatusCode::OK, response.status()); - let body = response.into_body().to_bytes(); - let body = str::from_utf8(&body)?; - for line in body.lines() { - let (url, hash) = line - .split_once(": ") - .ok_or_else(|| anyhow!("expected string of form `: `; got {line}"))?; - - let path = url - .strip_prefix(&prefix) - .ok_or_else(|| anyhow!("expected string with prefix {prefix}; got {url}"))?; - - let mut hasher = Sha256::new(); - hasher.update( - bodies - .get(path) - .ok_or_else(|| anyhow!("unexpected path: {path}"))?, - ); - - use base64::Engine; - assert_eq!( - hash, - base64::engine::general_purpose::STANDARD_NO_PAD.encode(hasher.finalize()) - ); - } - - Ok(()) -} - -// ensure the runtime rejects the outgoing request -#[test_log::test(tokio::test)] -async fn wasi_http_hash_all_with_reject() -> Result<()> { - let request = hyper::Request::builder() - .method(http::Method::GET) - .uri("http://example.com:8080/hash-all"); - let request = request.header("url", format!("http://forbidden.com")); - let request = request.header("url", format!("http://localhost")); - let request = request.body(body::empty())?; - - let response = run_wasi_http( - test_programs_artifacts::API_PROXY_STREAMING_COMPONENT, - request, - None, - Some("forbidden.com".to_string()), - ) - .await??; - - let body = response.into_body().to_bytes(); - let body = str::from_utf8(&body).unwrap(); - for line in body.lines() { - println!("{line}"); - if line.contains("forbidden.com") { - assert!(line.contains("HttpRequestDenied")); - } - if line.contains("localhost") { - assert!(!line.contains("HttpRequestDenied")); - } - } - - Ok(()) -} - -#[test_log::test(tokio::test)] -async fn wasi_http_echo() -> Result<()> { - do_wasi_http_echo("echo", None).await -} - -#[test_log::test(tokio::test)] -async fn wasi_http_double_echo() -> Result<()> { - let listener = tokio::net::TcpListener::bind((Ipv4Addr::new(127, 0, 0, 1), 0)).await?; - - let prefix = format!("http://{}", listener.local_addr()?); - - let (_tx, rx) = oneshot::channel::<()>(); - - let server = async move { - loop { - let (stream, _) = listener.accept().await?; - let stream = TokioIo::new(stream); - task::spawn(async move { - if let Err(e) = http1::Builder::new() - .keep_alive(true) - .serve_connection( - stream, - service_fn( - move |request: hyper::Request| async move { - use http_body_util::BodyExt; - - if let (&Method::POST, "/echo") = - (request.method(), request.uri().path()) - { - Ok::<_, anyhow::Error>(hyper::Response::new( - request.into_body().boxed(), - )) - } else { - Ok(hyper::Response::builder() - .status(StatusCode::METHOD_NOT_ALLOWED) - .body(BoxBody::new( - Empty::new().map_err(|_| unreachable!()), - ))?) - } - }, - ), - ) - .await - { - eprintln!("error serving connection: {e:?}"); - } - }); - - // Help rustc with type inference: - if false { - return Ok::<_, anyhow::Error>(()); - } - } - } - .then(|result| { - if let Err(e) = result { - eprintln!("error listening for connections: {e:?}"); - } - future::ready(()) - }) - .boxed(); - - task::spawn(async move { - drop(future::select(server, rx).await); - }); - - do_wasi_http_echo("double-echo", Some(&format!("{prefix}/echo"))).await -} - -async fn do_wasi_http_echo(uri: &str, url_header: Option<&str>) -> Result<()> { - let body = { - // A sorta-random-ish megabyte - let mut n = 0_u8; - iter::repeat_with(move || { - n = n.wrapping_add(251); - n - }) - .take(1024 * 1024) - .collect::>() - }; - - let mut request = hyper::Request::builder() - .method(http::Method::POST) - .uri(format!("http://example.com:8080/{uri}")) - .header("content-type", "application/octet-stream"); - - if let Some(url_header) = url_header { - request = request.header("url", url_header); - } - - let request = request.body(BoxBody::new(StreamBody::new(stream::iter( - body.chunks(16 * 1024) - .map(|chunk| Ok::<_, hyper::Error>(Frame::data(Bytes::copy_from_slice(chunk)))) - .collect::>(), - ))))?; - - let response = run_wasi_http( - test_programs_artifacts::API_PROXY_STREAMING_COMPONENT, - request, - None, - None, - ) - .await??; - - assert_eq!(StatusCode::OK, response.status()); - assert_eq!( - response.headers()["content-type"], - "application/octet-stream" - ); - let received = Vec::from(response.into_body().to_bytes()); - if body != received { - panic!( - "body content mismatch (expected length {}; actual length {})", - body.len(), - received.len() - ); - } - - Ok(()) -} - -#[test_log::test(tokio::test)] -async fn wasi_http_without_port() -> Result<()> { - let req = hyper::Request::builder() - .method(http::Method::GET) - .uri("https://httpbin.org/get"); - - let _response: hyper::Response<_> = run_wasi_http( - test_programs_artifacts::API_PROXY_FORWARD_REQUEST_COMPONENT, - req.body(body::empty())?, - None, - None, - ) - .await??; - - // NB: don't test the actual return code of `response`. This is testing a - // live http request against a live server and things happen. If we got this - // far it's already successful that the request was made and the lack of - // port in the URI was handled. - - Ok(()) -} - mod body { use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full}; use hyper::body::Bytes; diff --git a/crates/wasi-http/tests/all/async_.rs b/crates/wasi-http/tests/all/p2/async_.rs similarity index 87% rename from crates/wasi-http/tests/all/async_.rs rename to crates/wasi-http/tests/all/p2/async_.rs index 4ea9c7c078..45ae769248 100644 --- a/crates/wasi-http/tests/all/async_.rs +++ b/crates/wasi-http/tests/all/p2/async_.rs @@ -1,4 +1,5 @@ use super::*; +use crate::*; use test_programs_artifacts::*; use wasmtime_wasi::bindings::Command; @@ -21,85 +22,85 @@ async fn run(path: &str, server: &Server) -> Result<()> { #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_outbound_request_get() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_GET_COMPONENT, &server).await } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_outbound_request_timeout() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_TIMEOUT_COMPONENT, &server).await } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_outbound_request_post() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_POST_COMPONENT, &server).await } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_outbound_request_large_post() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_LARGE_POST_COMPONENT, &server).await } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_outbound_request_put() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_PUT_COMPONENT, &server).await } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_outbound_request_invalid_version() -> Result<()> { - let server = Server::http2()?; + let server = Server::http2(1)?; run(HTTP_OUTBOUND_REQUEST_INVALID_VERSION_COMPONENT, &server).await } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_outbound_request_invalid_header() -> Result<()> { - let server = Server::http2()?; + let server = Server::http2(1)?; run(HTTP_OUTBOUND_REQUEST_INVALID_HEADER_COMPONENT, &server).await } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_outbound_request_unknown_method() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_UNKNOWN_METHOD_COMPONENT, &server).await } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_outbound_request_unsupported_scheme() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_UNSUPPORTED_SCHEME_COMPONENT, &server).await } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_outbound_request_invalid_port() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_INVALID_PORT_COMPONENT, &server).await } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_outbound_request_invalid_dnsname() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_INVALID_DNSNAME_COMPONENT, &server).await } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_outbound_request_response_build() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_RESPONSE_BUILD_COMPONENT, &server).await } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_outbound_request_content_length() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(0)?; run(HTTP_OUTBOUND_REQUEST_CONTENT_LENGTH_COMPONENT, &server).await } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_outbound_request_missing_path_and_query() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run( HTTP_OUTBOUND_REQUEST_MISSING_PATH_AND_QUERY_COMPONENT, &server, diff --git a/crates/wasi-http/tests/all/p2/mod.rs b/crates/wasi-http/tests/all/p2/mod.rs new file mode 100644 index 0000000000..cde1d2f783 --- /dev/null +++ b/crates/wasi-http/tests/all/p2/mod.rs @@ -0,0 +1,540 @@ +use crate::body; +use crate::http_server::Server; +use anyhow::{anyhow, Context, Result}; +use futures::{channel::oneshot, future, stream, FutureExt}; +use http_body::Frame; +use http_body_util::{combinators::BoxBody, BodyExt, Collected, Empty, StreamBody}; +use hyper::{body::Bytes, server::conn::http1, service::service_fn, Method, StatusCode}; +use sha2::{Digest, Sha256}; +use std::{collections::HashMap, iter, net::Ipv4Addr, str, sync::Arc}; +use tokio::task; +use wasmtime::{ + component::{Component, Linker, ResourceTable}, + Config, Engine, Store, +}; +use wasmtime_wasi::{self, pipe::MemoryOutputPipe, IoView, WasiCtx, WasiCtxBuilder, WasiView}; +use wasmtime_wasi_http::{ + bindings::http::types::{ErrorCode, Scheme}, + body::HyperOutgoingBody, + io::TokioIo, + types::{self, HostFutureIncomingResponse, IncomingResponse, OutgoingRequestConfig}, + HttpResult, WasiHttpCtx, WasiHttpView, +}; + +type RequestSender = Arc< + dyn Fn(hyper::Request, OutgoingRequestConfig) -> HostFutureIncomingResponse + + Send + + Sync, +>; + +struct Ctx { + table: ResourceTable, + wasi: WasiCtx, + http: WasiHttpCtx, + stdout: MemoryOutputPipe, + stderr: MemoryOutputPipe, + send_request: Option, + rejected_authority: Option, +} + +impl IoView for Ctx { + fn table(&mut self) -> &mut ResourceTable { + &mut self.table + } +} +impl WasiView for Ctx { + fn ctx(&mut self) -> &mut WasiCtx { + &mut self.wasi + } +} + +impl WasiHttpView for Ctx { + fn ctx(&mut self) -> &mut WasiHttpCtx { + &mut self.http + } + + fn send_request( + &mut self, + request: hyper::Request, + config: OutgoingRequestConfig, + ) -> HttpResult { + if let Some(rejected_authority) = &self.rejected_authority { + let authority = request.uri().authority().map(ToString::to_string).unwrap(); + if &authority == rejected_authority { + return Err(ErrorCode::HttpRequestDenied.into()); + } + } + if let Some(send_request) = self.send_request.clone() { + Ok(send_request(request, config)) + } else { + Ok(types::default_send_request(request, config)) + } + } + + fn is_forbidden_header(&mut self, name: &hyper::header::HeaderName) -> bool { + name.as_str() == "custom-forbidden-header" + } +} + +fn store(engine: &Engine, server: &Server) -> Store { + let stdout = MemoryOutputPipe::new(4096); + let stderr = MemoryOutputPipe::new(4096); + + // Create our wasi context. + let mut builder = WasiCtxBuilder::new(); + builder.stdout(stdout.clone()); + builder.stderr(stderr.clone()); + builder.env("HTTP_SERVER", &server.addr()); + let ctx = Ctx { + table: ResourceTable::new(), + wasi: builder.build(), + http: WasiHttpCtx::new(), + stderr, + stdout, + send_request: None, + rejected_authority: None, + }; + + Store::new(&engine, ctx) +} + +impl Drop for Ctx { + fn drop(&mut self) { + let stdout = self.stdout.contents(); + if !stdout.is_empty() { + println!("[guest] stdout:\n{}\n===", String::from_utf8_lossy(&stdout)); + } + let stderr = self.stderr.contents(); + if !stderr.is_empty() { + println!("[guest] stderr:\n{}\n===", String::from_utf8_lossy(&stderr)); + } + } +} + +mod async_; +mod sync; + +async fn run_wasi_http( + component_filename: &str, + req: hyper::Request>, + send_request: Option, + rejected_authority: Option, +) -> anyhow::Result>, ErrorCode>> { + let stdout = MemoryOutputPipe::new(4096); + let stderr = MemoryOutputPipe::new(4096); + let table = ResourceTable::new(); + + 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)?; + let component = Component::from_file(&engine, component_filename)?; + + // Create our wasi context. + let mut builder = WasiCtxBuilder::new(); + builder.stdout(stdout.clone()); + builder.stderr(stderr.clone()); + let wasi = builder.build(); + let http = WasiHttpCtx::new(); + let ctx = Ctx { + table, + wasi, + http, + stderr, + stdout, + send_request, + rejected_authority, + }; + let mut store = Store::new(&engine, ctx); + + let mut linker = Linker::new(&engine); + wasmtime_wasi_http::add_to_linker_async(&mut linker)?; + let proxy = + wasmtime_wasi_http::bindings::Proxy::instantiate_async(&mut store, &component, &linker) + .await?; + + let req = store.data_mut().new_incoming_request(Scheme::Http, req)?; + + let (sender, receiver) = tokio::sync::oneshot::channel(); + let out = store.data_mut().new_response_outparam(sender)?; + + let handle = wasmtime_wasi::runtime::spawn(async move { + proxy + .wasi_http_incoming_handler() + .call_handle(&mut store, req, out) + .await?; + + Ok::<_, anyhow::Error>(()) + }); + + let resp = match receiver.await { + Ok(Ok(resp)) => { + let (parts, body) = resp.into_parts(); + let collected = BodyExt::collect(body).await?; + Some(Ok(hyper::Response::from_parts(parts, collected))) + } + Ok(Err(e)) => Some(Err(e)), + + // Fall through below to the `resp.expect(...)` which will hopefully + // return a more specific error from `handle.await`. + Err(_) => None, + }; + + // Now that the response has been processed, we can wait on the wasm to + // finish without deadlocking. + handle.await.context("Component execution")?; + + Ok(resp.expect("wasm never called set-response-outparam")) +} + +#[test_log::test(tokio::test)] +async fn wasi_http_proxy_tests() -> anyhow::Result<()> { + let req = hyper::Request::builder() + .header("custom-forbidden-header", "yes") + .uri("http://example.com:8080/test-path") + .method(http::Method::GET); + + let resp = run_wasi_http( + test_programs_artifacts::API_PROXY_COMPONENT, + req.body(body::empty())?, + None, + None, + ) + .await?; + + match resp { + Ok(resp) => println!("response: {resp:?}"), + Err(e) => panic!("Error given in response: {e:?}"), + }; + + Ok(()) +} + +#[test_log::test(tokio::test)] +async fn wasi_http_hash_all() -> Result<()> { + do_wasi_http_hash_all(false).await +} + +#[test_log::test(tokio::test)] +async fn wasi_http_hash_all_with_override() -> Result<()> { + do_wasi_http_hash_all(true).await +} + +async fn do_wasi_http_hash_all(override_send_request: bool) -> Result<()> { + let bodies = Arc::new( + [ + ("/a", "’Twas brillig, and the slithy toves"), + ("/b", "Did gyre and gimble in the wabe:"), + ("/c", "All mimsy were the borogoves,"), + ("/d", "And the mome raths outgrabe."), + ] + .into_iter() + .collect::>(), + ); + + let listener = tokio::net::TcpListener::bind((Ipv4Addr::new(127, 0, 0, 1), 0)).await?; + + let prefix = format!("http://{}", listener.local_addr()?); + + let (_tx, rx) = oneshot::channel::<()>(); + + let handle = { + let bodies = bodies.clone(); + + move |request: http::request::Parts| { + if let (Method::GET, Some(body)) = (request.method, bodies.get(request.uri.path())) { + Ok::<_, anyhow::Error>(hyper::Response::new(body::full(Bytes::copy_from_slice( + body.as_bytes(), + )))) + } else { + Ok(hyper::Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .body(body::empty())?) + } + } + }; + + let send_request = if override_send_request { + Some(Arc::new( + move |request: hyper::Request, + OutgoingRequestConfig { + between_bytes_timeout, + .. + }| { + let response = handle(request.into_parts().0).map(|resp| { + Ok(IncomingResponse { + resp: resp.map(|body| { + body.map_err(wasmtime_wasi_http::hyper_response_error) + .boxed() + }), + worker: None, + between_bytes_timeout, + }) + }); + HostFutureIncomingResponse::ready(response) + }, + ) as RequestSender) + } else { + let server = async move { + loop { + let (stream, _) = listener.accept().await?; + let stream = TokioIo::new(stream); + let handle = handle.clone(); + task::spawn(async move { + if let Err(e) = http1::Builder::new() + .keep_alive(true) + .serve_connection( + stream, + service_fn(move |request| { + let handle = handle.clone(); + async move { handle(request.into_parts().0) } + }), + ) + .await + { + eprintln!("error serving connection: {e:?}"); + } + }); + + // Help rustc with type inference: + if false { + return Ok::<_, anyhow::Error>(()); + } + } + } + .then(|result| { + if let Err(e) = result { + eprintln!("error listening for connections: {e:?}"); + } + future::ready(()) + }) + .boxed(); + + task::spawn(async move { + drop(future::select(server, rx).await); + }); + + None + }; + + let mut request = hyper::Request::builder() + .method(http::Method::GET) + .uri("http://example.com:8080/hash-all"); + for path in bodies.keys() { + request = request.header("url", format!("{prefix}{path}")); + } + let request = request.body(body::empty())?; + + let response = run_wasi_http( + test_programs_artifacts::API_PROXY_STREAMING_COMPONENT, + request, + send_request, + None, + ) + .await??; + + assert_eq!(StatusCode::OK, response.status()); + let body = response.into_body().to_bytes(); + let body = str::from_utf8(&body)?; + for line in body.lines() { + let (url, hash) = line + .split_once(": ") + .ok_or_else(|| anyhow!("expected string of form `: `; got {line}"))?; + + let path = url + .strip_prefix(&prefix) + .ok_or_else(|| anyhow!("expected string with prefix {prefix}; got {url}"))?; + + let mut hasher = Sha256::new(); + hasher.update( + bodies + .get(path) + .ok_or_else(|| anyhow!("unexpected path: {path}"))?, + ); + + use base64::Engine; + assert_eq!( + hash, + base64::engine::general_purpose::STANDARD_NO_PAD.encode(hasher.finalize()) + ); + } + + Ok(()) +} + +// ensure the runtime rejects the outgoing request +#[test_log::test(tokio::test)] +async fn wasi_http_hash_all_with_reject() -> Result<()> { + let request = hyper::Request::builder() + .method(http::Method::GET) + .uri("http://example.com:8080/hash-all"); + let request = request.header("url", format!("http://forbidden.com")); + let request = request.header("url", format!("http://localhost")); + let request = request.body(body::empty())?; + + let response = run_wasi_http( + test_programs_artifacts::API_PROXY_STREAMING_COMPONENT, + request, + None, + Some("forbidden.com".to_string()), + ) + .await??; + + let body = response.into_body().to_bytes(); + let body = str::from_utf8(&body).unwrap(); + for line in body.lines() { + println!("{line}"); + if line.contains("forbidden.com") { + assert!(line.contains("HttpRequestDenied")); + } + if line.contains("localhost") { + assert!(!line.contains("HttpRequestDenied")); + } + } + + Ok(()) +} + +#[test_log::test(tokio::test)] +async fn wasi_http_echo() -> Result<()> { + do_wasi_http_echo("echo", None).await +} + +#[test_log::test(tokio::test)] +async fn wasi_http_double_echo() -> Result<()> { + let listener = tokio::net::TcpListener::bind((Ipv4Addr::new(127, 0, 0, 1), 0)).await?; + + let prefix = format!("http://{}", listener.local_addr()?); + + let (_tx, rx) = oneshot::channel::<()>(); + + let server = async move { + loop { + let (stream, _) = listener.accept().await?; + let stream = TokioIo::new(stream); + task::spawn(async move { + if let Err(e) = http1::Builder::new() + .keep_alive(true) + .serve_connection( + stream, + service_fn( + move |request: hyper::Request| async move { + use http_body_util::BodyExt; + + if let (&Method::POST, "/echo") = + (request.method(), request.uri().path()) + { + Ok::<_, anyhow::Error>(hyper::Response::new( + request.into_body().boxed(), + )) + } else { + Ok(hyper::Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .body(BoxBody::new( + Empty::new().map_err(|_| unreachable!()), + ))?) + } + }, + ), + ) + .await + { + eprintln!("error serving connection: {e:?}"); + } + }); + + // Help rustc with type inference: + if false { + return Ok::<_, anyhow::Error>(()); + } + } + } + .then(|result| { + if let Err(e) = result { + eprintln!("error listening for connections: {e:?}"); + } + future::ready(()) + }) + .boxed(); + + task::spawn(async move { + drop(future::select(server, rx).await); + }); + + do_wasi_http_echo("double-echo", Some(&format!("{prefix}/echo"))).await +} + +async fn do_wasi_http_echo(uri: &str, url_header: Option<&str>) -> Result<()> { + let body = { + // A sorta-random-ish megabyte + let mut n = 0_u8; + iter::repeat_with(move || { + n = n.wrapping_add(251); + n + }) + .take(1024 * 1024) + .collect::>() + }; + + let mut request = hyper::Request::builder() + .method(http::Method::POST) + .uri(format!("http://example.com:8080/{uri}")) + .header("content-type", "application/octet-stream"); + + if let Some(url_header) = url_header { + request = request.header("url", url_header); + } + + let request = request.body(BoxBody::new(StreamBody::new(stream::iter( + body.chunks(16 * 1024) + .map(|chunk| Ok::<_, hyper::Error>(Frame::data(Bytes::copy_from_slice(chunk)))) + .collect::>(), + ))))?; + + let response = run_wasi_http( + test_programs_artifacts::API_PROXY_STREAMING_COMPONENT, + request, + None, + None, + ) + .await??; + + assert_eq!(StatusCode::OK, response.status()); + assert_eq!( + response.headers()["content-type"], + "application/octet-stream" + ); + let received = Vec::from(response.into_body().to_bytes()); + if body != received { + panic!( + "body content mismatch (expected length {}; actual length {})", + body.len(), + received.len() + ); + } + + Ok(()) +} + +#[test_log::test(tokio::test)] +async fn wasi_http_without_port() -> Result<()> { + let req = hyper::Request::builder() + .method(http::Method::GET) + .uri("https://httpbin.org/get"); + + let _response: hyper::Response<_> = run_wasi_http( + test_programs_artifacts::API_PROXY_FORWARD_REQUEST_COMPONENT, + req.body(body::empty())?, + None, + None, + ) + .await??; + + // NB: don't test the actual return code of `response`. This is testing a + // live http request against a live server and things happen. If we got this + // far it's already successful that the request was made and the lack of + // port in the URI was handled. + + Ok(()) +} diff --git a/crates/wasi-http/tests/all/sync.rs b/crates/wasi-http/tests/all/p2/sync.rs similarity index 84% rename from crates/wasi-http/tests/all/sync.rs rename to crates/wasi-http/tests/all/p2/sync.rs index 64147a5a0e..46403efbfc 100644 --- a/crates/wasi-http/tests/all/sync.rs +++ b/crates/wasi-http/tests/all/p2/sync.rs @@ -1,4 +1,5 @@ use super::*; +use crate::*; use test_programs_artifacts::*; use wasmtime_wasi::bindings::sync::Command; @@ -20,85 +21,85 @@ fn run(path: &str, server: &Server) -> Result<()> { #[test_log::test] fn http_outbound_request_get() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_GET_COMPONENT, &server) } #[test_log::test] fn http_outbound_request_timeout() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_TIMEOUT_COMPONENT, &server) } #[test_log::test] fn http_outbound_request_post() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_POST_COMPONENT, &server) } #[test_log::test] fn http_outbound_request_large_post() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_LARGE_POST_COMPONENT, &server) } #[test_log::test] fn http_outbound_request_put() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_PUT_COMPONENT, &server) } #[test_log::test] fn http_outbound_request_invalid_version() -> Result<()> { - let server = Server::http2()?; + let server = Server::http2(1)?; run(HTTP_OUTBOUND_REQUEST_INVALID_VERSION_COMPONENT, &server) } #[test_log::test] fn http_outbound_request_invalid_header() -> Result<()> { - let server = Server::http2()?; + let server = Server::http2(1)?; run(HTTP_OUTBOUND_REQUEST_INVALID_HEADER_COMPONENT, &server) } #[test_log::test] fn http_outbound_request_unknown_method() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_UNKNOWN_METHOD_COMPONENT, &server) } #[test_log::test] fn http_outbound_request_unsupported_scheme() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_UNSUPPORTED_SCHEME_COMPONENT, &server) } #[test_log::test] fn http_outbound_request_invalid_port() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_INVALID_PORT_COMPONENT, &server) } #[test_log::test] fn http_outbound_request_invalid_dnsname() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_INVALID_DNSNAME_COMPONENT, &server) } #[test_log::test] fn http_outbound_request_response_build() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_RESPONSE_BUILD_COMPONENT, &server) } #[test_log::test] fn http_outbound_request_content_length() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run(HTTP_OUTBOUND_REQUEST_CONTENT_LENGTH_COMPONENT, &server) } #[test_log::test] fn http_outbound_request_missing_path_and_query() -> Result<()> { - let server = Server::http1()?; + let server = Server::http1(1)?; run( HTTP_OUTBOUND_REQUEST_MISSING_PATH_AND_QUERY_COMPONENT, &server, diff --git a/crates/wasi-http/tests/all/p3/mod.rs b/crates/wasi-http/tests/all/p3/mod.rs new file mode 100644 index 0000000000..3336edcb65 --- /dev/null +++ b/crates/wasi-http/tests/all/p3/mod.rs @@ -0,0 +1,562 @@ +use core::future::Future; + +use bytes::Bytes; +use futures::try_join; +use http_body::Body; +use http_body_util::{BodyExt as _, Collected, Empty}; +use wasmtime::component::{Component, Linker, ResourceTable}; +use wasmtime::{AsContextMut as _, Store}; +use wasmtime_wasi::p3::cli::{WasiCliCtx, WasiCliView}; +use wasmtime_wasi::p3::clocks::{WasiClocksCtx, WasiClocksView}; +use wasmtime_wasi::p3::filesystem::{WasiFilesystemCtx, WasiFilesystemView}; +use wasmtime_wasi::p3::random::{WasiRandomCtx, WasiRandomView}; +use wasmtime_wasi::p3::sockets::{WasiSocketsCtx, WasiSocketsView}; +use wasmtime_wasi::p3::ResourceView; +use wasmtime_wasi::{IoView, WasiCtx, WasiCtxBuilder, WasiView}; +use wasmtime_wasi_http::p3::bindings::http::types::ErrorCode; +use wasmtime_wasi_http::p3::bindings::Proxy; +use wasmtime_wasi_http::p3::{ + default_send_request, Client, RequestOptions, Response, WasiHttpCtx, WasiHttpView, + DEFAULT_FORBIDDEN_HEADERS, +}; + +use crate::http_server::Server; + +mod outgoing; + +struct Ctx { + cli: WasiCliCtx, + clocks: WasiClocksCtx, + filesystem: WasiFilesystemCtx, + random: WasiRandomCtx, + sockets: WasiSocketsCtx, + table: ResourceTable, + wasip2: WasiCtx, + http: WasiHttpCtx, +} + +impl Default for Ctx +where + C: Client + Default, +{ + fn default() -> Self { + Self { + cli: WasiCliCtx::default(), + clocks: WasiClocksCtx::default(), + filesystem: WasiFilesystemCtx::default(), + sockets: WasiSocketsCtx::default(), + random: WasiRandomCtx::default(), + table: ResourceTable::default(), + wasip2: WasiCtxBuilder::new().inherit_stdio().build(), + http: WasiHttpCtx::default(), + } + } +} + +impl WasiView for Ctx { + fn ctx(&mut self) -> &mut WasiCtx { + &mut self.wasip2 + } +} + +impl IoView for Ctx { + fn table(&mut self) -> &mut ResourceTable { + &mut self.table + } +} + +impl ResourceView for Ctx { + fn table(&mut self) -> &mut ResourceTable { + &mut self.table + } +} + +impl WasiCliView for Ctx { + fn cli(&mut self) -> &WasiCliCtx { + &self.cli + } +} + +impl WasiClocksView for Ctx { + fn clocks(&mut self) -> &WasiClocksCtx { + &self.clocks + } +} + +impl WasiFilesystemView for Ctx { + fn filesystem(&self) -> &WasiFilesystemCtx { + &self.filesystem + } +} + +impl WasiRandomView for Ctx { + fn random(&mut self) -> &mut WasiRandomCtx { + &mut self.random + } +} + +impl WasiSocketsView for Ctx { + fn sockets(&self) -> &WasiSocketsCtx { + &self.sockets + } +} + +impl WasiHttpView for Ctx { + type Client = C; + + fn http(&self) -> &WasiHttpCtx { + &self.http + } + + fn is_forbidden_header(&mut self, name: &http::header::HeaderName) -> bool { + name.as_str() == "custom-forbidden-header" || DEFAULT_FORBIDDEN_HEADERS.contains(name) + } +} + +#[derive(Clone, Default)] +struct TestClient { + rejected_authority: Option, +} + +impl Client for TestClient { + type Error = ErrorCode; + + async fn send_request( + &mut self, + request: http::Request< + impl http_body::Body> + Send + 'static, + >, + options: Option, + ) -> wasmtime::Result< + Result< + ( + impl Future< + Output = Result< + http::Response< + impl http_body::Body + 'static, + >, + ErrorCode, + >, + >, + impl Future> + 'static, + ), + ErrorCode, + >, + > { + if let Some(rejected_authority) = &self.rejected_authority { + let authority = request.uri().authority().map(ToString::to_string).unwrap(); + if &authority == rejected_authority { + return Ok(Err(ErrorCode::HttpRequestDenied.into())); + } + } + Ok(default_send_request(request, options).await) + } +} + +async fn run_wasi_http + 'static>( + component_filename: &str, + req: http::Request + Send + Sync + 'static>, + client: TestClient, +) -> anyhow::Result>, Option>> { + let engine = test_programs_artifacts::engine(|config| { + config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable); + config.async_support(true); + config.wasm_component_model_async(true); + }); + let component = Component::from_file(&engine, component_filename)?; + + let mut store = Store::new( + &engine, + Ctx { + cli: WasiCliCtx { + ..WasiCliCtx::default() + }, + http: WasiHttpCtx { client }, + ..Ctx::default() + }, + ); + + let mut linker = Linker::new(&engine); + wasmtime_wasi::add_to_linker_async(&mut linker)?; + wasmtime_wasi_http::p3::add_to_linker(&mut linker)?; + let instance = linker.instantiate_async(&mut store, &component).await?; + let proxy = Proxy::new(&mut store, &instance)?; + let handle = proxy.handle(&mut store, req).await?; + let res = match handle.get(&mut store).await? { + Ok(res) => res, + Err(err) => return Ok(Err(Some(err))), + }; + let (res, tx, io) = Response::resource_into_http(&mut store, &instance, res)?; + let (parts, body) = res.into_parts(); + let ((), body) = try_join!( + async { + if let Some(io) = io { + let closure = io.get(&mut store).await?; + closure(store.as_context_mut())?; + } + anyhow::Ok(()) + }, + async { Ok(body.collect().await) }, + )?; + let body = match body { + Ok(body) => body, + Err(err) => return Ok(Err(err)), + }; + if let Some(tx) = tx { + tx.write(Ok(())).get(store).await?; + } + Ok(Ok(http::Response::from_parts(parts, body))) +} + +#[test_log::test(tokio::test)] +async fn wasi_http_proxy_tests() -> anyhow::Result<()> { + let req = http::Request::builder() + .uri("http://example.com:8080/test-path") + .method(http::Method::GET); + + let resp = run_wasi_http( + test_programs_artifacts::API_0_3_PROXY_COMPONENT, + req.body(Empty::new())?, + TestClient::default(), + ) + .await?; + + match resp { + Ok(resp) => println!("response: {resp:?}"), + Err(e) => panic!("Error given in response: {e:?}"), + }; + + Ok(()) +} + +// TODO: Port below +// +//#[test_log::test(tokio::test)] +//async fn wasi_http_hash_all() -> Result<()> { +// do_wasi_http_hash_all(false).await +//} +// +//#[test_log::test(tokio::test)] +//async fn wasi_http_hash_all_with_override() -> Result<()> { +// do_wasi_http_hash_all(true).await +//} +// +//async fn do_wasi_http_hash_all(override_send_request: bool) -> Result<()> { +// let bodies = Arc::new( +// [ +// ("/a", "’Twas brillig, and the slithy toves"), +// ("/b", "Did gyre and gimble in the wabe:"), +// ("/c", "All mimsy were the borogoves,"), +// ("/d", "And the mome raths outgrabe."), +// ] +// .into_iter() +// .collect::>(), +// ); +// +// let listener = tokio::net::TcpListener::bind((Ipv4Addr::new(127, 0, 0, 1), 0)).await?; +// +// let prefix = format!("http://{}", listener.local_addr()?); +// +// let (_tx, rx) = oneshot::channel::<()>(); +// +// let handle = { +// let bodies = bodies.clone(); +// +// move |request: http::request::Parts| { +// if let (Method::GET, Some(body)) = (request.method, bodies.get(request.uri.path())) { +// Ok::<_, anyhow::Error>(hyper::Response::new(body::full(Bytes::copy_from_slice( +// body.as_bytes(), +// )))) +// } else { +// Ok(hyper::Response::builder() +// .status(StatusCode::METHOD_NOT_ALLOWED) +// .body(body::empty())?) +// } +// } +// }; +// +// let send_request = if override_send_request { +// Some(Arc::new( +// move |request: hyper::Request, +// OutgoingRequestConfig { +// between_bytes_timeout, +// .. +// }| { +// let response = handle(request.into_parts().0).map(|resp| { +// Ok(IncomingResponse { +// resp: resp.map(|body| { +// body.map_err(wasmtime_wasi_http::hyper_response_error) +// .boxed() +// }), +// worker: None, +// between_bytes_timeout, +// }) +// }); +// HostFutureIncomingResponse::ready(response) +// }, +// ) as RequestSender) +// } else { +// let server = async move { +// loop { +// let (stream, _) = listener.accept().await?; +// let stream = TokioIo::new(stream); +// let handle = handle.clone(); +// task::spawn(async move { +// if let Err(e) = http1::Builder::new() +// .keep_alive(true) +// .serve_connection( +// stream, +// service_fn(move |request| { +// let handle = handle.clone(); +// async move { handle(request.into_parts().0) } +// }), +// ) +// .await +// { +// eprintln!("error serving connection: {e:?}"); +// } +// }); +// +// // Help rustc with type inference: +// if false { +// return Ok::<_, anyhow::Error>(()); +// } +// } +// } +// .then(|result| { +// if let Err(e) = result { +// eprintln!("error listening for connections: {e:?}"); +// } +// future::ready(()) +// }) +// .boxed(); +// +// task::spawn(async move { +// drop(future::select(server, rx).await); +// }); +// +// None +// }; +// +// let mut request = hyper::Request::builder() +// .method(http::Method::GET) +// .uri("http://example.com:8080/hash-all"); +// for path in bodies.keys() { +// request = request.header("url", format!("{prefix}{path}")); +// } +// let request = request.body(body::empty())?; +// +// let response = run_wasi_http( +// test_programs_artifacts::API_PROXY_STREAMING_COMPONENT, +// request, +// send_request, +// None, +// ) +// .await??; +// +// assert_eq!(StatusCode::OK, response.status()); +// let body = response.into_body().to_bytes(); +// let body = str::from_utf8(&body)?; +// for line in body.lines() { +// let (url, hash) = line +// .split_once(": ") +// .ok_or_else(|| anyhow!("expected string of form `: `; got {line}"))?; +// +// let path = url +// .strip_prefix(&prefix) +// .ok_or_else(|| anyhow!("expected string with prefix {prefix}; got {url}"))?; +// +// let mut hasher = Sha256::new(); +// hasher.update( +// bodies +// .get(path) +// .ok_or_else(|| anyhow!("unexpected path: {path}"))?, +// ); +// +// use base64::Engine; +// assert_eq!( +// hash, +// base64::engine::general_purpose::STANDARD_NO_PAD.encode(hasher.finalize()) +// ); +// } +// +// Ok(()) +//} +// +//// ensure the runtime rejects the outgoing request +//#[test_log::test(tokio::test)] +//async fn wasi_http_hash_all_with_reject() -> Result<()> { +// let request = hyper::Request::builder() +// .method(http::Method::GET) +// .uri("http://example.com:8080/hash-all"); +// let request = request.header("url", format!("http://forbidden.com")); +// let request = request.header("url", format!("http://localhost")); +// let request = request.body(body::empty())?; +// +// let response = run_wasi_http( +// test_programs_artifacts::API_PROXY_STREAMING_COMPONENT, +// request, +// None, +// Some("forbidden.com".to_string()), +// ) +// .await??; +// +// let body = response.into_body().to_bytes(); +// let body = str::from_utf8(&body).unwrap(); +// for line in body.lines() { +// println!("{line}"); +// if line.contains("forbidden.com") { +// assert!(line.contains("HttpRequestDenied")); +// } +// if line.contains("localhost") { +// assert!(!line.contains("HttpRequestDenied")); +// } +// } +// +// Ok(()) +//} +// +//#[test_log::test(tokio::test)] +//async fn wasi_http_echo() -> Result<()> { +// do_wasi_http_echo("echo", None).await +//} +// +//#[test_log::test(tokio::test)] +//async fn wasi_http_double_echo() -> Result<()> { +// let listener = tokio::net::TcpListener::bind((Ipv4Addr::new(127, 0, 0, 1), 0)).await?; +// +// let prefix = format!("http://{}", listener.local_addr()?); +// +// let (_tx, rx) = oneshot::channel::<()>(); +// +// let server = async move { +// loop { +// let (stream, _) = listener.accept().await?; +// let stream = TokioIo::new(stream); +// task::spawn(async move { +// if let Err(e) = http1::Builder::new() +// .keep_alive(true) +// .serve_connection( +// stream, +// service_fn( +// move |request: hyper::Request| async move { +// use http_body_util::BodyExt; +// +// if let (&Method::POST, "/echo") = +// (request.method(), request.uri().path()) +// { +// Ok::<_, anyhow::Error>(hyper::Response::new( +// request.into_body().boxed(), +// )) +// } else { +// Ok(hyper::Response::builder() +// .status(StatusCode::METHOD_NOT_ALLOWED) +// .body(BoxBody::new( +// Empty::new().map_err(|_| unreachable!()), +// ))?) +// } +// }, +// ), +// ) +// .await +// { +// eprintln!("error serving connection: {e:?}"); +// } +// }); +// +// // Help rustc with type inference: +// if false { +// return Ok::<_, anyhow::Error>(()); +// } +// } +// } +// .then(|result| { +// if let Err(e) = result { +// eprintln!("error listening for connections: {e:?}"); +// } +// future::ready(()) +// }) +// .boxed(); +// +// task::spawn(async move { +// drop(future::select(server, rx).await); +// }); +// +// do_wasi_http_echo("double-echo", Some(&format!("{prefix}/echo"))).await +//} +// +//async fn do_wasi_http_echo(uri: &str, url_header: Option<&str>) -> Result<()> { +// let body = { +// // A sorta-random-ish megabyte +// let mut n = 0_u8; +// iter::repeat_with(move || { +// n = n.wrapping_add(251); +// n +// }) +// .take(1024 * 1024) +// .collect::>() +// }; +// +// let mut request = hyper::Request::builder() +// .method(http::Method::POST) +// .uri(format!("http://example.com:8080/{uri}")) +// .header("content-type", "application/octet-stream"); +// +// if let Some(url_header) = url_header { +// request = request.header("url", url_header); +// } +// +// let request = request.body(BoxBody::new(StreamBody::new(stream::iter( +// body.chunks(16 * 1024) +// .map(|chunk| Ok::<_, hyper::Error>(Frame::data(Bytes::copy_from_slice(chunk)))) +// .collect::>(), +// ))))?; +// +// let response = run_wasi_http( +// test_programs_artifacts::API_PROXY_STREAMING_COMPONENT, +// request, +// None, +// None, +// ) +// .await??; +// +// assert_eq!(StatusCode::OK, response.status()); +// assert_eq!( +// response.headers()["content-type"], +// "application/octet-stream" +// ); +// let received = Vec::from(response.into_body().to_bytes()); +// if body != received { +// panic!( +// "body content mismatch (expected length {}; actual length {})", +// body.len(), +// received.len() +// ); +// } +// +// Ok(()) +//} +// +//#[test_log::test(tokio::test)] +//// test uses TLS but riscv/s390x don't support that yet +//#[cfg_attr(any(target_arch = "riscv64", target_arch = "s390x"), ignore)] +//async fn wasi_http_without_port() -> Result<()> { +// let req = hyper::Request::builder() +// .method(http::Method::GET) +// .uri("https://httpbin.org/get"); +// +// let _response: hyper::Response<_> = run_wasi_http( +// test_programs_artifacts::API_PROXY_FORWARD_REQUEST_COMPONENT, +// req.body(body::empty())?, +// None, +// None, +// ) +// .await??; +// +// // NB: don't test the actual return code of `response`. This is testing a +// // live http request against a live server and things happen. If we got this +// // far it's already successful that the request was made and the lack of +// // port in the URI was handled. +// +// Ok(()) +//} diff --git a/crates/wasi-http/tests/all/p3/outgoing.rs b/crates/wasi-http/tests/all/p3/outgoing.rs new file mode 100644 index 0000000000..701902ce33 --- /dev/null +++ b/crates/wasi-http/tests/all/p3/outgoing.rs @@ -0,0 +1,132 @@ +use super::*; +use crate::*; +use anyhow::{anyhow, Context as _}; +use test_programs_artifacts::*; +use wasmtime_wasi::p3::bindings::Command; + +foreach_http_0_3!(assert_test_exists); + +async fn run(path: &str, server: &Server) -> anyhow::Result<()> { + let engine = test_programs_artifacts::engine(|config| { + config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable); + config.async_support(true); + config.wasm_component_model_async(true); + }); + let component = Component::from_file(&engine, path)?; + let mut store = Store::new( + &engine, + Ctx { + cli: WasiCliCtx { + environment: vec![("HTTP_SERVER".into(), server.addr())], + ..WasiCliCtx::default() + }, + ..Ctx::::default() + }, + ); + let mut linker = Linker::new(&engine); + wasmtime_wasi::add_to_linker_async(&mut linker).context("failed to link `wasi:cli@0.2.x`")?; + wasmtime_wasi::p3::add_to_linker(&mut linker).context("failed to link `wasi:cli@0.3.x`")?; + wasmtime_wasi_http::p3::add_only_http_to_linker(&mut linker)?; + let command = Command::instantiate_async(&mut store, &component, &linker).await?; + let p = command + .wasi_cli_run() + .call_run(&mut store) + .await + .context("failed to call `wasi:cli/run#run`")?; + p.get(&mut store) + .await + .context("guest trapped")? + .map_err(|()| anyhow!("`wasi:cli/run#run` failed")) +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn http_0_3_outbound_request_get() -> anyhow::Result<()> { + let server = Server::http1(1)?; + run(HTTP_0_3_OUTBOUND_REQUEST_GET_COMPONENT, &server).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn http_0_3_outbound_request_timeout() -> anyhow::Result<()> { + let server = Server::http1(1)?; + run(HTTP_0_3_OUTBOUND_REQUEST_TIMEOUT_COMPONENT, &server).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn http_0_3_outbound_request_post() -> anyhow::Result<()> { + let server = Server::http1(1)?; + run(HTTP_0_3_OUTBOUND_REQUEST_POST_COMPONENT, &server).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn http_0_3_outbound_request_large_post() -> anyhow::Result<()> { + let server = Server::http1(1)?; + run(HTTP_0_3_OUTBOUND_REQUEST_LARGE_POST_COMPONENT, &server).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn http_0_3_outbound_request_put() -> anyhow::Result<()> { + let server = Server::http1(1)?; + run(HTTP_0_3_OUTBOUND_REQUEST_PUT_COMPONENT, &server).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn http_0_3_outbound_request_invalid_version() -> anyhow::Result<()> { + let server = Server::http2(1)?; + run(HTTP_0_3_OUTBOUND_REQUEST_INVALID_VERSION_COMPONENT, &server).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn http_0_3_outbound_request_invalid_header() -> anyhow::Result<()> { + let server = Server::http2(1)?; + run(HTTP_0_3_OUTBOUND_REQUEST_INVALID_HEADER_COMPONENT, &server).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn http_0_3_outbound_request_unknown_method() -> anyhow::Result<()> { + let server = Server::http1(1)?; + run(HTTP_0_3_OUTBOUND_REQUEST_UNKNOWN_METHOD_COMPONENT, &server).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn http_0_3_outbound_request_unsupported_scheme() -> anyhow::Result<()> { + let server = Server::http1(1)?; + run( + HTTP_0_3_OUTBOUND_REQUEST_UNSUPPORTED_SCHEME_COMPONENT, + &server, + ) + .await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn http_0_3_outbound_request_invalid_port() -> anyhow::Result<()> { + let server = Server::http1(1)?; + run(HTTP_0_3_OUTBOUND_REQUEST_INVALID_PORT_COMPONENT, &server).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn http_0_3_outbound_request_invalid_dnsname() -> anyhow::Result<()> { + let server = Server::http1(1)?; + run(HTTP_0_3_OUTBOUND_REQUEST_INVALID_DNSNAME_COMPONENT, &server).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn http_0_3_outbound_request_response_build() -> anyhow::Result<()> { + let server = Server::http1(1)?; + run(HTTP_0_3_OUTBOUND_REQUEST_RESPONSE_BUILD_COMPONENT, &server).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn http_0_3_outbound_request_content_length() -> anyhow::Result<()> { + let server = Server::http1(3)?; + run(HTTP_0_3_OUTBOUND_REQUEST_CONTENT_LENGTH_COMPONENT, &server).await +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn http_0_3_outbound_request_missing_path_and_query() -> anyhow::Result<()> { + let server = Server::http1(1)?; + run( + HTTP_0_3_OUTBOUND_REQUEST_MISSING_PATH_AND_QUERY_COMPONENT, + &server, + ) + .await +} diff --git a/crates/wasi/src/p3/mod.rs b/crates/wasi/src/p3/mod.rs index 134cfeb1b3..e7c4076f57 100644 --- a/crates/wasi/src/p3/mod.rs +++ b/crates/wasi/src/p3/mod.rs @@ -1,7 +1,10 @@ use core::future::Future; +use core::ops::{Deref, DerefMut}; use std::collections::HashMap; +use std::sync::Arc; +use anyhow::{anyhow, bail}; use tokio::sync::mpsc; use wasmtime::component::{ AbortOnDropHandle, Accessor, AccessorTask, FutureWriter, Linker, Lower, ResourceTable, @@ -216,3 +219,87 @@ impl TaskTable { Some(handle) } } + +#[derive(Debug)] +pub enum WithChildren { + Parent(Arc>), + Child(Arc>), +} + +impl Default for WithChildren { + fn default() -> Self { + Self::Parent(Arc::default()) + } +} + +impl WithChildren { + pub fn new(v: T) -> Self { + Self::Parent(Arc::new(std::sync::RwLock::new(v))) + } + + fn as_arc(&self) -> &Arc> { + match self { + Self::Parent(v) | Self::Child(v) => v, + } + } + + fn into_arc(self) -> Arc> { + match self { + Self::Parent(v) | Self::Child(v) => v, + } + } + + /// Returns a new child referencing the same value as `self`. + pub fn child(&self) -> Self { + Self::Child(Arc::clone(self.as_arc())) + } + + /// Clone `T` and return the clone as a parent reference. + /// Fails if the inner lock is poisoned. + pub fn clone(&self) -> wasmtime::Result + where + T: Clone, + { + if let Ok(v) = self.as_arc().read() { + Ok(Self::Parent(Arc::new(std::sync::RwLock::new(v.clone())))) + } else { + bail!("lock poisoned") + } + } + + /// If this is the only reference to `T` then unwrap it. + /// Otherwise, clone `T` and return the clone. + /// Fails if the inner lock is poisoned. + pub fn unwrap_or_clone(self) -> wasmtime::Result + where + T: Clone, + { + match Arc::try_unwrap(self.into_arc()) { + Ok(v) => v.into_inner().map_err(|_| anyhow!("lock poisoned")), + Err(v) => { + if let Ok(v) = v.read() { + Ok(v.clone()) + } else { + bail!("lock poisoned") + } + } + } + } + + pub fn get(&self) -> wasmtime::Result + '_> { + self.as_arc().read().map_err(|_| anyhow!("lock poisoned")) + } + + pub fn get_mut(&mut self) -> wasmtime::Result + '_>> { + match self { + Self::Parent(v) => { + if let Ok(v) = v.write() { + Ok(Some(v)) + } else { + bail!("lock poisoned") + } + } + Self::Child(..) => Ok(None), + } + } +} diff --git a/src/commands/serve.rs b/src/commands/serve.rs index c6d89a21bd..c39b99f219 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -1,25 +1,23 @@ use crate::common::{Profile, RunCommon, RunTarget}; -use anyhow::{anyhow, bail, Result}; +use anyhow::{anyhow, bail, Context as _, Result}; use clap::Parser; +use http_body_util::BodyExt as _; use std::net::SocketAddr; -use std::time::Instant; -use std::{ - path::PathBuf, - sync::{ - atomic::{AtomicBool, AtomicU64, Ordering}, - Arc, Mutex, - }, - time::Duration, -}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; +use tokio::io::{stderr, stdin, stdout}; use tokio::sync::Notify; use wasmtime::component::{Component, Linker}; -use wasmtime::{Engine, Store, StoreLimits, UpdateDeadline}; +use wasmtime::{AsContextMut as _, Engine, Store, StoreLimits, UpdateDeadline}; use wasmtime_wasi::{IoView, StreamError, StreamResult, WasiCtx, WasiCtxBuilder, WasiView}; use wasmtime_wasi_http::bindings::http::types::Scheme; use wasmtime_wasi_http::bindings::ProxyPre; +use wasmtime_wasi_http::body::HyperOutgoingBody; use wasmtime_wasi_http::io::TokioIo; use wasmtime_wasi_http::{ - body::HyperOutgoingBody, WasiHttpCtx, WasiHttpView, DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, + WasiHttpCtx, WasiHttpView, DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, DEFAULT_OUTGOING_BODY_CHUNK_SIZE, }; @@ -37,6 +35,13 @@ struct Host { http_outgoing_body_buffer_chunks: Option, http_outgoing_body_chunk_size: Option, + p3_cli: Option>>, + p3_clocks: Option>>, + p3_filesystem: Option, + p3_random: Option>>, + p3_sockets: Option, + p3_http: wasmtime_wasi_http::p3::WasiHttpCtx, + limits: StoreLimits, #[cfg(feature = "wasi-nn")] @@ -79,6 +84,68 @@ impl WasiHttpView for Host { } } +impl wasmtime_wasi::p3::ResourceView for Host { + fn table(&mut self) -> &mut wasmtime::component::ResourceTable { + &mut self.table + } +} +impl wasmtime_wasi::p3::cli::WasiCliView for Host { + fn cli(&mut self) -> &wasmtime_wasi::p3::cli::WasiCliCtx { + let cli = self + .p3_cli + .as_mut() + .and_then(Arc::get_mut) + .expect("`wasi:cli@0.3` not configured"); + cli.get_mut().unwrap() + } +} + +impl wasmtime_wasi::p3::clocks::WasiClocksView for Host { + fn clocks(&mut self) -> &wasmtime_wasi::p3::clocks::WasiClocksCtx { + let clocks = self + .p3_clocks + .as_mut() + .and_then(Arc::get_mut) + .expect("`wasi:clocks@0.3` not configured"); + clocks.get_mut().unwrap() + } +} + +impl wasmtime_wasi::p3::filesystem::WasiFilesystemView for Host { + fn filesystem(&self) -> &wasmtime_wasi::p3::filesystem::WasiFilesystemCtx { + self.p3_filesystem + .as_ref() + .expect("`wasi:filesystem@0.3` not configured") + } +} + +impl wasmtime_wasi::p3::random::WasiRandomView for Host { + fn random(&mut self) -> &mut wasmtime_wasi::p3::random::WasiRandomCtx { + let random = self + .p3_random + .as_mut() + .and_then(Arc::get_mut) + .expect("`wasi:random@0.3` not configured"); + random.get_mut().unwrap() + } +} + +impl wasmtime_wasi::p3::sockets::WasiSocketsView for Host { + fn sockets(&self) -> &wasmtime_wasi::p3::sockets::WasiSocketsCtx { + self.p3_sockets + .as_ref() + .expect("`wasi:sockets@0.3` not configured") + } +} + +impl wasmtime_wasi_http::p3::WasiHttpView for Host { + type Client = wasmtime_wasi_http::p3::DefaultClient; + + fn http(&self) -> &wasmtime_wasi_http::p3::WasiHttpCtx { + &self.p3_http + } +} + const DEFAULT_ADDR: std::net::SocketAddr = std::net::SocketAddr::new( std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), 8080, @@ -149,6 +216,78 @@ impl ServeCommand { Ok(()) } + fn set_p3_ctx(&self, store: &mut Store) -> Result<()> { + store.data_mut().p3_clocks = Some(Arc::default()); + store.data_mut().p3_random = Some(Arc::default()); + + let mut environment = Vec::default(); + if self.run.common.wasi.inherit_env == Some(true) { + for (k, v) in std::env::vars() { + environment.push((k, v)); + } + } + for (key, value) in self.run.vars.iter() { + let value = match value { + Some(value) => value.clone(), + None => match std::env::var_os(key) { + Some(val) => val + .into_string() + .map_err(|_| anyhow!("environment variable `{key}` not valid utf-8"))?, + None => { + // leave the env var un-set in the guest + continue; + } + }, + }; + environment.push((key.clone(), value)); + } + store.data_mut().p3_cli = Some(Arc::new(Mutex::new(wasmtime_wasi::p3::cli::WasiCliCtx { + environment, + arguments: vec![], + initial_cwd: None, + stdin: Box::new(stdin()), + stdout: Box::new(stdout()), + stderr: Box::new(stderr()), + }))); + + let mut p3_filesystem = wasmtime_wasi::p3::filesystem::WasiFilesystemCtx::default(); + p3_filesystem.allow_blocking_current_thread = self.run.common.wasm.timeout.is_none(); + for (host, guest) in self.run.dirs.iter() { + p3_filesystem.preopened_dir( + host, + guest, + wasmtime_wasi::p3::filesystem::DirPerms::all(), + wasmtime_wasi::p3::filesystem::FilePerms::all(), + )?; + } + store.data_mut().p3_filesystem = Some(p3_filesystem); + + if self.run.common.wasi.listenfd == Some(true) { + bail!("components do not support --listenfd"); + } + for _ in self.run.compute_preopen_sockets()? { + bail!("components do not support --tcplisten"); + } + + let mut p3_sockets = wasmtime_wasi::p3::sockets::WasiSocketsCtx::default(); + if self.run.common.wasi.inherit_network == Some(true) { + p3_sockets.socket_addr_check = + wasmtime_wasi::p3::sockets::SocketAddrCheck::new(|_, _| Box::pin(async { true })) + } + if let Some(enable) = self.run.common.wasi.allow_ip_name_lookup { + p3_sockets.allowed_network_uses.ip_name_lookup = enable; + } + if let Some(enable) = self.run.common.wasi.tcp { + p3_sockets.allowed_network_uses.tcp = enable; + } + if let Some(enable) = self.run.common.wasi.udp { + p3_sockets.allowed_network_uses.udp = enable; + } + store.data_mut().p3_sockets = Some(p3_sockets); + + Ok(()) + } + fn new_store(&self, engine: &Engine, req_id: u64) -> Result> { let mut builder = WasiCtxBuilder::new(); self.run.configure_wasip2(&mut builder)?; @@ -184,6 +323,13 @@ impl ServeCommand { wasi_keyvalue: None, #[cfg(feature = "profiling")] guest_profiler: None, + + p3_cli: None, + p3_clocks: None, + p3_filesystem: None, + p3_random: None, + p3_sockets: None, + p3_http: wasmtime_wasi_http::p3::WasiHttpCtx::default(), }; if self.run.common.wasi.nn == Some(true) { @@ -235,6 +381,7 @@ impl ServeCommand { } let mut store = Store::new(engine, host); + self.set_p3_ctx(&mut store)?; store.data_mut().limits = self.run.store_limits(); store.limiter(|t| &mut t.limits); @@ -276,8 +423,10 @@ impl ServeCommand { let link_options = self.run.compute_wasi_features(); wasmtime_wasi::add_to_linker_with_options_async(linker, &link_options)?; wasmtime_wasi_http::add_only_http_to_linker_async(linker)?; + wasmtime_wasi_http::p3::add_only_http_to_linker(linker)?; } else { wasmtime_wasi_http::add_to_linker_async(linker)?; + wasmtime_wasi_http::p3::add_to_linker(linker)?; } if self.run.common.wasi.nn == Some(true) { @@ -340,6 +489,7 @@ impl ServeCommand { .config(use_pooling_allocator_by_default().unwrap_or(None))?; config.wasm_component_model(true); config.async_support(true); + config.wasm_component_model_async(true); if self.run.common.wasm.timeout.is_some() { config.epoch_interruption(true); @@ -364,9 +514,7 @@ impl ServeCommand { RunTarget::Core(_) => bail!("The serve command currently requires a component"), RunTarget::Component(c) => c, }; - let instance = linker.instantiate_pre(&component)?; - let instance = ProxyPre::new(instance)?; // Spawn background task(s) waiting for graceful shutdown signals. This // always listens for ctrl-c but additionally can listen for a TCP @@ -412,35 +560,97 @@ impl ServeCommand { log::info!("Listening on {}", self.addr); - let handler = ProxyHandler::new(self, engine, instance); - - loop { - // Wait for a socket, but also "race" against shutdown to break out - // of this loop. Once the graceful shutdown signal is received then - // this loop exits immediately. - let (stream, _) = tokio::select! { - _ = shutdown.requested.notified() => break, - v = listener.accept() => v?, - }; - let comp = component.clone(); - let stream = TokioIo::new(stream); - let h = handler.clone(); - let shutdown_guard = shutdown.clone().increment(); - tokio::task::spawn(async move { - if let Err(e) = http1::Builder::new() - .keep_alive(true) - .serve_connection( - stream, - hyper::service::service_fn(move |req| { - handle_request(h.clone(), req, comp.clone()) - }), - ) - .await - { - eprintln!("error: {e:?}"); - } - drop(shutdown_guard); - }); + if let Ok(..) = wasmtime_wasi_http::p3::bindings::ProxyPre::new(instance.clone()) { + let next_id = Arc::new(AtomicU64::default()); + let cmd = Arc::new(self); + loop { + let (stream, _) = listener.accept().await?; + let engine = engine.clone(); + let cmd = Arc::clone(&cmd); + let next_id = Arc::clone(&next_id); + let instance = instance.clone(); + tokio::task::spawn(async { + let service = hyper::service::service_fn( + move |req: hyper::Request| { + let req_id = next_id.fetch_add(1, Ordering::Relaxed); + let engine = engine.clone(); + let cmd = Arc::clone(&cmd); + let instance = instance.clone(); + async move { + let mut store = cmd + .new_store(&engine, req_id) + .context("failed to create new store")?; + let instance = instance.instantiate_async(&mut store).await?; + let proxy = wasmtime_wasi_http::p3::bindings::Proxy::new( + &mut store, &instance, + )?; + let (req, body) = req.into_parts(); + let body = body.map_err(wasmtime_wasi_http::p3::bindings::http::types::ErrorCode::from_hyper_request_error); + let handle = proxy + .handle(&mut store, http::Request::from_parts(req, body)) + .await + .context("failed to call `handle`")?; + let res = handle.get(&mut store).await??; + let (res, tx, io) = + wasmtime_wasi_http::p3::Response::resource_into_http( + &mut store, &instance, res, + )?; + tokio::task::spawn(async move { + if let Some(io) = io { + let closure = io.get(&mut store).await?; + closure(store.as_context_mut())?; + } + // TODO: Report transmit errors + if let Some(tx) = tx { + tx.write(Ok(())).get(&mut store).await?; + } + anyhow::Ok(()) + }); + anyhow::Ok(res.map(|body| body.map_err(|err| err.unwrap_or(wasmtime_wasi_http::p3::bindings::http::types::ErrorCode::InternalError(None))))) + } + }, + ); + if let Err(e) = http1::Builder::new() + .keep_alive(true) + .serve_connection(TokioIo::new(stream), service) + .await + { + eprintln!("error: {e:?}"); + } + }); + } + } else { + let instance = wasmtime_wasi_http::bindings::ProxyPre::new(instance)?; + + let handler = ProxyHandler::new(self, engine, instance); + loop { + // Wait for a socket, but also "race" against shutdown to break out + // of this loop. Once the graceful shutdown signal is received then + // this loop exits immediately. + let (stream, _) = tokio::select! { + _ = shutdown.requested.notified() => break, + v = listener.accept() => v?, + }; + let comp = component.clone(); + let stream = TokioIo::new(stream); + let h = handler.clone(); + let shutdown_guard = shutdown.clone().increment(); + tokio::task::spawn(async move { + if let Err(e) = http1::Builder::new() + .keep_alive(true) + .serve_connection( + stream, + hyper::service::service_fn(move |req| { + handle_request(h.clone(), req, comp.clone()) + }), + ) + .await + { + eprintln!("error: {e:?}"); + } + drop(shutdown_guard); + }); + } } // Upon exiting the loop we'll no longer process any more incoming From e35bb4bbe8d3a4668bb70a1610589992b66522c3 Mon Sep 17 00:00:00 2001 From: Joel Dice Date: Wed, 26 Mar 2025 16:44:37 -0600 Subject: [PATCH 02/19] fix rebase breakage Signed-off-by: Joel Dice --- crates/wasi-http/src/p3/body.rs | 17 +++--- crates/wasi-http/src/p3/client.rs | 15 +++--- crates/wasi-http/src/p3/host/handle.rs | 63 ++++++++++++---------- crates/wasi-http/src/p3/host/types.rs | 74 ++++++++++++++++---------- crates/wasi-http/src/p3/proxy.rs | 4 +- crates/wasi-http/src/p3/response.rs | 24 ++++----- 6 files changed, 112 insertions(+), 85 deletions(-) diff --git a/crates/wasi-http/src/p3/body.rs b/crates/wasi-http/src/p3/body.rs index bb23abcb3a..ac18e897c1 100644 --- a/crates/wasi-http/src/p3/body.rs +++ b/crates/wasi-http/src/p3/body.rs @@ -6,7 +6,7 @@ use core::task::{ready, Context, Poll}; use anyhow::Context as _; use bytes::Bytes; use http::HeaderMap; -use http_body_util::{combinators::BoxBody, BodyExt as _}; +use http_body_util::{combinators::UnsyncBoxBody, BodyExt as _}; use tokio::sync::oneshot; use wasmtime::{ component::{AbortOnDropHandle, ErrorContext, FutureWriter, Resource, StreamReader}, @@ -20,7 +20,6 @@ pub(crate) type OutgoingContentsStreamFuture = Pin< Box< dyn Future, Bytes), Option>> + Send - + Sync + 'static, >, >; @@ -33,7 +32,6 @@ pub(crate) type OutgoingTrailerFuture = Pin< Option, >, > + Send - + Sync + 'static, >, >; @@ -45,7 +43,6 @@ pub(crate) type OutgoingTrailerFutureMut<'a> = Pin< Option, >, > + Send - + Sync + 'static), >; @@ -77,7 +74,7 @@ pub enum Body { /// Body constructed by the host Host { /// Underlying body stream - stream: Option>, + stream: Option>, /// Buffered frame, if any buffer: Option, }, @@ -89,11 +86,11 @@ impl Body { /// Construct a new [Body] pub fn new(body: T) -> Self where - T: http_body::Body + Send + Sync + 'static, + T: http_body::Body + Send + 'static, T::Error: Into, { Self::Host { - stream: Some(body.map_err(Into::into).boxed()), + stream: Some(body.map_err(Into::into).boxed_unsync()), buffer: None, } } @@ -101,7 +98,11 @@ impl Body { /// Construct a new empty [Body] pub fn empty() -> Self { Self::Host { - stream: Some(http_body_util::Empty::new().map_err(Into::into).boxed()), + stream: Some( + http_body_util::Empty::new() + .map_err(Into::into) + .boxed_unsync(), + ), buffer: None, } } diff --git a/crates/wasi-http/src/p3/client.rs b/crates/wasi-http/src/p3/client.rs index 38b1d329f2..b7fdc1b1cf 100644 --- a/crates/wasi-http/src/p3/client.rs +++ b/crates/wasi-http/src/p3/client.rs @@ -82,7 +82,7 @@ pub trait Client: Clone + Send + Sync { fn send_request( &mut self, request: http::Request< - impl http_body::Body> + Send + Sync + 'static, + impl http_body::Body> + Send + 'static, >, options: Option, ) -> impl Future< @@ -94,20 +94,17 @@ pub trait Client: Clone + Send + Sync { http::Response< impl http_body::Body + Send - + Sync + 'static, >, ErrorCode, >, - > + Send - + Sync, - impl Future> + Send + Sync + 'static, + > + Send, + impl Future> + Send + 'static, ), ErrorCode, >, >, - > + Send - + Sync; + > + Send; } /// Default HTTP client @@ -120,7 +117,7 @@ impl Client for DefaultClient { async fn send_request( &mut self, request: http::Request< - impl http_body::Body> + Send + Sync + 'static, + impl http_body::Body> + Send + 'static, >, options: Option, ) -> wasmtime::Result< @@ -177,7 +174,7 @@ impl http_body::Body for IncomingBody { /// default implementation. pub async fn default_send_request( mut request: http::Request< - impl http_body::Body> + Send + Sync + 'static, + impl http_body::Body> + Send + 'static, >, options: Option, ) -> Result< diff --git a/crates/wasi-http/src/p3/host/handle.rs b/crates/wasi-http/src/p3/host/handle.rs index 374c0d5923..2c3301f99e 100644 --- a/crates/wasi-http/src/p3/host/handle.rs +++ b/crates/wasi-http/src/p3/host/handle.rs @@ -135,9 +135,10 @@ where })); let task = task.abort_handle(); match response.await { - Ok(response) => { - (response.map(|body| body.map_err(Into::into).boxed()), task) - } + Ok(response) => ( + response.map(|body| body.map_err(Into::into).boxed_unsync()), + task, + ), Err(err) => return Ok(Err(err)), } } @@ -165,9 +166,10 @@ where })); let task = task.abort_handle(); match response.await { - Ok(response) => { - (response.map(|body| body.map_err(Into::into).boxed()), task) - } + Ok(response) => ( + response.map(|body| body.map_err(Into::into).boxed_unsync()), + task, + ), Err(err) => return Ok(Err(err)), } } @@ -211,9 +213,10 @@ where eprintln!("[host] return Ok response"); let task = task.abort_handle(); match response.await { - Ok(response) => { - (response.map(|body| body.map_err(Into::into).boxed()), task) - } + Ok(response) => ( + response.map(|body| body.map_err(Into::into).boxed_unsync()), + task, + ), Err(err) => return Ok(Err(err)), } } @@ -256,9 +259,10 @@ where eprintln!("[host] return Ok response"); let task = task.abort_handle(); match response.await { - Ok(response) => { - (response.map(|body| body.map_err(Into::into).boxed()), task) - } + Ok(response) => ( + response.map(|body| body.map_err(Into::into).boxed_unsync()), + task, + ), Err(err) => return Ok(Err(err)), } } @@ -280,9 +284,10 @@ where })); let task = task.abort_handle(); match response.await { - Ok(response) => { - (response.map(|body| body.map_err(Into::into).boxed()), task) - } + Ok(response) => ( + response.map(|body| body.map_err(Into::into).boxed_unsync()), + task, + ), Err(err) => return Ok(Err(err)), } } @@ -304,9 +309,10 @@ where })); let task = task.abort_handle(); match response.await { - Ok(response) => { - (response.map(|body| body.map_err(Into::into).boxed()), task) - } + Ok(response) => ( + response.map(|body| body.map_err(Into::into).boxed_unsync()), + task, + ), Err(err) => return Ok(Err(err)), } } @@ -331,9 +337,10 @@ where })); let task = task.abort_handle(); match response.await { - Ok(response) => { - (response.map(|body| body.map_err(Into::into).boxed()), task) - } + Ok(response) => ( + response.map(|body| body.map_err(Into::into).boxed_unsync()), + task, + ), Err(err) => return Ok(Err(err)), } } @@ -354,9 +361,10 @@ where })); let task = task.abort_handle(); match response.await { - Ok(response) => { - (response.map(|body| body.map_err(Into::into).boxed()), task) - } + Ok(response) => ( + response.map(|body| body.map_err(Into::into).boxed_unsync()), + task, + ), Err(err) => return Ok(Err(err)), } } @@ -379,9 +387,10 @@ where })); let task = task.abort_handle(); match response.await { - Ok(response) => { - (response.map(|body| body.map_err(Into::into).boxed()), task) - } + Ok(response) => ( + response.map(|body| body.map_err(Into::into).boxed_unsync()), + task, + ), Err(err) => return Ok(Err(err)), } } diff --git a/crates/wasi-http/src/p3/host/types.rs b/crates/wasi-http/src/p3/host/types.rs index a089573622..019de037e4 100644 --- a/crates/wasi-http/src/p3/host/types.rs +++ b/crates/wasi-http/src/p3/host/types.rs @@ -1,4 +1,4 @@ -use core::future::{poll_fn, Future as _}; +use core::future::poll_fn; use core::mem; use core::ops::{Deref, DerefMut}; use core::pin::Pin; @@ -10,8 +10,7 @@ use anyhow::{bail, Context as _}; use bytes::Bytes; use http_body::Body as _; use wasmtime::component::{ - future, stream, Accessor, AccessorTask, FutureWriter, HostFuture, HostStream, Resource, - StreamWriter, + Accessor, AccessorTask, FutureWriter, HostFuture, HostStream, Resource, StreamWriter, }; use wasmtime_wasi::p3::bindings::clocks::monotonic_clock::Duration; use wasmtime_wasi::p3::{ResourceView as _, WithChildren}; @@ -135,8 +134,9 @@ where tx, } => { drop(self.contents_tx); - let mut trailers_tx = self.trailers_tx.watch_reader(); - let Some(Ok(res)) = poll_fn(|cx| match Pin::new(&mut trailers_tx).poll(cx) { + let (watch_reader, trailers_tx) = self.trailers_tx.watch_reader(); + let mut watch_reader = watch_reader.into_future(); + let Some(Ok(res)) = poll_fn(|cx| match watch_reader.as_mut().poll(cx) { Poll::Ready(()) => return Poll::Ready(None), Poll::Pending => trailers_rx.as_mut().poll(cx).map(Some), }) @@ -153,7 +153,7 @@ where }; return Ok(()); }; - let trailers_tx = trailers_tx.into_inner().await; + let trailers_tx = trailers_tx.into_inner(); if !trailers_tx .write(clone_trailer_result(&res)) .into_future() @@ -227,9 +227,10 @@ where Some(BodyFrame::Trailers(..)) => bail!("corrupted guest body state"), None => {} } - let mut contents_tx = contents_tx.watch_reader(); + let (watch_reader, mut contents_tx) = contents_tx.watch_reader(); + let mut watch_reader = watch_reader.into_future(); loop { - let Some(rx) = poll_fn(|cx| match Pin::new(&mut contents_tx).poll(cx) { + let Some(rx) = poll_fn(|cx| match watch_reader.as_mut().poll(cx) { Poll::Ready(()) => return Poll::Ready(None), Poll::Pending => contents_rx.as_mut().poll(cx).map(Some), }) @@ -251,7 +252,7 @@ where break; }; contents_rx = rx_tail.read().into_future(); - let tx_tail = contents_tx.into_inner().await; + let tx_tail = contents_tx.into_inner(); let Some(tx_tail) = tx_tail.write(buf.clone()).into_future().await else { let Ok(mut body) = self.body.lock() else { bail!("lock poisoned"); @@ -264,12 +265,15 @@ where }; return Ok(()); }; - contents_tx = tx_tail.watch_reader(); + let (new_watch_reader, new_contents_tx) = tx_tail.watch_reader(); + contents_tx = new_contents_tx; + watch_reader = new_watch_reader.into_future(); } drop(contents_tx); - let mut trailers_tx = self.trailers_tx.watch_reader(); - let Some(Ok(res)) = poll_fn(|cx| match Pin::new(&mut trailers_tx).poll(cx) { + let (watch_reader, trailers_tx) = self.trailers_tx.watch_reader(); + let mut watch_reader = watch_reader.into_future(); + let Some(Ok(res)) = poll_fn(|cx| match watch_reader.as_mut().poll(cx) { Poll::Ready(()) => return Poll::Ready(None), Poll::Pending => trailers_rx.as_mut().poll(cx).map(Some), }) @@ -286,7 +290,7 @@ where }; return Ok(()); }; - let trailers_tx = trailers_tx.into_inner().await; + let trailers_tx = trailers_tx.into_inner(); if !trailers_tx .write(clone_trailer_result(&res)) .into_future() @@ -330,9 +334,10 @@ where Some(BodyFrame::Trailers(..)) => bail!("corrupted guest body state"), None => {} } - let mut contents_tx = contents_tx.watch_reader(); + let (watch_reader, mut contents_tx) = contents_tx.watch_reader(); + let mut watch_reader = watch_reader.into_future(); loop { - match poll_fn(|cx| match Pin::new(&mut contents_tx).poll(cx) { + match poll_fn(|cx| match watch_reader.as_mut().poll(cx) { Poll::Ready(()) => return Poll::Ready(None), Poll::Pending => Pin::new(&mut stream).poll_frame(cx).map(Some), }) @@ -365,7 +370,7 @@ where Some(Some(Ok(frame))) => { match frame.into_data().map_err(http_body::Frame::into_trailers) { Ok(buf) => { - let tx_tail = contents_tx.into_inner().await; + let tx_tail = contents_tx.into_inner(); let Some(tx_tail) = tx_tail.write(buf.clone()).into_future().await else { @@ -378,7 +383,10 @@ where }; return Ok(()); }; - contents_tx = tx_tail.watch_reader(); + let (new_watch_reader, new_contents_tx) = + tx_tail.watch_reader(); + contents_tx = new_contents_tx; + watch_reader = new_watch_reader.into_future(); } Err(Ok(trailers)) => { drop(contents_tx); @@ -654,7 +662,10 @@ where options: Option>>, ) -> wasmtime::Result<(Resource, HostFuture>)> { store.with(|mut view| { - let (res_tx, res_rx) = future(&mut view).context("failed to create future")?; + let instance = view.instance(); + let (res_tx, res_rx) = instance + .future(&mut view) + .context("failed to create future")?; let contents = contents.map(|contents| contents.into_reader(&mut view).read().into_future()); let trailers = trailers.into_reader(&mut view).read().into_future(); @@ -799,10 +810,13 @@ where req: Resource, ) -> wasmtime::Result, TrailerFuture), ()>> { store.with(|mut view| { - let (contents_tx, contents_rx) = - stream(&mut view).context("failed to create stream")?; - let (trailers_tx, trailers_rx) = - future(&mut view).context("failed to create future")?; + let instance = view.instance(); + let (contents_tx, contents_rx) = instance + .stream(&mut view) + .context("failed to create stream")?; + let (trailers_tx, trailers_rx) = instance + .future(&mut view) + .context("failed to create future")?; let Request { body, .. } = get_request_mut(view.table(), &req)?; { let Some(body) = Arc::get_mut(body) else { @@ -960,7 +974,10 @@ where trailers: TrailerFuture, ) -> wasmtime::Result<(Resource, HostFuture>)> { store.with(|mut view| { - let (res_tx, res_rx) = future(&mut view).context("failed to create future")?; + let instance = view.instance(); + let (res_tx, res_rx) = instance + .future(&mut view) + .context("failed to create future")?; let contents = contents.map(|contents| contents.into_reader(&mut view).read().into_future()); let trailers = trailers.into_reader(&mut view).read().into_future(); @@ -1010,10 +1027,13 @@ where res: Resource, ) -> wasmtime::Result, TrailerFuture), ()>> { store.with(|mut view| { - let (contents_tx, contents_rx) = - stream(&mut view).context("failed to create stream")?; - let (trailers_tx, trailers_rx) = - future(&mut view).context("failed to create future")?; + let instance = view.instance(); + let (contents_tx, contents_rx) = instance + .stream(&mut view) + .context("failed to create stream")?; + let (trailers_tx, trailers_rx) = instance + .future(&mut view) + .context("failed to create future")?; let Response { body, .. } = get_response_mut(view.table(), &res)?; { let Some(body) = Arc::get_mut(body) else { diff --git a/crates/wasi-http/src/p3/proxy.rs b/crates/wasi-http/src/p3/proxy.rs index 40992bde68..ed1646c1f0 100644 --- a/crates/wasi-http/src/p3/proxy.rs +++ b/crates/wasi-http/src/p3/proxy.rs @@ -1,6 +1,6 @@ use anyhow::Context as _; use bytes::Bytes; -use http_body_util::combinators::BoxBody; +use http_body_util::combinators::UnsyncBoxBody; use wasmtime::component::{FutureWriter, Promise, Resource}; use wasmtime::AsContextMut; use wasmtime_wasi::p3::ResourceView; @@ -35,7 +35,7 @@ impl Proxy { ) -> wasmtime::Result< Result< ( - http::Response>>, + http::Response>>, Option>>, ), ErrorCode, diff --git a/crates/wasi-http/src/p3/response.rs b/crates/wasi-http/src/p3/response.rs index 10b76e90a6..6fe70c2c95 100644 --- a/crates/wasi-http/src/p3/response.rs +++ b/crates/wasi-http/src/p3/response.rs @@ -6,7 +6,7 @@ use anyhow::{bail, Context as _}; use bytes::Bytes; use futures::StreamExt as _; use http::{HeaderMap, StatusCode}; -use http_body_util::combinators::BoxBody; +use http_body_util::combinators::UnsyncBoxBody; use http_body_util::{BodyExt, BodyStream, StreamBody}; use wasmtime::component::{AbortOnDropHandle, FutureWriter}; use wasmtime::AsContextMut; @@ -63,7 +63,7 @@ impl Response { self, mut store: impl AsContextMut + Send + 'static, ) -> anyhow::Result<( - http::Response>>, + http::Response>>, Option>>, )> { let headers = self.headers.unwrap_or_clone()?; @@ -83,7 +83,7 @@ impl Response { trailers: None, buffer: Some(BodyFrame::Trailers(Ok(None))), tx, - } => (empty_body().boxed(), Some(tx)), + } => (empty_body().boxed_unsync(), Some(tx)), Body::Guest { contents: None, trailers: None, @@ -99,7 +99,7 @@ impl Response { ( empty_body() .with_trailers(async move { Some(Ok(trailers)) }) - .boxed(), + .boxed_unsync(), Some(tx), ) } @@ -111,7 +111,7 @@ impl Response { } => ( empty_body() .with_trailers(async move { Some(Err(Some(err))) }) - .boxed(), + .boxed_unsync(), Some(tx), ), Body::Guest { @@ -122,7 +122,7 @@ impl Response { } => { let body = empty_body() .with_trailers(guest_response_trailers(store, trailers)) - .boxed(); + .boxed_unsync(); (body, Some(tx)) } Body::Guest { @@ -138,7 +138,7 @@ impl Response { }; let body = GuestBody::new(contents, buffer) .with_trailers(guest_response_trailers(store, trailers)) - .boxed(); + .boxed_unsync(); (body, Some(tx)) } Body::Guest { .. } => bail!("guest body is corrupted"), @@ -146,7 +146,7 @@ impl Response { | Body::Host { stream: None, buffer: Some(BodyFrame::Trailers(Ok(None))), - } => (empty_body().boxed(), None), + } => (empty_body().boxed_unsync(), None), Body::Host { stream: None, buffer: Some(BodyFrame::Trailers(Ok(Some(trailers)))), @@ -160,7 +160,7 @@ impl Response { ( empty_body() .with_trailers(async move { Some(Ok(trailers)) }) - .boxed(), + .boxed_unsync(), None, ) } @@ -170,20 +170,20 @@ impl Response { } => ( empty_body() .with_trailers(async move { Some(Err(Some(err))) }) - .boxed(), + .boxed_unsync(), None, ), Body::Host { stream: Some(stream), buffer: None, - } => (stream.map_err(Some).boxed(), None), + } => (stream.map_err(Some).boxed_unsync(), None), Body::Host { stream: Some(stream), buffer: Some(BodyFrame::Data(buffer)), } => { let buffer = futures::stream::iter(iter::once(Ok(http_body::Frame::data(buffer)))); ( - BodyExt::boxed(StreamBody::new( + BodyExt::boxed_unsync(StreamBody::new( buffer.chain(BodyStream::new(stream.map_err(Some))), )), None, From 76bf1627791e5d635c852bc1d61bb8b717a24f3b Mon Sep 17 00:00:00 2001 From: Joel Dice Date: Fri, 28 Mar 2025 10:09:37 -0600 Subject: [PATCH 03/19] drop `Watch>::into_inner` in wasi-http Dropping the `Watch` itself just decrements the reference count, which won't actually drop the `StreamWriter` until the promise completes. The result was that some of the HTTP tests were hanging. This was my mistake, sorry! Signed-off-by: Joel Dice --- crates/wasi-http/src/p3/host/types.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/wasi-http/src/p3/host/types.rs b/crates/wasi-http/src/p3/host/types.rs index 019de037e4..25661250b6 100644 --- a/crates/wasi-http/src/p3/host/types.rs +++ b/crates/wasi-http/src/p3/host/types.rs @@ -269,7 +269,7 @@ where contents_tx = new_contents_tx; watch_reader = new_watch_reader.into_future(); } - drop(contents_tx); + drop(contents_tx.into_inner()); let (watch_reader, trailers_tx) = self.trailers_tx.watch_reader(); let mut watch_reader = watch_reader.into_future(); @@ -355,7 +355,7 @@ where return Ok(()); } Some(None) => { - drop(contents_tx); + drop(contents_tx.into_inner()); if !self.trailers_tx.write(Ok(None)).into_future().await { let Ok(mut body) = self.body.lock() else { bail!("lock poisoned"); @@ -389,7 +389,7 @@ where watch_reader = new_watch_reader.into_future(); } Err(Ok(trailers)) => { - drop(contents_tx); + drop(contents_tx.into_inner()); let trailers = store.with(|mut view| { push_fields(view.table(), WithChildren::new(trailers)) })?; @@ -410,7 +410,7 @@ where return Ok(()); } Err(Err(..)) => { - drop(contents_tx); + drop(contents_tx.into_inner()); if !self .trailers_tx .write(Err(ErrorCode::HttpProtocolError)) @@ -432,7 +432,7 @@ where } } Some(Some(Err(err))) => { - drop(contents_tx); + drop(contents_tx.into_inner()); if !self.trailers_tx.write(Err(err.clone())).into_future().await { let Ok(mut body) = self.body.lock() else { bail!("lock poisoned"); From 2c4b9cb0a110601c1b7a63abff9e3ba4412c9dd3 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Tue, 1 Apr 2025 20:20:02 +0200 Subject: [PATCH 04/19] http: do not strip incoming request headers As discussed with @pchickey See also https://github.com/bytecodealliance/wasmtime/pull/7538#issuecomment-1867011562 and following discussion Signed-off-by: Roman Volosatovs --- crates/test-programs/src/bin/api_0_3_proxy.rs | 5 ---- crates/wasi-http/src/p3/host/types.rs | 28 ++++--------------- crates/wasi-http/src/p3/mod.rs | 22 +++++++++++++-- 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/crates/test-programs/src/bin/api_0_3_proxy.rs b/crates/test-programs/src/bin/api_0_3_proxy.rs index 86fb80e2b4..e676e69f9f 100644 --- a/crates/test-programs/src/bin/api_0_3_proxy.rs +++ b/crates/test-programs/src/bin/api_0_3_proxy.rs @@ -32,11 +32,6 @@ impl test_programs::p3::proxy::exports::wasi::http::handler::Guest for T { "append of forbidden header succeeded" ); - assert!( - !req_hdrs.has("host"), - "forbidden host header present in incoming request" - ); - let hdrs = Headers::new(); let (mut contents_tx, contents_rx) = wit_stream::new(); let (trailers_tx, trailers_rx) = wit_future::new(); diff --git a/crates/wasi-http/src/p3/host/types.rs b/crates/wasi-http/src/p3/host/types.rs index 25661250b6..4f20d45698 100644 --- a/crates/wasi-http/src/p3/host/types.rs +++ b/crates/wasi-http/src/p3/host/types.rs @@ -79,24 +79,6 @@ fn delete_request_options( .context("failed to delete request options from table") } -/// Returns `true` when the header is forbidden according to this [`WasiHttpView`] implementation. -fn is_forbidden_header(view: &mut impl WasiHttpView, name: &http::header::HeaderName) -> bool { - static FORBIDDEN_HEADERS: [http::header::HeaderName; 10] = [ - http::header::CONNECTION, - http::header::HeaderName::from_static("keep-alive"), - http::header::PROXY_AUTHENTICATE, - http::header::PROXY_AUTHORIZATION, - http::header::HeaderName::from_static("proxy-connection"), - http::header::TE, - http::header::TRANSFER_ENCODING, - http::header::UPGRADE, - http::header::HOST, - http::header::HeaderName::from_static("http2-settings"), - ]; - - FORBIDDEN_HEADERS.contains(name) || view.is_forbidden_header(name) -} - fn clone_trailer_result( res: &Result>, ErrorCode>, ) -> Result>, ErrorCode> { @@ -494,7 +476,7 @@ where let Ok(header) = header.parse() else { return Ok(Err(HeaderError::InvalidSyntax)); }; - if is_forbidden_header(self, &header) { + if self.is_forbidden_header(&header) { return Ok(Err(HeaderError::Forbidden)); } let value = match http::header::HeaderValue::from_bytes(&value) { @@ -538,7 +520,7 @@ where let Ok(name) = name.parse() else { return Ok(Err(HeaderError::InvalidSyntax)); }; - if is_forbidden_header(self, &name) { + if self.is_forbidden_header(&name) { return Ok(Err(HeaderError::Forbidden)); } let mut values = Vec::with_capacity(value.len()); @@ -567,7 +549,7 @@ where Ok(header) => header, Err(_) => return Ok(Err(HeaderError::InvalidSyntax)), }; - if is_forbidden_header(self, &header) { + if self.is_forbidden_header(&header) { return Ok(Err(HeaderError::Forbidden)); } let Some(mut fields) = get_fields_inner_mut(self.table(), &fields)? else { @@ -585,7 +567,7 @@ where let Ok(header) = http::header::HeaderName::from_bytes(name.as_bytes()) else { return Ok(Err(HeaderError::InvalidSyntax)); }; - if is_forbidden_header(self, &header) { + if self.is_forbidden_header(&header) { return Ok(Err(HeaderError::Forbidden)); } let Some(mut fields) = get_fields_inner_mut(self.table(), &fields)? else { @@ -608,7 +590,7 @@ where Ok(header) => header, Err(_) => return Ok(Err(HeaderError::InvalidSyntax)), }; - if is_forbidden_header(self, &header) { + if self.is_forbidden_header(&header) { return Ok(Err(HeaderError::Forbidden)); } let value = match http::header::HeaderValue::from_bytes(&value) { diff --git a/crates/wasi-http/src/p3/mod.rs b/crates/wasi-http/src/p3/mod.rs index d96927ad31..393ae95bb6 100644 --- a/crates/wasi-http/src/p3/mod.rs +++ b/crates/wasi-http/src/p3/mod.rs @@ -373,6 +373,21 @@ impl ResourceView for WasiHttpImpl { } } +/// Set of [http::header::HeaderName], that are forbidden by default +/// for requests and responses originating in the guest. +pub const DEFAULT_FORBIDDEN_HEADERS: [http::header::HeaderName; 10] = [ + http::header::CONNECTION, + http::header::HeaderName::from_static("keep-alive"), + http::header::PROXY_AUTHENTICATE, + http::header::PROXY_AUTHORIZATION, + http::header::HeaderName::from_static("proxy-connection"), + http::header::TE, + http::header::TRANSFER_ENCODING, + http::header::UPGRADE, + http::header::HOST, + http::header::HeaderName::from_static("http2-settings"), +]; + /// A trait which provides internal WASI HTTP state. pub trait WasiHttpView: ResourceView + Send { /// HTTP client @@ -381,10 +396,11 @@ pub trait WasiHttpView: ResourceView + Send { /// Returns a reference to [WasiHttpCtx] fn http(&self) -> &WasiHttpCtx; - /// Whether a given header should be considered forbidden and not allowed. + /// Whether a given header should be considered forbidden and not allowed + /// for requests and responses originating in the guest. + /// Note: headers of incoming requests and responses are not validated. fn is_forbidden_header(&mut self, name: &http::header::HeaderName) -> bool { - _ = name; - false + DEFAULT_FORBIDDEN_HEADERS.contains(name) } } From 4090f01bdf0cc01056655f7529f148ab1815c71b Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Tue, 1 Apr 2025 11:47:20 -0700 Subject: [PATCH 05/19] Update tests with new wit-bindgen APIs --- crates/test-programs/src/bin/api_0_3_proxy.rs | 9 +++------ ...ttp_0_3_outbound_request_content_length.rs | 19 +++++++++---------- ...ttp_0_3_outbound_request_response_build.rs | 7 ++++--- crates/test-programs/src/p3/http.rs | 18 ++++++------------ 4 files changed, 22 insertions(+), 31 deletions(-) diff --git a/crates/test-programs/src/bin/api_0_3_proxy.rs b/crates/test-programs/src/bin/api_0_3_proxy.rs index e676e69f9f..932fac04a0 100644 --- a/crates/test-programs/src/bin/api_0_3_proxy.rs +++ b/crates/test-programs/src/bin/api_0_3_proxy.rs @@ -1,4 +1,4 @@ -use futures::{join, SinkExt as _}; +use futures::join; use test_programs::p3::wasi::http::types::{ErrorCode, Headers, Request, Response}; use test_programs::p3::{wit_future, wit_stream}; use wit_bindgen_rt::async_support::spawn; @@ -39,10 +39,8 @@ impl test_programs::p3::proxy::exports::wasi::http::handler::Guest for T { spawn(async { join!( async { - contents_tx - .send(b"hello, world!".to_vec()) - .await - .expect("writing response"); + let remaining = contents_tx.write_all(b"hello, world!".to_vec()).await; + assert!(remaining.is_empty()); drop(contents_tx); trailers_tx.write(Ok(None)); }, @@ -51,7 +49,6 @@ impl test_programs::p3::proxy::exports::wasi::http::handler::Guest for T { .await .expect("failed to transmit response") .unwrap() - .unwrap() } ); }); diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_content_length.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_content_length.rs index 12e44f1c01..0fd640b483 100644 --- a/crates/test-programs/src/bin/http_0_3_outbound_request_content_length.rs +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_content_length.rs @@ -1,4 +1,3 @@ -use futures::SinkExt as _; use test_programs::p3::wasi::http::types::{ErrorCode, Headers, Method, Request, Scheme, Trailers}; use test_programs::p3::{wit_future, wit_stream}; use wit_bindgen_rt::async_support::{FutureWriter, StreamWriter}; @@ -44,17 +43,19 @@ impl test_programs::p3::exports::wasi::cli::run::Guest for Component { { println!("writing enough"); let (_, mut contents_tx, trailers_tx) = make_request(); - contents_tx.send(b"long enough".to_vec()).await.unwrap(); + let remaining = contents_tx.write_all(b"long enough".to_vec()).await; + assert!(remaining.is_empty()); drop(contents_tx); - trailers_tx.write(Ok(None)).await; + trailers_tx.write(Ok(None)).await.unwrap(); } { println!("writing too little"); let (_, mut contents_tx, trailers_tx) = make_request(); - contents_tx.send(b"msg".to_vec()).await.unwrap(); + let remaining = contents_tx.write_all(b"msg".to_vec()).await; + assert!(remaining.is_empty()); drop(contents_tx); - trailers_tx.write(Ok(None)).await; + trailers_tx.write(Ok(None)).await.unwrap(); // handle() @@ -71,12 +72,10 @@ impl test_programs::p3::exports::wasi::cli::run::Guest for Component { { println!("writing too much"); let (_, mut contents_tx, trailers_tx) = make_request(); - contents_tx - .send(b"more than 11 bytes".to_vec()) - .await - .unwrap(); + let remaining = contents_tx.write_all(b"more than 11 bytes".to_vec()).await; + assert!(remaining.is_empty()); drop(contents_tx); - trailers_tx.write(Ok(None)).await; + trailers_tx.write(Ok(None)).await.unwrap(); // TODO: Figure out how/if to represent this in wasip3 //let e = request_body diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_response_build.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_response_build.rs index 0f72912891..aeb08ef404 100644 --- a/crates/test-programs/src/bin/http_0_3_outbound_request_response_build.rs +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_response_build.rs @@ -1,4 +1,3 @@ -use futures::SinkExt as _; use test_programs::p3::wasi::http::types::{Fields, Headers, Method, Request, Response, Scheme}; use test_programs::p3::{wit_future, wit_stream}; @@ -26,7 +25,8 @@ impl test_programs::p3::exports::wasi::cli::run::Guest for Component { request .set_authority(Some("www.example.com")) .expect("setting authority"); - contents_tx.send(b"request-body".to_vec()).await.unwrap(); + let remaining = contents_tx.write_all(b"request-body".to_vec()).await; + assert!(remaining.is_empty()); } { let headers = Headers::from_list(&[( @@ -37,7 +37,8 @@ impl test_programs::p3::exports::wasi::cli::run::Guest for Component { let (mut contents_tx, contents_rx) = wit_stream::new(); let (_, trailers_rx) = wit_future::new(); let _ = Response::new(headers, Some(contents_rx), trailers_rx); - contents_tx.send(b"response-body".to_vec()).await.unwrap(); + let remaining = contents_tx.write_all(b"response-body".to_vec()).await; + assert!(remaining.is_empty()); } { diff --git a/crates/test-programs/src/p3/http.rs b/crates/test-programs/src/p3/http.rs index c30e7b787b..13dc143d39 100644 --- a/crates/test-programs/src/p3/http.rs +++ b/crates/test-programs/src/p3/http.rs @@ -1,7 +1,6 @@ -use core::fmt; - use anyhow::{anyhow, Context as _, Result}; -use futures::{try_join, SinkExt as _, TryStreamExt as _}; +use core::fmt; +use futures::try_join; use crate::p3::wasi::http::{handler, types}; use crate::p3::{wit_future, wit_stream}; @@ -92,20 +91,17 @@ pub async fn request( let ((), (), response) = try_join!( async { if let Some(buf) = body { - contents_tx - .send(buf.into()) - .await - .expect("failed to send body content chunk"); + let remaining = contents_tx.write_all(buf.into()).await; + assert!(remaining.is_empty()); } drop(contents_tx); - trailers_tx.write(Ok(None)).await; + trailers_tx.write(Ok(None)).await.unwrap(); anyhow::Ok(()) }, async { transmit .await .expect("transmit sender dropped") - .expect("failed to receive request transmit result") .context("failed to transmit request")?; Ok(()) }, @@ -115,12 +111,10 @@ pub async fn request( let headers = response.headers().entries(); let (body, trailers) = response.body().expect("failed to get response body"); - let body = body.try_collect::>().await?; - let body = body.concat(); + let body = body.collect().await; let trailers = trailers .await .expect("trailers sender dropped") - .expect("failed to receive response trailers result") .context("failed to read body")?; let trailers = trailers.map(|trailers| trailers.entries()); Ok(Response { From e77002c3a2a3a345eed17a7370f4fc5b0d35b9f5 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Wed, 2 Apr 2025 19:40:25 +0200 Subject: [PATCH 06/19] feat(p3/http): handle `content-length` Signed-off-by: Roman Volosatovs --- ...ttp_0_3_outbound_request_content_length.rs | 140 +++++++++------ .../src/bin/http_0_3_outbound_request_post.rs | 5 +- crates/test-programs/src/p3/http.rs | 63 +++---- crates/wasi-http/src/p3/body.rs | 98 ++++++++++- crates/wasi-http/src/p3/client.rs | 57 ++---- crates/wasi-http/src/p3/conv.rs | 36 ++++ .../src/p3/host/{handle.rs => handler.rs} | 158 ++++++++--------- crates/wasi-http/src/p3/host/mod.rs | 2 +- crates/wasi-http/src/p3/host/types.rs | 164 +++++++++++++++--- crates/wasi-http/src/p3/response.rs | 39 ++--- crates/wasi-http/tests/all/p3/mod.rs | 1 + 11 files changed, 500 insertions(+), 263 deletions(-) rename crates/wasi-http/src/p3/host/{handle.rs => handler.rs} (76%) diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_content_length.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_content_length.rs index 0fd640b483..6c2234aa7f 100644 --- a/crates/test-programs/src/bin/http_0_3_outbound_request_content_length.rs +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_content_length.rs @@ -1,5 +1,9 @@ +use anyhow::Context as _; +use futures::join; +use test_programs::p3::wasi::http::handler; use test_programs::p3::wasi::http::types::{ErrorCode, Headers, Method, Request, Scheme, Trailers}; use test_programs::p3::{wit_future, wit_stream}; +use wit_bindgen::FutureReader; use wit_bindgen_rt::async_support::{FutureWriter, StreamWriter}; struct Component; @@ -10,10 +14,11 @@ fn make_request() -> ( Request, StreamWriter, FutureWriter, ErrorCode>>, + FutureReader>, ) { let (contents_tx, contents_rx) = wit_stream::new(); let (trailers_tx, trailers_rx) = wit_future::new(); - let (request, _) = Request::new( + let (request, transmit) = Request::new( Headers::from_list(&[("Content-Length".to_string(), b"11".to_vec())]).unwrap(), Some(contents_rx), trailers_rx, @@ -35,72 +40,95 @@ fn make_request() -> ( .set_path_with_query(Some("/")) .expect("setting path with query"); - (request, contents_tx, trailers_tx) + (request, contents_tx, trailers_tx, transmit) } impl test_programs::p3::exports::wasi::cli::run::Guest for Component { async fn run() -> Result<(), ()> { { - println!("writing enough"); - let (_, mut contents_tx, trailers_tx) = make_request(); - let remaining = contents_tx.write_all(b"long enough".to_vec()).await; - assert!(remaining.is_empty()); - drop(contents_tx); - trailers_tx.write(Ok(None)).await.unwrap(); + let (request, mut contents_tx, trailers_tx, transmit) = make_request(); + let (transmit, handle) = join!(async { transmit.await }, async { + let res = handler::handle(request) + .await + .context("failed to send request")?; + println!("writing enough"); + let remaining = contents_tx.write_all(b"long enough".to_vec()).await; + assert!( + remaining.is_empty(), + "{}", + String::from_utf8_lossy(&remaining) + ); + drop(contents_tx); + trailers_tx + .write(Ok(None)) + .await + .context("failed to finish body")?; + anyhow::Ok(res) + }); + let res = handle.unwrap(); + drop(res); + transmit + .expect("transmit sender dropped") + .expect("failed to transmit request"); } { - println!("writing too little"); - let (_, mut contents_tx, trailers_tx) = make_request(); - let remaining = contents_tx.write_all(b"msg".to_vec()).await; - assert!(remaining.is_empty()); - drop(contents_tx); - trailers_tx.write(Ok(None)).await.unwrap(); - - // handle() - - // TODO: Figure out how/if to represent this in wasip3 - //let e = OutgoingBody::finish(outgoing_body, None) - // .expect_err("finish should fail"); - - //assert!( - // matches!(&e, ErrorCode::HttpRequestBodySize(Some(3))), - // "unexpected error: {e:#?}" - //); + let (request, mut contents_tx, trailers_tx, transmit) = make_request(); + let (transmit, handle) = join!(async { transmit.await }, async { + let res = handler::handle(request) + .await + .context("failed to send request")?; + println!("writing too little"); + let remaining = contents_tx.write_all(b"msg".to_vec()).await; + assert!( + remaining.is_empty(), + "{}", + String::from_utf8_lossy(&remaining) + ); + drop(contents_tx); + trailers_tx + .write(Ok(None)) + .await + .context("failed to finish body")?; + anyhow::Ok(res) + }); + let res = handle.unwrap(); + drop(res); + let err = transmit + .expect("transmit sender dropped") + .expect_err("request transmission should have failed"); + assert!( + matches!(err, ErrorCode::HttpRequestBodySize(Some(3))), + "unexpected error: {err:#?}" + ); } { - println!("writing too much"); - let (_, mut contents_tx, trailers_tx) = make_request(); - let remaining = contents_tx.write_all(b"more than 11 bytes".to_vec()).await; - assert!(remaining.is_empty()); - drop(contents_tx); - trailers_tx.write(Ok(None)).await.unwrap(); - - // TODO: Figure out how/if to represent this in wasip3 - //let e = request_body - // .blocking_write_and_flush("more than 11 bytes".as_bytes()) - // .expect_err("write should fail"); - //let e = match e { - // test_programs::wasi::io::streams::StreamError::LastOperationFailed(e) => { - // http_error_code(&e) - // } - // test_programs::wasi::io::streams::StreamError::Closed => panic!("request closed"), - //}; - //assert!( - // matches!( - // e, - // Some(ErrorCode::HttpRequestBodySize(Some(18))) - // ), - // "unexpected error {e:?}" - //); - //let e = OutgoingBody::finish(outgoing_body, None) - // .expect_err("finish should fail"); - - //assert!( - // matches!(&e, ErrorCode::HttpRequestBodySize(Some(18))), - // "unexpected error: {e:#?}" - //); + let (request, mut contents_tx, trailers_tx, transmit) = make_request(); + let (transmit, handle) = join!(async { transmit.await }, async { + let res = handler::handle(request) + .await + .context("failed to send request")?; + println!("writing too much"); + let remaining = contents_tx.write_all(b"more than 11 bytes".to_vec()).await; + assert!( + remaining.is_empty(), + "{}", + String::from_utf8_lossy(&remaining) + ); + drop(contents_tx); + _ = trailers_tx.write(Ok(None)).await; + anyhow::Ok(res) + }); + let res = handle.unwrap(); + drop(res); + let err = transmit + .expect("transmit sender dropped") + .expect_err("request transmission should have failed"); + assert!( + matches!(err, ErrorCode::HttpRequestBodySize(Some(18))), + "unexpected error: {err:#?}" + ); } Ok(()) } diff --git a/crates/test-programs/src/bin/http_0_3_outbound_request_post.rs b/crates/test-programs/src/bin/http_0_3_outbound_request_post.rs index 67b437c5a2..282a6b2742 100644 --- a/crates/test-programs/src/bin/http_0_3_outbound_request_post.rs +++ b/crates/test-programs/src/bin/http_0_3_outbound_request_post.rs @@ -7,6 +7,7 @@ test_programs::p3::export!(Component); impl test_programs::p3::exports::wasi::cli::run::Guest for Component { async fn run() -> Result<(), ()> { + const BODY: &[u8] = b"{\"foo\": \"bar\"}"; let addr = test_programs::p3::wasi::cli::environment::get_environment() .into_iter() .find_map(|(k, v)| k.eq("HTTP_SERVER").then_some(v)) @@ -16,7 +17,7 @@ impl test_programs::p3::exports::wasi::cli::run::Guest for Component { Scheme::Http, &addr, "/post", - Some(b"{\"foo\": \"bar\"}"), + Some(BODY), None, None, None, @@ -32,7 +33,7 @@ impl test_programs::p3::exports::wasi::cli::run::Guest for Component { assert_eq!(std::str::from_utf8(method).unwrap(), "POST"); let uri = res.header("x-wasmtime-test-uri").unwrap(); assert_eq!(std::str::from_utf8(uri).unwrap(), format!("/post")); - assert_eq!(res.body, b"{\"foo\": \"bar\"}", "invalid body returned"); + assert_eq!(res.body, BODY, "invalid body returned"); Ok(()) } } diff --git a/crates/test-programs/src/p3/http.rs b/crates/test-programs/src/p3/http.rs index 13dc143d39..dfeb3c36fa 100644 --- a/crates/test-programs/src/p3/http.rs +++ b/crates/test-programs/src/p3/http.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Context as _, Result}; use core::fmt; -use futures::try_join; +use futures::join; use crate::p3::wasi::http::{handler, types}; use crate::p3::{wit_future, wit_stream}; @@ -88,42 +88,47 @@ pub async fn request( .set_path_with_query(Some(&path_with_query)) .map_err(|()| anyhow!("failed to set path_with_query"))?; - let ((), (), response) = try_join!( - async { - if let Some(buf) = body { - let remaining = contents_tx.write_all(buf.into()).await; - assert!(remaining.is_empty()); - } - drop(contents_tx); - trailers_tx.write(Ok(None)).await.unwrap(); - anyhow::Ok(()) - }, + let (transmit, handle) = join!( async { transmit .await - .expect("transmit sender dropped") - .context("failed to transmit request")?; - Ok(()) + .context("transmit sender dropped")? + .context("failed to transmit request") }, async { let response = handler::handle(request).await?; let status = response.status_code(); let headers = response.headers().entries(); - - let (body, trailers) = response.body().expect("failed to get response body"); - let body = body.collect().await; - let trailers = trailers - .await - .expect("trailers sender dropped") - .context("failed to read body")?; - let trailers = trailers.map(|trailers| trailers.entries()); - Ok(Response { - status, - headers, - body, - trailers, - }) + let (body_rx, trailers_rx) = response.body().expect("failed to get response body"); + let ((), rx) = join!( + async { + if let Some(buf) = body { + let remaining = contents_tx.write_all(buf.into()).await; + assert!(remaining.is_empty()); + } + drop(contents_tx); + // This can fail in HTTP/1.1, since the connection might already be closed + _ = trailers_tx.write(Ok(None)).await; + }, + async { + let body = body_rx.collect().await; + let trailers = trailers_rx + .await + .context("trailers sender dropped")? + .context("failed to read body")?; + let trailers = trailers.map(|trailers| trailers.entries()); + anyhow::Ok(Response { + status, + headers, + body, + trailers, + }) + } + ); + rx }, - )?; + ); + let response = handle?; + transmit?; Ok(response) } diff --git a/crates/wasi-http/src/p3/body.rs b/crates/wasi-http/src/p3/body.rs index ac18e897c1..4231db2f8f 100644 --- a/crates/wasi-http/src/p3/body.rs +++ b/crates/wasi-http/src/p3/body.rs @@ -6,12 +6,11 @@ use core::task::{ready, Context, Poll}; use anyhow::Context as _; use bytes::Bytes; use http::HeaderMap; -use http_body_util::{combinators::UnsyncBoxBody, BodyExt as _}; +use http_body_util::combinators::UnsyncBoxBody; +use http_body_util::BodyExt as _; use tokio::sync::oneshot; -use wasmtime::{ - component::{AbortOnDropHandle, ErrorContext, FutureWriter, Resource, StreamReader}, - AsContextMut, -}; +use wasmtime::component::{AbortOnDropHandle, ErrorContext, FutureWriter, Resource, StreamReader}; +use wasmtime::AsContextMut; use wasmtime_wasi::p3::{ResourceView, WithChildren}; use crate::p3::bindings::http::types::ErrorCode; @@ -58,6 +57,25 @@ pub enum BodyFrame { Trailers(Result>>, ErrorCode>), } +/// Whether the body is a request or response body. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum BodyContext { + /// The body is a request body. + Request, + /// The body is a response body. + Response, +} + +impl BodyContext { + /// Construct the correct [`ErrorCode`] body size error. + pub fn as_body_size_error(&self, size: u64) -> ErrorCode { + match self { + Self::Request => ErrorCode::HttpRequestBodySize(Some(size)), + Self::Response => ErrorCode::HttpResponseBodySize(Some(size)), + } + } +} + /// The concrete type behind a `wasi:http/types/body` resource. pub enum Body { /// Body constructed by the guest @@ -70,6 +88,8 @@ pub enum Body { buffer: Option, /// Future, on which transmission result will be written tx: FutureWriter>, + /// Optional `Content-Length` header limit and state + content_length: Option, }, /// Body constructed by the host Host { @@ -200,17 +220,42 @@ where } } +/// Represents `Content-Length` limit and state +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct ContentLength { + /// Limit of bytes to be sent + pub limit: u64, + /// Number of bytes sent + pub sent: u64, +} + +impl ContentLength { + /// Constructs new [ContentLength] + pub fn new(limit: u64) -> Self { + Self { limit, sent: 0 } + } +} + /// Body constructed by the guest pub(crate) struct GuestBody { + pub cx: BodyContext, pub contents: Option, pub buffer: Bytes, + pub content_length: Option, } impl GuestBody { - pub fn new(contents: OutgoingContentsStreamFuture, buffer: Bytes) -> Self { + pub fn new( + cx: BodyContext, + contents: OutgoingContentsStreamFuture, + buffer: Bytes, + content_length: Option, + ) -> Self { Self { + cx, contents: Some(contents), buffer, + content_length, } } } @@ -225,6 +270,18 @@ impl http_body::Body for GuestBody { ) -> Poll, Self::Error>>> { if !self.buffer.is_empty() { let buffer = mem::take(&mut self.buffer); + if let Some(ContentLength { limit, sent }) = &mut self.content_length { + let Ok(n) = buffer.len().try_into() else { + return Poll::Ready(Some(Err(Some(self.cx.as_body_size_error(u64::MAX))))); + }; + let Some(n) = sent.checked_add(n) else { + return Poll::Ready(Some(Err(Some(self.cx.as_body_size_error(u64::MAX))))); + }; + if n > *limit { + return Poll::Ready(Some(Err(Some(self.cx.as_body_size_error(n))))); + } + *sent = n; + } return Poll::Ready(Some(Ok(http_body::Frame::data(buffer)))); } let Some(stream) = &mut self.contents else { @@ -233,12 +290,41 @@ impl http_body::Body for GuestBody { match ready!(Pin::new(stream).poll(cx)) { Ok((tail, buf)) => { self.contents = Some(tail.read().into_future()); + if let Some(ContentLength { limit, sent }) = &mut self.content_length { + let Ok(n) = buf.len().try_into() else { + return Poll::Ready(Some(Err(Some(self.cx.as_body_size_error(u64::MAX))))); + }; + let Some(n) = sent.checked_add(n) else { + return Poll::Ready(Some(Err(Some(self.cx.as_body_size_error(u64::MAX))))); + }; + if n > *limit { + return Poll::Ready(Some(Err(Some(self.cx.as_body_size_error(n))))); + } + *sent = n; + } Poll::Ready(Some(Ok(http_body::Frame::data(buf)))) } Err(..) => { self.contents = None; + if let Some(ContentLength { limit, sent }) = self.content_length { + if limit != sent { + return Poll::Ready(Some(Err(Some(self.cx.as_body_size_error(sent))))); + } + } Poll::Ready(None) } } } + + fn is_end_stream(&self) -> bool { + self.contents.is_none() + } + + fn size_hint(&self) -> http_body::SizeHint { + if let Some(ContentLength { limit, sent }) = self.content_length { + http_body::SizeHint::with_exact(limit.saturating_sub(sent)) + } else { + http_body::SizeHint::default() + } + } } diff --git a/crates/wasi-http/src/p3/client.rs b/crates/wasi-http/src/p3/client.rs index b7fdc1b1cf..8acd2eee05 100644 --- a/crates/wasi-http/src/p3/client.rs +++ b/crates/wasi-http/src/p3/client.rs @@ -1,7 +1,7 @@ +use core::future::Future; use core::pin::Pin; use core::task::{ready, Context, Poll}; use core::time::Duration; -use core::{error::Error as _, future::Future}; use bytes::Bytes; use http::uri::Scheme; @@ -14,38 +14,6 @@ use crate::io::TokioIo; use crate::p3::bindings::http::types::{DnsErrorPayload, ErrorCode}; use crate::p3::RequestOptions; -/// Translate a [`hyper::Error`] to a wasi-http `ErrorCode` in the context of a request. -pub fn hyper_request_error(err: hyper::Error) -> ErrorCode { - // If there's a source, we might be able to extract a wasi-http error from it. - if let Some(cause) = err.source() { - if let Some(err) = cause.downcast_ref::() { - return err.clone(); - } - } - - warn!("hyper request error: {err:?}"); - - ErrorCode::HttpProtocolError -} - -/// Translate a [`hyper::Error`] to a wasi-http `ErrorCode` in the context of a response. -pub fn hyper_response_error(err: hyper::Error) -> ErrorCode { - if err.is_timeout() { - return ErrorCode::HttpResponseTimeout; - } - - // If there's a source, we might be able to extract a wasi-http error from it. - if let Some(cause) = err.source() { - if let Some(err) = cause.downcast_ref::() { - return err.clone(); - } - } - - warn!("hyper response error: {err:?}"); - - ErrorCode::HttpProtocolError -} - fn dns_error(rcode: String, info_code: u16) -> ErrorCode { ErrorCode::DnsError(DnsErrorPayload { rcode: Some(rcode), @@ -140,12 +108,12 @@ impl Client for DefaultClient { } } -struct IncomingBody { +struct IncomingResponseBody { incoming: hyper::body::Incoming, timeout: tokio::time::Interval, } -impl http_body::Body for IncomingBody { +impl http_body::Body for IncomingResponseBody { type Data = ::Data; type Error = ErrorCode; @@ -155,7 +123,9 @@ impl http_body::Body for IncomingBody { ) -> Poll, Self::Error>>> { match Pin::new(&mut self.as_mut().incoming).poll_frame(cx) { Poll::Ready(None) => Poll::Ready(None), - Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(hyper_response_error(err)))), + Poll::Ready(Some(Err(err))) => { + Poll::Ready(Some(Err(ErrorCode::from_hyper_response_error(err)))) + } Poll::Ready(Some(Ok(frame))) => { self.timeout.reset(); Poll::Ready(Some(Ok(frame))) @@ -199,8 +169,6 @@ pub async fn default_send_request( } impl TokioStream for T where T: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static {} - eprintln!("[host] default send request"); - let uri = request.uri(); let authority = uri.authority().ok_or(ErrorCode::HttpRequestUriInvalid)?; let use_tls = uri.scheme() == Some(&Scheme::HTTPS); @@ -239,7 +207,6 @@ pub async fn default_send_request( ) .unwrap_or(Duration::from_secs(600)); - eprintln!("[host] connect..."); let stream = match tokio::time::timeout(connect_timeout, TcpStream::connect(&authority)).await { Ok(Ok(stream)) => stream, Ok(Err(err)) if err.kind() == std::io::ErrorKind::AddrNotAvailable => { @@ -292,7 +259,6 @@ pub async fn default_send_request( } else { stream.boxed() }; - eprintln!("[host] handshake..."); let (mut sender, conn) = tokio::time::timeout( connect_timeout, // TODO: we should plumb the builder through the http context, and use it here @@ -300,7 +266,7 @@ pub async fn default_send_request( ) .await .map_err(|_| ErrorCode::ConnectionTimeout)? - .map_err(hyper_request_error)?; + .map_err(ErrorCode::from_hyper_request_error)?; // at this point, the request contains the scheme and the authority, but // the http packet should only include those if addressing a proxy, so @@ -320,19 +286,16 @@ pub async fn default_send_request( request.map(|body| body.map_err(|err| err.unwrap_or(ErrorCode::InternalError(None)))); Ok(( async move { - eprintln!("[host] real send request..."); let response = tokio::time::timeout(first_byte_timeout, sender.send_request(request)) .await .map_err(|_| ErrorCode::ConnectionReadTimeout)? - .map_err(hyper_request_error)?; + .map_err(ErrorCode::from_hyper_request_error)?; let mut timeout = tokio::time::interval(between_bytes_timeout); timeout.reset(); - Ok(response.map(|incoming| IncomingBody { incoming, timeout })) + Ok(response.map(|incoming| IncomingResponseBody { incoming, timeout })) }, async move { - eprintln!("[host] await conn..."); - conn.await.map_err(hyper_request_error)?; - eprintln!("[host] conn awaited"); + conn.await.map_err(ErrorCode::from_hyper_request_error)?; Ok(()) }, )) diff --git a/crates/wasi-http/src/p3/conv.rs b/crates/wasi-http/src/p3/conv.rs index a7cc596c7f..9bf50ed38d 100644 --- a/crates/wasi-http/src/p3/conv.rs +++ b/crates/wasi-http/src/p3/conv.rs @@ -1,10 +1,46 @@ use core::convert::Infallible; +use core::error::Error as _; use bytes::Bytes; +use tracing::warn; use crate::p3::bindings::http::types::{ErrorCode, Method, Scheme}; use crate::p3::{Body, Request}; +impl ErrorCode { + /// Translate a [`hyper::Error`] to a wasi-http [ErrorCode] in the context of a request. + pub fn from_hyper_request_error(err: hyper::Error) -> Self { + // If there's a source, we might be able to extract a wasi-http error from it. + if let Some(cause) = err.source() { + if let Some(err) = cause.downcast_ref::() { + return err.clone(); + } + } + + warn!("hyper request error: {err:?}"); + + Self::HttpProtocolError + } + + /// Translate a [`hyper::Error`] to a wasi-http [ErrorCode] in the context of a response. + pub fn from_hyper_response_error(err: hyper::Error) -> Self { + if err.is_timeout() { + return ErrorCode::HttpResponseTimeout; + } + + // If there's a source, we might be able to extract a wasi-http error from it. + if let Some(cause) = err.source() { + if let Some(err) = cause.downcast_ref::() { + return err.clone(); + } + } + + warn!("hyper response error: {err:?}"); + + ErrorCode::HttpProtocolError + } +} + impl From for ErrorCode { fn from(_: Infallible) -> Self { unreachable!() diff --git a/crates/wasi-http/src/p3/host/handle.rs b/crates/wasi-http/src/p3/host/handler.rs similarity index 76% rename from crates/wasi-http/src/p3/host/handle.rs rename to crates/wasi-http/src/p3/host/handler.rs index 2c3301f99e..dee7900bf3 100644 --- a/crates/wasi-http/src/p3/host/handle.rs +++ b/crates/wasi-http/src/p3/host/handler.rs @@ -15,8 +15,8 @@ use wasmtime_wasi::p3::{AccessorTaskFn, ResourceView as _}; use crate::p3::bindings::http::handler; use crate::p3::bindings::http::types::ErrorCode; use crate::p3::{ - empty_body, Body, BodyFrame, Client as _, GuestBody, GuestRequestTrailers, - OutgoingTrailerFuture, Request, Response, WasiHttpImpl, WasiHttpView, + empty_body, Body, BodyContext, BodyFrame, Client as _, ContentLength, GuestBody, + GuestRequestTrailers, OutgoingTrailerFuture, Request, Response, WasiHttpImpl, WasiHttpView, }; use super::{delete_request, get_fields_inner, push_response}; @@ -53,7 +53,6 @@ where store: &mut Accessor, request: Resource, ) -> wasmtime::Result, ErrorCode>> { - eprintln!("[host] call handle"); let Request { method, scheme, @@ -116,29 +115,44 @@ where Ok(request) => request, Err(err) => return Ok(Err(ErrorCode::InternalError(Some(err.to_string())))), }; - eprintln!("[host] have built req {request:?}"); - let (response, task) = match body { + let response = match body { + Body::Guest { + contents: None, + buffer: Some(BodyFrame::Trailers(Ok(None))) | None, + tx, + content_length: Some(ContentLength { limit, sent }), + .. + } if limit != sent => { + store.spawn(AccessorTaskFn( + move |_: &mut Accessor| async move { + tx.write(Err(ErrorCode::HttpRequestBodySize(Some(sent)))) + .into_future() + .await; + Ok(()) + }, + )); + return Ok(Err(ErrorCode::HttpRequestBodySize(Some(sent)))); + } Body::Guest { contents: None, trailers: None, buffer: Some(BodyFrame::Trailers(Ok(None))), tx, + content_length: None, } => { let body = empty_body(); let request = request.map(|()| body); match client.send_request(request, options).await? { Ok((response, io)) => { - let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { + store.spawn(AccessorTaskFn(|_: &mut Accessor| async { let res = io.await; tx.write(res.map_err(Into::into)).into_future().await; Ok(()) })); - let task = task.abort_handle(); match response.await { - Ok(response) => ( - response.map(|body| body.map_err(Into::into).boxed_unsync()), - task, - ), + Ok(response) => { + response.map(|body| body.map_err(Into::into).boxed_unsync()) + } Err(err) => return Ok(Err(err)), } } @@ -150,6 +164,7 @@ where trailers: None, buffer: Some(BodyFrame::Trailers(Ok(Some(trailers)))), tx, + content_length: None, } => { let trailers = store.with(|mut view| { let trailers = get_fields_inner(view.table(), &trailers)?; @@ -159,17 +174,15 @@ where let request = request.map(|()| body); match client.send_request(request, options).await? { Ok((response, io)) => { - let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { + store.spawn(AccessorTaskFn(|_: &mut Accessor| async { let res = io.await; tx.write(res.map_err(Into::into)).into_future().await; Ok(()) })); - let task = task.abort_handle(); match response.await { - Ok(response) => ( - response.map(|body| body.map_err(Into::into).boxed_unsync()), - task, - ), + Ok(response) => { + response.map(|body| body.map_err(Into::into).boxed_unsync()) + } Err(err) => return Ok(Err(err)), } } @@ -180,15 +193,25 @@ where contents: None, trailers: None, buffer: Some(BodyFrame::Trailers(Err(err))), - tx: _, - } => return Ok(Err(err)), + tx, + content_length: None, + } => { + store.spawn({ + let err = err.clone(); + AccessorTaskFn(move |_: &mut Accessor| async move { + tx.write(Err(err)).into_future().await; + Ok(()) + }) + }); + return Ok(Err(err)); + } Body::Guest { contents: None, trailers: Some(trailers), buffer: None, tx, + content_length: None, } => { - eprintln!("[host] no contents, only trailers"); let (trailers_tx, trailers_rx) = oneshot::channel(); let task = store.spawn(TrailerTask { rx: trailers, @@ -199,24 +222,17 @@ where trailer_task: task.abort_handle(), }); let request = request.map(|()| body); - eprintln!("[host] send request.."); match client.send_request(request, options).await? { Ok((response, io)) => { - let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { - eprintln!("[host] await Tx result.."); + store.spawn(AccessorTaskFn(|_: &mut Accessor| async { let res = io.await; - eprintln!("[host] write Tx result.."); tx.write(res.map_err(Into::into)).into_future().await; - eprintln!("[host] done writing Tx result"); Ok(()) })); - eprintln!("[host] return Ok response"); - let task = task.abort_handle(); match response.await { - Ok(response) => ( - response.map(|body| body.map_err(Into::into).boxed_unsync()), - task, - ), + Ok(response) => { + response.map(|body| body.map_err(Into::into).boxed_unsync()) + } Err(err) => return Ok(Err(err)), } } @@ -228,8 +244,8 @@ where trailers: Some(trailers), buffer, tx, + content_length, } => { - eprintln!("[host] contents, trailers"); let (trailers_tx, trailers_rx) = oneshot::channel(); let task = store.spawn(TrailerTask { rx: trailers, @@ -240,29 +256,24 @@ where Some(BodyFrame::Trailers(..)) => bail!("guest body is corrupted"), None => Bytes::default(), }; - let body = GuestBody::new(contents, buffer).with_trailers(GuestRequestTrailers { - trailers: Some(trailers_rx), - trailer_task: task.abort_handle(), - }); + let body = GuestBody::new(BodyContext::Request, contents, buffer, content_length) + .with_trailers(GuestRequestTrailers { + trailers: Some(trailers_rx), + trailer_task: task.abort_handle(), + }); let request = request.map(|()| body); - eprintln!("[host] send request.."); match client.send_request(request, options).await? { Ok((response, io)) => { - let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { - eprintln!("[host] await Tx result.."); + store.spawn(AccessorTaskFn(|_: &mut Accessor| async { let res = io.await; - eprintln!("[host] write Tx result.."); tx.write(res.map_err(Into::into)).into_future().await; - eprintln!("[host] done writing Tx result"); Ok(()) })); - eprintln!("[host] return Ok response"); - let task = task.abort_handle(); match response.await { - Ok(response) => ( - response.map(|body| body.map_err(Into::into).boxed_unsync()), - task, - ), + Ok(response) => { + response.map(|body| body.map_err(Into::into).boxed_unsync()) + } + Err(err) => return Ok(Err(err)), } } @@ -278,16 +289,14 @@ where let request = request.map(|()| body); match client.send_request(request, options).await? { Ok((response, io)) => { - let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { + store.spawn(AccessorTaskFn(|_: &mut Accessor| async { _ = io.await; Ok(()) })); - let task = task.abort_handle(); match response.await { - Ok(response) => ( - response.map(|body| body.map_err(Into::into).boxed_unsync()), - task, - ), + Ok(response) => { + response.map(|body| body.map_err(Into::into).boxed_unsync()) + } Err(err) => return Ok(Err(err)), } } @@ -303,16 +312,14 @@ where let request = request.map(|()| body); match client.send_request(request, options).await? { Ok((response, io)) => { - let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { + store.spawn(AccessorTaskFn(|_: &mut Accessor| async { _ = io.await; Ok(()) })); - let task = task.abort_handle(); match response.await { - Ok(response) => ( - response.map(|body| body.map_err(Into::into).boxed_unsync()), - task, - ), + Ok(response) => { + response.map(|body| body.map_err(Into::into).boxed_unsync()) + } Err(err) => return Ok(Err(err)), } } @@ -331,16 +338,14 @@ where let request = request.map(|()| body); match client.send_request(request, options).await? { Ok((response, io)) => { - let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { + store.spawn(AccessorTaskFn(|_: &mut Accessor| async { _ = io.await; Ok(()) })); - let task = task.abort_handle(); match response.await { - Ok(response) => ( - response.map(|body| body.map_err(Into::into).boxed_unsync()), - task, - ), + Ok(response) => { + response.map(|body| body.map_err(Into::into).boxed_unsync()) + } Err(err) => return Ok(Err(err)), } } @@ -355,16 +360,14 @@ where let request = request.map(|()| body); match client.send_request(request, options).await? { Ok((response, io)) => { - let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { + store.spawn(AccessorTaskFn(|_: &mut Accessor| async { _ = io.await; Ok(()) })); - let task = task.abort_handle(); match response.await { - Ok(response) => ( - response.map(|body| body.map_err(Into::into).boxed_unsync()), - task, - ), + Ok(response) => { + response.map(|body| body.map_err(Into::into).boxed_unsync()) + } Err(err) => return Ok(Err(err)), } } @@ -381,16 +384,14 @@ where let request = request.map(|()| body); match client.send_request(request, options).await? { Ok((response, io)) => { - let task = store.spawn(AccessorTaskFn(|_: &mut Accessor| async { + store.spawn(AccessorTaskFn(|_: &mut Accessor| async { _ = io.await; Ok(()) })); - let task = task.abort_handle(); match response.await { - Ok(response) => ( - response.map(|body| body.map_err(Into::into).boxed_unsync()), - task, - ), + Ok(response) => { + response.map(|body| body.map_err(Into::into).boxed_unsync()) + } Err(err) => return Ok(Err(err)), } } @@ -398,7 +399,6 @@ where } } }; - eprintln!("[host] handle return"); let ( http::response::Parts { status, headers, .. @@ -410,7 +410,7 @@ where stream: Some(body), buffer: None, }; - let response = Response::new_incoming(status, headers, body, task); + let response = Response::new(status, headers, body); let response = push_response(view.table(), response)?; Ok(Ok(response)) }) diff --git a/crates/wasi-http/src/p3/host/mod.rs b/crates/wasi-http/src/p3/host/mod.rs index ffcdb362f2..1dc7581d1b 100644 --- a/crates/wasi-http/src/p3/host/mod.rs +++ b/crates/wasi-http/src/p3/host/mod.rs @@ -7,7 +7,7 @@ use wasmtime_wasi::ResourceTable; use crate::p3::{Request, Response}; -mod handle; +mod handler; mod types; fn get_fields<'a>( diff --git a/crates/wasi-http/src/p3/host/types.rs b/crates/wasi-http/src/p3/host/types.rs index 4f20d45698..e5a002d837 100644 --- a/crates/wasi-http/src/p3/host/types.rs +++ b/crates/wasi-http/src/p3/host/types.rs @@ -2,12 +2,15 @@ use core::future::poll_fn; use core::mem; use core::ops::{Deref, DerefMut}; use core::pin::Pin; +use core::str; use core::task::Poll; use std::sync::Arc; use anyhow::{bail, Context as _}; use bytes::Bytes; +use futures::join; +use http::header::CONTENT_LENGTH; use http_body::Body as _; use wasmtime::component::{ Accessor, AccessorTask, FutureWriter, HostFuture, HostStream, Resource, StreamWriter, @@ -25,7 +28,10 @@ use crate::p3::host::{ get_request, get_request_mut, get_response, get_response_mut, push_fields, push_fields_child, push_request, push_response, }; -use crate::p3::{Body, BodyFrame, Request, RequestOptions, Response, WasiHttpImpl, WasiHttpView}; +use crate::p3::{ + Body, BodyContext, BodyFrame, ContentLength, Request, RequestOptions, Response, WasiHttpImpl, + WasiHttpView, +}; fn get_request_options<'a>( table: &'a ResourceTable, @@ -89,9 +95,22 @@ fn clone_trailer_result( } } +/// Extract the `Content-Length` header value from a [`http::HeaderMap`], returning `None` if it's not +/// present. This function will return `Err` if it's not possible to parse the `Content-Length` +/// header. +fn get_content_length(headers: &http::HeaderMap) -> wasmtime::Result> { + let Some(v) = headers.get(CONTENT_LENGTH) else { + return Ok(None); + }; + let v = v.to_str()?; + let v = v.parse()?; + Ok(Some(v)) +} + type TrailerFuture = HostFuture>, ErrorCode>>; struct BodyTask { + cx: BodyContext, body: Arc>, contents_tx: StreamWriter, trailers_tx: FutureWriter>, ErrorCode>>, @@ -109,11 +128,35 @@ where mem::replace(&mut *body, Body::Consumed) }; match body { + Body::Guest { + contents: None, + buffer: Some(BodyFrame::Trailers(Ok(None))) | None, + tx, + content_length: Some(ContentLength { limit, sent }), + .. + } if limit != sent => { + drop(self.contents_tx); + join!( + async { + tx.write(Err(self.cx.as_body_size_error(sent))) + .into_future() + .await; + }, + async { + self.trailers_tx + .write(Err(self.cx.as_body_size_error(sent))) + .into_future() + .await; + } + ); + return Ok(()); + } Body::Guest { contents: None, trailers: Some(mut trailers_rx), buffer: None, tx, + content_length: None, } => { drop(self.contents_tx); let (watch_reader, trailers_tx) = self.trailers_tx.watch_reader(); @@ -132,6 +175,7 @@ where trailers: Some(trailers_rx), buffer: None, tx, + content_length: None, }; return Ok(()); }; @@ -149,6 +193,7 @@ where trailers: None, buffer: Some(BodyFrame::Trailers(res)), tx, + content_length: None, }; return Ok(()); } @@ -160,6 +205,7 @@ where trailers: None, buffer: Some(BodyFrame::Trailers(res)), tx, + content_length: None, } => { drop(self.contents_tx); if !self @@ -176,6 +222,7 @@ where trailers: None, buffer: Some(BodyFrame::Trailers(res)), tx, + content_length: None, }; return Ok(()); } @@ -187,6 +234,7 @@ where trailers: Some(mut trailers_rx), buffer, tx, + mut content_length, } => { let mut contents_tx = self.contents_tx; match buffer { @@ -201,6 +249,7 @@ where trailers: Some(trailers_rx), buffer: Some(BodyFrame::Data(buf)), tx, + content_length, }; return Ok(()); }; @@ -227,12 +276,63 @@ where trailers: Some(trailers_rx), buffer: None, tx, + content_length, }; return Ok(()); }; let Ok((rx_tail, buf)) = rx else { + if let Some(ContentLength { limit, sent }) = content_length { + if limit != sent { + drop(contents_tx.into_inner()); + join!( + async { + tx.write(Err(self.cx.as_body_size_error(sent))) + .into_future() + .await; + }, + async { + self.trailers_tx + .write(Err(self.cx.as_body_size_error(sent))) + .into_future() + .await; + } + ); + return Ok(()); + } + } break; }; + if let Some(ContentLength { limit, sent }) = &mut content_length { + let n = buf.len().try_into().ok(); + let n = n.and_then(|n| sent.checked_add(n)); + if let Err(n) = n + .map(|n| { + if n > *limit { + Err(n) + } else { + *sent = n; + Ok(()) + } + }) + .unwrap_or(Err(u64::MAX)) + { + drop(contents_tx.into_inner()); + join!( + async { + tx.write(Err(self.cx.as_body_size_error(n))) + .into_future() + .await; + }, + async { + self.trailers_tx + .write(Err(self.cx.as_body_size_error(n))) + .into_future() + .await; + } + ); + return Ok(()); + } + } contents_rx = rx_tail.read().into_future(); let tx_tail = contents_tx.into_inner(); let Some(tx_tail) = tx_tail.write(buf.clone()).into_future().await else { @@ -244,6 +344,7 @@ where trailers: Some(trailers_rx), buffer: Some(BodyFrame::Data(buf)), tx, + content_length, }; return Ok(()); }; @@ -269,6 +370,7 @@ where trailers: Some(trailers_rx), buffer: None, tx, + content_length: None, }; return Ok(()); }; @@ -286,6 +388,7 @@ where trailers: None, buffer: Some(BodyFrame::Trailers(res)), tx, + content_length: None, }; return Ok(()); } @@ -458,6 +561,19 @@ where impl Host for WasiHttpImpl where T: WasiHttpView {} +fn parse_header_value( + name: &http::HeaderName, + value: impl AsRef<[u8]>, +) -> Result { + if name == CONTENT_LENGTH { + let s = str::from_utf8(value.as_ref()).or(Err(HeaderError::InvalidSyntax))?; + let v: u64 = s.parse().or(Err(HeaderError::InvalidSyntax))?; + Ok(v.into()) + } else { + http::header::HeaderValue::from_bytes(value.as_ref()).or(Err(HeaderError::InvalidSyntax)) + } +} + impl HostFields for WasiHttpImpl where T: WasiHttpView, @@ -472,18 +588,19 @@ where ) -> wasmtime::Result>, HeaderError>> { let mut fields = http::HeaderMap::new(); - for (header, value) in entries { - let Ok(header) = header.parse() else { + for (name, value) in entries { + let Ok(name) = name.parse() else { return Ok(Err(HeaderError::InvalidSyntax)); }; - if self.is_forbidden_header(&header) { + if self.is_forbidden_header(&name) { return Ok(Err(HeaderError::Forbidden)); } - let value = match http::header::HeaderValue::from_bytes(&value) { - Ok(value) => value, - Err(_) => return Ok(Err(HeaderError::InvalidSyntax)), - }; - fields.append(header, value); + match parse_header_value(&name, value) { + Ok(value) => { + fields.append(name, value); + } + Err(err) => return Ok(Err(err)), + } } let fields = push_fields(self.table(), WithChildren::new(fields))?; Ok(Ok(fields)) @@ -525,9 +642,11 @@ where } let mut values = Vec::with_capacity(value.len()); for value in value { - match http::header::HeaderValue::from_bytes(&value) { - Ok(value) => values.push(value), - Err(_) => return Ok(Err(HeaderError::InvalidSyntax)), + match parse_header_value(&name, value) { + Ok(value) => { + values.push(value); + } + Err(err) => return Ok(Err(err)), } } let Some(mut fields) = get_fields_inner_mut(self.table(), &fields)? else { @@ -586,21 +705,20 @@ where name: FieldName, value: FieldValue, ) -> wasmtime::Result> { - let header = match http::header::HeaderName::from_bytes(name.as_bytes()) { - Ok(header) => header, - Err(_) => return Ok(Err(HeaderError::InvalidSyntax)), + let Ok(name) = name.parse() else { + return Ok(Err(HeaderError::InvalidSyntax)); }; - if self.is_forbidden_header(&header) { + if self.is_forbidden_header(&name) { return Ok(Err(HeaderError::Forbidden)); } - let value = match http::header::HeaderValue::from_bytes(&value) { + let value = match parse_header_value(&name, value) { Ok(value) => value, - Err(_) => return Ok(Err(HeaderError::InvalidSyntax)), + Err(err) => return Ok(Err(err)), }; let Some(mut fields) = get_fields_inner_mut(self.table(), &fields)? else { return Ok(Err(HeaderError::Immutable)); }; - fields.append(header, value); + fields.append(name, value); Ok(Ok(())) } @@ -654,6 +772,7 @@ where let table = view.table(); let headers = delete_fields(table, headers)?; let headers = headers.unwrap_or_clone()?; + let content_length = get_content_length(&headers)?; let options = options .map(|options| { let options = delete_request_options(table, options)?; @@ -665,6 +784,7 @@ where trailers: Some(trailers), buffer: None, tx: res_tx, + content_length: content_length.map(ContentLength::new), }; let req = push_request( table, @@ -813,6 +933,7 @@ where } let body = Arc::clone(&body); let task = view.spawn(BodyTask { + cx: BodyContext::Request, body, contents_tx, trailers_tx, @@ -966,11 +1087,13 @@ where let table = view.table(); let headers = delete_fields(table, headers)?; let headers = headers.unwrap_or_clone()?; + let content_length = get_content_length(&headers)?; let body = Body::Guest { contents, trailers: Some(trailers), buffer: None, tx: res_tx, + content_length: content_length.map(ContentLength::new), }; let res = push_response(table, Response::new(http::StatusCode::OK, headers, body))?; Ok((res, res_rx.into())) @@ -1030,6 +1153,7 @@ where } let body = Arc::clone(&body); let task = view.spawn(BodyTask { + cx: BodyContext::Response, body, contents_tx, trailers_tx, @@ -1041,9 +1165,7 @@ where } fn drop(&mut self, res: Resource) -> wasmtime::Result<()> { - eprintln!("[host] drop response"); delete_response(self.table(), res)?; - eprintln!("[host] response dropped"); Ok(()) } } diff --git a/crates/wasi-http/src/p3/response.rs b/crates/wasi-http/src/p3/response.rs index 6fe70c2c95..a194295afc 100644 --- a/crates/wasi-http/src/p3/response.rs +++ b/crates/wasi-http/src/p3/response.rs @@ -13,7 +13,9 @@ use wasmtime::AsContextMut; use wasmtime_wasi::p3::{ResourceView, WithChildren}; use crate::p3::bindings::http::types::ErrorCode; -use crate::p3::{empty_body, guest_response_trailers, Body, BodyFrame, GuestBody}; +use crate::p3::{ + empty_body, guest_response_trailers, Body, BodyContext, BodyFrame, ContentLength, GuestBody, +}; /// The concrete type behind a `wasi:http/types/response` resource. pub struct Response { @@ -25,9 +27,6 @@ pub struct Response { pub(crate) body: Arc>, /// Body stream task handle pub(crate) body_task: Option, - /// I/O task handle - #[allow(dead_code)] - pub(crate) io_task: Option, } impl Response { @@ -38,23 +37,6 @@ impl Response { headers: WithChildren::new(headers), body: Arc::new(std::sync::Mutex::new(body)), body_task: None, - io_task: None, - } - } - - /// Construct a new [Response] - pub(crate) fn new_incoming( - status: StatusCode, - headers: HeaderMap, - body: Body, - task: AbortOnDropHandle, - ) -> Self { - Self { - status, - headers: WithChildren::new(headers), - body: Arc::new(std::sync::Mutex::new(body)), - body_task: None, - io_task: Some(task), } } @@ -78,17 +60,27 @@ impl Response { bail!("lock poisoned"); }; let (body, tx) = match body { + Body::Guest { + contents: None, + buffer: None | Some(BodyFrame::Trailers(Ok(None))), + content_length: Some(ContentLength { limit, sent }), + .. + } if limit != sent => { + bail!("guest response Content-Length mismatch, limit: {limit}, sent: {sent}") + } Body::Guest { contents: None, trailers: None, buffer: Some(BodyFrame::Trailers(Ok(None))), tx, + .. } => (empty_body().boxed_unsync(), Some(tx)), Body::Guest { contents: None, trailers: None, buffer: Some(BodyFrame::Trailers(Ok(Some(trailers)))), tx, + .. } => { let mut store = store.as_context_mut(); let table = store.data_mut().table(); @@ -108,6 +100,7 @@ impl Response { trailers: None, buffer: Some(BodyFrame::Trailers(Err(err))), tx, + .. } => ( empty_body() .with_trailers(async move { Some(Err(Some(err))) }) @@ -119,6 +112,7 @@ impl Response { trailers: Some(trailers), buffer: None, tx, + .. } => { let body = empty_body() .with_trailers(guest_response_trailers(store, trailers)) @@ -130,13 +124,14 @@ impl Response { trailers: Some(trailers), buffer, tx, + content_length, } => { let buffer = match buffer { Some(BodyFrame::Data(buffer)) => buffer, Some(BodyFrame::Trailers(..)) => bail!("guest body is corrupted"), None => Bytes::default(), }; - let body = GuestBody::new(contents, buffer) + let body = GuestBody::new(BodyContext::Response, contents, buffer, content_length) .with_trailers(guest_response_trailers(store, trailers)) .boxed_unsync(); (body, Some(tx)) diff --git a/crates/wasi-http/tests/all/p3/mod.rs b/crates/wasi-http/tests/all/p3/mod.rs index 3336edcb65..fae481e5ba 100644 --- a/crates/wasi-http/tests/all/p3/mod.rs +++ b/crates/wasi-http/tests/all/p3/mod.rs @@ -208,6 +208,7 @@ async fn run_wasi_http + 'static>( Ok(Ok(http::Response::from_parts(parts, body))) } +#[ignore = "TODO"] #[test_log::test(tokio::test)] async fn wasi_http_proxy_tests() -> anyhow::Result<()> { let req = http::Request::builder() From 2e65763c02c992d59c4f4816ebd6b26f0706fb56 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Thu, 3 Apr 2025 19:51:44 +0200 Subject: [PATCH 07/19] chore: update to new API Signed-off-by: Roman Volosatovs --- crates/wasi-http/src/p3/body.rs | 248 +++++++++++++++++++----- crates/wasi-http/src/p3/client.rs | 32 +-- crates/wasi-http/src/p3/host/handler.rs | 16 +- crates/wasi-http/src/p3/host/types.rs | 96 +++++---- crates/wasi-http/src/p3/proxy.rs | 2 +- crates/wasi-http/src/p3/response.rs | 15 +- crates/wasi-http/tests/all/p3/mod.rs | 1 - 7 files changed, 275 insertions(+), 135 deletions(-) diff --git a/crates/wasi-http/src/p3/body.rs b/crates/wasi-http/src/p3/body.rs index 4231db2f8f..edf1ac9d39 100644 --- a/crates/wasi-http/src/p3/body.rs +++ b/crates/wasi-http/src/p3/body.rs @@ -4,43 +4,34 @@ use core::pin::Pin; use core::task::{ready, Context, Poll}; use anyhow::Context as _; -use bytes::Bytes; +use bytes::{Bytes, BytesMut}; use http::HeaderMap; use http_body_util::combinators::UnsyncBoxBody; use http_body_util::BodyExt as _; use tokio::sync::oneshot; -use wasmtime::component::{AbortOnDropHandle, ErrorContext, FutureWriter, Resource, StreamReader}; +use wasmtime::component::{ + AbortOnDropHandle, BytesBuffer, ErrorContext, FutureWriter, PromisesUnordered, Resource, + StreamReader, +}; use wasmtime::AsContextMut; use wasmtime_wasi::p3::{ResourceView, WithChildren}; use crate::p3::bindings::http::types::ErrorCode; -pub(crate) type OutgoingContentsStreamFuture = Pin< - Box< - dyn Future, Bytes), Option>> - + Send - + 'static, - >, ->; +pub(crate) type OutgoingContentsStreamFuture = + Pin>, BytesMut)> + Send + 'static>>; pub(crate) type OutgoingTrailerFuture = Pin< Box< - dyn Future< - Output = Result< - Result>>, ErrorCode>, - Option, - >, - > + Send + dyn Future>>, ErrorCode>>> + + Send + 'static, >, >; pub(crate) type OutgoingTrailerFutureMut<'a> = Pin< &'a mut (dyn Future< - Output = Result< - Result>>, ErrorCode>, - Option, - >, + Output = Option>>, ErrorCode>>, > + Send + 'static), >; @@ -128,13 +119,13 @@ impl Body { } } -pub(crate) struct GuestRequestTrailers { +pub(crate) struct OutgoingRequestTrailers { pub trailers: Option, ErrorCode>>>, #[allow(dead_code)] pub trailer_task: AbortOnDropHandle, } -impl Future for GuestRequestTrailers { +impl Future for OutgoingRequestTrailers { type Output = Option>>; fn poll( @@ -155,13 +146,13 @@ impl Future for GuestRequestTrailers { } } -fn poll_guest_response_trailers( +fn poll_outgoing_response_trailers( cx: &mut Context<'_>, mut store: impl AsContextMut, trailers: OutgoingTrailerFutureMut<'_>, ) -> Poll>>> { match ready!(trailers.poll(cx)) { - Ok(Ok(Some(trailers))) => { + Some(Ok(Some(trailers))) => { let mut store = store.as_context_mut(); let table = store.data_mut().table(); match table @@ -178,28 +169,28 @@ fn poll_guest_response_trailers( ))))))), } } - Ok(Ok(None)) => Poll::Ready(None), - Ok(Err(err)) => Poll::Ready(Some(Err(Some(err)))), - Err(..) => Poll::Ready(Some(Err(None))), + Some(Ok(None)) => Poll::Ready(None), + Some(Err(err)) => Poll::Ready(Some(Err(Some(err)))), + None => Poll::Ready(Some(Err(None))), } } -pub(crate) async fn guest_response_trailers( +pub(crate) async fn outgoing_response_trailers( mut store: impl AsContextMut, mut trailers: OutgoingTrailerFuture, ) -> Option>> where T: ResourceView, { - poll_fn(move |cx| poll_guest_response_trailers(cx, &mut store, trailers.as_mut())).await + poll_fn(move |cx| poll_outgoing_response_trailers(cx, &mut store, trailers.as_mut())).await } -pub(crate) struct GuestResponseTrailers { +pub(crate) struct OutgoingResponseTrailers { pub store: T, pub trailers: Option, } -impl Future for GuestResponseTrailers +impl Future for OutgoingResponseTrailers where T: AsContextMut + Unpin, T::Data: ResourceView, @@ -214,7 +205,11 @@ where else { return Poll::Ready(None); }; - let trailers = ready!(poll_guest_response_trailers(cx, store, trailers.as_mut())); + let trailers = ready!(poll_outgoing_response_trailers( + cx, + store, + trailers.as_mut() + )); self.trailers = None; Poll::Ready(trailers) } @@ -236,23 +231,129 @@ impl ContentLength { } } -/// Body constructed by the guest -pub(crate) struct GuestBody { - pub cx: BodyContext, +/// Response body constructed by the guest +pub(crate) struct OutgoingResponseBody { + store: T, + contents: Option, + trailers: Option, + buffer: Bytes, + content_length: Option, + promises: PromisesUnordered, Bytes), Option>>, +} + +impl OutgoingResponseBody { + pub fn new( + store: T, + contents: OutgoingContentsStreamFuture, + trailers: OutgoingTrailerFuture, + buffer: Bytes, + content_length: Option, + ) -> Self { + Self { + store, + contents: Some(contents), + trailers: Some(trailers), + buffer, + content_length, + promises: PromisesUnordered::new(), + } + } +} + +impl http_body::Body for OutgoingResponseBody +where + T: AsContextMut + Send + Unpin, + T::Data: ResourceView + Send, +{ + type Data = Bytes; + type Error = Option; + + fn poll_frame( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + if !self.buffer.is_empty() { + let buffer = mem::take(&mut self.buffer); + if let Some(ContentLength { limit, sent }) = &mut self.content_length { + let Ok(n) = buffer.len().try_into() else { + return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(None))))); + }; + let Some(n) = sent.checked_add(n) else { + return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(None))))); + }; + if n > *limit { + return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(Some(n)))))); + } + *sent = n; + } + return Poll::Ready(Some(Ok(http_body::Frame::data(buffer)))); + } + let Some(stream) = &mut self.contents else { + return Poll::Ready(None); + }; + todo!() + //self.contents.poll_promise(cx, &mut self.store); + //core::pin::pin!(&mut self.promises.next(&mut self.store)).poll(cx); + //match ready!(Pin::new(stream).poll(cx)) { + // Ok((tail, buf)) => { + // core::pin::pin!(tail.read().get(&mut self.store)).poll(cx); + // if let Some(ContentLength { limit, sent }) = &mut self.content_length { + // let Ok(n) = buf.len().try_into() else { + // return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(None))))); + // }; + // let Some(n) = sent.checked_add(n) else { + // return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(None))))); + // }; + // if n > *limit { + // return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(Some( + // n, + // )))))); + // } + // *sent = n; + // } + // Poll::Ready(Some(Ok(http_body::Frame::data(buf)))) + // } + // Err(..) => { + // self.contents = None; + // if let Some(ContentLength { limit, sent }) = self.content_length { + // if limit != sent { + // return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(Some( + // sent, + // )))))); + // } + // } + // Poll::Ready(None) + // } + //} + } + + fn is_end_stream(&self) -> bool { + self.contents.is_none() + } + + fn size_hint(&self) -> http_body::SizeHint { + if let Some(ContentLength { limit, sent }) = self.content_length { + http_body::SizeHint::with_exact(limit.saturating_sub(sent)) + } else { + http_body::SizeHint::default() + } + } +} + +/// Request body constructed by the guest +pub(crate) struct OutgoingRequestBody { pub contents: Option, pub buffer: Bytes, pub content_length: Option, } -impl GuestBody { +impl OutgoingRequestBody { pub fn new( - cx: BodyContext, contents: OutgoingContentsStreamFuture, buffer: Bytes, content_length: Option, ) -> Self { Self { - cx, contents: Some(contents), buffer, content_length, @@ -260,7 +361,7 @@ impl GuestBody { } } -impl http_body::Body for GuestBody { +impl http_body::Body for OutgoingRequestBody { type Data = Bytes; type Error = Option; @@ -272,13 +373,13 @@ impl http_body::Body for GuestBody { let buffer = mem::take(&mut self.buffer); if let Some(ContentLength { limit, sent }) = &mut self.content_length { let Ok(n) = buffer.len().try_into() else { - return Poll::Ready(Some(Err(Some(self.cx.as_body_size_error(u64::MAX))))); + return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(None))))); }; let Some(n) = sent.checked_add(n) else { - return Poll::Ready(Some(Err(Some(self.cx.as_body_size_error(u64::MAX))))); + return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(None))))); }; if n > *limit { - return Poll::Ready(Some(Err(Some(self.cx.as_body_size_error(n))))); + return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(Some(n)))))); } *sent = n; } @@ -287,28 +388,35 @@ impl http_body::Body for GuestBody { let Some(stream) = &mut self.contents else { return Poll::Ready(None); }; - match ready!(Pin::new(stream).poll(cx)) { - Ok((tail, buf)) => { - self.contents = Some(tail.read().into_future()); + let (tail, mut buf) = ready!(Pin::new(stream).poll(cx)); + match tail { + Some(tail) => { + let frame = buf.split(); + self.contents = Some(tail.read(buf).into_future()); if let Some(ContentLength { limit, sent }) = &mut self.content_length { - let Ok(n) = buf.len().try_into() else { - return Poll::Ready(Some(Err(Some(self.cx.as_body_size_error(u64::MAX))))); + let Ok(n) = frame.len().try_into() else { + return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(None))))); }; let Some(n) = sent.checked_add(n) else { - return Poll::Ready(Some(Err(Some(self.cx.as_body_size_error(u64::MAX))))); + return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(None))))); }; if n > *limit { - return Poll::Ready(Some(Err(Some(self.cx.as_body_size_error(n))))); + return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(Some( + n, + )))))); } *sent = n; } - Poll::Ready(Some(Ok(http_body::Frame::data(buf)))) + Poll::Ready(Some(Ok(http_body::Frame::data(frame.freeze())))) } - Err(..) => { + None => { + debug_assert!(buf.is_empty()); self.contents = None; if let Some(ContentLength { limit, sent }) = self.content_length { if limit != sent { - return Poll::Ready(Some(Err(Some(self.cx.as_body_size_error(sent))))); + return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(Some( + sent, + )))))); } } Poll::Ready(None) @@ -328,3 +436,41 @@ impl http_body::Body for GuestBody { } } } + +pub(crate) struct IncomingResponseBody { + pub incoming: hyper::body::Incoming, + pub timeout: tokio::time::Interval, +} + +impl http_body::Body for IncomingResponseBody { + type Data = ::Data; + type Error = ErrorCode; + + fn poll_frame( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + match Pin::new(&mut self.as_mut().incoming).poll_frame(cx) { + Poll::Ready(None) => Poll::Ready(None), + Poll::Ready(Some(Err(err))) => { + Poll::Ready(Some(Err(ErrorCode::from_hyper_response_error(err)))) + } + Poll::Ready(Some(Ok(frame))) => { + self.timeout.reset(); + Poll::Ready(Some(Ok(frame))) + } + Poll::Pending => { + ready!(self.timeout.poll_tick(cx)); + Poll::Ready(Some(Err(ErrorCode::ConnectionReadTimeout))) + } + } + } + + fn is_end_stream(&self) -> bool { + self.incoming.is_end_stream() + } + + fn size_hint(&self) -> http_body::SizeHint { + self.incoming.size_hint() + } +} diff --git a/crates/wasi-http/src/p3/client.rs b/crates/wasi-http/src/p3/client.rs index 8acd2eee05..5063cc0e99 100644 --- a/crates/wasi-http/src/p3/client.rs +++ b/crates/wasi-http/src/p3/client.rs @@ -10,9 +10,9 @@ use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::TcpStream; use tracing::warn; -use crate::io::TokioIo; use crate::p3::bindings::http::types::{DnsErrorPayload, ErrorCode}; use crate::p3::RequestOptions; +use crate::{io::TokioIo, p3::IncomingResponseBody}; fn dns_error(rcode: String, info_code: u16) -> ErrorCode { ErrorCode::DnsError(DnsErrorPayload { @@ -108,36 +108,6 @@ impl Client for DefaultClient { } } -struct IncomingResponseBody { - incoming: hyper::body::Incoming, - timeout: tokio::time::Interval, -} - -impl http_body::Body for IncomingResponseBody { - type Data = ::Data; - type Error = ErrorCode; - - fn poll_frame( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll, Self::Error>>> { - match Pin::new(&mut self.as_mut().incoming).poll_frame(cx) { - Poll::Ready(None) => Poll::Ready(None), - Poll::Ready(Some(Err(err))) => { - Poll::Ready(Some(Err(ErrorCode::from_hyper_response_error(err)))) - } - Poll::Ready(Some(Ok(frame))) => { - self.timeout.reset(); - Poll::Ready(Some(Ok(frame))) - } - Poll::Pending => { - ready!(self.timeout.poll_tick(cx)); - Poll::Ready(Some(Err(ErrorCode::ConnectionReadTimeout))) - } - } - } -} - /// The default implementation of how an outgoing request is sent. /// /// This implementation is used by the `wasi:http/outgoing-handler` interface diff --git a/crates/wasi-http/src/p3/host/handler.rs b/crates/wasi-http/src/p3/host/handler.rs index dee7900bf3..1403474ddc 100644 --- a/crates/wasi-http/src/p3/host/handler.rs +++ b/crates/wasi-http/src/p3/host/handler.rs @@ -15,8 +15,8 @@ use wasmtime_wasi::p3::{AccessorTaskFn, ResourceView as _}; use crate::p3::bindings::http::handler; use crate::p3::bindings::http::types::ErrorCode; use crate::p3::{ - empty_body, Body, BodyContext, BodyFrame, Client as _, ContentLength, GuestBody, - GuestRequestTrailers, OutgoingTrailerFuture, Request, Response, WasiHttpImpl, WasiHttpView, + empty_body, Body, BodyFrame, Client as _, ContentLength, OutgoingRequestBody, + OutgoingRequestTrailers, OutgoingTrailerFuture, Request, Response, WasiHttpImpl, WasiHttpView, }; use super::{delete_request, get_fields_inner, push_response}; @@ -29,18 +29,18 @@ struct TrailerTask { impl AccessorTask> for TrailerTask { async fn run(self, store: &mut Accessor) -> wasmtime::Result<()> { match self.rx.await { - Ok(Ok(trailers)) => store.with(|mut view| { + Some(Ok(trailers)) => store.with(|mut view| { let trailers = trailers .map(|trailers| get_fields_inner(view.table(), &trailers)) .transpose()?; _ = self.tx.send(Ok(trailers.as_deref().cloned())); Ok(()) }), - Ok(Err(err)) => { + Some(Err(err)) => { _ = self.tx.send(Err(err)); Ok(()) } - Err(..) => Ok(()), + None => Ok(()), } } } @@ -217,7 +217,7 @@ where rx: trailers, tx: trailers_tx, }); - let body = empty_body().with_trailers(GuestRequestTrailers { + let body = empty_body().with_trailers(OutgoingRequestTrailers { trailers: Some(trailers_rx), trailer_task: task.abort_handle(), }); @@ -256,8 +256,8 @@ where Some(BodyFrame::Trailers(..)) => bail!("guest body is corrupted"), None => Bytes::default(), }; - let body = GuestBody::new(BodyContext::Request, contents, buffer, content_length) - .with_trailers(GuestRequestTrailers { + let body = OutgoingRequestBody::new(contents, buffer, content_length) + .with_trailers(OutgoingRequestTrailers { trailers: Some(trailers_rx), trailer_task: task.abort_handle(), }); diff --git a/crates/wasi-http/src/p3/host/types.rs b/crates/wasi-http/src/p3/host/types.rs index e5a002d837..dedcda6f2f 100644 --- a/crates/wasi-http/src/p3/host/types.rs +++ b/crates/wasi-http/src/p3/host/types.rs @@ -8,12 +8,13 @@ use core::task::Poll; use std::sync::Arc; use anyhow::{bail, Context as _}; -use bytes::Bytes; +use bytes::{Bytes, BytesMut}; use futures::join; use http::header::CONTENT_LENGTH; use http_body::Body as _; use wasmtime::component::{ - Accessor, AccessorTask, FutureWriter, HostFuture, HostStream, Resource, StreamWriter, + Accessor, AccessorTask, BytesBuffer, FutureWriter, HostFuture, HostStream, Resource, + StreamWriter, }; use wasmtime_wasi::p3::bindings::clocks::monotonic_clock::Duration; use wasmtime_wasi::p3::{ResourceView as _, WithChildren}; @@ -112,7 +113,7 @@ type TrailerFuture = HostFuture>, ErrorCode>>; struct BodyTask { cx: BodyContext, body: Arc>, - contents_tx: StreamWriter, + contents_tx: StreamWriter, trailers_tx: FutureWriter>, ErrorCode>>, } @@ -161,7 +162,7 @@ where drop(self.contents_tx); let (watch_reader, trailers_tx) = self.trailers_tx.watch_reader(); let mut watch_reader = watch_reader.into_future(); - let Some(Ok(res)) = poll_fn(|cx| match watch_reader.as_mut().poll(cx) { + let Some(Some(res)) = poll_fn(|cx| match watch_reader.as_mut().poll(cx) { Poll::Ready(()) => return Poll::Ready(None), Poll::Pending => trailers_rx.as_mut().poll(cx).map(Some), }) @@ -239,11 +240,14 @@ where let mut contents_tx = self.contents_tx; match buffer { Some(BodyFrame::Data(buf)) => { - let Some(tx_tail) = contents_tx.write(buf.clone()).into_future().await - else { + let (tx_tail, buf) = contents_tx.write_all(buf.into()).into_future().await; + let Some(tx_tail) = tx_tail else { let Ok(mut body) = self.body.lock() else { bail!("lock poisoned"); }; + // TODO: split + //buf = buf.into_inner(); + let buf = Bytes::copy_from_slice(&buf); *body = Body::Guest { contents: Some(contents_rx), trailers: Some(trailers_rx), @@ -258,14 +262,15 @@ where Some(BodyFrame::Trailers(..)) => bail!("corrupted guest body state"), None => {} } - let (watch_reader, mut contents_tx) = contents_tx.watch_reader(); - let mut watch_reader = watch_reader.into_future(); + let (contents_tx_drop, mut contents_tx) = contents_tx.watch_reader(); + let mut contents_tx_drop = contents_tx_drop.into_future(); loop { - let Some(rx) = poll_fn(|cx| match watch_reader.as_mut().poll(cx) { - Poll::Ready(()) => return Poll::Ready(None), - Poll::Pending => contents_rx.as_mut().poll(cx).map(Some), - }) - .await + let Some((rx_tail, mut rx_buffer)) = + poll_fn(|cx| match contents_tx_drop.as_mut().poll(cx) { + Poll::Ready(()) => return Poll::Ready(None), + Poll::Pending => contents_rx.as_mut().poll(cx).map(Some), + }) + .await else { // read handle dropped let Ok(mut body) = self.body.lock() else { @@ -280,10 +285,12 @@ where }; return Ok(()); }; - let Ok((rx_tail, buf)) = rx else { + let tx_tail = contents_tx.into_inner(); + let Some(rx_tail) = rx_tail else { + debug_assert!(rx_buffer.is_empty()); if let Some(ContentLength { limit, sent }) = content_length { if limit != sent { - drop(contents_tx.into_inner()); + drop(tx_tail); join!( async { tx.write(Err(self.cx.as_body_size_error(sent))) @@ -303,7 +310,7 @@ where break; }; if let Some(ContentLength { limit, sent }) = &mut content_length { - let n = buf.len().try_into().ok(); + let n = rx_buffer.len().try_into().ok(); let n = n.and_then(|n| sent.checked_add(n)); if let Err(n) = n .map(|n| { @@ -316,7 +323,7 @@ where }) .unwrap_or(Err(u64::MAX)) { - drop(contents_tx.into_inner()); + drop(tx_tail); join!( async { tx.write(Err(self.cx.as_body_size_error(n))) @@ -333,12 +340,16 @@ where return Ok(()); } } - contents_rx = rx_tail.read().into_future(); - let tx_tail = contents_tx.into_inner(); - let Some(tx_tail) = tx_tail.write(buf.clone()).into_future().await else { + let buf = rx_buffer.split().freeze(); + contents_rx = rx_tail.read(rx_buffer).into_future(); + let (tx_tail, buf) = tx_tail.write_all(buf.into()).into_future().await; + let Some(tx_tail) = tx_tail else { let Ok(mut body) = self.body.lock() else { bail!("lock poisoned"); }; + // TODO: split + //buf = buf.into_inner(); + let buf = Bytes::copy_from_slice(&buf); *body = Body::Guest { contents: Some(contents_rx), trailers: Some(trailers_rx), @@ -348,15 +359,14 @@ where }; return Ok(()); }; - let (new_watch_reader, new_contents_tx) = tx_tail.watch_reader(); - contents_tx = new_contents_tx; - watch_reader = new_watch_reader.into_future(); + let (tx_tail_drop, tx_tail) = tx_tail.watch_reader(); + contents_tx = tx_tail; + contents_tx_drop = tx_tail_drop.into_future(); } - drop(contents_tx.into_inner()); let (watch_reader, trailers_tx) = self.trailers_tx.watch_reader(); let mut watch_reader = watch_reader.into_future(); - let Some(Ok(res)) = poll_fn(|cx| match watch_reader.as_mut().poll(cx) { + let Some(Some(res)) = poll_fn(|cx| match watch_reader.as_mut().poll(cx) { Poll::Ready(()) => return Poll::Ready(None), Poll::Pending => trailers_rx.as_mut().poll(cx).map(Some), }) @@ -403,11 +413,14 @@ where let mut contents_tx = self.contents_tx; match buffer { Some(BodyFrame::Data(buf)) => { - let Some(tx_tail) = contents_tx.write(buf.clone()).into_future().await - else { + let (tx_tail, buf) = contents_tx.write_all(buf.into()).into_future().await; + let Some(tx_tail) = tx_tail else { let Ok(mut body) = self.body.lock() else { bail!("lock poisoned"); }; + // TODO: split + //buf = buf.into_inner(); + let buf = Bytes::copy_from_slice(&buf); *body = Body::Host { stream: Some(stream), buffer: Some(BodyFrame::Data(buf)), @@ -456,12 +469,15 @@ where match frame.into_data().map_err(http_body::Frame::into_trailers) { Ok(buf) => { let tx_tail = contents_tx.into_inner(); - let Some(tx_tail) = - tx_tail.write(buf.clone()).into_future().await - else { + let (tx_tail, buf) = + tx_tail.write_all(buf.into()).into_future().await; + let Some(tx_tail) = tx_tail else { let Ok(mut body) = self.body.lock() else { bail!("lock poisoned"); }; + // TODO: split + //buf = buf.into_inner(); + let buf = Bytes::copy_from_slice(&buf); *body = Body::Host { stream: Some(stream), buffer: Some(BodyFrame::Data(buf)), @@ -766,8 +782,12 @@ where let (res_tx, res_rx) = instance .future(&mut view) .context("failed to create future")?; - let contents = - contents.map(|contents| contents.into_reader(&mut view).read().into_future()); + let contents = contents.map(|contents| { + contents + .into_reader(&mut view) + .read(BytesMut::with_capacity(8096)) + .into_future() + }); let trailers = trailers.into_reader(&mut view).read().into_future(); let table = view.table(); let headers = delete_fields(table, headers)?; @@ -914,7 +934,7 @@ where store.with(|mut view| { let instance = view.instance(); let (contents_tx, contents_rx) = instance - .stream(&mut view) + .stream::<_, _, Vec<_>, _, _>(&mut view) .context("failed to create stream")?; let (trailers_tx, trailers_rx) = instance .future(&mut view) @@ -1081,8 +1101,12 @@ where let (res_tx, res_rx) = instance .future(&mut view) .context("failed to create future")?; - let contents = - contents.map(|contents| contents.into_reader(&mut view).read().into_future()); + let contents = contents.map(|contents| { + contents + .into_reader(&mut view) + .read(BytesMut::with_capacity(8096)) + .into_future() + }); let trailers = trailers.into_reader(&mut view).read().into_future(); let table = view.table(); let headers = delete_fields(table, headers)?; @@ -1134,7 +1158,7 @@ where store.with(|mut view| { let instance = view.instance(); let (contents_tx, contents_rx) = instance - .stream(&mut view) + .stream::<_, _, Vec<_>, _, _>(&mut view) .context("failed to create stream")?; let (trailers_tx, trailers_rx) = instance .future(&mut view) diff --git a/crates/wasi-http/src/p3/proxy.rs b/crates/wasi-http/src/p3/proxy.rs index ed1646c1f0..501e7e3646 100644 --- a/crates/wasi-http/src/p3/proxy.rs +++ b/crates/wasi-http/src/p3/proxy.rs @@ -30,7 +30,7 @@ impl Proxy { /// Call `handle` on [Proxy]. pub async fn handle( &self, - mut store: impl AsContextMut + Send + 'static, + mut store: impl AsContextMut + Send + Unpin + 'static, req: impl Into, ) -> wasmtime::Result< Result< diff --git a/crates/wasi-http/src/p3/response.rs b/crates/wasi-http/src/p3/response.rs index a194295afc..00e279068b 100644 --- a/crates/wasi-http/src/p3/response.rs +++ b/crates/wasi-http/src/p3/response.rs @@ -14,7 +14,7 @@ use wasmtime_wasi::p3::{ResourceView, WithChildren}; use crate::p3::bindings::http::types::ErrorCode; use crate::p3::{ - empty_body, guest_response_trailers, Body, BodyContext, BodyFrame, ContentLength, GuestBody, + empty_body, outgoing_response_trailers, Body, BodyFrame, ContentLength, OutgoingResponseBody, }; /// The concrete type behind a `wasi:http/types/response` resource. @@ -41,9 +41,9 @@ impl Response { } /// Convert [Response] into [http::Response]. - pub fn into_http( + pub fn into_http( self, - mut store: impl AsContextMut + Send + 'static, + mut store: impl AsContextMut + Send + Unpin + 'static, ) -> anyhow::Result<( http::Response>>, Option>>, @@ -115,7 +115,7 @@ impl Response { .. } => { let body = empty_body() - .with_trailers(guest_response_trailers(store, trailers)) + .with_trailers(outgoing_response_trailers(store, trailers)) .boxed_unsync(); (body, Some(tx)) } @@ -131,9 +131,10 @@ impl Response { Some(BodyFrame::Trailers(..)) => bail!("guest body is corrupted"), None => Bytes::default(), }; - let body = GuestBody::new(BodyContext::Response, contents, buffer, content_length) - .with_trailers(guest_response_trailers(store, trailers)) - .boxed_unsync(); + eprintln!("return body with trailers"); + let body = + OutgoingResponseBody::new(store, contents, trailers, buffer, content_length) + .boxed_unsync(); (body, Some(tx)) } Body::Guest { .. } => bail!("guest body is corrupted"), diff --git a/crates/wasi-http/tests/all/p3/mod.rs b/crates/wasi-http/tests/all/p3/mod.rs index fae481e5ba..3336edcb65 100644 --- a/crates/wasi-http/tests/all/p3/mod.rs +++ b/crates/wasi-http/tests/all/p3/mod.rs @@ -208,7 +208,6 @@ async fn run_wasi_http + 'static>( Ok(Ok(http::Response::from_parts(parts, body))) } -#[ignore = "TODO"] #[test_log::test(tokio::test)] async fn wasi_http_proxy_tests() -> anyhow::Result<()> { let req = http::Request::builder() From bfbb830dd2a102d938ac2b10e0b99ed82ff990b8 Mon Sep 17 00:00:00 2001 From: Joel Dice Date: Thu, 3 Apr 2025 13:51:23 -0600 Subject: [PATCH 08/19] fix more rebase damage; assert read buffers have non-zero capacity This should make #100 more debuggable by turning it in to an assertion failure in `OutgoingRequestBody::poll_frame`. It seems to be doing a zero-length read (presumably by accident), but I haven't traced it further than that, yet. Signed-off-by: Joel Dice --- crates/wasi-http/src/p3/body.rs | 8 +++++--- crates/wasi-http/src/p3/host/handler.rs | 14 +++++++++++-- crates/wasi-http/src/p3/host/types.rs | 27 ++++++++----------------- crates/wasi-http/src/p3/response.rs | 13 ++++++++++-- 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/crates/wasi-http/src/p3/body.rs b/crates/wasi-http/src/p3/body.rs index edf1ac9d39..aa390ac9e3 100644 --- a/crates/wasi-http/src/p3/body.rs +++ b/crates/wasi-http/src/p3/body.rs @@ -3,6 +3,8 @@ use core::mem; use core::pin::Pin; use core::task::{ready, Context, Poll}; +use std::io::Cursor; + use anyhow::Context as _; use bytes::{Bytes, BytesMut}; use http::HeaderMap; @@ -10,8 +12,7 @@ use http_body_util::combinators::UnsyncBoxBody; use http_body_util::BodyExt as _; use tokio::sync::oneshot; use wasmtime::component::{ - AbortOnDropHandle, BytesBuffer, ErrorContext, FutureWriter, PromisesUnordered, Resource, - StreamReader, + AbortOnDropHandle, ErrorContext, FutureWriter, PromisesUnordered, Resource, StreamReader, }; use wasmtime::AsContextMut; use wasmtime_wasi::p3::{ResourceView, WithChildren}; @@ -43,7 +44,7 @@ pub(crate) fn empty_body() -> impl http_body::Body), /// Trailer frame, this is the last frame of the body and it includes the transmit/receipt result Trailers(Result>>, ErrorCode>), } @@ -392,6 +393,7 @@ impl http_body::Body for OutgoingRequestBody { match tail { Some(tail) => { let frame = buf.split(); + assert!(buf.capacity() > 0); self.contents = Some(tail.read(buf).into_future()); if let Some(ContentLength { limit, sent }) = &mut self.content_length { let Ok(n) = frame.len().try_into() else { diff --git a/crates/wasi-http/src/p3/host/handler.rs b/crates/wasi-http/src/p3/host/handler.rs index 1403474ddc..80e58106de 100644 --- a/crates/wasi-http/src/p3/host/handler.rs +++ b/crates/wasi-http/src/p3/host/handler.rs @@ -1,5 +1,6 @@ use core::iter; +use std::io::Cursor; use std::sync::Arc; use anyhow::bail; @@ -252,7 +253,11 @@ where tx: trailers_tx, }); let buffer = match buffer { - Some(BodyFrame::Data(buf)) => buf, + Some(BodyFrame::Data(buf)) => { + let position = buf.position(); + buf.into_inner() + .split_off(usize::try_from(position).unwrap()) + } Some(BodyFrame::Trailers(..)) => bail!("guest body is corrupted"), None => Bytes::default(), }; @@ -307,7 +312,12 @@ where stream: Some(stream), buffer: Some(BodyFrame::Data(buffer)), } => { - let buffer = futures::stream::iter(iter::once(Ok(http_body::Frame::data(buffer)))); + let position = buffer.position(); + let buffer = futures::stream::iter(iter::once(Ok(http_body::Frame::data( + buffer + .into_inner() + .split_off(usize::try_from(position).unwrap()), + )))); let body = StreamBody::new(buffer.chain(BodyStream::new(stream.map_err(Some)))); let request = request.map(|()| body); match client.send_request(request, options).await? { diff --git a/crates/wasi-http/src/p3/host/types.rs b/crates/wasi-http/src/p3/host/types.rs index dedcda6f2f..92d702fe1a 100644 --- a/crates/wasi-http/src/p3/host/types.rs +++ b/crates/wasi-http/src/p3/host/types.rs @@ -5,6 +5,7 @@ use core::pin::Pin; use core::str; use core::task::Poll; +use std::io::Cursor; use std::sync::Arc; use anyhow::{bail, Context as _}; @@ -13,8 +14,7 @@ use futures::join; use http::header::CONTENT_LENGTH; use http_body::Body as _; use wasmtime::component::{ - Accessor, AccessorTask, BytesBuffer, FutureWriter, HostFuture, HostStream, Resource, - StreamWriter, + Accessor, AccessorTask, FutureWriter, HostFuture, HostStream, Resource, StreamWriter, }; use wasmtime_wasi::p3::bindings::clocks::monotonic_clock::Duration; use wasmtime_wasi::p3::{ResourceView as _, WithChildren}; @@ -113,7 +113,7 @@ type TrailerFuture = HostFuture>, ErrorCode>>; struct BodyTask { cx: BodyContext, body: Arc>, - contents_tx: StreamWriter, + contents_tx: StreamWriter>, trailers_tx: FutureWriter>, ErrorCode>>, } @@ -240,14 +240,11 @@ where let mut contents_tx = self.contents_tx; match buffer { Some(BodyFrame::Data(buf)) => { - let (tx_tail, buf) = contents_tx.write_all(buf.into()).into_future().await; + let (tx_tail, buf) = contents_tx.write_all(buf).into_future().await; let Some(tx_tail) = tx_tail else { let Ok(mut body) = self.body.lock() else { bail!("lock poisoned"); }; - // TODO: split - //buf = buf.into_inner(); - let buf = Bytes::copy_from_slice(&buf); *body = Body::Guest { contents: Some(contents_rx), trailers: Some(trailers_rx), @@ -341,15 +338,13 @@ where } } let buf = rx_buffer.split().freeze(); + assert!(rx_buffer.capacity() > 0); contents_rx = rx_tail.read(rx_buffer).into_future(); - let (tx_tail, buf) = tx_tail.write_all(buf.into()).into_future().await; + let (tx_tail, buf) = tx_tail.write_all(Cursor::new(buf)).into_future().await; let Some(tx_tail) = tx_tail else { let Ok(mut body) = self.body.lock() else { bail!("lock poisoned"); }; - // TODO: split - //buf = buf.into_inner(); - let buf = Bytes::copy_from_slice(&buf); *body = Body::Guest { contents: Some(contents_rx), trailers: Some(trailers_rx), @@ -413,14 +408,11 @@ where let mut contents_tx = self.contents_tx; match buffer { Some(BodyFrame::Data(buf)) => { - let (tx_tail, buf) = contents_tx.write_all(buf.into()).into_future().await; + let (tx_tail, buf) = contents_tx.write_all(buf).into_future().await; let Some(tx_tail) = tx_tail else { let Ok(mut body) = self.body.lock() else { bail!("lock poisoned"); }; - // TODO: split - //buf = buf.into_inner(); - let buf = Bytes::copy_from_slice(&buf); *body = Body::Host { stream: Some(stream), buffer: Some(BodyFrame::Data(buf)), @@ -470,14 +462,11 @@ where Ok(buf) => { let tx_tail = contents_tx.into_inner(); let (tx_tail, buf) = - tx_tail.write_all(buf.into()).into_future().await; + tx_tail.write_all(Cursor::new(buf)).into_future().await; let Some(tx_tail) = tx_tail else { let Ok(mut body) = self.body.lock() else { bail!("lock poisoned"); }; - // TODO: split - //buf = buf.into_inner(); - let buf = Bytes::copy_from_slice(&buf); *body = Body::Host { stream: Some(stream), buffer: Some(BodyFrame::Data(buf)), diff --git a/crates/wasi-http/src/p3/response.rs b/crates/wasi-http/src/p3/response.rs index 00e279068b..7dc9d1941f 100644 --- a/crates/wasi-http/src/p3/response.rs +++ b/crates/wasi-http/src/p3/response.rs @@ -127,7 +127,11 @@ impl Response { content_length, } => { let buffer = match buffer { - Some(BodyFrame::Data(buffer)) => buffer, + Some(BodyFrame::Data(buf)) => { + let position = buf.position(); + buf.into_inner() + .split_off(usize::try_from(position).unwrap()) + } Some(BodyFrame::Trailers(..)) => bail!("guest body is corrupted"), None => Bytes::default(), }; @@ -177,7 +181,12 @@ impl Response { stream: Some(stream), buffer: Some(BodyFrame::Data(buffer)), } => { - let buffer = futures::stream::iter(iter::once(Ok(http_body::Frame::data(buffer)))); + let position = buffer.position(); + let buffer = futures::stream::iter(iter::once(Ok(http_body::Frame::data( + buffer + .into_inner() + .split_off(usize::try_from(position).unwrap()), + )))); ( BodyExt::boxed_unsync(StreamBody::new( buffer.chain(BodyStream::new(stream.map_err(Some))), From 022978d9e4fa1cfb6461c8a7f7b0c9b018c02221 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Fri, 4 Apr 2025 11:48:31 +0200 Subject: [PATCH 09/19] fix: reserve buffer capacity after reads Signed-off-by: Roman Volosatovs --- crates/wasi-http/src/p3/body.rs | 17 +++++---- crates/wasi-http/src/p3/host/handler.rs | 14 +------ crates/wasi-http/src/p3/host/types.rs | 49 ++++++++++++++++--------- crates/wasi-http/src/p3/mod.rs | 3 ++ crates/wasi-http/src/p3/response.rs | 27 ++++---------- 5 files changed, 54 insertions(+), 56 deletions(-) diff --git a/crates/wasi-http/src/p3/body.rs b/crates/wasi-http/src/p3/body.rs index aa390ac9e3..0ea4b1e130 100644 --- a/crates/wasi-http/src/p3/body.rs +++ b/crates/wasi-http/src/p3/body.rs @@ -18,6 +18,7 @@ use wasmtime::AsContextMut; use wasmtime_wasi::p3::{ResourceView, WithChildren}; use crate::p3::bindings::http::types::ErrorCode; +use crate::p3::DEFAULT_BUFFER_CAPACITY; pub(crate) type OutgoingContentsStreamFuture = Pin>, BytesMut)> + Send + 'static>>; @@ -44,7 +45,7 @@ pub(crate) fn empty_body() -> impl http_body::Body), + Data(Bytes), /// Trailer frame, this is the last frame of the body and it includes the transmit/receipt result Trailers(Result>>, ErrorCode>), } @@ -389,14 +390,14 @@ impl http_body::Body for OutgoingRequestBody { let Some(stream) = &mut self.contents else { return Poll::Ready(None); }; - let (tail, mut buf) = ready!(Pin::new(stream).poll(cx)); + let (tail, mut rx_buffer) = ready!(Pin::new(stream).poll(cx)); match tail { Some(tail) => { - let frame = buf.split(); - assert!(buf.capacity() > 0); - self.contents = Some(tail.read(buf).into_future()); + let buffer = rx_buffer.split(); + rx_buffer.reserve(DEFAULT_BUFFER_CAPACITY); + self.contents = Some(tail.read(rx_buffer).into_future()); if let Some(ContentLength { limit, sent }) = &mut self.content_length { - let Ok(n) = frame.len().try_into() else { + let Ok(n) = buffer.len().try_into() else { return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(None))))); }; let Some(n) = sent.checked_add(n) else { @@ -409,10 +410,10 @@ impl http_body::Body for OutgoingRequestBody { } *sent = n; } - Poll::Ready(Some(Ok(http_body::Frame::data(frame.freeze())))) + Poll::Ready(Some(Ok(http_body::Frame::data(buffer.freeze())))) } None => { - debug_assert!(buf.is_empty()); + debug_assert!(rx_buffer.is_empty()); self.contents = None; if let Some(ContentLength { limit, sent }) = self.content_length { if limit != sent { diff --git a/crates/wasi-http/src/p3/host/handler.rs b/crates/wasi-http/src/p3/host/handler.rs index 80e58106de..3bcf39b5cb 100644 --- a/crates/wasi-http/src/p3/host/handler.rs +++ b/crates/wasi-http/src/p3/host/handler.rs @@ -1,6 +1,5 @@ use core::iter; -use std::io::Cursor; use std::sync::Arc; use anyhow::bail; @@ -253,11 +252,7 @@ where tx: trailers_tx, }); let buffer = match buffer { - Some(BodyFrame::Data(buf)) => { - let position = buf.position(); - buf.into_inner() - .split_off(usize::try_from(position).unwrap()) - } + Some(BodyFrame::Data(buffer)) => buffer, Some(BodyFrame::Trailers(..)) => bail!("guest body is corrupted"), None => Bytes::default(), }; @@ -312,12 +307,7 @@ where stream: Some(stream), buffer: Some(BodyFrame::Data(buffer)), } => { - let position = buffer.position(); - let buffer = futures::stream::iter(iter::once(Ok(http_body::Frame::data( - buffer - .into_inner() - .split_off(usize::try_from(position).unwrap()), - )))); + let buffer = futures::stream::iter(iter::once(Ok(http_body::Frame::data(buffer)))); let body = StreamBody::new(buffer.chain(BodyStream::new(stream.map_err(Some)))); let request = request.map(|()| body); match client.send_request(request, options).await? { diff --git a/crates/wasi-http/src/p3/host/types.rs b/crates/wasi-http/src/p3/host/types.rs index 92d702fe1a..b48c9e181d 100644 --- a/crates/wasi-http/src/p3/host/types.rs +++ b/crates/wasi-http/src/p3/host/types.rs @@ -31,7 +31,7 @@ use crate::p3::host::{ }; use crate::p3::{ Body, BodyContext, BodyFrame, ContentLength, Request, RequestOptions, Response, WasiHttpImpl, - WasiHttpView, + WasiHttpView, DEFAULT_BUFFER_CAPACITY, }; fn get_request_options<'a>( @@ -239,16 +239,21 @@ where } => { let mut contents_tx = self.contents_tx; match buffer { - Some(BodyFrame::Data(buf)) => { - let (tx_tail, buf) = contents_tx.write_all(buf).into_future().await; + Some(BodyFrame::Data(buffer)) => { + let (tx_tail, buffer) = contents_tx + .write_all(Cursor::new(buffer)) + .into_future() + .await; let Some(tx_tail) = tx_tail else { let Ok(mut body) = self.body.lock() else { bail!("lock poisoned"); }; + let pos = buffer.position().try_into()?; + let buffer = buffer.into_inner().split_off(pos); *body = Body::Guest { contents: Some(contents_rx), trailers: Some(trailers_rx), - buffer: Some(BodyFrame::Data(buf)), + buffer: Some(BodyFrame::Data(buffer)), tx, content_length, }; @@ -337,18 +342,21 @@ where return Ok(()); } } - let buf = rx_buffer.split().freeze(); - assert!(rx_buffer.capacity() > 0); + let buffer = rx_buffer.split().freeze(); + rx_buffer.reserve(DEFAULT_BUFFER_CAPACITY); contents_rx = rx_tail.read(rx_buffer).into_future(); - let (tx_tail, buf) = tx_tail.write_all(Cursor::new(buf)).into_future().await; + let (tx_tail, buffer) = + tx_tail.write_all(Cursor::new(buffer)).into_future().await; let Some(tx_tail) = tx_tail else { let Ok(mut body) = self.body.lock() else { bail!("lock poisoned"); }; + let pos = buffer.position().try_into()?; + let buffer = buffer.into_inner().split_off(pos); *body = Body::Guest { contents: Some(contents_rx), trailers: Some(trailers_rx), - buffer: Some(BodyFrame::Data(buf)), + buffer: Some(BodyFrame::Data(buffer)), tx, content_length, }; @@ -407,15 +415,20 @@ where } => { let mut contents_tx = self.contents_tx; match buffer { - Some(BodyFrame::Data(buf)) => { - let (tx_tail, buf) = contents_tx.write_all(buf).into_future().await; + Some(BodyFrame::Data(buffer)) => { + let (tx_tail, buffer) = contents_tx + .write_all(Cursor::new(buffer)) + .into_future() + .await; let Some(tx_tail) = tx_tail else { let Ok(mut body) = self.body.lock() else { bail!("lock poisoned"); }; + let pos = buffer.position().try_into()?; + let buffer = buffer.into_inner().split_off(pos); *body = Body::Host { stream: Some(stream), - buffer: Some(BodyFrame::Data(buf)), + buffer: Some(BodyFrame::Data(buffer)), }; return Ok(()); }; @@ -459,17 +472,19 @@ where } Some(Some(Ok(frame))) => { match frame.into_data().map_err(http_body::Frame::into_trailers) { - Ok(buf) => { + Ok(buffer) => { let tx_tail = contents_tx.into_inner(); - let (tx_tail, buf) = - tx_tail.write_all(Cursor::new(buf)).into_future().await; + let (tx_tail, buffer) = + tx_tail.write_all(Cursor::new(buffer)).into_future().await; let Some(tx_tail) = tx_tail else { let Ok(mut body) = self.body.lock() else { bail!("lock poisoned"); }; + let pos = buffer.position().try_into()?; + let buffer = buffer.into_inner().split_off(pos); *body = Body::Host { stream: Some(stream), - buffer: Some(BodyFrame::Data(buf)), + buffer: Some(BodyFrame::Data(buffer)), }; return Ok(()); }; @@ -774,7 +789,7 @@ where let contents = contents.map(|contents| { contents .into_reader(&mut view) - .read(BytesMut::with_capacity(8096)) + .read(BytesMut::with_capacity(DEFAULT_BUFFER_CAPACITY)) .into_future() }); let trailers = trailers.into_reader(&mut view).read().into_future(); @@ -1093,7 +1108,7 @@ where let contents = contents.map(|contents| { contents .into_reader(&mut view) - .read(BytesMut::with_capacity(8096)) + .read(BytesMut::with_capacity(DEFAULT_BUFFER_CAPACITY)) .into_future() }); let trailers = trailers.into_reader(&mut view).read().into_future(); diff --git a/crates/wasi-http/src/p3/mod.rs b/crates/wasi-http/src/p3/mod.rs index 393ae95bb6..bbc0558469 100644 --- a/crates/wasi-http/src/p3/mod.rs +++ b/crates/wasi-http/src/p3/mod.rs @@ -373,6 +373,9 @@ impl ResourceView for WasiHttpImpl { } } +/// Default byte buffer capacity to use +const DEFAULT_BUFFER_CAPACITY: usize = 1 << 13; + /// Set of [http::header::HeaderName], that are forbidden by default /// for requests and responses originating in the guest. pub const DEFAULT_FORBIDDEN_HEADERS: [http::header::HeaderName; 10] = [ diff --git a/crates/wasi-http/src/p3/response.rs b/crates/wasi-http/src/p3/response.rs index 7dc9d1941f..9b769cd023 100644 --- a/crates/wasi-http/src/p3/response.rs +++ b/crates/wasi-http/src/p3/response.rs @@ -127,11 +127,7 @@ impl Response { content_length, } => { let buffer = match buffer { - Some(BodyFrame::Data(buf)) => { - let position = buf.position(); - buf.into_inner() - .split_off(usize::try_from(position).unwrap()) - } + Some(BodyFrame::Data(buffer)) => buffer, Some(BodyFrame::Trailers(..)) => bail!("guest body is corrupted"), None => Bytes::default(), }; @@ -180,20 +176,13 @@ impl Response { Body::Host { stream: Some(stream), buffer: Some(BodyFrame::Data(buffer)), - } => { - let position = buffer.position(); - let buffer = futures::stream::iter(iter::once(Ok(http_body::Frame::data( - buffer - .into_inner() - .split_off(usize::try_from(position).unwrap()), - )))); - ( - BodyExt::boxed_unsync(StreamBody::new( - buffer.chain(BodyStream::new(stream.map_err(Some))), - )), - None, - ) - } + } => ( + BodyExt::boxed_unsync(StreamBody::new( + futures::stream::iter(iter::once(Ok(http_body::Frame::data(buffer)))) + .chain(BodyStream::new(stream.map_err(Some))), + )), + None, + ), Body::Host { .. } => bail!("host body is corrupted"), }; Ok((response.map(|()| body), tx)) From 3a25bb0587913d5151c518a95c3351cdb37d964c Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Fri, 4 Apr 2025 15:03:49 +0200 Subject: [PATCH 10/19] feat(p3/http): implement serving Signed-off-by: Roman Volosatovs --- crates/test-programs/src/bin/api_0_3_proxy.rs | 5 +- crates/wasi-http/src/p3/body.rs | 176 ++++-------------- crates/wasi-http/src/p3/client.rs | 2 - crates/wasi-http/src/p3/conv.rs | 29 ++- crates/wasi-http/src/p3/host/handler.rs | 19 +- crates/wasi-http/src/p3/proxy.rs | 38 +--- crates/wasi-http/src/p3/response.rs | 156 +++++++++++++--- 7 files changed, 211 insertions(+), 214 deletions(-) diff --git a/crates/test-programs/src/bin/api_0_3_proxy.rs b/crates/test-programs/src/bin/api_0_3_proxy.rs index 932fac04a0..420b0e404a 100644 --- a/crates/test-programs/src/bin/api_0_3_proxy.rs +++ b/crates/test-programs/src/bin/api_0_3_proxy.rs @@ -42,7 +42,10 @@ impl test_programs::p3::proxy::exports::wasi::http::handler::Guest for T { let remaining = contents_tx.write_all(b"hello, world!".to_vec()).await; assert!(remaining.is_empty()); drop(contents_tx); - trailers_tx.write(Ok(None)); + trailers_tx + .write(Ok(None)) + .await + .expect("failed to write trailers"); }, async { transmit diff --git a/crates/wasi-http/src/p3/body.rs b/crates/wasi-http/src/p3/body.rs index 0ea4b1e130..e79370f194 100644 --- a/crates/wasi-http/src/p3/body.rs +++ b/crates/wasi-http/src/p3/body.rs @@ -1,21 +1,15 @@ -use core::future::{poll_fn, Future}; +use core::future::Future; use core::mem; use core::pin::Pin; use core::task::{ready, Context, Poll}; -use std::io::Cursor; - -use anyhow::Context as _; use bytes::{Bytes, BytesMut}; use http::HeaderMap; use http_body_util::combinators::UnsyncBoxBody; use http_body_util::BodyExt as _; -use tokio::sync::oneshot; -use wasmtime::component::{ - AbortOnDropHandle, ErrorContext, FutureWriter, PromisesUnordered, Resource, StreamReader, -}; -use wasmtime::AsContextMut; -use wasmtime_wasi::p3::{ResourceView, WithChildren}; +use tokio::sync::{mpsc, oneshot}; +use wasmtime::component::{AbortOnDropHandle, FutureWriter, Resource, StreamReader}; +use wasmtime_wasi::p3::WithChildren; use crate::p3::bindings::http::types::ErrorCode; use crate::p3::DEFAULT_BUFFER_CAPACITY; @@ -31,13 +25,6 @@ pub(crate) type OutgoingTrailerFuture = Pin< >, >; -pub(crate) type OutgoingTrailerFutureMut<'a> = Pin< - &'a mut (dyn Future< - Output = Option>>, ErrorCode>>, - > + Send - + 'static), ->; - pub(crate) fn empty_body() -> impl http_body::Body> { http_body_util::Empty::new().map_err(|_| None) } @@ -148,75 +135,6 @@ impl Future for OutgoingRequestTrailers { } } -fn poll_outgoing_response_trailers( - cx: &mut Context<'_>, - mut store: impl AsContextMut, - trailers: OutgoingTrailerFutureMut<'_>, -) -> Poll>>> { - match ready!(trailers.poll(cx)) { - Some(Ok(Some(trailers))) => { - let mut store = store.as_context_mut(); - let table = store.data_mut().table(); - match table - .delete(trailers) - .context("failed to delete trailers") - .map(WithChildren::unwrap_or_clone) - { - Ok(Ok(trailers)) => Poll::Ready(Some(Ok(trailers))), - Ok(Err(err)) => Poll::Ready(Some(Err(Some(ErrorCode::InternalError(Some( - format!("{err:#}"), - )))))), - Err(err) => Poll::Ready(Some(Err(Some(ErrorCode::InternalError(Some(format!( - "{err:#}" - ))))))), - } - } - Some(Ok(None)) => Poll::Ready(None), - Some(Err(err)) => Poll::Ready(Some(Err(Some(err)))), - None => Poll::Ready(Some(Err(None))), - } -} - -pub(crate) async fn outgoing_response_trailers( - mut store: impl AsContextMut, - mut trailers: OutgoingTrailerFuture, -) -> Option>> -where - T: ResourceView, -{ - poll_fn(move |cx| poll_outgoing_response_trailers(cx, &mut store, trailers.as_mut())).await -} - -pub(crate) struct OutgoingResponseTrailers { - pub store: T, - pub trailers: Option, -} - -impl Future for OutgoingResponseTrailers -where - T: AsContextMut + Unpin, - T::Data: ResourceView, -{ - type Output = Option>>; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let &mut Self { - ref mut store, - trailers: Some(ref mut trailers), - } = &mut *self.as_mut() - else { - return Poll::Ready(None); - }; - let trailers = ready!(poll_outgoing_response_trailers( - cx, - store, - trailers.as_mut() - )); - self.trailers = None; - Poll::Ready(trailers) - } -} - /// Represents `Content-Length` limit and state #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct ContentLength { @@ -234,39 +152,27 @@ impl ContentLength { } /// Response body constructed by the guest -pub(crate) struct OutgoingResponseBody { - store: T, - contents: Option, - trailers: Option, +pub(crate) struct OutgoingResponseBody { + contents: Option>, buffer: Bytes, content_length: Option, - promises: PromisesUnordered, Bytes), Option>>, } -impl OutgoingResponseBody { +impl OutgoingResponseBody { pub fn new( - store: T, - contents: OutgoingContentsStreamFuture, - trailers: OutgoingTrailerFuture, + contents: mpsc::Receiver, buffer: Bytes, content_length: Option, ) -> Self { Self { - store, contents: Some(contents), - trailers: Some(trailers), buffer, content_length, - promises: PromisesUnordered::new(), } } } -impl http_body::Body for OutgoingResponseBody -where - T: AsContextMut + Send + Unpin, - T::Data: ResourceView + Send, -{ +impl http_body::Body for OutgoingResponseBody { type Data = Bytes; type Error = Option; @@ -293,40 +199,36 @@ where let Some(stream) = &mut self.contents else { return Poll::Ready(None); }; - todo!() - //self.contents.poll_promise(cx, &mut self.store); - //core::pin::pin!(&mut self.promises.next(&mut self.store)).poll(cx); - //match ready!(Pin::new(stream).poll(cx)) { - // Ok((tail, buf)) => { - // core::pin::pin!(tail.read().get(&mut self.store)).poll(cx); - // if let Some(ContentLength { limit, sent }) = &mut self.content_length { - // let Ok(n) = buf.len().try_into() else { - // return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(None))))); - // }; - // let Some(n) = sent.checked_add(n) else { - // return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(None))))); - // }; - // if n > *limit { - // return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(Some( - // n, - // )))))); - // } - // *sent = n; - // } - // Poll::Ready(Some(Ok(http_body::Frame::data(buf)))) - // } - // Err(..) => { - // self.contents = None; - // if let Some(ContentLength { limit, sent }) = self.content_length { - // if limit != sent { - // return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(Some( - // sent, - // )))))); - // } - // } - // Poll::Ready(None) - // } - //} + match ready!(stream.poll_recv(cx)) { + Some(buf) => { + if let Some(ContentLength { limit, sent }) = &mut self.content_length { + let Ok(n) = buf.len().try_into() else { + return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(None))))); + }; + let Some(n) = sent.checked_add(n) else { + return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(None))))); + }; + if n > *limit { + return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(Some( + n, + )))))); + } + *sent = n; + } + Poll::Ready(Some(Ok(http_body::Frame::data(buf)))) + } + None => { + self.contents = None; + if let Some(ContentLength { limit, sent }) = self.content_length { + if limit != sent { + return Poll::Ready(Some(Err(Some(ErrorCode::HttpRequestBodySize(Some( + sent, + )))))); + } + } + Poll::Ready(None) + } + } } fn is_end_stream(&self) -> bool { diff --git a/crates/wasi-http/src/p3/client.rs b/crates/wasi-http/src/p3/client.rs index 5063cc0e99..18de9bb74a 100644 --- a/crates/wasi-http/src/p3/client.rs +++ b/crates/wasi-http/src/p3/client.rs @@ -1,6 +1,4 @@ use core::future::Future; -use core::pin::Pin; -use core::task::{ready, Context, Poll}; use core::time::Duration; use bytes::Bytes; diff --git a/crates/wasi-http/src/p3/conv.rs b/crates/wasi-http/src/p3/conv.rs index 9bf50ed38d..aeafe2bd11 100644 --- a/crates/wasi-http/src/p3/conv.rs +++ b/crates/wasi-http/src/p3/conv.rs @@ -1,11 +1,13 @@ use core::convert::Infallible; use core::error::Error as _; +use std::sync::Arc; +use anyhow::{bail, Context as _}; use bytes::Bytes; use tracing::warn; use crate::p3::bindings::http::types::{ErrorCode, Method, Scheme}; -use crate::p3::{Body, Request}; +use crate::p3::{Body, Request, Response}; impl ErrorCode { /// Translate a [`hyper::Error`] to a wasi-http [ErrorCode] in the context of a request. @@ -186,3 +188,28 @@ where Self::new(body) } } + +impl TryFrom for http::Response { + type Error = anyhow::Error; + + fn try_from( + Response { + status, + headers, + body, + .. + }: Response, + ) -> Result { + let headers = headers.unwrap_or_clone()?; + let mut response = http::Response::builder().status(status); + *response.headers_mut().unwrap() = headers; + + let Some(body) = Arc::into_inner(body) else { + bail!("body is borrowed") + }; + let Ok(body) = body.into_inner() else { + bail!("lock poisoned"); + }; + response.body(body).context("failed to build response") + } +} diff --git a/crates/wasi-http/src/p3/host/handler.rs b/crates/wasi-http/src/p3/host/handler.rs index 3bcf39b5cb..6a6541acd9 100644 --- a/crates/wasi-http/src/p3/host/handler.rs +++ b/crates/wasi-http/src/p3/host/handler.rs @@ -115,6 +115,7 @@ where Ok(request) => request, Err(err) => return Ok(Err(ErrorCode::InternalError(Some(err.to_string())))), }; + let (request, ()) = request.into_parts(); let response = match body { Body::Guest { contents: None, @@ -141,7 +142,7 @@ where content_length: None, } => { let body = empty_body(); - let request = request.map(|()| body); + let request = http::Request::from_parts(request, body); match client.send_request(request, options).await? { Ok((response, io)) => { store.spawn(AccessorTaskFn(|_: &mut Accessor| async { @@ -171,7 +172,7 @@ where anyhow::Ok(trailers.clone()) })?; let body = empty_body().with_trailers(async move { Some(Ok(trailers)) }); - let request = request.map(|()| body); + let request = http::Request::from_parts(request, body); match client.send_request(request, options).await? { Ok((response, io)) => { store.spawn(AccessorTaskFn(|_: &mut Accessor| async { @@ -221,7 +222,7 @@ where trailers: Some(trailers_rx), trailer_task: task.abort_handle(), }); - let request = request.map(|()| body); + let request = http::Request::from_parts(request, body); match client.send_request(request, options).await? { Ok((response, io)) => { store.spawn(AccessorTaskFn(|_: &mut Accessor| async { @@ -261,7 +262,7 @@ where trailers: Some(trailers_rx), trailer_task: task.abort_handle(), }); - let request = request.map(|()| body); + let request = http::Request::from_parts(request, body); match client.send_request(request, options).await? { Ok((response, io)) => { store.spawn(AccessorTaskFn(|_: &mut Accessor| async { @@ -286,7 +287,7 @@ where buffer: None, } => { let body = stream.map_err(Some); - let request = request.map(|()| body); + let request = http::Request::from_parts(request, body); match client.send_request(request, options).await? { Ok((response, io)) => { store.spawn(AccessorTaskFn(|_: &mut Accessor| async { @@ -309,7 +310,7 @@ where } => { let buffer = futures::stream::iter(iter::once(Ok(http_body::Frame::data(buffer)))); let body = StreamBody::new(buffer.chain(BodyStream::new(stream.map_err(Some)))); - let request = request.map(|()| body); + let request = http::Request::from_parts(request, body); match client.send_request(request, options).await? { Ok((response, io)) => { store.spawn(AccessorTaskFn(|_: &mut Accessor| async { @@ -335,7 +336,7 @@ where anyhow::Ok(trailers.clone()) })?; let body = empty_body().with_trailers(async move { Some(Ok(trailers)) }); - let request = request.map(|()| body); + let request = http::Request::from_parts(request, body); match client.send_request(request, options).await? { Ok((response, io)) => { store.spawn(AccessorTaskFn(|_: &mut Accessor| async { @@ -357,7 +358,7 @@ where buffer: Some(BodyFrame::Trailers(Ok(None))), } => { let body = empty_body(); - let request = request.map(|()| body); + let request = http::Request::from_parts(request, body); match client.send_request(request, options).await? { Ok((response, io)) => { store.spawn(AccessorTaskFn(|_: &mut Accessor| async { @@ -381,7 +382,7 @@ where Body::Host { .. } => bail!("host body is corrupted"), Body::Consumed => { let body = empty_body(); - let request = request.map(|()| body); + let request = http::Request::from_parts(request, body); match client.send_request(request, options).await? { Ok((response, io)) => { store.spawn(AccessorTaskFn(|_: &mut Accessor| async { diff --git a/crates/wasi-http/src/p3/proxy.rs b/crates/wasi-http/src/p3/proxy.rs index 501e7e3646..2a13f1d686 100644 --- a/crates/wasi-http/src/p3/proxy.rs +++ b/crates/wasi-http/src/p3/proxy.rs @@ -1,7 +1,5 @@ use anyhow::Context as _; -use bytes::Bytes; -use http_body_util::combinators::UnsyncBoxBody; -use wasmtime::component::{FutureWriter, Promise, Resource}; +use wasmtime::component::{Promise, Resource}; use wasmtime::AsContextMut; use wasmtime_wasi::p3::ResourceView; @@ -11,7 +9,7 @@ use crate::p3::{Request, Response}; impl Proxy { /// Call `handle` on [Proxy] getting a [Promise] back. - async fn handle_promise( + pub async fn handle( &self, mut store: impl AsContextMut, req: impl Into, @@ -26,36 +24,4 @@ impl Proxy { .context("failed to push request to table")?; self.wasi_http_handler().call_handle(&mut store, req).await } - - /// Call `handle` on [Proxy]. - pub async fn handle( - &self, - mut store: impl AsContextMut + Send + Unpin + 'static, - req: impl Into, - ) -> wasmtime::Result< - Result< - ( - http::Response>>, - Option>>, - ), - ErrorCode, - >, - > - where - T: ResourceView + Send + 'static, - { - let handle = self.handle_promise(&mut store, req).await?; - match handle.get(&mut store).await? { - Ok(res) => { - let res = store - .as_context_mut() - .data_mut() - .table() - .delete(res) - .context("failed to delete response from table")?; - res.into_http(store).map(Ok) - } - Err(err) => Ok(Err(err)), - } - } } diff --git a/crates/wasi-http/src/p3/response.rs b/crates/wasi-http/src/p3/response.rs index 9b769cd023..c87184fc26 100644 --- a/crates/wasi-http/src/p3/response.rs +++ b/crates/wasi-http/src/p3/response.rs @@ -8,13 +8,15 @@ use futures::StreamExt as _; use http::{HeaderMap, StatusCode}; use http_body_util::combinators::UnsyncBoxBody; use http_body_util::{BodyExt, BodyStream, StreamBody}; -use wasmtime::component::{AbortOnDropHandle, FutureWriter}; -use wasmtime::AsContextMut; +use tokio::sync::{mpsc, oneshot}; +use wasmtime::component::{AbortOnDropHandle, FutureWriter, Instance, Promise, Resource}; +use wasmtime::{AsContextMut, StoreContextMut}; use wasmtime_wasi::p3::{ResourceView, WithChildren}; use crate::p3::bindings::http::types::ErrorCode; use crate::p3::{ - empty_body, outgoing_response_trailers, Body, BodyFrame, ContentLength, OutgoingResponseBody, + empty_body, Body, BodyFrame, ContentLength, OutgoingResponseBody, OutgoingTrailerFuture, + DEFAULT_BUFFER_CAPACITY, }; /// The concrete type behind a `wasi:http/types/response` resource. @@ -29,6 +31,53 @@ pub struct Response { pub(crate) body_task: Option, } +async fn receive_trailers( + rx: oneshot::Receiver, ErrorCode>>>, +) -> Option>> { + match rx.await { + Ok(Some(Ok(trailers))) => match trailers.unwrap_or_clone() { + Ok(trailers) => Some(Ok(trailers)), + Err(err) => Some(Err(Some(ErrorCode::InternalError(Some(format!( + "{err:#}" + )))))), + }, + Ok(Some(Err(err))) => Some(Err(Some(err))), + Ok(None) => None, + Err(..) => Some(Err(None)), + } +} + +async fn handle_guest_trailers( + rx: OutgoingTrailerFuture, + tx: oneshot::Sender, ErrorCode>>>, +) -> ResponsePromiseClosure { + let Some(trailers) = rx.await else { + return Box::new(|_| Ok(())); + }; + match trailers { + Ok(Some(trailers)) => Box::new(|mut store| { + let table = store.data_mut().table(); + let trailers = table + .delete(trailers) + .context("failed to delete trailers")?; + _ = tx.send(Some(Ok(trailers))); + Ok(()) + }), + Ok(None) => Box::new(|_| { + _ = tx.send(None); + Ok(()) + }), + Err(err) => Box::new(|_| { + _ = tx.send(Some(Err(err))); + Ok(()) + }), + } +} + +/// Closure returned by promise returned by [`Response::into_http`] +pub type ResponsePromiseClosure = + Box FnOnce(StoreContextMut<'a, T>) -> wasmtime::Result<()> + Send + Sync + 'static>; + impl Response { /// Construct a new [Response] pub fn new(status: StatusCode, headers: HeaderMap, body: Body) -> Self { @@ -40,26 +89,47 @@ impl Response { } } + /// Delete [Response] from table associated with `T` + /// and call [Self::into_http]. + /// See [Self::into_http] for documentation on return values of this function. + pub fn resource_into_http( + mut store: impl AsContextMut, + instance: &Instance, + res: Resource, + ) -> wasmtime::Result<( + http::Response>>, + Option>>, + Option>>, + )> + where + T: ResourceView + Send + 'static, + { + let mut store = store.as_context_mut(); + let res = store + .data_mut() + .table() + .delete(res) + .context("failed to delete response from table")?; + res.into_http(store, instance) + } + /// Convert [Response] into [http::Response]. + /// This function will return a [`FutureWriter`], if the response was created + /// by the guest using `wasi:http/types#[constructor]response.new` + /// This function may return a [`Promise`], which must be awaited + /// to drive I/O for bodies originating from the guest. pub fn into_http( self, - mut store: impl AsContextMut + Send + Unpin + 'static, + mut store: impl AsContextMut, + instance: &Instance, ) -> anyhow::Result<( http::Response>>, Option>>, + Option>>, )> { - let headers = self.headers.unwrap_or_clone()?; - let mut response = http::Response::builder().status(self.status); - *response.headers_mut().unwrap() = headers; - let response = response.body(()).context("failed to build response")?; - - let Some(body) = Arc::into_inner(self.body) else { - bail!("body is borrowed") - }; - let Ok(body) = body.into_inner() else { - bail!("lock poisoned"); - }; - let (body, tx) = match body { + let response = http::Response::try_from(self)?; + let (response, body) = response.into_parts(); + let (body, tx, promise) = match body { Body::Guest { contents: None, buffer: None | Some(BodyFrame::Trailers(Ok(None))), @@ -74,7 +144,7 @@ impl Response { buffer: Some(BodyFrame::Trailers(Ok(None))), tx, .. - } => (empty_body().boxed_unsync(), Some(tx)), + } => (empty_body().boxed_unsync(), Some(tx), None), Body::Guest { contents: None, trailers: None, @@ -93,6 +163,7 @@ impl Response { .with_trailers(async move { Some(Ok(trailers)) }) .boxed_unsync(), Some(tx), + None, ) } Body::Guest { @@ -106,6 +177,7 @@ impl Response { .with_trailers(async move { Some(Err(Some(err))) }) .boxed_unsync(), Some(tx), + None, ), Body::Guest { contents: None, @@ -114,35 +186,60 @@ impl Response { tx, .. } => { + let (trailers_tx, trailers_rx) = oneshot::channel(); let body = empty_body() - .with_trailers(outgoing_response_trailers(store, trailers)) + .with_trailers(receive_trailers(trailers_rx)) .boxed_unsync(); - (body, Some(tx)) + let fut = Box::pin(handle_guest_trailers(trailers, trailers_tx)); + let promise = instance.promise(store, fut); + (body, Some(tx), Some(promise)) } Body::Guest { - contents: Some(contents), + contents: Some(mut contents), trailers: Some(trailers), buffer, tx, content_length, } => { + let (contents_tx, contents_rx) = mpsc::channel(1); + let (trailers_tx, trailers_rx) = oneshot::channel(); let buffer = match buffer { Some(BodyFrame::Data(buffer)) => buffer, Some(BodyFrame::Trailers(..)) => bail!("guest body is corrupted"), None => Bytes::default(), }; - eprintln!("return body with trailers"); - let body = - OutgoingResponseBody::new(store, contents, trailers, buffer, content_length) - .boxed_unsync(); - (body, Some(tx)) + let body = OutgoingResponseBody::new(contents_rx, buffer, content_length) + .with_trailers(receive_trailers(trailers_rx)) + .boxed_unsync(); + let fut = Box::pin(async move { + loop { + let (tail, mut rx_buffer) = contents.await; + if let Some(tail) = tail { + let buffer = rx_buffer.split(); + if !buffer.is_empty() { + if let Err(..) = contents_tx.send(buffer.freeze()).await { + break; + } + rx_buffer.reserve(DEFAULT_BUFFER_CAPACITY); + } + contents = tail.read(rx_buffer).into_future(); + } else { + debug_assert!(rx_buffer.is_empty()); + break; + } + } + drop(contents_tx); + handle_guest_trailers(trailers, trailers_tx).await + }); + let promise = instance.promise(store, fut); + (body, Some(tx), Some(promise)) } Body::Guest { .. } => bail!("guest body is corrupted"), Body::Consumed | Body::Host { stream: None, buffer: Some(BodyFrame::Trailers(Ok(None))), - } => (empty_body().boxed_unsync(), None), + } => (empty_body().boxed_unsync(), None, None), Body::Host { stream: None, buffer: Some(BodyFrame::Trailers(Ok(Some(trailers)))), @@ -158,6 +255,7 @@ impl Response { .with_trailers(async move { Some(Ok(trailers)) }) .boxed_unsync(), None, + None, ) } Body::Host { @@ -168,11 +266,12 @@ impl Response { .with_trailers(async move { Some(Err(Some(err))) }) .boxed_unsync(), None, + None, ), Body::Host { stream: Some(stream), buffer: None, - } => (stream.map_err(Some).boxed_unsync(), None), + } => (stream.map_err(Some).boxed_unsync(), None, None), Body::Host { stream: Some(stream), buffer: Some(BodyFrame::Data(buffer)), @@ -182,9 +281,10 @@ impl Response { .chain(BodyStream::new(stream.map_err(Some))), )), None, + None, ), Body::Host { .. } => bail!("host body is corrupted"), }; - Ok((response.map(|()| body), tx)) + Ok((http::Response::from_parts(response, body), tx, promise)) } } From 045ef9eb73220a81b337e0e91b5ad8c210fee233 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Fri, 4 Apr 2025 15:39:15 +0200 Subject: [PATCH 11/19] feat(p3/http): implement `wasmtime serve` Signed-off-by: Roman Volosatovs --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ccbed2c141..63e32cb656 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -476,7 +476,7 @@ disable-logging = ["log/max_level_off", "tracing/max_level_off"] wasi-nn = ["dep:wasmtime-wasi-nn"] wasi-tls = ["dep:wasmtime-wasi-tls"] wasi-threads = ["dep:wasmtime-wasi-threads", "threads"] -wasi-http = ["component-model", "dep:wasmtime-wasi-http", "dep:tokio", "dep:hyper"] +wasi-http = ["component-model", "dep:wasmtime-wasi-http", "wasmtime-wasi-http/p3", "dep:tokio", "dep:hyper"] wasi-config = ["dep:wasmtime-wasi-config"] wasi-keyvalue = ["dep:wasmtime-wasi-keyvalue"] pooling-allocator = ["wasmtime/pooling-allocator", "wasmtime-cli-flags/pooling-allocator"] From c5bd4c8e403307a553640a87d6ad84bb81a87d26 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Fri, 4 Apr 2025 16:52:13 +0200 Subject: [PATCH 12/19] feat(p3/http): link p3 http in `run` Signed-off-by: Roman Volosatovs --- crates/wasi-http/src/p3/mod.rs | 2 +- src/commands/run.rs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/wasi-http/src/p3/mod.rs b/crates/wasi-http/src/p3/mod.rs index bbc0558469..09aba21bf5 100644 --- a/crates/wasi-http/src/p3/mod.rs +++ b/crates/wasi-http/src/p3/mod.rs @@ -408,7 +408,7 @@ pub trait WasiHttpView: ResourceView + Send { } /// Capture the state necessary for use in the wasi-http API implementation. -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] pub struct WasiHttpCtx where C: Client, diff --git a/src/commands/run.rs b/src/commands/run.rs index e0b52fec8f..4734e1411a 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -872,10 +872,12 @@ impl RunCommand { } CliLinker::Component(linker) => { wasmtime_wasi_http::add_only_http_to_linker_sync(linker)?; + wasmtime_wasi_http::p3::add_only_http_to_linker(linker)?; } } store.data_mut().wasi_http = Some(Arc::new(WasiHttpCtx::new())); + store.data_mut().p3_http = Some(wasmtime_wasi_http::p3::WasiHttpCtx::default()); } } @@ -1067,6 +1069,7 @@ struct Host { p3_filesystem: Option, p3_random: Option>>, p3_sockets: Option, + p3_http: Option, #[cfg(feature = "wasi-nn")] wasi_nn_wit: Option>, @@ -1186,6 +1189,17 @@ impl wasmtime_wasi_http::types::WasiHttpView for Host { } } +#[cfg(feature = "wasi-http")] +impl wasmtime_wasi_http::p3::WasiHttpView for Host { + type Client = wasmtime_wasi_http::p3::DefaultClient; + + fn http(&self) -> &wasmtime_wasi_http::p3::WasiHttpCtx { + self.p3_http + .as_ref() + .expect("`wasi:http@0.3` not configured") + } +} + #[cfg(not(unix))] fn ctx_set_listenfd(num_fd: usize, _builder: &mut WasiCtxBuilder) -> Result { Ok(num_fd) From ae3783609ce84d03f7cfd652e20dd7f1706db727 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Fri, 4 Apr 2025 17:15:15 +0200 Subject: [PATCH 13/19] fix(p3/http): always set "path and query" Signed-off-by: Roman Volosatovs --- crates/wasi-http/src/p3/host/handler.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/wasi-http/src/p3/host/handler.rs b/crates/wasi-http/src/p3/host/handler.rs index 6a6541acd9..f6ebc21e73 100644 --- a/crates/wasi-http/src/p3/host/handler.rs +++ b/crates/wasi-http/src/p3/host/handler.rs @@ -9,6 +9,7 @@ use http::header::HOST; use http::{HeaderValue, Uri}; use http_body_util::{BodyExt as _, BodyStream, StreamBody}; use tokio::sync::oneshot; +use tracing::debug; use wasmtime::component::{Accessor, AccessorTask, Resource}; use wasmtime_wasi::p3::{AccessorTaskFn, ResourceView as _}; @@ -95,9 +96,15 @@ where }; if let Some(path_with_query) = path_with_query { uri = uri.path_and_query(path_with_query) + } else { + uri = uri.path_and_query("/") }; - let Ok(uri) = uri.build() else { - return Ok(Err(ErrorCode::HttpRequestUriInvalid)); + let uri = match uri.build() { + Ok(uri) => uri, + Err(err) => { + debug!(?err, "failed to build request URI"); + return Ok(Err(ErrorCode::HttpRequestUriInvalid)); + } }; let Some(body) = Arc::into_inner(body) else { From 50528df5bc19107c8fe97435ec89f1794f273663 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Fri, 4 Apr 2025 17:21:21 +0200 Subject: [PATCH 14/19] fix: feature-gate CLI HTTP support Signed-off-by: Roman Volosatovs --- src/commands/run.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/run.rs b/src/commands/run.rs index 4734e1411a..79c3d72721 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -1069,6 +1069,7 @@ struct Host { p3_filesystem: Option, p3_random: Option>>, p3_sockets: Option, + #[cfg(feature = "wasi-http")] p3_http: Option, #[cfg(feature = "wasi-nn")] From bd2d948d7fa5a8cb3082561e960d932544a1ab09 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Fri, 4 Apr 2025 17:32:27 +0200 Subject: [PATCH 15/19] avoid setting a default path and query Personally, I think defaulting to `/` if no path with query is set would be the expected behavior, but wasip2 contained tests explicitly testing against that behavior, so I disagree and commit Signed-off-by: Roman Volosatovs --- crates/wasi-http/src/p3/host/handler.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/wasi-http/src/p3/host/handler.rs b/crates/wasi-http/src/p3/host/handler.rs index f6ebc21e73..260c3717a9 100644 --- a/crates/wasi-http/src/p3/host/handler.rs +++ b/crates/wasi-http/src/p3/host/handler.rs @@ -96,8 +96,6 @@ where }; if let Some(path_with_query) = path_with_query { uri = uri.path_and_query(path_with_query) - } else { - uri = uri.path_and_query("/") }; let uri = match uri.build() { Ok(uri) => uri, From 85f88367a7c4f620759cdc764b170694cfa7b3af Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Fri, 4 Apr 2025 18:31:27 +0200 Subject: [PATCH 16/19] fix tracing import Signed-off-by: Roman Volosatovs --- crates/wasi-http/src/p3/client.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/wasi-http/src/p3/client.rs b/crates/wasi-http/src/p3/client.rs index 18de9bb74a..20bd953e2f 100644 --- a/crates/wasi-http/src/p3/client.rs +++ b/crates/wasi-http/src/p3/client.rs @@ -6,7 +6,6 @@ use http::uri::Scheme; use http_body_util::BodyExt as _; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::TcpStream; -use tracing::warn; use crate::p3::bindings::http::types::{DnsErrorPayload, ErrorCode}; use crate::p3::RequestOptions; @@ -214,12 +213,12 @@ pub async fn default_send_request( let host = parts.next().unwrap_or(&authority); let domain = ServerName::try_from(host) .map_err(|e| { - warn!("dns lookup error: {e:?}"); + tracing::warn!("dns lookup error: {e:?}"); dns_error("invalid dns name".to_string(), 0) })? .to_owned(); let stream = connector.connect(domain, stream).await.map_err(|e| { - warn!("tls protocol error: {e:?}"); + tracing::warn!("tls protocol error: {e:?}"); ErrorCode::TlsProtocolError })?; stream.boxed() From d9865b3b0f28441216d6a3905f6cc9376d9f917e Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Mon, 7 Apr 2025 13:57:08 +0200 Subject: [PATCH 17/19] http: enable TLS on riscv64 Signed-off-by: Roman Volosatovs --- crates/wasi-http/src/p3/client.rs | 58 ++++++++++++---------------- crates/wasi-http/tests/all/p3/mod.rs | 2 - 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/crates/wasi-http/src/p3/client.rs b/crates/wasi-http/src/p3/client.rs index 20bd953e2f..c1da60a2bb 100644 --- a/crates/wasi-http/src/p3/client.rs +++ b/crates/wasi-http/src/p3/client.rs @@ -7,9 +7,9 @@ use http_body_util::BodyExt as _; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::TcpStream; +use crate::io::TokioIo; use crate::p3::bindings::http::types::{DnsErrorPayload, ErrorCode}; -use crate::p3::RequestOptions; -use crate::{io::TokioIo, p3::IncomingResponseBody}; +use crate::p3::{IncomingResponseBody, RequestOptions}; fn dns_error(rcode: String, info_code: u16) -> ErrorCode { ErrorCode::DnsError(DnsErrorPayload { @@ -190,39 +190,29 @@ pub async fn default_send_request( Err(..) => return Err(ErrorCode::ConnectionTimeout), }; let stream = if use_tls { - #[cfg(any(target_arch = "riscv64", target_arch = "s390x"))] - { - return Err(ErrorCode::InternalError(Some( - "unsupported architecture for SSL".to_string(), - ))); - } - - #[cfg(not(any(target_arch = "riscv64", target_arch = "s390x")))] - { - use rustls::pki_types::ServerName; + use rustls::pki_types::ServerName; - // derived from https://github.com/rustls/rustls/blob/main/examples/src/bin/simpleclient.rs - let root_cert_store = rustls::RootCertStore { - roots: webpki_roots::TLS_SERVER_ROOTS.into(), - }; - let config = rustls::ClientConfig::builder() - .with_root_certificates(root_cert_store) - .with_no_client_auth(); - let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(config)); - let mut parts = authority.split(":"); - let host = parts.next().unwrap_or(&authority); - let domain = ServerName::try_from(host) - .map_err(|e| { - tracing::warn!("dns lookup error: {e:?}"); - dns_error("invalid dns name".to_string(), 0) - })? - .to_owned(); - let stream = connector.connect(domain, stream).await.map_err(|e| { - tracing::warn!("tls protocol error: {e:?}"); - ErrorCode::TlsProtocolError - })?; - stream.boxed() - } + // derived from https://github.com/rustls/rustls/blob/main/examples/src/bin/simpleclient.rs + let root_cert_store = rustls::RootCertStore { + roots: webpki_roots::TLS_SERVER_ROOTS.into(), + }; + let config = rustls::ClientConfig::builder() + .with_root_certificates(root_cert_store) + .with_no_client_auth(); + let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(config)); + let mut parts = authority.split(":"); + let host = parts.next().unwrap_or(&authority); + let domain = ServerName::try_from(host) + .map_err(|e| { + tracing::warn!("dns lookup error: {e:?}"); + dns_error("invalid dns name".to_string(), 0) + })? + .to_owned(); + let stream = connector.connect(domain, stream).await.map_err(|e| { + tracing::warn!("tls protocol error: {e:?}"); + ErrorCode::TlsProtocolError + })?; + stream.boxed() } else { stream.boxed() }; diff --git a/crates/wasi-http/tests/all/p3/mod.rs b/crates/wasi-http/tests/all/p3/mod.rs index 3336edcb65..0e4092de2b 100644 --- a/crates/wasi-http/tests/all/p3/mod.rs +++ b/crates/wasi-http/tests/all/p3/mod.rs @@ -538,8 +538,6 @@ async fn wasi_http_proxy_tests() -> anyhow::Result<()> { //} // //#[test_log::test(tokio::test)] -//// test uses TLS but riscv/s390x don't support that yet -//#[cfg_attr(any(target_arch = "riscv64", target_arch = "s390x"), ignore)] //async fn wasi_http_without_port() -> Result<()> { // let req = hyper::Request::builder() // .method(http::Method::GET) From 9a4a0ae508e66554430d0a2296a99050b324c5f2 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Mon, 7 Apr 2025 13:59:49 +0200 Subject: [PATCH 18/19] cli/serve: integrate shutdown from upstream Signed-off-by: Roman Volosatovs --- src/commands/serve.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/commands/serve.rs b/src/commands/serve.rs index c39b99f219..13c9ee079c 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -564,7 +564,13 @@ impl ServeCommand { let next_id = Arc::new(AtomicU64::default()); let cmd = Arc::new(self); loop { - let (stream, _) = listener.accept().await?; + // Wait for a socket, but also "race" against shutdown to break out + // of this loop. Once the graceful shutdown signal is received then + // this loop exits immediately. + let (stream, _) = tokio::select! { + _ = shutdown.requested.notified() => break, + v = listener.accept() => v?, + }; let engine = engine.clone(); let cmd = Arc::clone(&cmd); let next_id = Arc::clone(&next_id); From 5e8f880803098f244ef000297ec03f7151d22e01 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Mon, 7 Apr 2025 14:33:16 +0200 Subject: [PATCH 19/19] ignore failing riscv64 tests https://github.com/bytecodealliance/wasip3-prototyping/issues/105 Signed-off-by: Roman Volosatovs --- crates/wasi-http/tests/all/p3/outgoing.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/wasi-http/tests/all/p3/outgoing.rs b/crates/wasi-http/tests/all/p3/outgoing.rs index 701902ce33..cb7399be40 100644 --- a/crates/wasi-http/tests/all/p3/outgoing.rs +++ b/crates/wasi-http/tests/all/p3/outgoing.rs @@ -75,12 +75,20 @@ async fn http_0_3_outbound_request_invalid_version() -> anyhow::Result<()> { run(HTTP_0_3_OUTBOUND_REQUEST_INVALID_VERSION_COMPONENT, &server).await } +#[cfg_attr( + target_arch = "riscv64", + ignore = "https://github.com/bytecodealliance/wasip3-prototyping/issues/105" +)] #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_0_3_outbound_request_invalid_header() -> anyhow::Result<()> { let server = Server::http2(1)?; run(HTTP_0_3_OUTBOUND_REQUEST_INVALID_HEADER_COMPONENT, &server).await } +#[cfg_attr( + target_arch = "riscv64", + ignore = "https://github.com/bytecodealliance/wasip3-prototyping/issues/105" +)] #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_0_3_outbound_request_unknown_method() -> anyhow::Result<()> { let server = Server::http1(1)?; @@ -97,6 +105,10 @@ async fn http_0_3_outbound_request_unsupported_scheme() -> anyhow::Result<()> { .await } +#[cfg_attr( + target_arch = "riscv64", + ignore = "https://github.com/bytecodealliance/wasip3-prototyping/issues/105" +)] #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_0_3_outbound_request_invalid_port() -> anyhow::Result<()> { let server = Server::http1(1)?; @@ -109,6 +121,10 @@ async fn http_0_3_outbound_request_invalid_dnsname() -> anyhow::Result<()> { run(HTTP_0_3_OUTBOUND_REQUEST_INVALID_DNSNAME_COMPONENT, &server).await } +#[cfg_attr( + target_arch = "riscv64", + ignore = "https://github.com/bytecodealliance/wasip3-prototyping/issues/105" +)] #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_0_3_outbound_request_response_build() -> anyhow::Result<()> { let server = Server::http1(1)?; @@ -121,6 +137,10 @@ async fn http_0_3_outbound_request_content_length() -> anyhow::Result<()> { run(HTTP_0_3_OUTBOUND_REQUEST_CONTENT_LENGTH_COMPONENT, &server).await } +#[cfg_attr( + target_arch = "riscv64", + ignore = "https://github.com/bytecodealliance/wasip3-prototyping/issues/105" +)] #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn http_0_3_outbound_request_missing_path_and_query() -> anyhow::Result<()> { let server = Server::http1(1)?;