diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index df3f87b..a4895dd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,6 +27,7 @@ jobs: with: version: "1.202.0" - run: curl -LO https://github.com/bytecodealliance/wasmtime/releases/download/v19.0.0/wasi_snapshot_preview1.command.wasm + - run: curl -LO https://github.com/bytecodealliance/wasmtime/releases/download/v19.0.0/wasi_snapshot_preview1.reactor.wasm - run: cargo build --examples --target wasm32-wasi --no-default-features @@ -51,9 +52,6 @@ jobs: - run: wasm-tools component new ./target/wasm32-unknown-unknown/debug/examples/cli_command.wasm -o component.wasm - run: wasmtime run component.wasm - - run: wasm-tools component new ./target/wasm32-unknown-unknown/debug/examples/http_proxy.wasm -o component.wasm - - run: wasm-tools component targets wit component.wasm -w wasi:http/proxy - - run: cargo build --examples --target wasm32-wasi --no-default-features --features rand - run: wasm-tools component new ./target/wasm32-wasi/debug/examples/rand-no_std.wasm --adapt ./wasi_snapshot_preview1.command.wasm -o component.wasm @@ -64,6 +62,11 @@ jobs: - run: wasm-tools component new ./target/wasm32-wasi/debug/examples/rand.wasm --adapt ./wasi_snapshot_preview1.command.wasm -o component.wasm - run: wasmtime run component.wasm + - run: cargo build --examples --target wasm32-unknown-unknown --features http + + - run: wasm-tools component new ./target/wasm32-unknown-unknown/debug/examples/http_proxy.wasm -o component.wasm + - run: wasm-tools component targets wit component.wasm -w wasi:http/proxy + rustfmt: name: Rustfmt runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 64e6dc7..f439e6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,12 +19,16 @@ compiler_builtins = { version = "0.1", optional = true } core = { version = "1.0", optional = true, package = "rustc-std-workspace-core" } rustc-std-workspace-alloc = { version = "1.0", optional = true } +# When built with `http` crate integration +http = { version = "1.1", default-features = false, optional = true } + # When built with `rand` crate integration rand = { version = "0.8.5", default-features = false, optional = true } [features] default = ["std"] std = [] +http = ["dep:http", "http/std", "std"] # Unstable feature to support being a libstd dependency rustc-dep-of-std = ["compiler_builtins", "core", "rustc-std-workspace-alloc"] @@ -48,7 +52,7 @@ crate-type = ["cdylib"] [[example]] name = "http-proxy" crate-type = ["cdylib"] -required-features = ["std"] +required-features = ["http", "std"] [[example]] name = "rand-no_std" diff --git a/examples/http-proxy.rs b/examples/http-proxy.rs index 8226e8b..48c559a 100644 --- a/examples/http-proxy.rs +++ b/examples/http-proxy.rs @@ -1,21 +1,38 @@ use std::io::Write as _; -use wasi::http::types::{ - Fields, IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam, -}; +use wasi::http::types::{IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam}; wasi::http::proxy::export!(Example); struct Example; impl wasi::exports::http::incoming_handler::Guest for Example { - fn handle(_request: IncomingRequest, response_out: ResponseOutparam) { - let resp = OutgoingResponse::new(Fields::new()); + fn handle(request: IncomingRequest, response_out: ResponseOutparam) { + let req_headers = + http::HeaderMap::try_from(request.headers()).expect("failed to parse headers"); + let mut resp_headers = http::HeaderMap::new(); + for (name, value) in req_headers.iter() { + // Append `-orig` to all request headers and send them back to the client + resp_headers.append( + http::HeaderName::try_from(format!("{name}-orig")).unwrap(), + value.clone(), + ); + } + let resp = OutgoingResponse::new(resp_headers.into()); let body = resp.body().unwrap(); ResponseOutparam::set(response_out, Ok(resp)); let mut out = body.write().unwrap(); + + let method = http::Method::try_from(request.method()).unwrap(); + writeln!(out, "method: {method}").unwrap(); + + if let Some(scheme) = request.scheme() { + let scheme = http::uri::Scheme::try_from(scheme).unwrap(); + writeln!(out, "scheme: {scheme}").unwrap(); + } + out.write_all(b"Hello, WASI!").unwrap(); out.flush().unwrap(); drop(out); diff --git a/src/ext/http.rs b/src/ext/http.rs new file mode 100644 index 0000000..91b6f7c --- /dev/null +++ b/src/ext/http.rs @@ -0,0 +1,119 @@ +pub use http; + +use core::fmt::Display; + +impl From for crate::http::types::Method { + fn from(method: http::Method) -> Self { + use std::string::ToString; + + match method.as_str() { + "GET" => Self::Get, + "HEAD" => Self::Head, + "POST" => Self::Post, + "PUT" => Self::Put, + "DELETE" => Self::Delete, + "CONNECT" => Self::Connect, + "OPTIONS" => Self::Options, + "TRACE" => Self::Trace, + "PATCH" => Self::Patch, + _ => Self::Other(method.to_string()), + } + } +} + +impl TryFrom for http::Method { + type Error = http::method::InvalidMethod; + + fn try_from(method: crate::http::types::Method) -> Result { + match method { + crate::http::types::Method::Get => Ok(Self::GET), + crate::http::types::Method::Head => Ok(Self::HEAD), + crate::http::types::Method::Post => Ok(Self::POST), + crate::http::types::Method::Put => Ok(Self::PUT), + crate::http::types::Method::Delete => Ok(Self::DELETE), + crate::http::types::Method::Connect => Ok(Self::CONNECT), + crate::http::types::Method::Options => Ok(Self::OPTIONS), + crate::http::types::Method::Trace => Ok(Self::TRACE), + crate::http::types::Method::Patch => Ok(Self::PATCH), + crate::http::types::Method::Other(method) => method.parse(), + } + } +} + +impl From for crate::http::types::Scheme { + fn from(scheme: http::uri::Scheme) -> Self { + use std::string::ToString; + + match scheme.as_str() { + "http" => Self::Http, + "https" => Self::Https, + _ => Self::Other(scheme.to_string()), + } + } +} + +impl TryFrom for http::uri::Scheme { + type Error = http::uri::InvalidUri; + + fn try_from(scheme: crate::http::types::Scheme) -> Result { + match scheme { + crate::http::types::Scheme::Http => Ok(Self::HTTP), + crate::http::types::Scheme::Https => Ok(Self::HTTPS), + crate::http::types::Scheme::Other(scheme) => scheme.parse(), + } + } +} + +#[derive(Debug)] +pub enum FieldsToHeaderMapError { + InvalidHeaderName(http::header::InvalidHeaderName), + InvalidHeaderValue(http::header::InvalidHeaderValue), +} + +impl Display for FieldsToHeaderMapError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + FieldsToHeaderMapError::InvalidHeaderName(e) => write!(f, "invalid header name: {e}"), + FieldsToHeaderMapError::InvalidHeaderValue(e) => write!(f, "invalid header value: {e}"), + } + } +} + +impl std::error::Error for FieldsToHeaderMapError {} + +impl TryFrom for http::HeaderMap { + type Error = FieldsToHeaderMapError; + + fn try_from(fields: crate::http::types::Fields) -> Result { + let mut headers = http::HeaderMap::new(); + for (name, value) in fields.entries() { + let name = http::HeaderName::try_from(name) + .map_err(FieldsToHeaderMapError::InvalidHeaderName)?; + let value = http::HeaderValue::try_from(value) + .map_err(FieldsToHeaderMapError::InvalidHeaderValue)?; + match headers.entry(name) { + http::header::Entry::Vacant(entry) => { + entry.insert(value); + } + http::header::Entry::Occupied(mut entry) => { + entry.append(value); + } + }; + } + Ok(headers) + } +} + +impl From for crate::http::types::Fields { + fn from(headers: http::HeaderMap) -> Self { + use std::string::ToString; + + let fields = crate::http::types::Fields::new(); + for (name, value) in headers.iter() { + fields + .append(&name.to_string(), &value.as_bytes().to_vec()) + .expect("failed to append header") + } + fields + } +} diff --git a/src/ext/mod.rs b/src/ext/mod.rs index c982618..f632a13 100644 --- a/src/ext/mod.rs +++ b/src/ext/mod.rs @@ -1,6 +1,9 @@ #[cfg(feature = "std")] mod std; +#[cfg(feature = "http")] +pub mod http; + #[cfg(feature = "rand")] pub mod rand;