diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 782cbe97c3cc..508d9efb2390 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -413,6 +413,7 @@ jobs: - name: wasmtime-wasi-http checks: | -p wasmtime-wasi-http --no-default-features + -p wasmtime-wasi-http --no-default-features --features p2 -p wasmtime-wasi-http --no-default-features --features p3 -p wasmtime-wasi-http --no-default-features --features p3 --all-targets diff --git a/Cargo.toml b/Cargo.toml index 5eb62d5956fa..7e5d171bfb1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -576,6 +576,7 @@ serve = [ "dep:http-body-util", "dep:http", "wasmtime-cli-flags/async", + "wasmtime-wasi-http?/p2", ] explore = ["dep:wasmtime-explorer", "dep:tempfile"] wast = ["dep:wasmtime-wast"] @@ -589,6 +590,7 @@ run = [ "dep:wasi-common", "dep:tokio", "wasmtime-cli-flags/async", + "wasmtime-wasi-http?/p2", ] completion = ["dep:clap_complete"] objdump = [ diff --git a/crates/wasi-http/Cargo.toml b/crates/wasi-http/Cargo.toml index 042e04a2f3a9..7eb31ae2de4e 100644 --- a/crates/wasi-http/Cargo.toml +++ b/crates/wasi-http/Cargo.toml @@ -15,8 +15,9 @@ workspace = true all-features = true [features] -default = ["default-send-request"] +default = ["default-send-request", "p2"] default-send-request = ["dep:tokio-rustls", "dep:rustls", "dep:webpki-roots"] +p2 = ["wasmtime-wasi/p2"] p3 = ["wasmtime-wasi/p3", "dep:tokio-util"] component-model-async = ["futures/alloc", "wasmtime/component-model-async"] diff --git a/crates/wasi-http/src/ctx.rs b/crates/wasi-http/src/ctx.rs new file mode 100644 index 000000000000..0ae1a9678e22 --- /dev/null +++ b/crates/wasi-http/src/ctx.rs @@ -0,0 +1,41 @@ +/// Default maximum size for the contents of a fields resource. +/// +/// Typically, HTTP proxies limit headers to 8k. This number is higher than that +/// because it not only includes the wire-size of headers but it additionally +/// includes factors for the in-memory representation of `HeaderMap`. This is in +/// theory high enough that no one runs into it but low enough such that a +/// completely full `HeaderMap` doesn't break the bank in terms of memory +/// consumption. +const DEFAULT_FIELD_SIZE_LIMIT: usize = 128 * 1024; + +/// Capture the state necessary for use in the wasi-http API implementation. +#[derive(Debug, Clone)] +pub struct WasiHttpCtx { + pub(crate) field_size_limit: usize, +} + +impl WasiHttpCtx { + /// Create a new context. + pub fn new() -> Self { + Self { + field_size_limit: DEFAULT_FIELD_SIZE_LIMIT, + } + } + + /// Set the maximum size for any fields resources created by this context. + /// + /// The limit specified here is roughly a byte limit for the size of the + /// in-memory representation of headers. This means that the limit needs to + /// be larger than the literal representation of headers on the wire to + /// account for in-memory Rust-side data structures representing the header + /// names/values/etc. + pub fn set_field_size_limit(&mut self, limit: usize) { + self.field_size_limit = limit; + } +} + +impl Default for WasiHttpCtx { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/wasi-http/src/handler.rs b/crates/wasi-http/src/handler.rs index c8496710d199..ce646a78a560 100644 --- a/crates/wasi-http/src/handler.rs +++ b/crates/wasi-http/src/handler.rs @@ -24,6 +24,7 @@ use wasmtime::{Result, Store, StoreContextMut, format_err}; /// Alternative p2 bindings generated with `exports: { default: async | store }` /// so we can use `TypedFunc::call_concurrent` with both p2 and p3 instances. +#[cfg(feature = "p2")] pub mod p2 { #[expect(missing_docs, reason = "bindgen-generated code")] pub mod bindings { @@ -35,7 +36,7 @@ pub mod p2 { require_store_data_send: true, with: { // http is in this crate - "wasi:http": crate::bindings::http, + "wasi:http": crate::p2::bindings::http, // Upstream package dependencies "wasi:io": wasmtime_wasi::p2::bindings::io, } @@ -49,6 +50,7 @@ pub mod p2 { /// `wasi:http/handler@0.3.x` pre-instance. pub enum ProxyPre { /// A `wasi:http/incoming-handler@0.2.x` pre-instance. + #[cfg(feature = "p2")] P2(p2::bindings::ProxyPre), /// A `wasi:http/handler@0.3.x` pre-instance. #[cfg(feature = "p3")] @@ -61,6 +63,7 @@ impl ProxyPre { T: Send, { Ok(match self { + #[cfg(feature = "p2")] Self::P2(pre) => Proxy::P2(pre.instantiate_async(store).await?), #[cfg(feature = "p3")] Self::P3(pre) => Proxy::P3(pre.instantiate_async(store).await?), @@ -72,6 +75,7 @@ impl ProxyPre { /// `wasi:http/handler@0.3.x` instance. pub enum Proxy { /// A `wasi:http/incoming-handler@0.2.x` instance. + #[cfg(feature = "p2")] P2(p2::bindings::Proxy), /// A `wasi:http/handler@0.3.x` instance. #[cfg(feature = "p3")] diff --git a/crates/wasi-http/src/lib.rs b/crates/wasi-http/src/lib.rs index 358682ddc91b..cdf8648ac37a 100644 --- a/crates/wasi-http/src/lib.rs +++ b/crates/wasi-http/src/lib.rs @@ -1,402 +1,50 @@ -//! # Wasmtime's WASI HTTP Implementation +//! Wasmtime's implementation of `wasi:http` //! -//! 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::p2::add_to_linker_async`] for example. -//! * Add individual interfaces such as with the -//! [`bindings::http::outgoing_handler::add_to_linker`] 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 wasmtime::bail; -//! use hyper::server::conn::http1; -//! use std::sync::Arc; -//! use tokio::net::TcpListener; -//! use wasmtime::component::{Component, Linker, ResourceTable}; -//! use wasmtime::{Engine, Result, Store}; -//! use wasmtime_wasi::{WasiCtx, WasiCtxView, 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 engine = Engine::default(); -//! -//! // 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::p2::add_to_linker_async(&mut linker)?; -//! wasmtime_wasi_http::add_only_http_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: WasiCtx::builder().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(Ok(())) => { -//! bail!("guest never invoked `response-outparam::set` method") -//! } -//! Ok(Err(e)) => e, -//! Err(e) => e.into(), -//! }; -//! return Err(e.context("guest never invoked `response-outparam::set` method")); -//! } -//! } -//! } -//! } -//! -//! struct MyClientState { -//! wasi: WasiCtx, -//! http: WasiHttpCtx, -//! table: ResourceTable, -//! } -//! -//! impl WasiView for MyClientState { -//! fn ctx(&mut self) -> WasiCtxView<'_> { -//! WasiCtxView { ctx: &mut self.wasi, table: &mut self.table } -//! } -//! } -//! -//! impl WasiHttpView for MyClientState { -//! fn ctx(&mut self) -> &mut WasiHttpCtx { -//! &mut self.http -//! } -//! -//! fn table(&mut self) -> &mut ResourceTable { -//! &mut self.table -//! } -//! } -//! ``` +//! This crate is organized similarly to [`wasmtime_wasi`] where there is a +//! top-level [`p2`] and [`p3`] module corresponding to the implementation for +//! WASIp2 and WASIp3. #![deny(missing_docs)] #![doc(test(attr(deny(warnings))))] #![doc(test(attr(allow(dead_code, unused_variables, unused_mut))))] #![cfg_attr(docsrs, feature(doc_cfg))] -mod error; -mod http_impl; -mod types_impl; +use http::{HeaderName, header}; -pub mod body; +mod ctx; #[cfg(feature = "component-model-async")] pub mod handler; pub mod io; -pub mod types; - -pub mod bindings; - +#[cfg(feature = "p2")] +pub mod p2; #[cfg(feature = "p3")] pub mod p3; -pub use crate::error::{ - HttpError, HttpResult, http_request_error, hyper_request_error, hyper_response_error, -}; -#[doc(inline)] -pub use crate::types::{ - DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, DEFAULT_OUTGOING_BODY_CHUNK_SIZE, WasiHttpCtx, - WasiHttpImpl, WasiHttpView, -}; -use http::header::CONTENT_LENGTH; -use wasmtime::component::{HasData, Linker}; - -/// 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. For embeddings with async support disabled see -/// [`add_to_linker_sync`] instead. -/// -/// # Example -/// -/// ``` -/// use wasmtime::{Engine, Result}; -/// use wasmtime::component::{ResourceTable, Linker}; -/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; -/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; -/// -/// fn main() -> Result<()> { -/// let engine = Engine::default(); -/// -/// 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 WasiHttpView for MyState { -/// fn ctx(&mut self) -> &mut WasiHttpCtx { &mut self.http_ctx } -/// fn table(&mut self) -> &mut ResourceTable { &mut self.table } -/// } -/// -/// impl WasiView for MyState { -/// fn ctx(&mut self) -> WasiCtxView<'_> { -/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } -/// } -/// } -/// ``` -pub fn add_to_linker_async(l: &mut wasmtime::component::Linker) -> wasmtime::Result<()> -where - T: WasiHttpView + wasmtime_wasi::WasiView + 'static, -{ - wasmtime_wasi::p2::add_to_linker_proxy_interfaces_async(l)?; - add_only_http_to_linker_async(l) -} - -/// 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::p2::add_to_linker_async`] for -/// example to avoid re-adding the same interfaces twice. -pub fn add_only_http_to_linker_async( - l: &mut wasmtime::component::Linker, -) -> wasmtime::Result<()> -where - T: WasiHttpView + 'static, -{ - let options = crate::bindings::LinkOptions::default(); // FIXME: Thread through to the CLI options. - crate::bindings::http::outgoing_handler::add_to_linker::<_, WasiHttp>(l, |x| { - WasiHttpImpl(x) - })?; - crate::bindings::http::types::add_to_linker::<_, WasiHttp>(l, &options.into(), |x| { - WasiHttpImpl(x) - })?; - - Ok(()) -} - -struct WasiHttp(T); - -impl HasData for WasiHttp { - type Data<'a> = WasiHttpImpl<&'a mut T>; -} - -/// Add all of the `wasi:http/proxy` world's interfaces to a [`wasmtime::component::Linker`]. -/// -/// This function will add the `sync` variant of all interfaces into the -/// `Linker` provided. For embeddings with async support see -/// [`add_to_linker_async`] instead. -/// -/// # Example -/// -/// ``` -/// use wasmtime::{Engine, Result, Config}; -/// use wasmtime::component::{ResourceTable, Linker}; -/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; -/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; -/// -/// fn main() -> Result<()> { -/// let config = Config::default(); -/// let engine = Engine::new(&config)?; -/// -/// let mut linker = Linker::::new(&engine); -/// wasmtime_wasi_http::add_to_linker_sync(&mut linker)?; -/// // ... add any further functionality to `linker` if desired ... -/// -/// Ok(()) -/// } -/// -/// struct MyState { -/// ctx: WasiCtx, -/// http_ctx: WasiHttpCtx, -/// table: ResourceTable, -/// } -/// impl WasiHttpView for MyState { -/// fn ctx(&mut self) -> &mut WasiHttpCtx { &mut self.http_ctx } -/// fn table(&mut self) -> &mut ResourceTable { &mut self.table } -/// } -/// impl WasiView for MyState { -/// fn ctx(&mut self) -> WasiCtxView<'_> { -/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } -/// } -/// } -/// ``` -pub fn add_to_linker_sync(l: &mut Linker) -> wasmtime::Result<()> -where - T: WasiHttpView + wasmtime_wasi::WasiView + 'static, -{ - wasmtime_wasi::p2::add_to_linker_proxy_interfaces_sync(l)?; - add_only_http_to_linker_sync(l) -} - -/// A slimmed down version of [`add_to_linker_sync`] which only adds -/// `wasi:http` interfaces to the linker. -/// -/// This is useful when using [`wasmtime_wasi::p2::add_to_linker_sync`] for -/// example to avoid re-adding the same interfaces twice. -pub fn add_only_http_to_linker_sync(l: &mut Linker) -> wasmtime::Result<()> -where - T: WasiHttpView + 'static, -{ - let options = crate::bindings::LinkOptions::default(); // FIXME: Thread through to the CLI options. - crate::bindings::sync::http::outgoing_handler::add_to_linker::<_, WasiHttp>(l, |x| { - WasiHttpImpl(x) - })?; - crate::bindings::sync::http::types::add_to_linker::<_, WasiHttp>(l, &options.into(), |x| { - WasiHttpImpl(x) - })?; - - Ok(()) -} +pub use ctx::*; /// 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. +#[cfg(any(feature = "p2", feature = "p3"))] fn get_content_length(headers: &http::HeaderMap) -> wasmtime::Result> { - let Some(v) = headers.get(CONTENT_LENGTH) else { + let Some(v) = headers.get(header::CONTENT_LENGTH) else { return Ok(None); }; let v = v.to_str()?; let v = v.parse()?; Ok(Some(v)) } + +/// Set of [http::header::HeaderName], that are forbidden by default +/// for requests and responses originating in the guest. +pub const DEFAULT_FORBIDDEN_HEADERS: [HeaderName; 9] = [ + header::CONNECTION, + HeaderName::from_static("keep-alive"), + header::PROXY_AUTHENTICATE, + header::PROXY_AUTHORIZATION, + HeaderName::from_static("proxy-connection"), + header::TRANSFER_ENCODING, + header::UPGRADE, + header::HOST, + HeaderName::from_static("http2-settings"), +]; diff --git a/crates/wasi-http/src/bindings.rs b/crates/wasi-http/src/p2/bindings.rs similarity index 93% rename from crates/wasi-http/src/bindings.rs rename to crates/wasi-http/src/p2/bindings.rs index bec9402a8775..e870ea033042 100644 --- a/crates/wasi-http/src/bindings.rs +++ b/crates/wasi-http/src/p2/bindings.rs @@ -2,8 +2,8 @@ #[expect(missing_docs, reason = "bindgen-generated code")] mod generated { - use crate::body; - use crate::types; + use crate::p2::body; + use crate::p2::types; wasmtime::component::bindgen!({ path: "wit", @@ -30,7 +30,7 @@ mod generated { "wasi:http/types.request-options": types::HostRequestOptions, }, trappable_error_type: { - "wasi:http/types.error-code" => crate::HttpError, + "wasi:http/types.error-code" => crate::p2::HttpError, }, }); } @@ -52,7 +52,7 @@ pub mod sync { imports: { default: tracing }, with: { // http is in this crate - "wasi:http": crate::bindings::http, + "wasi:http": crate::p2::bindings::http, // sync requires the wrapper in the wasmtime_wasi crate, in // order to have in_tokio "wasi:io": wasmtime_wasi::p2::bindings::sync::io, diff --git a/crates/wasi-http/src/body.rs b/crates/wasi-http/src/p2/body.rs similarity index 99% rename from crates/wasi-http/src/body.rs rename to crates/wasi-http/src/p2/body.rs index 000bd567c8c7..5d8c929b4278 100644 --- a/crates/wasi-http/src/body.rs +++ b/crates/wasi-http/src/p2/body.rs @@ -1,7 +1,7 @@ //! Implementation of the `wasi:http/types` interface's various body types. -use crate::bindings::http::types; -use crate::types::FieldMap; +use crate::p2::bindings::http::types; +use crate::p2::types::FieldMap; use bytes::Bytes; use http_body::{Body, Frame}; use http_body_util::BodyExt; diff --git a/crates/wasi-http/src/error.rs b/crates/wasi-http/src/p2/error.rs similarity index 96% rename from crates/wasi-http/src/error.rs rename to crates/wasi-http/src/p2/error.rs index 4279bd8483f3..9bc1ebd6f057 100644 --- a/crates/wasi-http/src/error.rs +++ b/crates/wasi-http/src/p2/error.rs @@ -1,4 +1,4 @@ -use crate::bindings::http::types::ErrorCode; +use crate::p2::bindings::http::types::ErrorCode; use std::error::Error; use std::fmt; use wasmtime::component::ResourceTableError; @@ -60,7 +60,7 @@ impl Error for HttpError {} #[cfg(feature = "default-send-request")] pub(crate) fn dns_error(rcode: String, info_code: u16) -> ErrorCode { - ErrorCode::DnsError(crate::bindings::http::types::DnsErrorPayload { + ErrorCode::DnsError(crate::p2::bindings::http::types::DnsErrorPayload { rcode: Some(rcode), info_code: Some(info_code), }) diff --git a/crates/wasi-http/src/http_impl.rs b/crates/wasi-http/src/p2/http_impl.rs similarity index 89% rename from crates/wasi-http/src/http_impl.rs rename to crates/wasi-http/src/p2/http_impl.rs index 903072cd43aa..45b4312044d9 100644 --- a/crates/wasi-http/src/http_impl.rs +++ b/crates/wasi-http/src/p2/http_impl.rs @@ -1,7 +1,7 @@ //! Implementation of the `wasi:http/outgoing-handler` interface. -use crate::{ - WasiHttpImpl, WasiHttpView, +use crate::p2::{ + HttpResult, WasiHttpCtxView, bindings::http::{ outgoing_handler, types::{self, Scheme}, @@ -15,16 +15,13 @@ use http_body_util::{BodyExt, Empty}; use hyper::Method; use wasmtime::component::Resource; -impl outgoing_handler::Host for WasiHttpImpl -where - T: WasiHttpView, -{ +impl outgoing_handler::Host for WasiHttpCtxView<'_> { fn handle( &mut self, request_id: Resource, options: Option>, - ) -> crate::HttpResult> { - let opts = options.and_then(|opts| self.table().get(&opts).ok()); + ) -> HttpResult> { + let opts = options.and_then(|opts| self.table.get(&opts).ok()); let connect_timeout = opts .and_then(|opts| opts.connect_timeout) @@ -38,7 +35,7 @@ where .and_then(|opts| opts.between_bytes_timeout) .unwrap_or(std::time::Duration::from_secs(600)); - let req = self.table().delete(request_id)?; + let req = self.table.delete(request_id)?; let mut builder = hyper::Request::builder(); builder = builder.method(match req.method { @@ -93,7 +90,7 @@ where .body(body) .map_err(|err| internal_error(err.to_string()))?; - let future = self.send_request( + let future = self.hooks.send_request( request, OutgoingRequestConfig { use_tls, @@ -103,6 +100,6 @@ where }, )?; - Ok(self.table().push(future)?) + Ok(self.table.push(future)?) } } diff --git a/crates/wasi-http/src/p2/mod.rs b/crates/wasi-http/src/p2/mod.rs new file mode 100644 index 000000000000..6f42d9260c26 --- /dev/null +++ b/crates/wasi-http/src/p2/mod.rs @@ -0,0 +1,706 @@ +//! # Wasmtime's WASI HTTPp2 Implementation +//! +//! This module 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::p2::bindings::Proxy +//! [`wasi:http/outgoing-handler`]: crate::p2::bindings::http::outgoing_handler::Host +//! [`wasi:http/types`]: crate::p2::bindings::http::types::Host +//! [`wasi:http/incoming-handler`]: crate::p2::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 `WasiHttp{View,Hooks}` traits +//! +//! All `bindgen!`-generated `Host` traits are implemented for the +//! [`WasiHttpCtxView`] type. This type is created from a store's data `T` +//! through the [`WasiHttpView`] trait. The [`add_to_linker_async`] function, +//! for example, uses [`WasiHttpView`] to acquire the context view. +//! +//! The [`WasiHttpCtxView`] structure requires that a [`ResourceTable`] and +//! [`WasiHttpCtx`] live within the store. This is store-specific state that is +//! used to implement various APIs and store host state. +//! +//! The final `hooks` field within [`WasiHttpCtxView`] is a trait object of +//! [`WasiHttpHooks`]. This provides a few more hooks, dynamically, to configure +//! how `wasi:http` behaves. For example [`WasiHttpHooks::send_request`] can +//! customize how outgoing HTTP requests are handled. The `hooks` field can be +//! initialized with the [`default_hooks`] function for the default behavior. +//! +//! # 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::p2::add_to_linker_async`] for example. +//! * Add individual interfaces such as with the +//! [`bindings::http::outgoing_handler::add_to_linker`] 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 wasmtime::bail; +//! use hyper::server::conn::http1; +//! use std::sync::Arc; +//! use tokio::net::TcpListener; +//! use wasmtime::component::{Component, Linker, ResourceTable}; +//! use wasmtime::{Engine, Result, Store}; +//! use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; +//! use wasmtime_wasi_http::p2::bindings::ProxyPre; +//! use wasmtime_wasi_http::p2::bindings::http::types::Scheme; +//! use wasmtime_wasi_http::p2::body::HyperOutgoingBody; +//! use wasmtime_wasi_http::io::TokioIo; +//! use wasmtime_wasi_http::{WasiHttpCtx, p2::{WasiHttpView, WasiHttpCtxView}}; +//! +//! #[tokio::main] +//! async fn main() -> Result<()> { +//! let component = std::env::args().nth(1).unwrap(); +//! +//! // Prepare the `Engine` for Wasmtime +//! let engine = Engine::default(); +//! +//! // 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::p2::add_to_linker_async(&mut linker)?; +//! wasmtime_wasi_http::p2::add_only_http_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: WasiCtx::builder().inherit_stdio().build(), +//! http: WasiHttpCtx::new(), +//! }, +//! ); +//! let (sender, receiver) = tokio::sync::oneshot::channel(); +//! let req = store.data_mut().http().new_incoming_request(Scheme::Http, req)?; +//! let out = store.data_mut().http().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(Ok(())) => { +//! bail!("guest never invoked `response-outparam::set` method") +//! } +//! Ok(Err(e)) => e, +//! Err(e) => e.into(), +//! }; +//! return Err(e.context("guest never invoked `response-outparam::set` method")); +//! } +//! } +//! } +//! } +//! +//! struct MyClientState { +//! wasi: WasiCtx, +//! http: WasiHttpCtx, +//! table: ResourceTable, +//! } +//! +//! impl WasiView for MyClientState { +//! fn ctx(&mut self) -> WasiCtxView<'_> { +//! WasiCtxView { ctx: &mut self.wasi, table: &mut self.table } +//! } +//! } +//! +//! impl WasiHttpView for MyClientState { +//! fn http(&mut self) -> WasiHttpCtxView<'_> { +//! WasiHttpCtxView { +//! ctx: &mut self.http, +//! table: &mut self.table, +//! hooks: Default::default(), +//! } +//! } +//! } +//! ``` + +#[cfg(feature = "default-send-request")] +use self::bindings::http::types::ErrorCode; +use crate::{DEFAULT_FORBIDDEN_HEADERS, WasiHttpCtx}; +use http::HeaderName; +use wasmtime::component::{HasData, Linker, ResourceTable}; + +mod error; +mod http_impl; +mod types_impl; + +pub mod bindings; +pub mod body; +pub mod types; + +pub use self::error::{ + HttpError, HttpResult, http_request_error, hyper_request_error, hyper_response_error, +}; + +/// A trait which provides hooks into internal WASI HTTP operations. +/// +/// # Example +/// +/// ``` +/// use wasmtime::component::ResourceTable; +/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// use wasmtime_wasi_http::WasiHttpCtx; +/// use wasmtime_wasi_http::p2::{WasiHttpView, WasiHttpCtxView}; +/// +/// struct MyState { +/// ctx: WasiCtx, +/// http_ctx: WasiHttpCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiHttpView for MyState { +/// fn http(&mut self) -> WasiHttpCtxView<'_> { +/// WasiHttpCtxView { +/// ctx: &mut self.http_ctx, +/// table: &mut self.table, +/// hooks: Default::default(), +/// } +/// } +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } +/// } +/// } +/// +/// impl MyState { +/// fn new() -> MyState { +/// let mut wasi = WasiCtx::builder(); +/// wasi.arg("./foo.wasm"); +/// wasi.arg("--help"); +/// wasi.env("FOO", "bar"); +/// +/// MyState { +/// ctx: wasi.build(), +/// table: ResourceTable::new(), +/// http_ctx: WasiHttpCtx::new(), +/// } +/// } +/// } +/// ``` +pub trait WasiHttpHooks { + /// Send an outgoing request. + #[cfg(feature = "default-send-request")] + fn send_request( + &mut self, + request: hyper::Request, + config: types::OutgoingRequestConfig, + ) -> HttpResult { + Ok(default_send_request(request, config)) + } + + /// Send an outgoing request. + #[cfg(not(feature = "default-send-request"))] + fn send_request( + &mut self, + request: hyper::Request, + config: types::OutgoingRequestConfig, + ) -> HttpResult; + + /// Whether a given header should be considered forbidden and not allowed. + fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { + DEFAULT_FORBIDDEN_HEADERS.contains(name) + } + + /// Number of distinct write calls to the outgoing body's output-stream + /// that the implementation will buffer. + /// Default: 1. + fn outgoing_body_buffer_chunks(&mut self) -> usize { + DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS + } + + /// Maximum size allowed in a write call to the outgoing body's output-stream. + /// Default: 1024 * 1024. + fn outgoing_body_chunk_size(&mut self) -> usize { + DEFAULT_OUTGOING_BODY_CHUNK_SIZE + } +} + +#[cfg(feature = "default-send-request")] +impl<'a> Default for &'a mut dyn WasiHttpHooks { + fn default() -> Self { + let x: &mut [(); 0] = &mut []; + x + } +} + +#[doc(hidden)] +#[cfg(feature = "default-send-request")] +impl WasiHttpHooks for [(); 0] {} + +/// Returns a value suitable for the `WasiHttpCtxView::hooks` field which has +/// the default behavior for `wasi:http`. +#[cfg(feature = "default-send-request")] +pub fn default_hooks() -> &'static mut dyn WasiHttpHooks { + Default::default() +} + +/// The default value configured for [`WasiHttpHooks::outgoing_body_buffer_chunks`] in [`WasiHttpView`]. +pub const DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS: usize = 1; +/// The default value configured for [`WasiHttpHooks::outgoing_body_chunk_size`] in [`WasiHttpView`]. +pub const DEFAULT_OUTGOING_BODY_CHUNK_SIZE: usize = 1024 * 1024; + +/// Structure which `wasi:http` `Host`-style traits are implemented for. +/// +/// This structure is used by embedders with the [`WasiHttpView`] trait's return +/// value and is used to provide access to this crate all internals necessary to +/// implement `wasi:http`. This is similar to [`wasmtime_wasi::WasiCtxView`] +/// for example. +pub struct WasiHttpCtxView<'a> { + /// A reference to a per-store [`WasiHttpCtx`]. + pub ctx: &'a mut WasiHttpCtx, + /// A reference to a per-store table of resources to store host structures + /// within. + pub table: &'a mut ResourceTable, + /// A reference to a per-store set of hooks that can be used to customize + /// `wasi:http` behavior. + pub hooks: &'a mut dyn WasiHttpHooks, +} + +/// The type for which this crate implements the `wasi:http` interfaces. +pub struct WasiHttp; + +impl HasData for WasiHttp { + type Data<'a> = WasiHttpCtxView<'a>; +} + +/// A trait used to project state that this crate needs to implement `wasi:http` +/// from the `self` type. +/// +/// This trait is used in [`add_to_linker_sync`] and [`add_to_linker_async`] for +/// example as a bound on `T` in `Store`. This is used to access data from +/// `T`, the data within a `Store`, an instance of [`WasiHttpCtxView`]. The +/// [`WasiHttpCtxView`] contains contextual information such as the +/// [`ResourceTable`] for the store, HTTP context info in [`WasiHttpCtx`], and +/// any hooks via [`WasiHttpHooks`] if the embedder desires. +/// +/// # Example +/// +/// ``` +/// use wasmtime::component::ResourceTable; +/// use wasmtime_wasi_http::WasiHttpCtx; +/// use wasmtime_wasi_http::p2::{WasiHttpView, WasiHttpCtxView}; +/// +/// struct MyState { +/// http_ctx: WasiHttpCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiHttpView for MyState { +/// fn http(&mut self) -> WasiHttpCtxView<'_> { +/// WasiHttpCtxView { +/// ctx: &mut self.http_ctx, +/// table: &mut self.table, +/// hooks: Default::default(), +/// } +/// } +/// } +/// ``` +pub trait WasiHttpView { + /// Returns an instance of [`WasiHttpCtxView`] projected out of `self`. + fn http(&mut self) -> WasiHttpCtxView<'_>; +} + +/// 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. For embeddings with async support disabled see +/// [`add_to_linker_sync`] instead. +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result}; +/// use wasmtime::component::{ResourceTable, Linker}; +/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// use wasmtime_wasi_http::{WasiHttpCtx, p2::{WasiHttpView, WasiHttpCtxView}}; +/// +/// fn main() -> Result<()> { +/// let engine = Engine::default(); +/// +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi_http::p2::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 WasiHttpView for MyState { +/// fn http(&mut self) -> WasiHttpCtxView<'_> { +/// WasiHttpCtxView { +/// ctx: &mut self.http_ctx, +/// table: &mut self.table, +/// hooks: Default::default(), +/// } +/// } +/// } +/// +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } +/// } +/// } +/// ``` +pub fn add_to_linker_async(l: &mut wasmtime::component::Linker) -> wasmtime::Result<()> +where + T: WasiHttpView + wasmtime_wasi::WasiView + 'static, +{ + wasmtime_wasi::p2::add_to_linker_proxy_interfaces_async(l)?; + add_only_http_to_linker_async(l) +} + +/// 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::p2::add_to_linker_async`] for +/// example to avoid re-adding the same interfaces twice. +pub fn add_only_http_to_linker_async( + l: &mut wasmtime::component::Linker, +) -> wasmtime::Result<()> +where + T: WasiHttpView + 'static, +{ + let options = bindings::LinkOptions::default(); // FIXME: Thread through to the CLI options. + bindings::http::outgoing_handler::add_to_linker::<_, WasiHttp>(l, T::http)?; + bindings::http::types::add_to_linker::<_, WasiHttp>(l, &options.into(), T::http)?; + + Ok(()) +} + +/// Add all of the `wasi:http/proxy` world's interfaces to a [`wasmtime::component::Linker`]. +/// +/// This function will add the `sync` variant of all interfaces into the +/// `Linker` provided. For embeddings with async support see +/// [`add_to_linker_async`] instead. +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Config}; +/// use wasmtime::component::{ResourceTable, Linker}; +/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; +/// use wasmtime_wasi_http::WasiHttpCtx; +/// use wasmtime_wasi_http::p2::{WasiHttpView, WasiHttpCtxView}; +/// +/// fn main() -> Result<()> { +/// let config = Config::default(); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi_http::p2::add_to_linker_sync(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// Ok(()) +/// } +/// +/// struct MyState { +/// ctx: WasiCtx, +/// http_ctx: WasiHttpCtx, +/// table: ResourceTable, +/// } +/// impl WasiHttpView for MyState { +/// fn http(&mut self) -> WasiHttpCtxView<'_> { +/// WasiHttpCtxView { +/// ctx: &mut self.http_ctx, +/// table: &mut self.table, +/// hooks: Default::default(), +/// } +/// } +/// } +/// impl WasiView for MyState { +/// fn ctx(&mut self) -> WasiCtxView<'_> { +/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } +/// } +/// } +/// ``` +pub fn add_to_linker_sync(l: &mut Linker) -> wasmtime::Result<()> +where + T: WasiHttpView + wasmtime_wasi::WasiView + 'static, +{ + wasmtime_wasi::p2::add_to_linker_proxy_interfaces_sync(l)?; + add_only_http_to_linker_sync(l) +} + +/// A slimmed down version of [`add_to_linker_sync`] which only adds +/// `wasi:http` interfaces to the linker. +/// +/// This is useful when using [`wasmtime_wasi::p2::add_to_linker_sync`] for +/// example to avoid re-adding the same interfaces twice. +pub fn add_only_http_to_linker_sync(l: &mut Linker) -> wasmtime::Result<()> +where + T: WasiHttpView + 'static, +{ + let options = bindings::LinkOptions::default(); // FIXME: Thread through to the CLI options. + bindings::sync::http::outgoing_handler::add_to_linker::<_, WasiHttp>(l, T::http)?; + bindings::sync::http::types::add_to_linker::<_, WasiHttp>(l, &options.into(), T::http)?; + + Ok(()) +} + +/// The default implementation of how an outgoing request is sent. +/// +/// This implementation is used by the `wasi:http/outgoing-handler` interface +/// default implementation. +#[cfg(feature = "default-send-request")] +pub fn default_send_request( + request: hyper::Request, + config: types::OutgoingRequestConfig, +) -> types::HostFutureIncomingResponse { + let handle = wasmtime_wasi::runtime::spawn(async move { + Ok(default_send_request_handler(request, config).await) + }); + types::HostFutureIncomingResponse::pending(handle) +} + +/// The underlying implementation of how an outgoing request is sent. This should likely be spawned +/// in a task. +/// +/// This is called from [default_send_request] to actually send the request. +#[cfg(feature = "default-send-request")] +pub async fn default_send_request_handler( + mut request: hyper::Request, + types::OutgoingRequestConfig { + use_tls, + connect_timeout, + first_byte_timeout, + between_bytes_timeout, + }: types::OutgoingRequestConfig, +) -> Result { + use crate::io::TokioIo; + use crate::p2::{error::dns_error, hyper_request_error}; + use http_body_util::BodyExt; + use tokio::net::TcpStream; + use tokio::time::timeout; + + let authority = if let Some(authority) = request.uri().authority() { + if authority.port().is_some() { + authority.to_string() + } else { + let port = if use_tls { 443 } else { 80 }; + format!("{}:{port}", authority.to_string()) + } + } else { + return Err(ErrorCode::HttpRequestUriInvalid); + }; + let tcp_stream = timeout(connect_timeout, TcpStream::connect(&authority)) + .await + .map_err(|_| ErrorCode::ConnectionTimeout)? + .map_err(|e| match e.kind() { + std::io::ErrorKind::AddrNotAvailable => { + dns_error("address not available".to_string(), 0) + } + + _ => { + if e.to_string() + .starts_with("failed to lookup address information") + { + dns_error("address not available".to_string(), 0) + } else { + ErrorCode::ConnectionRefused + } + } + })?; + + let (mut sender, worker) = if use_tls { + 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, tcp_stream).await.map_err(|e| { + tracing::warn!("tls protocol error: {e:?}"); + ErrorCode::TlsProtocolError + })?; + let stream = TokioIo::new(stream); + + let (sender, conn) = timeout( + connect_timeout, + hyper::client::conn::http1::handshake(stream), + ) + .await + .map_err(|_| ErrorCode::ConnectionTimeout)? + .map_err(hyper_request_error)?; + + let worker = wasmtime_wasi::runtime::spawn(async move { + match conn.await { + Ok(()) => {} + // TODO: shouldn't throw away this error and ideally should + // surface somewhere. + Err(e) => tracing::warn!("dropping error {e}"), + } + }); + + (sender, worker) + } else { + let tcp_stream = TokioIo::new(tcp_stream); + let (sender, conn) = timeout( + connect_timeout, + // TODO: we should plumb the builder through the http context, and use it here + hyper::client::conn::http1::handshake(tcp_stream), + ) + .await + .map_err(|_| ErrorCode::ConnectionTimeout)? + .map_err(hyper_request_error)?; + + let worker = wasmtime_wasi::runtime::spawn(async move { + match conn.await { + Ok(()) => {} + // TODO: same as above, shouldn't throw this error away. + Err(e) => tracing::warn!("dropping error {e}"), + } + }); + + (sender, worker) + }; + + // 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 resp = timeout(first_byte_timeout, sender.send_request(request)) + .await + .map_err(|_| ErrorCode::ConnectionReadTimeout)? + .map_err(hyper_request_error)? + .map(|body| body.map_err(hyper_request_error).boxed_unsync()); + + Ok(types::IncomingResponse { + resp, + worker: Some(worker), + between_bytes_timeout, + }) +} diff --git a/crates/wasi-http/src/p2/types.rs b/crates/wasi-http/src/p2/types.rs new file mode 100644 index 000000000000..d53290acd306 --- /dev/null +++ b/crates/wasi-http/src/p2/types.rs @@ -0,0 +1,449 @@ +//! Implements the base structure that will provide the implementation of the +//! wasi-http API. + +use crate::p2::{ + WasiHttpCtxView, WasiHttpHooks, + bindings::http::types::{self, ErrorCode, Method, Scheme}, + body::{HostIncomingBody, HyperIncomingBody, HyperOutgoingBody}, +}; +use bytes::Bytes; +use http::header::{HeaderMap, HeaderName, HeaderValue}; +use http_body_util::BodyExt; +use hyper::body::Body; +use std::any::Any; +use std::fmt; +use std::time::Duration; +use wasmtime::component::Resource; +use wasmtime::{Result, bail}; +use wasmtime_wasi::p2::Pollable; +use wasmtime_wasi::runtime::AbortOnDropJoinHandle; + +/// Removes forbidden headers from a [`FieldMap`]. +pub(crate) fn remove_forbidden_headers(hooks: &mut dyn WasiHttpHooks, headers: &mut FieldMap) { + let forbidden_keys = Vec::from_iter(headers.as_ref().keys().filter_map(|name| { + if hooks.is_forbidden_header(name) { + Some(name.clone()) + } else { + None + } + })); + + for name in forbidden_keys { + headers.remove_all(&name); + } +} + +/// Configuration for an outgoing request. +pub struct OutgoingRequestConfig { + /// Whether to use TLS for the request. + pub use_tls: bool, + /// The timeout for connecting. + pub connect_timeout: Duration, + /// The timeout until the first byte. + pub first_byte_timeout: Duration, + /// The timeout between chunks of a streaming body + pub between_bytes_timeout: Duration, +} + +impl From for types::Method { + fn from(method: http::Method) -> Self { + if method == http::Method::GET { + types::Method::Get + } else if method == hyper::Method::HEAD { + types::Method::Head + } else if method == hyper::Method::POST { + types::Method::Post + } else if method == hyper::Method::PUT { + types::Method::Put + } else if method == hyper::Method::DELETE { + types::Method::Delete + } else if method == hyper::Method::CONNECT { + types::Method::Connect + } else if method == hyper::Method::OPTIONS { + types::Method::Options + } else if method == hyper::Method::TRACE { + types::Method::Trace + } else if method == hyper::Method::PATCH { + types::Method::Patch + } else { + types::Method::Other(method.to_string()) + } + } +} + +impl TryInto for types::Method { + type Error = http::method::InvalidMethod; + + fn try_into(self) -> Result { + match self { + Method::Get => Ok(http::Method::GET), + Method::Head => Ok(http::Method::HEAD), + Method::Post => Ok(http::Method::POST), + Method::Put => Ok(http::Method::PUT), + Method::Delete => Ok(http::Method::DELETE), + Method::Connect => Ok(http::Method::CONNECT), + Method::Options => Ok(http::Method::OPTIONS), + Method::Trace => Ok(http::Method::TRACE), + Method::Patch => Ok(http::Method::PATCH), + Method::Other(s) => http::Method::from_bytes(s.as_bytes()), + } + } +} + +/// The concrete type behind a `wasi:http/types.incoming-request` resource. +#[derive(Debug)] +pub struct HostIncomingRequest { + pub(crate) method: http::method::Method, + pub(crate) uri: http::uri::Uri, + pub(crate) headers: FieldMap, + pub(crate) scheme: Scheme, + pub(crate) authority: String, + /// The body of the incoming request. + pub body: Option, +} + +impl WasiHttpCtxView<'_> { + /// Create a new incoming request resource. + pub fn new_incoming_request( + &mut self, + scheme: Scheme, + req: hyper::Request, + ) -> wasmtime::Result> + where + B: Body + Send + 'static, + B::Error: Into, + { + let field_size_limit = self.ctx.field_size_limit; + let (parts, body) = req.into_parts(); + let body = body.map_err(Into::into).boxed_unsync(); + let body = HostIncomingBody::new( + body, + // TODO: this needs to be plumbed through + std::time::Duration::from_millis(600 * 1000), + field_size_limit, + ); + let authority = match parts.uri.authority() { + Some(authority) => authority.to_string(), + None => match parts.headers.get(http::header::HOST) { + Some(host) => host.to_str()?.to_string(), + None => bail!("invalid HTTP request missing authority in URI and host header"), + }, + }; + + let mut headers = FieldMap::new(parts.headers, field_size_limit); + remove_forbidden_headers(self.hooks, &mut headers); + + let req = HostIncomingRequest { + method: parts.method, + uri: parts.uri, + headers, + authority, + scheme, + body: Some(body), + }; + Ok(self.table.push(req)?) + } +} + +/// The concrete type behind a `wasi:http/types.response-outparam` resource. +pub struct HostResponseOutparam { + /// The sender for sending a response. + pub result: + tokio::sync::oneshot::Sender, types::ErrorCode>>, +} + +impl WasiHttpCtxView<'_> { + /// Create a new outgoing response resource. + pub fn new_response_outparam( + &mut self, + result: tokio::sync::oneshot::Sender< + Result, types::ErrorCode>, + >, + ) -> wasmtime::Result> { + let id = self.table.push(HostResponseOutparam { result })?; + Ok(id) + } +} + +/// The concrete type behind a `wasi:http/types.outgoing-response` resource. +pub struct HostOutgoingResponse { + /// The status of the response. + pub status: http::StatusCode, + /// The headers of the response. + pub headers: FieldMap, + /// The body of the response. + pub body: Option, +} + +impl TryFrom for hyper::Response { + type Error = http::Error; + + fn try_from( + resp: HostOutgoingResponse, + ) -> Result, Self::Error> { + use http_body_util::Empty; + + let mut builder = hyper::Response::builder().status(resp.status); + + *builder.headers_mut().unwrap() = resp.headers.map; + + match resp.body { + Some(body) => builder.body(body), + None => builder.body( + Empty::::new() + .map_err(|_| unreachable!("Infallible error")) + .boxed_unsync(), + ), + } + } +} + +/// The concrete type behind a `wasi:http/types.outgoing-request` resource. +#[derive(Debug)] +pub struct HostOutgoingRequest { + /// 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: FieldMap, + /// The request body. + pub body: Option, +} + +/// The concrete type behind a `wasi:http/types.request-options` resource. +#[derive(Debug, Default)] +pub struct HostRequestOptions { + /// 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.incoming-response` resource. +#[derive(Debug)] +pub struct HostIncomingResponse { + /// The response status + pub status: u16, + /// The response headers + pub headers: FieldMap, + /// The response body + pub body: Option, +} + +/// The concrete type behind a `wasi:http/types.fields` resource. +#[derive(Debug)] +pub enum HostFields { + /// A reference to the fields of a parent entry. + Ref { + /// The parent resource rep. + parent: u32, + + /// The function to get the fields from the parent. + // NOTE: there's not failure in the result here because we assume that HostFields will + // always be registered as a child of the entry with the `parent` id. This ensures that the + // entry will always exist while this `HostFields::Ref` entry exists in the table, thus we + // don't need to account for failure when fetching the fields ref from the parent. + get_fields: for<'a> fn(elem: &'a mut (dyn Any + 'static)) -> &'a mut FieldMap, + }, + /// An owned version of the fields. + Owned { + /// The fields themselves. + fields: FieldMap, + }, +} + +/// An owned version of `HostFields`. A wrapper on http `HeaderMap` that +/// keeps a running tally of memory consumed by header names and values. +#[derive(Debug, Clone)] +pub struct FieldMap { + map: HeaderMap, + limit: usize, + size: usize, +} + +/// Error given when a `FieldMap` has exceeded the size limit. +#[derive(Debug)] +pub struct FieldSizeLimitError { + /// The erroring `FieldMap` operation would require this content size + pub(crate) size: usize, + /// The limit set on `FieldMap` content size + pub(crate) limit: usize, +} +impl fmt::Display for FieldSizeLimitError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Field size limit {} exceeded: {}", self.limit, self.size) + } +} +impl std::error::Error for FieldSizeLimitError {} + +impl FieldMap { + /// Construct a `FieldMap` from a `HeaderMap` and a size limit. + /// + /// Construction with a `HeaderMap` which exceeds the size limit is + /// allowed, but subsequent operations to expand the resource use will + /// fail. + pub fn new(map: HeaderMap, limit: usize) -> Self { + let size = Self::content_size(&map); + Self { map, size, limit } + } + /// Construct an empty `FieldMap` + pub fn empty(limit: usize) -> Self { + Self { + map: HeaderMap::new(), + size: 0, + limit, + } + } + /// Get the `HeaderMap` out of the `FieldMap` + pub fn into_inner(self) -> HeaderMap { + self.map + } + /// Calculate the content size of a `HeaderMap`. This is a sum of the size + /// of all of the keys and all of the values. + pub(crate) fn content_size(map: &HeaderMap) -> usize { + let mut sum = 0; + for key in map.keys() { + sum += header_name_size(key); + } + for value in map.values() { + sum += header_value_size(value); + } + sum + } + /// Remove all values associated with a key in a map. + /// + /// Returns an empty list if the key is not already present within the map. + pub fn remove_all(&mut self, key: &HeaderName) -> Vec { + use http::header::Entry; + match self.map.try_entry(key) { + Ok(Entry::Vacant { .. }) | Err(_) => Vec::new(), + Ok(Entry::Occupied(e)) => { + let (name, value_drain) = e.remove_entry_mult(); + let mut removed = header_name_size(&name); + let values = value_drain.collect::>(); + for v in values.iter() { + removed += header_value_size(v); + } + self.size -= removed; + values + } + } + } + /// Add a value associated with a key to the map. + /// + /// If `key` is already present within the map then `value` is appended to + /// the list of values it already has. + pub fn append(&mut self, key: &HeaderName, value: HeaderValue) -> Result { + let key_size = header_name_size(key); + let val_size = header_value_size(&value); + let new_size = if !self.map.contains_key(key) { + self.size + key_size + val_size + } else { + self.size + val_size + }; + if new_size > self.limit { + bail!(FieldSizeLimitError { + limit: self.limit, + size: new_size + }) + } + self.size = new_size; + Ok(self.map.try_append(key, value)?) + } +} + +/// Returns the size, in accounting cost, to consider for `name`. +/// +/// This includes both the byte length of the `name` itself as well as the size +/// of the data structure itself as it'll reside within a `HeaderMap`. +fn header_name_size(name: &HeaderName) -> usize { + name.as_str().len() + size_of::() +} + +/// Same as `header_name_size`, but for values. +/// +/// This notably includes the size of `HeaderValue` itself to ensure that all +/// headers have a nonzero size as otherwise this would never limit addition of +/// an empty header value. +fn header_value_size(value: &HeaderValue) -> usize { + value.len() + size_of::() +} + +// We impl AsRef, but not AsMut, because any modifications of the +// underlying HeaderMap must account for changes in size +impl AsRef for FieldMap { + fn as_ref(&self) -> &HeaderMap { + &self.map + } +} + +/// A handle to a future incoming response. +pub type FutureIncomingResponseHandle = + AbortOnDropJoinHandle>>; + +/// A response that is in the process of being received. +#[derive(Debug)] +pub struct IncomingResponse { + /// The response itself. + pub resp: hyper::Response, + /// Optional worker task that continues to process the response. + pub worker: Option>, + /// The timeout between chunks of the response. + pub between_bytes_timeout: std::time::Duration, +} + +/// The concrete type behind a `wasi:http/types.future-incoming-response` resource. +#[derive(Debug)] +pub enum HostFutureIncomingResponse { + /// A pending response + Pending(FutureIncomingResponseHandle), + /// The response is ready. + /// + /// An outer error will trap while the inner error gets returned to the guest. + Ready(wasmtime::Result>), + /// The response has been consumed. + Consumed, +} + +impl HostFutureIncomingResponse { + /// Create a new `HostFutureIncomingResponse` that is pending on the provided task handle. + pub fn pending(handle: FutureIncomingResponseHandle) -> Self { + Self::Pending(handle) + } + + /// Create a new `HostFutureIncomingResponse` that is ready. + pub fn ready(result: wasmtime::Result>) -> Self { + Self::Ready(result) + } + + /// Returns `true` if the response is ready. + pub fn is_ready(&self) -> bool { + matches!(self, Self::Ready(_)) + } + + /// Unwrap the response, panicking if it is not ready. + pub fn unwrap_ready(self) -> wasmtime::Result> { + match self { + Self::Ready(res) => res, + Self::Pending(_) | Self::Consumed => { + panic!("unwrap_ready called on a pending HostFutureIncomingResponse") + } + } + } +} + +#[async_trait::async_trait] +impl Pollable for HostFutureIncomingResponse { + async fn ready(&mut self) { + if let Self::Pending(handle) = self { + *self = Self::Ready(handle.await); + } + } +} diff --git a/crates/wasi-http/src/types_impl.rs b/crates/wasi-http/src/p2/types_impl.rs similarity index 78% rename from crates/wasi-http/src/types_impl.rs rename to crates/wasi-http/src/p2/types_impl.rs index 45db0df7d8ab..3bf7e377cc07 100644 --- a/crates/wasi-http/src/types_impl.rs +++ b/crates/wasi-http/src/p2/types_impl.rs @@ -1,13 +1,14 @@ //! Implementation for the `wasi:http/types` interface. -use crate::bindings::http::types::{self, Headers, Method, Scheme, StatusCode, Trailers}; -use crate::body::{HostFutureTrailers, HostIncomingBody, HostOutgoingBody, StreamContext}; -use crate::types::{ +use crate::get_content_length; +use crate::p2::bindings::http::types::{self, Headers, Method, Scheme, StatusCode, Trailers}; +use crate::p2::body::{HostFutureTrailers, HostIncomingBody, HostOutgoingBody, StreamContext}; +use crate::p2::types::{ FieldMap, FieldSizeLimitError, HostFields, HostFutureIncomingResponse, HostIncomingRequest, HostIncomingResponse, HostOutgoingRequest, HostOutgoingResponse, HostResponseOutparam, remove_forbidden_headers, }; -use crate::{HttpError, HttpResult, WasiHttpImpl, WasiHttpView, get_content_length}; +use crate::p2::{HttpError, HttpResult, WasiHttpCtxView}; use std::any::Any; use std::str::FromStr; use wasmtime::bail; @@ -15,11 +16,8 @@ use wasmtime::component::{Resource, ResourceTable, ResourceTableError}; use wasmtime::{error::Context as _, format_err}; use wasmtime_wasi::p2::{DynInputStream, DynOutputStream, DynPollable}; -impl crate::bindings::http::types::Host for WasiHttpImpl -where - T: WasiHttpView, -{ - fn convert_error_code(&mut self, err: crate::HttpError) -> wasmtime::Result { +impl types::Host for WasiHttpCtxView<'_> { + fn convert_error_code(&mut self, err: HttpError) -> wasmtime::Result { err.downcast() } @@ -27,7 +25,7 @@ where &mut self, err: wasmtime::component::Resource, ) -> wasmtime::Result> { - let e = self.table().get(&err)?; + let e = self.table.get(&err)?; Ok(e.downcast_ref::().cloned()) } } @@ -77,14 +75,11 @@ fn get_fields_mut<'a>( } } -impl crate::bindings::http::types::HostFields for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostFields for WasiHttpCtxView<'_> { fn new(&mut self) -> wasmtime::Result> { - let limit = self.ctx().field_size_limit; + let limit = self.ctx.field_size_limit; let id = self - .table() + .table .push(HostFields::Owned { fields: FieldMap::empty(limit), }) @@ -105,7 +100,7 @@ where Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), }; - if self.is_forbidden_header(&header) { + if self.hooks.is_forbidden_header(&header) { return Ok(Err(types::HeaderError::Forbidden)); } @@ -118,15 +113,15 @@ where } let size = FieldMap::content_size(&fields); - if size > self.ctx().field_size_limit { + if size > self.ctx.field_size_limit { bail!(FieldSizeLimitError { size, - limit: self.ctx().field_size_limit, + limit: self.ctx.field_size_limit, }); } - let fields = FieldMap::new(fields, self.ctx().field_size_limit); + let fields = FieldMap::new(fields, self.ctx.field_size_limit); let id = self - .table() + .table .push(HostFields::Owned { fields }) .context("[new_fields] pushing fields")?; @@ -134,7 +129,7 @@ where } fn drop(&mut self, fields: Resource) -> wasmtime::Result<()> { - self.table() + self.table .delete(fields) .context("[drop_fields] deleting fields")?; Ok(()) @@ -145,7 +140,7 @@ where fields: Resource, name: String, ) -> wasmtime::Result>> { - let fields = get_fields(self.table(), &fields).context("[fields_get] getting fields")?; + let fields = get_fields(self.table, &fields).context("[fields_get] getting fields")?; let header = match hyper::header::HeaderName::from_bytes(name.as_bytes()) { Ok(header) => header, @@ -166,7 +161,7 @@ where } fn has(&mut self, fields: Resource, name: String) -> wasmtime::Result { - let fields = get_fields(self.table(), &fields).context("[fields_get] getting fields")?; + let fields = get_fields(self.table, &fields).context("[fields_get] getting fields")?; match hyper::header::HeaderName::from_bytes(name.as_bytes()) { Ok(header) => Ok(fields.as_ref().contains_key(&header)), @@ -185,7 +180,7 @@ where Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), }; - if self.is_forbidden_header(&header) { + if self.hooks.is_forbidden_header(&header) { return Ok(Err(types::HeaderError::Forbidden)); } @@ -197,9 +192,7 @@ where } } - match get_fields_mut(self.table(), &fields) - .context("[fields_set] getting mutable fields")? - { + match get_fields_mut(self.table, &fields).context("[fields_set] getting mutable fields")? { Ok(fields) => { fields.remove_all(&header); for value in values { @@ -221,11 +214,11 @@ where Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), }; - if self.is_forbidden_header(&header) { + if self.hooks.is_forbidden_header(&header) { return Ok(Err(types::HeaderError::Forbidden)); } - Ok(get_fields_mut(self.table(), &fields)?.map(|fields| { + Ok(get_fields_mut(self.table, &fields)?.map(|fields| { fields.remove_all(&header); })) } @@ -241,7 +234,7 @@ where Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), }; - if self.is_forbidden_header(&header) { + if self.hooks.is_forbidden_header(&header) { return Ok(Err(types::HeaderError::Forbidden)); } @@ -250,7 +243,7 @@ where Err(_) => return Ok(Err(types::HeaderError::InvalidSyntax)), }; - match get_fields_mut(self.table(), &fields) + match get_fields_mut(self.table, &fields) .context("[fields_append] getting mutable fields")? { Ok(fields) => { @@ -265,7 +258,7 @@ where &mut self, fields: Resource, ) -> wasmtime::Result)>> { - Ok(get_fields(self.table(), &fields)? + Ok(get_fields(self.table, &fields)? .as_ref() .iter() .map(|(name, value)| (name.as_str().to_owned(), value.as_bytes().to_owned())) @@ -273,12 +266,12 @@ where } fn clone(&mut self, fields: Resource) -> wasmtime::Result> { - let fields = get_fields(self.table(), &fields) + let fields = get_fields(self.table, &fields) .context("[fields_clone] getting fields")? .clone(); let id = self - .table() + .table .push(HostFields::Owned { fields }) .context("[fields_clone] pushing fields")?; @@ -286,30 +279,27 @@ where } } -impl crate::bindings::http::types::HostIncomingRequest for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostIncomingRequest for WasiHttpCtxView<'_> { fn method(&mut self, id: Resource) -> wasmtime::Result { - let method = self.table().get(&id)?.method.clone(); + let method = self.table.get(&id)?.method.clone(); Ok(method.into()) } fn path_with_query( &mut self, id: Resource, ) -> wasmtime::Result> { - let req = self.table().get(&id)?; + let req = self.table.get(&id)?; Ok(req .uri .path_and_query() .map(|path_and_query| path_and_query.as_str().to_owned())) } fn scheme(&mut self, id: Resource) -> wasmtime::Result> { - let req = self.table().get(&id)?; + let req = self.table.get(&id)?; Ok(Some(req.scheme.clone())) } fn authority(&mut self, id: Resource) -> wasmtime::Result> { - let req = self.table().get(&id)?; + let req = self.table.get(&id)?; Ok(Some(req.authority.clone())) } @@ -317,13 +307,13 @@ where &mut self, id: Resource, ) -> wasmtime::Result> { - let _ = self.table().get(&id)?; + let _ = self.table.get(&id)?; fn get_fields(elem: &mut dyn Any) -> &mut FieldMap { &mut elem.downcast_mut::().unwrap().headers } - let headers = self.table().push_child( + let headers = self.table.push_child( HostFields::Ref { parent: id.rep(), get_fields, @@ -338,10 +328,10 @@ where &mut self, id: Resource, ) -> wasmtime::Result, ()>> { - let req = self.table().get_mut(&id)?; + let req = self.table.get_mut(&id)?; match req.body.take() { Some(body) => { - let id = self.table().push(body)?; + let id = self.table.push(body)?; Ok(Ok(id)) } @@ -350,22 +340,19 @@ where } fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(id)?; + let _ = self.table.delete(id)?; Ok(()) } } -impl crate::bindings::http::types::HostOutgoingRequest for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostOutgoingRequest for WasiHttpCtxView<'_> { fn new( &mut self, headers: Resource, ) -> wasmtime::Result> { - let headers = move_fields(self.table(), headers)?; + let headers = move_fields(self.table, headers)?; - self.table() + self.table .push(HostOutgoingRequest { path_with_query: None, authority: None, @@ -381,10 +368,10 @@ where &mut self, request: Resource, ) -> wasmtime::Result, ()>> { - let buffer_chunks = self.outgoing_body_buffer_chunks(); - let chunk_size = self.outgoing_body_chunk_size(); + let buffer_chunks = self.hooks.outgoing_body_buffer_chunks(); + let chunk_size = self.hooks.outgoing_body_chunk_size(); let req = self - .table() + .table .get_mut(&request) .context("[outgoing_request_write] getting request")?; @@ -404,13 +391,13 @@ where // The output stream will necessarily outlive the request, because we could be still // writing to the stream after `outgoing-handler.handle` is called. - let outgoing_body = self.table().push(host_body)?; + let outgoing_body = self.table.push(host_body)?; Ok(Ok(outgoing_body)) } fn drop(&mut self, request: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(request)?; + let _ = self.table.delete(request)?; Ok(()) } @@ -418,7 +405,7 @@ where &mut self, request: wasmtime::component::Resource, ) -> wasmtime::Result { - Ok(self.table().get(&request)?.method.clone()) + Ok(self.table.get(&request)?.method.clone()) } fn set_method( @@ -426,7 +413,7 @@ where request: wasmtime::component::Resource, method: Method, ) -> wasmtime::Result> { - let req = self.table().get_mut(&request)?; + let req = self.table.get_mut(&request)?; if let Method::Other(s) = &method { if let Err(_) = http::Method::from_str(s) { @@ -443,7 +430,7 @@ where &mut self, request: wasmtime::component::Resource, ) -> wasmtime::Result> { - Ok(self.table().get(&request)?.path_with_query.clone()) + Ok(self.table.get(&request)?.path_with_query.clone()) } fn set_path_with_query( @@ -451,7 +438,7 @@ where request: wasmtime::component::Resource, path_with_query: Option, ) -> wasmtime::Result> { - let req = self.table().get_mut(&request)?; + let req = self.table.get_mut(&request)?; if let Some(s) = path_with_query.as_ref() { if let Err(_) = http::uri::PathAndQuery::from_str(s) { @@ -468,7 +455,7 @@ where &mut self, request: wasmtime::component::Resource, ) -> wasmtime::Result> { - Ok(self.table().get(&request)?.scheme.clone()) + Ok(self.table.get(&request)?.scheme.clone()) } fn set_scheme( @@ -476,7 +463,7 @@ where request: wasmtime::component::Resource, scheme: Option, ) -> wasmtime::Result> { - let req = self.table().get_mut(&request)?; + let req = self.table.get_mut(&request)?; if let Some(types::Scheme::Other(s)) = scheme.as_ref() { if let Err(_) = http::uri::Scheme::from_str(s.as_str()) { @@ -493,7 +480,7 @@ where &mut self, request: wasmtime::component::Resource, ) -> wasmtime::Result> { - Ok(self.table().get(&request)?.authority.clone()) + Ok(self.table.get(&request)?.authority.clone()) } fn set_authority( @@ -501,7 +488,7 @@ where request: wasmtime::component::Resource, authority: Option, ) -> wasmtime::Result> { - let req = self.table().get_mut(&request)?; + let req = self.table.get_mut(&request)?; if let Some(s) = authority.as_ref() { if let Err(_) = http::uri::Authority::from_str(s.as_str()) { @@ -519,7 +506,7 @@ where request: wasmtime::component::Resource, ) -> wasmtime::Result> { let _ = self - .table() + .table .get(&request) .context("[outgoing_request_headers] getting request")?; @@ -530,7 +517,7 @@ where .headers } - let id = self.table().push_child( + let id = self.table.push_child( HostFields::Ref { parent: request.rep(), get_fields, @@ -542,12 +529,9 @@ where } } -impl crate::bindings::http::types::HostResponseOutparam for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostResponseOutparam for WasiHttpCtxView<'_> { fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(id)?; + let _ = self.table.delete(id)?; Ok(()) } fn set( @@ -556,11 +540,11 @@ where resp: Result, types::ErrorCode>, ) -> wasmtime::Result<()> { let val = match resp { - Ok(resp) => Ok(self.table().delete(resp)?.try_into()?), + Ok(resp) => Ok(self.table.delete(resp)?.try_into()?), Err(e) => Err(e), }; - let resp = self.table().delete(id)?; + let resp = self.table.delete(id)?; // Giving the API doesn't return any error, it's probably // better to ignore the error than trap the guest, in case of // host timeout and dropped the receiver side of the channel. @@ -579,13 +563,10 @@ where } } -impl crate::bindings::http::types::HostIncomingResponse for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostIncomingResponse for WasiHttpCtxView<'_> { fn drop(&mut self, response: Resource) -> wasmtime::Result<()> { let _ = self - .table() + .table .delete(response) .context("[drop_incoming_response] deleting response")?; Ok(()) @@ -593,7 +574,7 @@ where fn status(&mut self, response: Resource) -> wasmtime::Result { let r = self - .table() + .table .get(&response) .context("[incoming_response_status] getting response")?; Ok(r.status) @@ -604,7 +585,7 @@ where response: Resource, ) -> wasmtime::Result> { let _ = self - .table() + .table .get(&response) .context("[incoming_response_headers] getting response")?; @@ -612,7 +593,7 @@ where &mut elem.downcast_mut::().unwrap().headers } - let id = self.table().push_child( + let id = self.table.push_child( HostFields::Ref { parent: response.rep(), get_fields, @@ -627,14 +608,14 @@ where &mut self, response: Resource, ) -> wasmtime::Result, ()>> { - let table = self.table(); - let r = table + let r = self + .table .get_mut(&response) .context("[incoming_response_consume] getting response")?; match r.body.take() { Some(body) => { - let id = self.table().push(body)?; + let id = self.table.push(body)?; Ok(Ok(id)) } @@ -643,13 +624,10 @@ where } } -impl crate::bindings::http::types::HostFutureTrailers for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostFutureTrailers for WasiHttpCtxView<'_> { fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { let _ = self - .table() + .table .delete(id) .context("[drop future-trailers] deleting future-trailers")?; Ok(()) @@ -659,7 +637,7 @@ where &mut self, index: Resource, ) -> wasmtime::Result> { - wasmtime_wasi::p2::subscribe(self.table(), index) + wasmtime_wasi::p2::subscribe(self.table, index) } fn get( @@ -667,7 +645,7 @@ where id: Resource, ) -> wasmtime::Result>, types::ErrorCode>, ()>>> { - let trailers = self.table().get_mut(&id)?; + let trailers = self.table.get_mut(&id)?; match trailers { HostFutureTrailers::Waiting { .. } => return Ok(None), HostFutureTrailers::Consumed => return Ok(Some(Err(()))), @@ -685,27 +663,24 @@ where Err(e) => return Ok(Some(Ok(Err(e)))), }; - remove_forbidden_headers(self, &mut fields); + remove_forbidden_headers(self.hooks, &mut fields); - let ts = self.table().push(HostFields::Owned { fields })?; + let ts = self.table.push(HostFields::Owned { fields })?; Ok(Some(Ok(Ok(Some(ts))))) } } -impl crate::bindings::http::types::HostIncomingBody for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostIncomingBody for WasiHttpCtxView<'_> { fn stream( &mut self, id: Resource, ) -> wasmtime::Result, ()>> { - let body = self.table().get_mut(&id)?; + let body = self.table.get_mut(&id)?; if let Some(stream) = body.take_stream() { let stream: DynInputStream = Box::new(stream); - let stream = self.table().push_child(stream, &id)?; + let stream = self.table.push_child(stream, &id)?; return Ok(Ok(stream)); } @@ -716,28 +691,25 @@ where &mut self, id: Resource, ) -> wasmtime::Result> { - let body = self.table().delete(id)?; - let trailers = self.table().push(body.into_future_trailers())?; + let body = self.table.delete(id)?; + let trailers = self.table.push(body.into_future_trailers())?; Ok(trailers) } fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(id)?; + let _ = self.table.delete(id)?; Ok(()) } } -impl crate::bindings::http::types::HostOutgoingResponse for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostOutgoingResponse for WasiHttpCtxView<'_> { fn new( &mut self, headers: Resource, ) -> wasmtime::Result> { - let fields = move_fields(self.table(), headers)?; + let fields = move_fields(self.table, headers)?; - let id = self.table().push(HostOutgoingResponse { + let id = self.table.push(HostOutgoingResponse { status: http::StatusCode::OK, headers: fields, body: None, @@ -750,9 +722,9 @@ where &mut self, id: Resource, ) -> wasmtime::Result, ()>> { - let buffer_chunks = self.outgoing_body_buffer_chunks(); - let chunk_size = self.outgoing_body_chunk_size(); - let resp = self.table().get_mut(&id)?; + let buffer_chunks = self.hooks.outgoing_body_buffer_chunks(); + let chunk_size = self.hooks.outgoing_body_chunk_size(); + let resp = self.table.get_mut(&id)?; if resp.body.is_some() { return Ok(Err(())); @@ -768,7 +740,7 @@ where resp.body.replace(body); - let id = self.table().push(host)?; + let id = self.table.push(host)?; Ok(Ok(id)) } @@ -777,7 +749,7 @@ where &mut self, id: Resource, ) -> wasmtime::Result { - Ok(self.table().get(&id)?.status.into()) + Ok(self.table.get(&id)?.status.into()) } fn set_status_code( @@ -785,7 +757,7 @@ where id: Resource, status: types::StatusCode, ) -> wasmtime::Result> { - let resp = self.table().get_mut(&id)?; + let resp = self.table.get_mut(&id)?; match http::StatusCode::from_u16(status) { Ok(status) => resp.status = status, @@ -800,14 +772,14 @@ where id: Resource, ) -> wasmtime::Result> { // Trap if the outgoing-response doesn't exist. - let _ = self.table().get(&id)?; + let _ = self.table.get(&id)?; fn get_fields(elem: &mut dyn Any) -> &mut FieldMap { let resp = elem.downcast_mut::().unwrap(); &mut resp.headers } - Ok(self.table().push_child( + Ok(self.table.push_child( HostFields::Ref { parent: id.rep(), get_fields, @@ -817,17 +789,14 @@ where } fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(id)?; + let _ = self.table.delete(id)?; Ok(()) } } -impl crate::bindings::http::types::HostFutureIncomingResponse for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostFutureIncomingResponse for WasiHttpCtxView<'_> { fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(id)?; + let _ = self.table.delete(id)?; Ok(()) } @@ -837,8 +806,8 @@ where ) -> wasmtime::Result< Option, types::ErrorCode>, ()>>, > { - let field_size_limit = self.ctx().field_size_limit; - let resp = self.table().get_mut(&id)?; + let field_size_limit = self.ctx.field_size_limit; + let resp = self.table.get_mut(&id)?; match resp { HostFutureIncomingResponse::Pending(_) => return Ok(None), @@ -861,9 +830,9 @@ where let (parts, body) = resp.resp.into_parts(); let mut headers = FieldMap::new(parts.headers, field_size_limit); - remove_forbidden_headers(self, &mut headers); + remove_forbidden_headers(self.hooks, &mut headers); - let resp = self.table().push(HostIncomingResponse { + let resp = self.table.push(HostIncomingResponse { status: parts.status.as_u16(), headers, body: Some({ @@ -883,21 +852,18 @@ where &mut self, id: Resource, ) -> wasmtime::Result> { - wasmtime_wasi::p2::subscribe(self.table(), id) + wasmtime_wasi::p2::subscribe(self.table, id) } } -impl crate::bindings::http::types::HostOutgoingBody for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostOutgoingBody for WasiHttpCtxView<'_> { fn write( &mut self, id: Resource, ) -> wasmtime::Result, ()>> { - let body = self.table().get_mut(&id)?; + let body = self.table.get_mut(&id)?; if let Some(stream) = body.take_output_stream() { - let id = self.table().push_child(stream, &id)?; + let id = self.table.push_child(stream, &id)?; Ok(Ok(id)) } else { Ok(Err(())) @@ -908,11 +874,11 @@ where &mut self, id: Resource, ts: Option>, - ) -> crate::HttpResult<()> { - let body = self.table().delete(id)?; + ) -> HttpResult<()> { + let body = self.table.delete(id)?; let ts = if let Some(ts) = ts { - Some(move_fields(self.table(), ts)?) + Some(move_fields(self.table, ts)?) } else { None }; @@ -922,17 +888,14 @@ where } fn drop(&mut self, id: Resource) -> wasmtime::Result<()> { - self.table().delete(id)?.abort(); + self.table.delete(id)?.abort(); Ok(()) } } -impl crate::bindings::http::types::HostRequestOptions for WasiHttpImpl -where - T: WasiHttpView, -{ +impl types::HostRequestOptions for WasiHttpCtxView<'_> { fn new(&mut self) -> wasmtime::Result> { - let id = self.table().push(types::RequestOptions::default())?; + let id = self.table.push(types::RequestOptions::default())?; Ok(id) } @@ -940,11 +903,7 @@ where &mut self, opts: Resource, ) -> wasmtime::Result> { - let nanos = self - .table() - .get(&opts)? - .connect_timeout - .map(|d| d.as_nanos()); + let nanos = self.table.get(&opts)?.connect_timeout.map(|d| d.as_nanos()); if let Some(nanos) = nanos { Ok(Some(nanos.try_into()?)) @@ -958,8 +917,7 @@ where opts: Resource, duration: Option, ) -> wasmtime::Result> { - self.table().get_mut(&opts)?.connect_timeout = - duration.map(std::time::Duration::from_nanos); + self.table.get_mut(&opts)?.connect_timeout = duration.map(std::time::Duration::from_nanos); Ok(Ok(())) } @@ -968,7 +926,7 @@ where opts: Resource, ) -> wasmtime::Result> { let nanos = self - .table() + .table .get(&opts)? .first_byte_timeout .map(|d| d.as_nanos()); @@ -985,7 +943,7 @@ where opts: Resource, duration: Option, ) -> wasmtime::Result> { - self.table().get_mut(&opts)?.first_byte_timeout = + self.table.get_mut(&opts)?.first_byte_timeout = duration.map(std::time::Duration::from_nanos); Ok(Ok(())) } @@ -995,7 +953,7 @@ where opts: Resource, ) -> wasmtime::Result> { let nanos = self - .table() + .table .get(&opts)? .between_bytes_timeout .map(|d| d.as_nanos()); @@ -1012,13 +970,13 @@ where opts: Resource, duration: Option, ) -> wasmtime::Result> { - self.table().get_mut(&opts)?.between_bytes_timeout = + self.table.get_mut(&opts)?.between_bytes_timeout = duration.map(std::time::Duration::from_nanos); Ok(Ok(())) } fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { - let _ = self.table().delete(rep)?; + let _ = self.table.delete(rep)?; Ok(()) } } diff --git a/crates/wasi-http/src/p3/host/handler.rs b/crates/wasi-http/src/p3/host/handler.rs index 8e57d898257b..ffa2fea1baa6 100644 --- a/crates/wasi-http/src/p3/host/handler.rs +++ b/crates/wasi-http/src/p3/host/handler.rs @@ -58,7 +58,7 @@ impl HostWithStore for WasiHttp { .map_err(HttpError::trap)?; let (req, options) = req.into_http_with_getter(&mut store, io_task_result(io_result_rx), getter)?; - HttpResult::Ok(store.get().ctx.send_request( + HttpResult::Ok(store.get().hooks.send_request( req.map(|body| body.with_state(io_task_rx).boxed_unsync()), options.as_deref().copied(), Box::new(async { diff --git a/crates/wasi-http/src/p3/host/types.rs b/crates/wasi-http/src/p3/host/types.rs index ff7f397f7665..6179bea36d26 100644 --- a/crates/wasi-http/src/p3/host/types.rs +++ b/crates/wasi-http/src/p3/host/types.rs @@ -189,7 +189,7 @@ impl HostFields for WasiHttpCtxView<'_> { let mut fields = http::HeaderMap::default(); for (name, value) in entries { let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; - if self.ctx.is_forbidden_header(&name) { + if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } let value = parse_header_value(&name, value)?; @@ -225,7 +225,7 @@ impl HostFields for WasiHttpCtxView<'_> { value: Vec, ) -> HeaderResult<()> { let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; - if self.ctx.is_forbidden_header(&name) { + if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } let mut values = Vec::with_capacity(value.len()); @@ -244,7 +244,7 @@ impl HostFields for WasiHttpCtxView<'_> { fn delete(&mut self, fields: Resource, name: FieldName) -> HeaderResult<()> { let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; - if self.ctx.is_forbidden_header(&name) { + if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } let fields = get_fields_mut(self.table, &fields)?; @@ -259,7 +259,7 @@ impl HostFields for WasiHttpCtxView<'_> { name: FieldName, ) -> HeaderResult> { let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; - if self.ctx.is_forbidden_header(&name) { + if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } let fields = get_fields_mut(self.table, &fields)?; @@ -278,7 +278,7 @@ impl HostFields for WasiHttpCtxView<'_> { value: FieldValue, ) -> HeaderResult<()> { let name = name.parse().or(Err(HeaderError::InvalidSyntax))?; - if self.ctx.is_forbidden_header(&name) { + if self.hooks.is_forbidden_header(&name) { return Err(HeaderError::Forbidden.into()); } let value = parse_header_value(&name, value)?; diff --git a/crates/wasi-http/src/p3/mod.rs b/crates/wasi-http/src/p3/mod.rs index d8af50b68533..bce694348e59 100644 --- a/crates/wasi-http/src/p3/mod.rs +++ b/crates/wasi-http/src/p3/mod.rs @@ -22,7 +22,7 @@ pub use request::{Request, RequestOptions}; pub use response::Response; use crate::p3::bindings::http::types::ErrorCode; -use crate::types::DEFAULT_FORBIDDEN_HEADERS; +use crate::{DEFAULT_FORBIDDEN_HEADERS, WasiHttpCtx}; use bindings::http::{client, types}; use bytes::Bytes; use core::ops::Deref; @@ -50,7 +50,7 @@ impl HasData for WasiHttp { } /// A trait which provides internal WASI HTTP state. -pub trait WasiHttpCtx: Send { +pub trait WasiHttpHooks: Send { /// Whether a given header should be considered forbidden and not allowed. fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { DEFAULT_FORBIDDEN_HEADERS.contains(name) @@ -150,21 +150,35 @@ pub trait WasiHttpCtx: Send { >; } -/// Default implementation of [WasiHttpCtx]. #[cfg(feature = "default-send-request")] -#[derive(Clone, Default)] -pub struct DefaultWasiHttpCtx; +impl<'a> Default for &'a mut dyn WasiHttpHooks { + fn default() -> Self { + let x: &mut [(); 0] = &mut []; + x + } +} + +#[doc(hidden)] +#[cfg(feature = "default-send-request")] +impl WasiHttpHooks for [(); 0] {} +/// Returns a value suitable for the `WasiHttpCtxView::hooks` field which has +/// the default behavior for `wasi:http`. #[cfg(feature = "default-send-request")] -impl WasiHttpCtx for DefaultWasiHttpCtx {} +pub fn default_hooks() -> &'static mut dyn WasiHttpHooks { + Default::default() +} /// View into [WasiHttpCtx] implementation and [ResourceTable]. pub struct WasiHttpCtxView<'a> { - /// Mutable reference to the WASI HTTP context. - pub ctx: &'a mut dyn WasiHttpCtx, + /// Mutable reference to the WASI HTTP hooks. + pub hooks: &'a mut dyn WasiHttpHooks, /// Mutable reference to table used to manage resources. pub table: &'a mut ResourceTable, + + /// Mutable reference to the WASI HTTP context. + pub ctx: &'a mut WasiHttpCtx, } /// A trait which provides internal WASI HTTP state. @@ -184,7 +198,7 @@ pub trait WasiHttpView: Send { /// ``` /// use wasmtime::{Engine, Result, Store, Config}; /// use wasmtime::component::{Linker, ResourceTable}; -/// use wasmtime_wasi_http::p3::{DefaultWasiHttpCtx, WasiHttpCtxView, WasiHttpView}; +/// use wasmtime_wasi_http::{WasiHttpCtx, p3::{WasiHttpCtxView, WasiHttpView}}; /// /// fn main() -> Result<()> { /// let mut config = Config::new(); @@ -207,7 +221,7 @@ pub trait WasiHttpView: Send { /// /// #[derive(Default)] /// struct MyState { -/// http: DefaultWasiHttpCtx, +/// http: WasiHttpCtx, /// table: ResourceTable, /// } /// @@ -216,6 +230,7 @@ pub trait WasiHttpView: Send { /// WasiHttpCtxView { /// ctx: &mut self.http, /// table: &mut self.table, +/// hooks: Default::default(), /// } /// } /// } diff --git a/crates/wasi-http/src/p3/request.rs b/crates/wasi-http/src/p3/request.rs index e0671fc0dba2..458919f56fd2 100644 --- a/crates/wasi-http/src/p3/request.rs +++ b/crates/wasi-http/src/p3/request.rs @@ -209,8 +209,8 @@ impl Request { }; let mut headers = Arc::unwrap_or_clone(headers); let mut store = store.as_context_mut(); - let WasiHttpCtxView { ctx, .. } = getter(store.data_mut()); - if ctx.set_host_header() { + let WasiHttpCtxView { hooks, .. } = getter(store.data_mut()); + if hooks.set_host_header() { let host = if let Some(authority) = authority.as_ref() { HeaderValue::try_from(authority.as_str()) .map_err(|err| ErrorCode::InternalError(Some(err.to_string())))? @@ -220,8 +220,8 @@ impl Request { headers.insert(HOST, host); } let scheme = match scheme { - None => ctx.default_scheme().ok_or(ErrorCode::HttpProtocolError)?, - Some(scheme) if ctx.is_supported_scheme(&scheme) => scheme, + None => hooks.default_scheme().ok_or(ErrorCode::HttpProtocolError)?, + Some(scheme) if hooks.is_supported_scheme(&scheme) => scheme, Some(..) => return Err(ErrorCode::HttpProtocolError.into()), }; let mut uri = Uri::builder().scheme(scheme); @@ -478,7 +478,7 @@ pub async fn default_send_request( #[cfg(test)] mod tests { use super::*; - use crate::p3::DefaultWasiHttpCtx; + use crate::WasiHttpCtx; use core::future::Future; use core::pin::pin; use core::str::FromStr; @@ -491,7 +491,7 @@ mod tests { struct TestCtx { table: ResourceTable, wasi: WasiCtx, - http: DefaultWasiHttpCtx, + http: WasiHttpCtx, } impl TestCtx { @@ -499,7 +499,7 @@ mod tests { Self { table: ResourceTable::default(), wasi: WasiCtxBuilder::new().build(), - http: DefaultWasiHttpCtx, + http: Default::default(), } } } @@ -518,6 +518,7 @@ mod tests { WasiHttpCtxView { ctx: &mut self.http, table: &mut self.table, + hooks: crate::p3::default_hooks(), } } } diff --git a/crates/wasi-http/src/types.rs b/crates/wasi-http/src/types.rs deleted file mode 100644 index d120e0555410..000000000000 --- a/crates/wasi-http/src/types.rs +++ /dev/null @@ -1,889 +0,0 @@ -//! Implements the base structure (i.e. [WasiHttpCtx]) that will provide the -//! implementation of the wasi-http API. - -use crate::{ - bindings::http::types::{self, ErrorCode, Method, Scheme}, - body::{HostIncomingBody, HyperIncomingBody, HyperOutgoingBody}, -}; -use bytes::Bytes; -use http::header::{HeaderMap, HeaderName, HeaderValue}; -use http_body_util::BodyExt; -use hyper::body::Body; -use std::any::Any; -use std::fmt; -use std::time::Duration; -use wasmtime::component::{Resource, ResourceTable}; -use wasmtime::{Result, bail}; -use wasmtime_wasi::p2::Pollable; -use wasmtime_wasi::runtime::AbortOnDropJoinHandle; - -#[cfg(feature = "default-send-request")] -use { - crate::io::TokioIo, - crate::{error::dns_error, hyper_request_error}, - tokio::net::TcpStream, - tokio::time::timeout, -}; - -/// Default maximum size for the contents of a fields resource. -/// -/// Typically, HTTP proxies limit headers to 8k. This number is higher than that -/// because it not only includes the wire-size of headers but it additionally -/// includes factors for the in-memory representation of `HeaderMap`. This is in -/// theory high enough that no one runs into it but low enough such that a -/// completely full `HeaderMap` doesn't break the bank in terms of memory -/// consumption. -const DEFAULT_FIELD_SIZE_LIMIT: usize = 128 * 1024; - -/// Capture the state necessary for use in the wasi-http API implementation. -#[derive(Debug)] -pub struct WasiHttpCtx { - pub(crate) field_size_limit: usize, -} - -impl WasiHttpCtx { - /// Create a new context. - pub fn new() -> Self { - Self { - field_size_limit: DEFAULT_FIELD_SIZE_LIMIT, - } - } - - /// Set the maximum size for any fields resources created by this context. - /// - /// The limit specified here is roughly a byte limit for the size of the - /// in-memory representation of headers. This means that the limit needs to - /// be larger than the literal representation of headers on the wire to - /// account for in-memory Rust-side data structures representing the header - /// names/values/etc. - pub fn set_field_size_limit(&mut self, limit: usize) { - self.field_size_limit = limit; - } -} - -/// A trait which provides internal WASI HTTP state. -/// -/// # Example -/// -/// ``` -/// use wasmtime::component::ResourceTable; -/// use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; -/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; -/// -/// struct MyState { -/// ctx: WasiCtx, -/// http_ctx: WasiHttpCtx, -/// table: ResourceTable, -/// } -/// -/// impl WasiHttpView for MyState { -/// fn ctx(&mut self) -> &mut WasiHttpCtx { &mut self.http_ctx } -/// fn table(&mut self) -> &mut ResourceTable { &mut self.table } -/// } -/// -/// impl WasiView for MyState { -/// fn ctx(&mut self) -> WasiCtxView<'_> { -/// WasiCtxView { ctx: &mut self.ctx, table: &mut self.table } -/// } -/// } -/// -/// impl MyState { -/// fn new() -> MyState { -/// let mut wasi = WasiCtx::builder(); -/// wasi.arg("./foo.wasm"); -/// wasi.arg("--help"); -/// wasi.env("FOO", "bar"); -/// -/// MyState { -/// ctx: wasi.build(), -/// table: ResourceTable::new(), -/// http_ctx: WasiHttpCtx::new(), -/// } -/// } -/// } -/// ``` -pub trait WasiHttpView { - /// Returns a mutable reference to the WASI HTTP context. - fn ctx(&mut self) -> &mut WasiHttpCtx; - - /// Returns the table used to manage resources. - fn table(&mut self) -> &mut ResourceTable; - - /// Create a new incoming request resource. - fn new_incoming_request( - &mut self, - scheme: Scheme, - req: hyper::Request, - ) -> wasmtime::Result> - where - B: Body + Send + 'static, - B::Error: Into, - Self: Sized, - { - let field_size_limit = self.ctx().field_size_limit; - let (parts, body) = req.into_parts(); - let body = body.map_err(Into::into).boxed_unsync(); - let body = HostIncomingBody::new( - body, - // TODO: this needs to be plumbed through - std::time::Duration::from_millis(600 * 1000), - field_size_limit, - ); - let incoming_req = - HostIncomingRequest::new(self, parts, scheme, Some(body), field_size_limit)?; - Ok(self.table().push(incoming_req)?) - } - - /// Create a new outgoing response resource. - fn new_response_outparam( - &mut self, - result: tokio::sync::oneshot::Sender< - Result, types::ErrorCode>, - >, - ) -> wasmtime::Result> { - let id = self.table().push(HostResponseOutparam { result })?; - Ok(id) - } - - /// Send an outgoing request. - #[cfg(feature = "default-send-request")] - fn send_request( - &mut self, - request: hyper::Request, - config: OutgoingRequestConfig, - ) -> crate::HttpResult { - Ok(default_send_request(request, config)) - } - - /// Send an outgoing request. - #[cfg(not(feature = "default-send-request"))] - fn send_request( - &mut self, - request: hyper::Request, - config: OutgoingRequestConfig, - ) -> crate::HttpResult; - - /// Whether a given header should be considered forbidden and not allowed. - fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { - DEFAULT_FORBIDDEN_HEADERS.contains(name) - } - - /// Number of distinct write calls to the outgoing body's output-stream - /// that the implementation will buffer. - /// Default: 1. - fn outgoing_body_buffer_chunks(&mut self) -> usize { - DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS - } - - /// Maximum size allowed in a write call to the outgoing body's output-stream. - /// Default: 1024 * 1024. - fn outgoing_body_chunk_size(&mut self) -> usize { - DEFAULT_OUTGOING_BODY_CHUNK_SIZE - } -} - -/// The default value configured for [`WasiHttpView::outgoing_body_buffer_chunks`] in [`WasiHttpView`]. -pub const DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS: usize = 1; -/// The default value configured for [`WasiHttpView::outgoing_body_chunk_size`] in [`WasiHttpView`]. -pub const DEFAULT_OUTGOING_BODY_CHUNK_SIZE: usize = 1024 * 1024; - -impl WasiHttpView for &mut T { - fn ctx(&mut self) -> &mut WasiHttpCtx { - T::ctx(self) - } - - fn table(&mut self) -> &mut ResourceTable { - T::table(self) - } - - fn new_response_outparam( - &mut self, - result: tokio::sync::oneshot::Sender< - Result, types::ErrorCode>, - >, - ) -> wasmtime::Result> { - T::new_response_outparam(self, result) - } - - fn send_request( - &mut self, - request: hyper::Request, - config: OutgoingRequestConfig, - ) -> crate::HttpResult { - T::send_request(self, request, config) - } - - fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { - T::is_forbidden_header(self, name) - } - - fn outgoing_body_buffer_chunks(&mut self) -> usize { - T::outgoing_body_buffer_chunks(self) - } - - fn outgoing_body_chunk_size(&mut self) -> usize { - T::outgoing_body_chunk_size(self) - } -} - -impl WasiHttpView for Box { - fn ctx(&mut self) -> &mut WasiHttpCtx { - T::ctx(self) - } - - fn table(&mut self) -> &mut ResourceTable { - T::table(self) - } - - fn new_response_outparam( - &mut self, - result: tokio::sync::oneshot::Sender< - Result, types::ErrorCode>, - >, - ) -> wasmtime::Result> { - T::new_response_outparam(self, result) - } - - fn send_request( - &mut self, - request: hyper::Request, - config: OutgoingRequestConfig, - ) -> crate::HttpResult { - T::send_request(self, request, config) - } - - fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { - T::is_forbidden_header(self, name) - } - - fn outgoing_body_buffer_chunks(&mut self) -> usize { - T::outgoing_body_buffer_chunks(self) - } - - fn outgoing_body_chunk_size(&mut self) -> usize { - T::outgoing_body_chunk_size(self) - } -} - -/// 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_async`](crate::add_to_linker_async) -/// or -/// [`add_to_linker_sync`](crate::add_to_linker_sync) -/// and doesn't need to be manually configured. -#[repr(transparent)] -pub struct WasiHttpImpl(pub T); - -impl WasiHttpView for WasiHttpImpl { - fn ctx(&mut self) -> &mut WasiHttpCtx { - self.0.ctx() - } - - fn table(&mut self) -> &mut ResourceTable { - self.0.table() - } - - fn new_response_outparam( - &mut self, - result: tokio::sync::oneshot::Sender< - Result, types::ErrorCode>, - >, - ) -> wasmtime::Result> { - self.0.new_response_outparam(result) - } - - fn send_request( - &mut self, - request: hyper::Request, - config: OutgoingRequestConfig, - ) -> crate::HttpResult { - self.0.send_request(request, config) - } - - fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { - self.0.is_forbidden_header(name) - } - - fn outgoing_body_buffer_chunks(&mut self) -> usize { - self.0.outgoing_body_buffer_chunks() - } - - fn outgoing_body_chunk_size(&mut self) -> usize { - self.0.outgoing_body_chunk_size() - } -} - -/// 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; 9] = [ - hyper::header::CONNECTION, - HeaderName::from_static("keep-alive"), - hyper::header::PROXY_AUTHENTICATE, - hyper::header::PROXY_AUTHORIZATION, - HeaderName::from_static("proxy-connection"), - hyper::header::TRANSFER_ENCODING, - hyper::header::UPGRADE, - hyper::header::HOST, - HeaderName::from_static("http2-settings"), -]; - -/// Removes forbidden headers from a [`FieldMap`]. -pub(crate) fn remove_forbidden_headers(view: &mut dyn WasiHttpView, headers: &mut FieldMap) { - let forbidden_keys = Vec::from_iter(headers.as_ref().keys().filter_map(|name| { - if view.is_forbidden_header(name) { - Some(name.clone()) - } else { - None - } - })); - - for name in forbidden_keys { - headers.remove_all(&name); - } -} - -/// Configuration for an outgoing request. -pub struct OutgoingRequestConfig { - /// Whether to use TLS for the request. - pub use_tls: bool, - /// The timeout for connecting. - pub connect_timeout: Duration, - /// The timeout until the first byte. - pub first_byte_timeout: Duration, - /// The timeout between chunks of a streaming body - pub between_bytes_timeout: Duration, -} - -/// The default implementation of how an outgoing request is sent. -/// -/// This implementation is used by the `wasi:http/outgoing-handler` interface -/// default implementation. -#[cfg(feature = "default-send-request")] -pub fn default_send_request( - request: hyper::Request, - config: OutgoingRequestConfig, -) -> HostFutureIncomingResponse { - let handle = wasmtime_wasi::runtime::spawn(async move { - Ok(default_send_request_handler(request, config).await) - }); - HostFutureIncomingResponse::pending(handle) -} - -/// The underlying implementation of how an outgoing request is sent. This should likely be spawned -/// in a task. -/// -/// This is called from [default_send_request] to actually send the request. -#[cfg(feature = "default-send-request")] -pub async fn default_send_request_handler( - mut request: hyper::Request, - OutgoingRequestConfig { - use_tls, - connect_timeout, - first_byte_timeout, - between_bytes_timeout, - }: OutgoingRequestConfig, -) -> Result { - let authority = if let Some(authority) = request.uri().authority() { - if authority.port().is_some() { - authority.to_string() - } else { - let port = if use_tls { 443 } else { 80 }; - format!("{}:{port}", authority.to_string()) - } - } else { - return Err(types::ErrorCode::HttpRequestUriInvalid); - }; - let tcp_stream = timeout(connect_timeout, TcpStream::connect(&authority)) - .await - .map_err(|_| types::ErrorCode::ConnectionTimeout)? - .map_err(|e| match e.kind() { - std::io::ErrorKind::AddrNotAvailable => { - dns_error("address not available".to_string(), 0) - } - - _ => { - if e.to_string() - .starts_with("failed to lookup address information") - { - dns_error("address not available".to_string(), 0) - } else { - types::ErrorCode::ConnectionRefused - } - } - })?; - - let (mut sender, worker) = if use_tls { - 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, tcp_stream).await.map_err(|e| { - tracing::warn!("tls protocol error: {e:?}"); - types::ErrorCode::TlsProtocolError - })?; - let stream = TokioIo::new(stream); - - let (sender, conn) = timeout( - connect_timeout, - hyper::client::conn::http1::handshake(stream), - ) - .await - .map_err(|_| types::ErrorCode::ConnectionTimeout)? - .map_err(hyper_request_error)?; - - let worker = wasmtime_wasi::runtime::spawn(async move { - match conn.await { - Ok(()) => {} - // TODO: shouldn't throw away this error and ideally should - // surface somewhere. - Err(e) => tracing::warn!("dropping error {e}"), - } - }); - - (sender, worker) - } else { - let tcp_stream = TokioIo::new(tcp_stream); - let (sender, conn) = timeout( - connect_timeout, - // TODO: we should plumb the builder through the http context, and use it here - hyper::client::conn::http1::handshake(tcp_stream), - ) - .await - .map_err(|_| types::ErrorCode::ConnectionTimeout)? - .map_err(hyper_request_error)?; - - let worker = wasmtime_wasi::runtime::spawn(async move { - match conn.await { - Ok(()) => {} - // TODO: same as above, shouldn't throw this error away. - Err(e) => tracing::warn!("dropping error {e}"), - } - }); - - (sender, worker) - }; - - // 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 resp = timeout(first_byte_timeout, sender.send_request(request)) - .await - .map_err(|_| types::ErrorCode::ConnectionReadTimeout)? - .map_err(hyper_request_error)? - .map(|body| body.map_err(hyper_request_error).boxed_unsync()); - - Ok(IncomingResponse { - resp, - worker: Some(worker), - between_bytes_timeout, - }) -} - -impl From for types::Method { - fn from(method: http::Method) -> Self { - if method == http::Method::GET { - types::Method::Get - } else if method == hyper::Method::HEAD { - types::Method::Head - } else if method == hyper::Method::POST { - types::Method::Post - } else if method == hyper::Method::PUT { - types::Method::Put - } else if method == hyper::Method::DELETE { - types::Method::Delete - } else if method == hyper::Method::CONNECT { - types::Method::Connect - } else if method == hyper::Method::OPTIONS { - types::Method::Options - } else if method == hyper::Method::TRACE { - types::Method::Trace - } else if method == hyper::Method::PATCH { - types::Method::Patch - } else { - types::Method::Other(method.to_string()) - } - } -} - -impl TryInto for types::Method { - type Error = http::method::InvalidMethod; - - fn try_into(self) -> Result { - match self { - Method::Get => Ok(http::Method::GET), - Method::Head => Ok(http::Method::HEAD), - Method::Post => Ok(http::Method::POST), - Method::Put => Ok(http::Method::PUT), - Method::Delete => Ok(http::Method::DELETE), - Method::Connect => Ok(http::Method::CONNECT), - Method::Options => Ok(http::Method::OPTIONS), - Method::Trace => Ok(http::Method::TRACE), - Method::Patch => Ok(http::Method::PATCH), - Method::Other(s) => http::Method::from_bytes(s.as_bytes()), - } - } -} - -/// The concrete type behind a `wasi:http/types.incoming-request` resource. -#[derive(Debug)] -pub struct HostIncomingRequest { - pub(crate) method: http::method::Method, - pub(crate) uri: http::uri::Uri, - pub(crate) headers: FieldMap, - pub(crate) scheme: Scheme, - pub(crate) authority: String, - /// The body of the incoming request. - pub body: Option, -} - -impl HostIncomingRequest { - /// Create a new `HostIncomingRequest`. - pub fn new( - view: &mut dyn WasiHttpView, - parts: http::request::Parts, - scheme: Scheme, - body: Option, - field_size_limit: usize, - ) -> wasmtime::Result { - let authority = match parts.uri.authority() { - Some(authority) => authority.to_string(), - None => match parts.headers.get(http::header::HOST) { - Some(host) => host.to_str()?.to_string(), - None => bail!("invalid HTTP request missing authority in URI and host header"), - }, - }; - - let mut headers = FieldMap::new(parts.headers, field_size_limit); - remove_forbidden_headers(view, &mut headers); - - Ok(Self { - method: parts.method, - uri: parts.uri, - headers, - authority, - scheme, - body, - }) - } -} - -/// The concrete type behind a `wasi:http/types.response-outparam` resource. -pub struct HostResponseOutparam { - /// The sender for sending a response. - pub result: - tokio::sync::oneshot::Sender, types::ErrorCode>>, -} - -/// The concrete type behind a `wasi:http/types.outgoing-response` resource. -pub struct HostOutgoingResponse { - /// The status of the response. - pub status: http::StatusCode, - /// The headers of the response. - pub headers: FieldMap, - /// The body of the response. - pub body: Option, -} - -impl TryFrom for hyper::Response { - type Error = http::Error; - - fn try_from( - resp: HostOutgoingResponse, - ) -> Result, Self::Error> { - use http_body_util::Empty; - - let mut builder = hyper::Response::builder().status(resp.status); - - *builder.headers_mut().unwrap() = resp.headers.map; - - match resp.body { - Some(body) => builder.body(body), - None => builder.body( - Empty::::new() - .map_err(|_| unreachable!("Infallible error")) - .boxed_unsync(), - ), - } - } -} - -/// The concrete type behind a `wasi:http/types.outgoing-request` resource. -#[derive(Debug)] -pub struct HostOutgoingRequest { - /// 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: FieldMap, - /// The request body. - pub body: Option, -} - -/// The concrete type behind a `wasi:http/types.request-options` resource. -#[derive(Debug, Default)] -pub struct HostRequestOptions { - /// 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.incoming-response` resource. -#[derive(Debug)] -pub struct HostIncomingResponse { - /// The response status - pub status: u16, - /// The response headers - pub headers: FieldMap, - /// The response body - pub body: Option, -} - -/// The concrete type behind a `wasi:http/types.fields` resource. -#[derive(Debug)] -pub enum HostFields { - /// A reference to the fields of a parent entry. - Ref { - /// The parent resource rep. - parent: u32, - - /// The function to get the fields from the parent. - // NOTE: there's not failure in the result here because we assume that HostFields will - // always be registered as a child of the entry with the `parent` id. This ensures that the - // entry will always exist while this `HostFields::Ref` entry exists in the table, thus we - // don't need to account for failure when fetching the fields ref from the parent. - get_fields: for<'a> fn(elem: &'a mut (dyn Any + 'static)) -> &'a mut FieldMap, - }, - /// An owned version of the fields. - Owned { - /// The fields themselves. - fields: FieldMap, - }, -} - -/// An owned version of `HostFields`. A wrapper on http `HeaderMap` that -/// keeps a running tally of memory consumed by header names and values. -#[derive(Debug, Clone)] -pub struct FieldMap { - map: HeaderMap, - limit: usize, - size: usize, -} - -/// Error given when a `FieldMap` has exceeded the size limit. -#[derive(Debug)] -pub struct FieldSizeLimitError { - /// The erroring `FieldMap` operation would require this content size - pub(crate) size: usize, - /// The limit set on `FieldMap` content size - pub(crate) limit: usize, -} -impl fmt::Display for FieldSizeLimitError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Field size limit {} exceeded: {}", self.limit, self.size) - } -} -impl std::error::Error for FieldSizeLimitError {} - -impl FieldMap { - /// Construct a `FieldMap` from a `HeaderMap` and a size limit. - /// - /// Construction with a `HeaderMap` which exceeds the size limit is - /// allowed, but subsequent operations to expand the resource use will - /// fail. - pub fn new(map: HeaderMap, limit: usize) -> Self { - let size = Self::content_size(&map); - Self { map, size, limit } - } - /// Construct an empty `FieldMap` - pub fn empty(limit: usize) -> Self { - Self { - map: HeaderMap::new(), - size: 0, - limit, - } - } - /// Get the `HeaderMap` out of the `FieldMap` - pub fn into_inner(self) -> HeaderMap { - self.map - } - /// Calculate the content size of a `HeaderMap`. This is a sum of the size - /// of all of the keys and all of the values. - pub(crate) fn content_size(map: &HeaderMap) -> usize { - let mut sum = 0; - for key in map.keys() { - sum += header_name_size(key); - } - for value in map.values() { - sum += header_value_size(value); - } - sum - } - /// Remove all values associated with a key in a map. - /// - /// Returns an empty list if the key is not already present within the map. - pub fn remove_all(&mut self, key: &HeaderName) -> Vec { - use http::header::Entry; - match self.map.try_entry(key) { - Ok(Entry::Vacant { .. }) | Err(_) => Vec::new(), - Ok(Entry::Occupied(e)) => { - let (name, value_drain) = e.remove_entry_mult(); - let mut removed = header_name_size(&name); - let values = value_drain.collect::>(); - for v in values.iter() { - removed += header_value_size(v); - } - self.size -= removed; - values - } - } - } - /// Add a value associated with a key to the map. - /// - /// If `key` is already present within the map then `value` is appended to - /// the list of values it already has. - pub fn append(&mut self, key: &HeaderName, value: HeaderValue) -> Result { - let key_size = header_name_size(key); - let val_size = header_value_size(&value); - let new_size = if !self.map.contains_key(key) { - self.size + key_size + val_size - } else { - self.size + val_size - }; - if new_size > self.limit { - bail!(FieldSizeLimitError { - limit: self.limit, - size: new_size - }) - } - self.size = new_size; - Ok(self.map.try_append(key, value)?) - } -} - -/// Returns the size, in accounting cost, to consider for `name`. -/// -/// This includes both the byte length of the `name` itself as well as the size -/// of the data structure itself as it'll reside within a `HeaderMap`. -fn header_name_size(name: &HeaderName) -> usize { - name.as_str().len() + size_of::() -} - -/// Same as `header_name_size`, but for values. -/// -/// This notably includes the size of `HeaderValue` itself to ensure that all -/// headers have a nonzero size as otherwise this would never limit addition of -/// an empty header value. -fn header_value_size(value: &HeaderValue) -> usize { - value.len() + size_of::() -} - -// We impl AsRef, but not AsMut, because any modifications of the -// underlying HeaderMap must account for changes in size -impl AsRef for FieldMap { - fn as_ref(&self) -> &HeaderMap { - &self.map - } -} - -/// A handle to a future incoming response. -pub type FutureIncomingResponseHandle = - AbortOnDropJoinHandle>>; - -/// A response that is in the process of being received. -#[derive(Debug)] -pub struct IncomingResponse { - /// The response itself. - pub resp: hyper::Response, - /// Optional worker task that continues to process the response. - pub worker: Option>, - /// The timeout between chunks of the response. - pub between_bytes_timeout: std::time::Duration, -} - -/// The concrete type behind a `wasi:http/types.future-incoming-response` resource. -#[derive(Debug)] -pub enum HostFutureIncomingResponse { - /// A pending response - Pending(FutureIncomingResponseHandle), - /// The response is ready. - /// - /// An outer error will trap while the inner error gets returned to the guest. - Ready(wasmtime::Result>), - /// The response has been consumed. - Consumed, -} - -impl HostFutureIncomingResponse { - /// Create a new `HostFutureIncomingResponse` that is pending on the provided task handle. - pub fn pending(handle: FutureIncomingResponseHandle) -> Self { - Self::Pending(handle) - } - - /// Create a new `HostFutureIncomingResponse` that is ready. - pub fn ready(result: wasmtime::Result>) -> Self { - Self::Ready(result) - } - - /// Returns `true` if the response is ready. - pub fn is_ready(&self) -> bool { - matches!(self, Self::Ready(_)) - } - - /// Unwrap the response, panicking if it is not ready. - pub fn unwrap_ready(self) -> wasmtime::Result> { - match self { - Self::Ready(res) => res, - Self::Pending(_) | Self::Consumed => { - panic!("unwrap_ready called on a pending HostFutureIncomingResponse") - } - } - } -} - -#[async_trait::async_trait] -impl Pollable for HostFutureIncomingResponse { - async fn ready(&mut self) { - if let Self::Pending(handle) = self { - *self = Self::Ready(handle.await); - } - } -} diff --git a/crates/wasi-http/tests/all/p2.rs b/crates/wasi-http/tests/all/p2.rs index 9d070789d027..7de12249ed4c 100644 --- a/crates/wasi-http/tests/all/p2.rs +++ b/crates/wasi-http/tests/all/p2.rs @@ -15,11 +15,12 @@ use wasmtime::{ }; use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView, p2::pipe::MemoryOutputPipe}; use wasmtime_wasi_http::{ - HttpResult, WasiHttpCtx, WasiHttpView, - bindings::http::types::{ErrorCode, Scheme}, - body::HyperOutgoingBody, + WasiHttpCtx, io::TokioIo, - types::{self, HostFutureIncomingResponse, IncomingResponse, OutgoingRequestConfig}, + p2::bindings::http::types::{ErrorCode, Scheme}, + p2::body::HyperOutgoingBody, + p2::types::{self, HostFutureIncomingResponse, IncomingResponse, OutgoingRequestConfig}, + p2::{HttpResult, WasiHttpCtxView, WasiHttpHooks, WasiHttpView}, }; type RequestSender = Arc< @@ -34,6 +35,10 @@ struct Ctx { http: WasiHttpCtx, stdout: MemoryOutputPipe, stderr: MemoryOutputPipe, + hooks: MyHttpHooks, +} + +struct MyHttpHooks { send_request: Option, rejected_authority: Option, } @@ -48,14 +53,16 @@ impl WasiView for Ctx { } impl WasiHttpView for Ctx { - fn ctx(&mut self) -> &mut WasiHttpCtx { - &mut self.http - } - - fn table(&mut self) -> &mut ResourceTable { - &mut self.table + fn http(&mut self) -> WasiHttpCtxView<'_> { + WasiHttpCtxView { + ctx: &mut self.http, + table: &mut self.table, + hooks: &mut self.hooks, + } } +} +impl WasiHttpHooks for MyHttpHooks { fn send_request( &mut self, request: hyper::Request, @@ -70,12 +77,14 @@ impl WasiHttpView for Ctx { if let Some(send_request) = self.send_request.clone() { Ok(send_request(request, config)) } else { - Ok(types::default_send_request(request, config)) + Ok(wasmtime_wasi_http::p2::default_send_request( + request, config, + )) } } fn is_forbidden_header(&mut self, name: &hyper::header::HeaderName) -> bool { - types::DEFAULT_FORBIDDEN_HEADERS.contains(name) + wasmtime_wasi_http::DEFAULT_FORBIDDEN_HEADERS.contains(name) || name.as_str() == "custom-forbidden-header" } } @@ -95,8 +104,10 @@ fn store(engine: &Engine, server: &Server) -> Store { http: WasiHttpCtx::new(), stderr, stdout, - send_request: None, - rejected_authority: None, + hooks: MyHttpHooks { + send_request: None, + rejected_authority: None, + }, }; Store::new(&engine, ctx) @@ -152,26 +163,30 @@ async fn run_wasi_http( http, stderr, stdout, - send_request, - rejected_authority, + hooks: MyHttpHooks { + 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).context("add crate to linker")?; + wasmtime_wasi_http::p2::add_to_linker_async(&mut linker).context("add crate to linker")?; let proxy = - wasmtime_wasi_http::bindings::Proxy::instantiate_async(&mut store, &component, &linker) + wasmtime_wasi_http::p2::bindings::Proxy::instantiate_async(&mut store, &component, &linker) .await .context("instantiate proxy")?; let req = store .data_mut() + .http() .new_incoming_request(Scheme::Http, req) .context("new incoming request")?; let (sender, receiver) = tokio::sync::oneshot::channel(); let out = store .data_mut() + .http() .new_response_outparam(sender) .context("new response outparam")?; @@ -298,7 +313,7 @@ async fn do_wasi_http_hash_all(override_send_request: bool) -> Result<()> { let response = handle(request.into_parts().0).map(|resp| { Ok(IncomingResponse { resp: resp.map(|body| { - body.map_err(wasmtime_wasi_http::hyper_response_error) + body.map_err(wasmtime_wasi_http::p2::hyper_response_error) .boxed_unsync() }), worker: None, diff --git a/crates/wasi-http/tests/all/p2/async_.rs b/crates/wasi-http/tests/all/p2/async_.rs index f5a8674255e5..176a713cd67b 100644 --- a/crates/wasi-http/tests/all/p2/async_.rs +++ b/crates/wasi-http/tests/all/p2/async_.rs @@ -12,7 +12,7 @@ async fn run(path: &str, server: &Server) -> Result<()> { let mut store = store(&engine, server); let mut linker = Linker::new(&engine); wasmtime_wasi::p2::add_to_linker_async(&mut linker)?; - wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)?; + wasmtime_wasi_http::p2::add_only_http_to_linker_async(&mut linker)?; let command = Command::instantiate_async(&mut store, &component, &linker).await?; let result = command.wasi_cli_run().call_run(&mut store).await?; result.map_err(|()| wasmtime::format_err!("run returned an error")) diff --git a/crates/wasi-http/tests/all/p2/sync.rs b/crates/wasi-http/tests/all/p2/sync.rs index 5ac57caa427e..a715c4ba31d2 100644 --- a/crates/wasi-http/tests/all/p2/sync.rs +++ b/crates/wasi-http/tests/all/p2/sync.rs @@ -12,7 +12,7 @@ fn run(path: &str, server: &Server) -> Result<()> { let mut store = store(&engine, server); let mut linker = Linker::new(&engine); wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?; - wasmtime_wasi_http::add_only_http_to_linker_sync(&mut linker)?; + wasmtime_wasi_http::p2::add_only_http_to_linker_sync(&mut linker)?; let command = Command::instantiate(&mut store, &component, &linker)?; let result = command.wasi_cli_run().call_run(&mut store)?; result.map_err(|()| wasmtime::format_err!("run returned an error")) diff --git a/crates/wasi-http/tests/all/p3/mod.rs b/crates/wasi-http/tests/all/p3/mod.rs index 9d11a50ecd39..fd7f67e3073e 100644 --- a/crates/wasi-http/tests/all/p3/mod.rs +++ b/crates/wasi-http/tests/all/p3/mod.rs @@ -22,17 +22,17 @@ use wasmtime_wasi::{TrappableError, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiVi use wasmtime_wasi_http::p3::bindings::Service; use wasmtime_wasi_http::p3::bindings::http::types::ErrorCode; use wasmtime_wasi_http::p3::{ - self, Request, RequestOptions, WasiHttpCtx, WasiHttpCtxView, WasiHttpView, + self, Request, RequestOptions, WasiHttpCtxView, WasiHttpHooks, WasiHttpView, }; -use wasmtime_wasi_http::types::DEFAULT_FORBIDDEN_HEADERS; +use wasmtime_wasi_http::{DEFAULT_FORBIDDEN_HEADERS, WasiHttpCtx}; foreach_p3_http!(assert_test_exists); -struct TestHttpCtx { +struct TestHooks { request_body_tx: Option>>, } -impl WasiHttpCtx for TestHttpCtx { +impl WasiHttpHooks for TestHooks { fn is_forbidden_header(&mut self, name: &http::header::HeaderName) -> bool { name.as_str() == "custom-forbidden-header" || DEFAULT_FORBIDDEN_HEADERS.contains(name) } @@ -83,7 +83,8 @@ impl WasiHttpCtx for TestHttpCtx { struct Ctx { table: ResourceTable, wasi: WasiCtx, - http: TestHttpCtx, + http: WasiHttpCtx, + hooks: TestHooks, } impl Ctx { @@ -91,7 +92,8 @@ impl Ctx { Self { table: ResourceTable::default(), wasi: WasiCtxBuilder::new().inherit_stdio().build(), - http: TestHttpCtx { + http: WasiHttpCtx::new(), + hooks: TestHooks { request_body_tx: Some(request_body_tx), }, } @@ -112,6 +114,7 @@ impl WasiHttpView for Ctx { WasiHttpCtxView { ctx: &mut self.http, table: &mut self.table, + hooks: &mut self.hooks, } } } diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index ec65711df966..ee2d4a95db80 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -15,6 +15,9 @@ include = ["src/**/*", "README.md", "LICENSE", "witx/*", "wit/**/*", "tests/*"] [lints] workspace = true +[package.metadata.docs.rs] +all-features = true + [dependencies] wasmtime = { workspace = true, features = ["runtime", "std"] } wasmtime-wasi-io = { workspace = true, features = ["std"] } diff --git a/src/commands/run.rs b/src/commands/run.rs index 91aa2bf2435c..ffb4f458a09c 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -21,9 +21,7 @@ use wasmtime_wasi::{WasiCtxView, WasiView}; #[cfg(feature = "wasi-config")] use wasmtime_wasi_config::{WasiConfig, WasiConfigVariables}; #[cfg(feature = "wasi-http")] -use wasmtime_wasi_http::{ - DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, DEFAULT_OUTGOING_BODY_CHUNK_SIZE, WasiHttpCtx, -}; +use wasmtime_wasi_http::WasiHttpCtx; #[cfg(feature = "wasi-keyvalue")] use wasmtime_wasi_keyvalue::{WasiKeyValue, WasiKeyValueCtx, WasiKeyValueCtxBuilder}; #[cfg(feature = "wasi-nn")] @@ -184,17 +182,7 @@ impl RunCommand { } } - let host = Host { - #[cfg(feature = "wasi-http")] - wasi_http_outgoing_body_buffer_chunks: self - .run - .common - .wasi - .http_outgoing_body_buffer_chunks, - #[cfg(feature = "wasi-http")] - wasi_http_outgoing_body_chunk_size: self.run.common.wasi.http_outgoing_body_chunk_size, - ..Default::default() - }; + let host = Host::default(); let mut store = Store::new(&engine, host); self.populate_with_wasi(&mut linker, &mut store, &main)?; @@ -1050,7 +1038,7 @@ impl RunCommand { bail!("Cannot enable wasi-http for core wasm modules"); } CliLinker::Component(linker) => { - wasmtime_wasi_http::add_only_http_to_linker_sync(linker)?; + wasmtime_wasi_http::p2::add_only_http_to_linker_async(linker)?; #[cfg(feature = "component-model-async")] if self.run.common.wasi.p3.unwrap_or(crate::common::P3_DEFAULT) { wasmtime_wasi_http::p3::add_to_linker(linker)?; @@ -1197,6 +1185,10 @@ impl RunCommand { /// not compatible with `wasi-threads`. #[derive(Default, Clone)] pub struct Host { + limits: StoreLimits, + #[cfg(feature = "profiling")] + guest_profiler: Option>, + // Legacy wasip1 context using `wasi_common`, not set unless opted-in-to // with the CLI. legacy_p1_ctx: Option, @@ -1220,14 +1212,7 @@ pub struct Host { #[cfg(feature = "wasi-http")] wasi_http: Option>, #[cfg(feature = "wasi-http")] - wasi_http_outgoing_body_buffer_chunks: Option, - #[cfg(feature = "wasi-http")] - wasi_http_outgoing_body_chunk_size: Option, - #[cfg(all(feature = "wasi-http", feature = "component-model-async"))] - p3_http: crate::common::DefaultP3Ctx, - limits: StoreLimits, - #[cfg(feature = "profiling")] - guest_profiler: Option>, + wasi_http_hooks: crate::common::HttpHooks, #[cfg(feature = "wasi-config")] wasi_config: Option>, @@ -1258,33 +1243,27 @@ impl WasiView for Host { } #[cfg(feature = "wasi-http")] -impl wasmtime_wasi_http::types::WasiHttpView for Host { - fn ctx(&mut self) -> &mut WasiHttpCtx { +impl wasmtime_wasi_http::p2::WasiHttpView for Host { + fn http(&mut self) -> wasmtime_wasi_http::p2::WasiHttpCtxView<'_> { let ctx = self.wasi_http.as_mut().unwrap(); - Arc::get_mut(ctx).expect("wasmtime_wasi is not compatible with threads") - } - - fn table(&mut self) -> &mut wasmtime::component::ResourceTable { - WasiView::ctx(self).table - } - - fn outgoing_body_buffer_chunks(&mut self) -> usize { - self.wasi_http_outgoing_body_buffer_chunks - .unwrap_or_else(|| DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS) - } - - fn outgoing_body_chunk_size(&mut self) -> usize { - self.wasi_http_outgoing_body_chunk_size - .unwrap_or_else(|| DEFAULT_OUTGOING_BODY_CHUNK_SIZE) + let ctx = Arc::get_mut(ctx).expect("wasmtime_wasi_http is not compatible with threads"); + wasmtime_wasi_http::p2::WasiHttpCtxView { + table: WasiView::ctx(unwrap_singlethread_context(&mut self.wasip1_ctx)).table, + ctx, + hooks: &mut self.wasi_http_hooks, + } } } #[cfg(all(feature = "wasi-http", feature = "component-model-async"))] impl wasmtime_wasi_http::p3::WasiHttpView for Host { fn http(&mut self) -> wasmtime_wasi_http::p3::WasiHttpCtxView<'_> { + let ctx = self.wasi_http.as_mut().unwrap(); + let ctx = Arc::get_mut(ctx).expect("wasmtime_wasi_http is not compatible with threads"); wasmtime_wasi_http::p3::WasiHttpCtxView { table: WasiView::ctx(unwrap_singlethread_context(&mut self.wasip1_ctx)).table, - ctx: &mut self.p3_http, + ctx, + hooks: &mut self.wasi_http_hooks, } } } diff --git a/src/commands/serve.rs b/src/commands/serve.rs index cacf7110faa9..4badbe845734 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -1,4 +1,4 @@ -use crate::common::{Profile, RunCommon, RunTarget}; +use crate::common::{HttpHooks, Profile, RunCommon, RunTarget}; use bytes::Bytes; use clap::Parser; use futures::future::FutureExt; @@ -20,7 +20,7 @@ use std::{ }; use tokio::io::{self, AsyncWrite}; use tokio::sync::Notify; -use wasmtime::component::{Component, Linker, ResourceTable}; +use wasmtime::component::{Component, Linker}; use wasmtime::{ Engine, Result, Store, StoreContextMut, StoreLimits, UpdateDeadline, bail, error::Context as _, }; @@ -31,10 +31,7 @@ use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; use wasmtime_wasi_http::handler::p2::bindings as p2; use wasmtime_wasi_http::handler::{HandlerState, Proxy, ProxyHandler, ProxyPre, StoreBundle}; use wasmtime_wasi_http::io::TokioIo; -use wasmtime_wasi_http::{ - DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, DEFAULT_OUTGOING_BODY_CHUNK_SIZE, WasiHttpCtx, - WasiHttpView, -}; +use wasmtime_wasi_http::{WasiHttpCtx, p2::WasiHttpView}; #[cfg(feature = "wasi-config")] use wasmtime_wasi_config::{WasiConfig, WasiConfigVariables}; @@ -51,11 +48,7 @@ struct Host { table: wasmtime::component::ResourceTable, ctx: WasiCtx, http: WasiHttpCtx, - http_outgoing_body_buffer_chunks: Option, - http_outgoing_body_chunk_size: Option, - - #[cfg(feature = "component-model-async")] - p3_http: crate::common::DefaultP3Ctx, + hooks: HttpHooks, limits: StoreLimits, @@ -81,22 +74,13 @@ impl WasiView for Host { } } -impl WasiHttpView for Host { - fn ctx(&mut self) -> &mut WasiHttpCtx { - &mut self.http - } - fn table(&mut self) -> &mut ResourceTable { - &mut self.table - } - - fn outgoing_body_buffer_chunks(&mut self) -> usize { - self.http_outgoing_body_buffer_chunks - .unwrap_or_else(|| DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS) - } - - fn outgoing_body_chunk_size(&mut self) -> usize { - self.http_outgoing_body_chunk_size - .unwrap_or_else(|| DEFAULT_OUTGOING_BODY_CHUNK_SIZE) +impl wasmtime_wasi_http::p2::WasiHttpView for Host { + fn http(&mut self) -> wasmtime_wasi_http::p2::WasiHttpCtxView<'_> { + wasmtime_wasi_http::p2::WasiHttpCtxView { + ctx: &mut self.http, + table: &mut self.table, + hooks: &mut self.hooks, + } } } @@ -105,7 +89,8 @@ impl wasmtime_wasi_http::p3::WasiHttpView for Host { fn http(&mut self) -> wasmtime_wasi_http::p3::WasiHttpCtxView<'_> { wasmtime_wasi_http::p3::WasiHttpCtxView { table: &mut self.table, - ctx: &mut self.p3_http, + ctx: &mut self.http, + hooks: &mut self.hooks, } } } @@ -240,8 +225,7 @@ impl ServeCommand { table, ctx: builder.build(), http: self.run.wasi_http_ctx()?, - http_outgoing_body_buffer_chunks: self.run.common.wasi.http_outgoing_body_buffer_chunks, - http_outgoing_body_chunk_size: self.run.common.wasi.http_outgoing_body_chunk_size, + hooks: self.run.wasi_http_hooks(), limits: StoreLimits::default(), @@ -253,8 +237,6 @@ impl ServeCommand { wasi_keyvalue: None, #[cfg(feature = "profiling")] guest_profiler: None, - #[cfg(feature = "component-model-async")] - p3_http: crate::common::DefaultP3Ctx, }; if self.run.common.wasi.nn == Some(true) { @@ -337,13 +319,13 @@ impl ServeCommand { // uses. if cli == Some(true) { self.run.add_wasmtime_wasi_to_linker(linker)?; - wasmtime_wasi_http::add_only_http_to_linker_async(linker)?; + wasmtime_wasi_http::p2::add_only_http_to_linker_async(linker)?; #[cfg(feature = "component-model-async")] if self.run.common.wasi.p3.unwrap_or(crate::common::P3_DEFAULT) { wasmtime_wasi_http::p3::add_to_linker(linker)?; } } else { - wasmtime_wasi_http::add_to_linker_async(linker)?; + wasmtime_wasi_http::p2::add_to_linker_async(linker)?; #[cfg(feature = "component-model-async")] if self.run.common.wasi.p3.unwrap_or(crate::common::P3_DEFAULT) { wasmtime_wasi_http::p3::add_to_linker(linker)?; @@ -854,7 +836,7 @@ async fn handle_request( // `wasmtime::Error`. type P2Response = Result< - hyper::Response, + hyper::Response, p2::http::types::ErrorCode, >; type P3Response = hyper::Response>; @@ -895,8 +877,9 @@ async fn handle_request( let (req, out) = store.with(move |mut store| { let req = store .data_mut() + .http() .new_incoming_request(p2::http::types::Scheme::Http, req)?; - let out = store.data_mut().new_response_outparam(tx)?; + let out = store.data_mut().http().new_response_outparam(tx)?; wasmtime::error::Ok((req, out)) })?; diff --git a/src/common.rs b/src/common.rs index fcaf4c9bfc69..fa7fb061e775 100644 --- a/src/common.rs +++ b/src/common.rs @@ -341,6 +341,22 @@ impl RunCommon { Ok(http) } + #[cfg(feature = "wasi-http")] + pub fn wasi_http_hooks(&self) -> HttpHooks { + HttpHooks { + p2_outgoing_body_buffer_chunks: self + .common + .wasi + .http_outgoing_body_buffer_chunks + .unwrap_or_else(|| wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS), + p2_outgoing_body_chunk_size: self + .common + .wasi + .http_outgoing_body_chunk_size + .unwrap_or_else(|| wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_CHUNK_SIZE), + } + } + pub fn compute_preopen_sockets(&self) -> Result> { let mut listeners = vec![]; @@ -444,8 +460,34 @@ impl Profile { } } -#[derive(Default, Clone)] -#[cfg(all(feature = "wasi-http", feature = "component-model-async"))] -pub struct DefaultP3Ctx; -#[cfg(all(feature = "wasi-http", feature = "component-model-async"))] -impl wasmtime_wasi_http::p3::WasiHttpCtx for DefaultP3Ctx {} +#[derive(Copy, Clone, Debug)] +#[cfg(feature = "wasi-http")] +pub struct HttpHooks { + p2_outgoing_body_buffer_chunks: usize, + p2_outgoing_body_chunk_size: usize, +} + +#[cfg(feature = "wasi-http")] +impl Default for HttpHooks { + fn default() -> Self { + Self { + p2_outgoing_body_buffer_chunks: + wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, + p2_outgoing_body_chunk_size: wasmtime_wasi_http::p2::DEFAULT_OUTGOING_BODY_CHUNK_SIZE, + } + } +} + +#[cfg(feature = "wasi-http")] +impl wasmtime_wasi_http::p2::WasiHttpHooks for HttpHooks { + fn outgoing_body_buffer_chunks(&mut self) -> usize { + self.p2_outgoing_body_buffer_chunks + } + + fn outgoing_body_chunk_size(&mut self) -> usize { + self.p2_outgoing_body_chunk_size + } +} + +#[cfg(feature = "wasi-http")] +impl wasmtime_wasi_http::p3::WasiHttpHooks for HttpHooks {}