diff --git a/Cargo.lock b/Cargo.lock index 6a380d5a..2a94ac01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -491,7 +491,7 @@ dependencies = [ "itertools 0.12.1", "log", "smallvec", - "wasmparser", + "wasmparser 0.202.0", "wasmtime-types", ] @@ -648,9 +648,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1762,18 +1762,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", @@ -1782,9 +1782,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -2333,6 +2333,7 @@ name = "viceroy-lib" version = "0.9.7" dependencies = [ "anyhow", + "async-trait", "bytes", "bytesize", "cfg-if", @@ -2362,6 +2363,7 @@ dependencies = [ "tracing", "tracing-futures", "url", + "wasmparser 0.207.0", "wasmtime", "wasmtime-wasi", "wasmtime-wasi-nn", @@ -2458,9 +2460,9 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.206.0" +version = "0.207.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d759312e1137f199096d80a70be685899cd7d3d09c572836bb2e9b69b4dc3b1e" +checksum = "d996306fb3aeaee0d9157adbe2f670df0236caf19f6728b221e92d0f27b3fe17" dependencies = [ "leb128", ] @@ -2476,6 +2478,19 @@ dependencies = [ "semver 1.0.23", ] +[[package]] +name = "wasmparser" +version = "0.207.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19bb9f8ab07616da582ef8adb24c54f1424c7ec876720b7da9db8ec0626c92c" +dependencies = [ + "ahash", + "bitflags 2.5.0", + "hashbrown 0.14.5", + "indexmap", + "semver 1.0.23", +] + [[package]] name = "wasmprinter" version = "0.202.0" @@ -2483,7 +2498,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab1cc9508685eef9502e787f4d4123745f5651a1e29aec047645d3cac1e2da7a" dependencies = [ "anyhow", - "wasmparser", + "wasmparser 0.202.0", ] [[package]] @@ -2516,7 +2531,7 @@ dependencies = [ "serde_json", "target-lexicon", "wasm-encoder 0.202.0", - "wasmparser", + "wasmparser 0.202.0", "wasmtime-cache", "wasmtime-component-macro", "wasmtime-component-util", @@ -2601,7 +2616,7 @@ dependencies = [ "object 0.33.0", "target-lexicon", "thiserror", - "wasmparser", + "wasmparser 0.202.0", "wasmtime-environ", "wasmtime-versioned-export-macros", ] @@ -2626,7 +2641,7 @@ dependencies = [ "target-lexicon", "thiserror", "wasm-encoder 0.202.0", - "wasmparser", + "wasmparser 0.202.0", "wasmprinter", "wasmtime-component-util", "wasmtime-types", @@ -2716,7 +2731,7 @@ dependencies = [ "serde", "serde_derive", "thiserror", - "wasmparser", + "wasmparser 0.202.0", ] [[package]] @@ -2787,7 +2802,7 @@ dependencies = [ "gimli", "object 0.33.0", "target-lexicon", - "wasmparser", + "wasmparser 0.202.0", "wasmtime-cranelift", "wasmtime-environ", "winch-codegen", @@ -2816,24 +2831,24 @@ dependencies = [ [[package]] name = "wast" -version = "206.0.0" +version = "207.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68586953ee4960b1f5d84ebf26df3b628b17e6173bc088e0acfbce431469795a" +checksum = "0e40be9fd494bfa501309487d2dc0b3f229be6842464ecbdc54eac2679c84c93" dependencies = [ "bumpalo", "leb128", "memchr", "unicode-width", - "wasm-encoder 0.206.0", + "wasm-encoder 0.207.0", ] [[package]] name = "wat" -version = "1.206.0" +version = "1.207.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da4c6f2606276c6e991aebf441b2fc92c517807393f039992a3e0ad873efe4ad" +checksum = "8eb2b15e2d5f300f5e1209e7dc237f2549edbd4203655b6c6cab5cf180561ee7" dependencies = [ - "wast 206.0.0", + "wast 207.0.0", ] [[package]] @@ -2922,7 +2937,7 @@ dependencies = [ "regalloc2", "smallvec", "target-lexicon", - "wasmparser", + "wasmparser 0.202.0", "wasmtime-cranelift", "wasmtime-environ", ] @@ -3109,7 +3124,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser", + "wasmparser 0.202.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1256dacc..1068a5ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,3 +42,4 @@ wasmtime = "20.0.0" wasmtime-wasi = "20.0.0" wasmtime-wasi-nn = "20.0.0" wiggle = "20.0.0" +wasmparser = "0.207.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index c5639d9b..62603b2f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -47,8 +47,8 @@ tracing-futures = { workspace = true } tracing-subscriber = { version = "^0.3.16", features = ["env-filter", "fmt"] } viceroy-lib = { path = "../lib", version = "^0.9.7" } wat = "^1.0.38" -wasmtime-wasi = { workspace = true } wasmtime = { workspace = true } +wasmtime-wasi = { workspace = true } libc = "^0.2.139" [dev-dependencies] diff --git a/cli/tests/trap-test/Cargo.lock b/cli/tests/trap-test/Cargo.lock index 30db39db..7b891d46 100644 --- a/cli/tests/trap-test/Cargo.lock +++ b/cli/tests/trap-test/Cargo.lock @@ -518,7 +518,7 @@ dependencies = [ "itertools 0.12.1", "log", "smallvec", - "wasmparser", + "wasmparser 0.202.0", "wasmtime-types", ] @@ -662,9 +662,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1763,18 +1763,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", @@ -1783,9 +1783,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -2282,6 +2282,7 @@ name = "viceroy-lib" version = "0.9.7" dependencies = [ "anyhow", + "async-trait", "bytes", "bytesize", "cfg-if", @@ -2310,6 +2311,7 @@ dependencies = [ "tracing", "tracing-futures", "url", + "wasmparser 0.207.0", "wasmtime", "wasmtime-wasi", "wasmtime-wasi-nn", @@ -2406,9 +2408,9 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.206.0" +version = "0.207.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d759312e1137f199096d80a70be685899cd7d3d09c572836bb2e9b69b4dc3b1e" +checksum = "d996306fb3aeaee0d9157adbe2f670df0236caf19f6728b221e92d0f27b3fe17" dependencies = [ "leb128", ] @@ -2424,6 +2426,19 @@ dependencies = [ "semver 1.0.23", ] +[[package]] +name = "wasmparser" +version = "0.207.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19bb9f8ab07616da582ef8adb24c54f1424c7ec876720b7da9db8ec0626c92c" +dependencies = [ + "ahash", + "bitflags 2.5.0", + "hashbrown 0.14.5", + "indexmap", + "semver 1.0.23", +] + [[package]] name = "wasmprinter" version = "0.202.0" @@ -2431,7 +2446,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab1cc9508685eef9502e787f4d4123745f5651a1e29aec047645d3cac1e2da7a" dependencies = [ "anyhow", - "wasmparser", + "wasmparser 0.202.0", ] [[package]] @@ -2464,7 +2479,7 @@ dependencies = [ "serde_json", "target-lexicon", "wasm-encoder 0.202.0", - "wasmparser", + "wasmparser 0.202.0", "wasmtime-cache", "wasmtime-component-macro", "wasmtime-component-util", @@ -2549,7 +2564,7 @@ dependencies = [ "object 0.33.0", "target-lexicon", "thiserror", - "wasmparser", + "wasmparser 0.202.0", "wasmtime-environ", "wasmtime-versioned-export-macros", ] @@ -2574,7 +2589,7 @@ dependencies = [ "target-lexicon", "thiserror", "wasm-encoder 0.202.0", - "wasmparser", + "wasmparser 0.202.0", "wasmprinter", "wasmtime-component-util", "wasmtime-types", @@ -2664,7 +2679,7 @@ dependencies = [ "serde", "serde_derive", "thiserror", - "wasmparser", + "wasmparser 0.202.0", ] [[package]] @@ -2735,7 +2750,7 @@ dependencies = [ "gimli", "object 0.33.0", "target-lexicon", - "wasmparser", + "wasmparser 0.202.0", "wasmtime-cranelift", "wasmtime-environ", "winch-codegen", @@ -2764,24 +2779,24 @@ dependencies = [ [[package]] name = "wast" -version = "206.0.0" +version = "207.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68586953ee4960b1f5d84ebf26df3b628b17e6173bc088e0acfbce431469795a" +checksum = "0e40be9fd494bfa501309487d2dc0b3f229be6842464ecbdc54eac2679c84c93" dependencies = [ "bumpalo", "leb128", "memchr", "unicode-width", - "wasm-encoder 0.206.0", + "wasm-encoder 0.207.0", ] [[package]] name = "wat" -version = "1.206.0" +version = "1.207.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da4c6f2606276c6e991aebf441b2fc92c517807393f039992a3e0ad873efe4ad" +checksum = "8eb2b15e2d5f300f5e1209e7dc237f2549edbd4203655b6c6cab5cf180561ee7" dependencies = [ - "wast 206.0.0", + "wast 207.0.0", ] [[package]] @@ -2870,7 +2885,7 @@ dependencies = [ "regalloc2", "smallvec", "target-lexicon", - "wasmparser", + "wasmparser 0.202.0", "wasmtime-cranelift", "wasmtime-environ", ] @@ -3057,7 +3072,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser", + "wasmparser 0.202.0", ] [[package]] diff --git a/lib/Cargo.toml b/lib/Cargo.toml index c91c8eca..b8c3f749 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -19,11 +19,13 @@ include = [ "../CHANGELOG.md", "../SECURITY.md", "src/**/*", + "wit/**/*", "compute-at-edge-abi/**/*.witx", ] [dependencies] anyhow = { workspace = true } +async-trait = "0.1.59" bytes = "^1.2.1" bytesize = "^1.1.0" cfg-if = "^1.0" @@ -52,6 +54,7 @@ toml = "^0.5.9" tracing = { workspace = true } tracing-futures = { workspace = true } url = { workspace = true } +wasmparser = { workspace = true } wasmtime = { workspace = true } wasmtime-wasi = { workspace = true } wasmtime-wasi-nn = { workspace = true } diff --git a/lib/src/component/async_io.rs b/lib/src/component/async_io.rs new file mode 100644 index 00000000..2c6bedfb --- /dev/null +++ b/lib/src/component/async_io.rs @@ -0,0 +1,53 @@ +use { + super::fastly::api::{async_io, types}, + super::FastlyError, + crate::{session::Session, wiggle_abi}, + futures::FutureExt, + std::time::Duration, +}; + +#[async_trait::async_trait] +impl async_io::Host for Session { + async fn select( + &mut self, + hs: Vec, + timeout_ms: u32, + ) -> Result, FastlyError> { + if hs.is_empty() && timeout_ms == 0 { + return Err(types::Error::InvalidArgument.into()); + } + + let select_fut = self.select_impl( + hs.iter() + .copied() + .map(|i| wiggle_abi::types::AsyncItemHandle::from(i).into()), + ); + + if timeout_ms == 0 { + let h = select_fut.await?; + return Ok(Some(h as u32)); + } + + let res = tokio::time::timeout(Duration::from_millis(timeout_ms as u64), select_fut).await; + + match res { + // got a handle + Ok(Ok(h)) => Ok(Some(h as u32)), + + // timeout elapsed + Err(_) => Ok(None), + + // some other error happened, but the future resolved + Ok(Err(e)) => Err(e.into()), + } + } + + async fn is_ready(&mut self, handle: async_io::Handle) -> Result { + let handle = wiggle_abi::types::AsyncItemHandle::from(handle); + Ok(self + .async_item_mut(handle.into())? + .await_ready() + .now_or_never() + .is_some()) + } +} diff --git a/lib/src/component/backend.rs b/lib/src/component/backend.rs new file mode 100644 index 00000000..d69443d0 --- /dev/null +++ b/lib/src/component/backend.rs @@ -0,0 +1,139 @@ +use { + super::fastly::api::{backend, http_types}, + super::FastlyError, + crate::{error::Error, session::Session}, +}; + +#[async_trait::async_trait] +impl backend::Host for Session { + async fn exists(&mut self, backend: String) -> Result { + Ok(self.backend(&backend).is_some()) + } + + async fn is_healthy(&mut self, backend: String) -> Result { + // just doing this to get a different error if the backend doesn't exist + let _ = self.backend(&backend).ok_or(Error::InvalidArgument)?; + Ok(backend::BackendHealth::Unknown) + } + + async fn is_dynamic(&mut self, backend: String) -> Result { + if self.dynamic_backend(&backend).is_some() { + Ok(true) + } else if self.backend(&backend).is_some() { + Ok(false) + } else { + Err(Error::InvalidArgument.into()) + } + } + + async fn get_host(&mut self, backend: String, max_len: u64) -> Result { + let backend = self.backend(&backend).ok_or(Error::InvalidArgument)?; + + let host = backend.uri.host().expect("backend uri has host"); + + if host.len() > usize::try_from(max_len).unwrap() { + return Err(Error::BufferLengthError { + buf: "host_out", + len: "host_max_len", + } + .into()); + } + + Ok(String::from(host)) + } + + async fn get_override_host( + &mut self, + backend: String, + max_len: u64, + ) -> Result, FastlyError> { + let backend = self.backend(&backend).ok_or(Error::InvalidArgument)?; + if let Some(host) = backend.override_host.as_ref() { + let host = host.to_str()?; + + if host.len() > usize::try_from(max_len).unwrap() { + return Err(Error::BufferLengthError { + buf: "host_out", + len: "host_max_len", + } + .into()); + } + + Ok(Some(String::from(host))) + } else { + Ok(None) + } + } + + async fn get_port(&mut self, backend: String) -> Result { + let backend = self.backend(&backend).ok_or(Error::InvalidArgument)?; + match backend.uri.port_u16() { + Some(port) => Ok(port), + None => { + if backend.uri.scheme() == Some(&http::uri::Scheme::HTTPS) { + Ok(443) + } else { + Ok(80) + } + } + } + } + + async fn get_connect_timeout_ms(&mut self, backend: String) -> Result { + // just doing this to get a different error if the backend doesn't exist + let _ = self.backend(&backend).ok_or(Error::InvalidArgument)?; + Err(Error::Unsupported { + msg: "connection timing is not actually supported in Viceroy", + } + .into()) + } + + async fn get_first_byte_timeout_ms(&mut self, backend: String) -> Result { + // just doing this to get a different error if the backend doesn't exist + let _ = self.backend(&backend).ok_or(Error::InvalidArgument)?; + Err(Error::Unsupported { + msg: "connection timing is not actually supported in Viceroy", + } + .into()) + } + + async fn get_between_bytes_timeout_ms(&mut self, backend: String) -> Result { + // just doing this to get a different error if the backend doesn't exist + let _ = self.backend(&backend).ok_or(Error::InvalidArgument)?; + Err(Error::Unsupported { + msg: "connection timing is not actually supported in Viceroy", + } + .into()) + } + + async fn is_ssl(&mut self, backend: String) -> Result { + let backend = self.backend(&backend).ok_or(Error::InvalidArgument)?; + Ok(backend.uri.scheme() == Some(&http::uri::Scheme::HTTPS)) + } + + async fn get_ssl_min_version( + &mut self, + backend: String, + ) -> Result { + // just doing this to get a different error if the backend doesn't exist + let _ = self.backend(&backend).ok_or(Error::InvalidArgument)?; + // health checks are not enabled in Viceroy :( + Err(Error::Unsupported { + msg: "ssl version flags are not supported in Viceroy", + } + .into()) + } + + async fn get_ssl_max_version( + &mut self, + backend: String, + ) -> Result { + // just doing this to get a different error if the backend doesn't exist + let _ = self.backend(&backend).ok_or(Error::InvalidArgument)?; + // health checks are not enabled in Viceroy :( + Err(Error::Unsupported { + msg: "ssl version flags are not supported in Viceroy", + } + .into()) + } +} diff --git a/lib/src/component/cache.rs b/lib/src/component/cache.rs new file mode 100644 index 00000000..391acdbf --- /dev/null +++ b/lib/src/component/cache.rs @@ -0,0 +1,161 @@ +use { + super::fastly::api::{cache, http_types}, + super::FastlyError, + crate::{error::Error, session::Session}, +}; + +#[async_trait::async_trait] +impl cache::Host for Session { + async fn lookup( + &mut self, + _key: String, + _options_mask: cache::LookupOptionsMask, + _options: cache::LookupOptions, + ) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn insert( + &mut self, + _key: String, + _options_mask: cache::WriteOptionsMask, + _options: cache::WriteOptions, + ) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn get_body( + &mut self, + _handle: cache::Handle, + _options_mask: cache::GetBodyOptionsMask, + _options: cache::GetBodyOptions, + ) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn transaction_lookup( + &mut self, + _key: String, + _options_mask: cache::LookupOptionsMask, + _options: cache::LookupOptions, + ) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn transaction_insert( + &mut self, + _handle: cache::Handle, + _options_mask: cache::WriteOptionsMask, + _options: cache::WriteOptions, + ) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn transaction_insert_and_stream_back( + &mut self, + _handle: cache::Handle, + _options_mask: cache::WriteOptionsMask, + _options: cache::WriteOptions, + ) -> Result<(http_types::BodyHandle, cache::Handle), FastlyError> { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn transaction_update( + &mut self, + _handle: cache::Handle, + _options_mask: cache::WriteOptionsMask, + _options: cache::WriteOptions, + ) -> Result<(), FastlyError> { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn transaction_cancel(&mut self, _handle: cache::Handle) -> Result<(), FastlyError> { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn close(&mut self, _handle: cache::Handle) -> Result<(), FastlyError> { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn get_state( + &mut self, + _handle: cache::Handle, + ) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn get_user_metadata(&mut self, _handle: cache::Handle) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn get_length(&mut self, _handle: cache::Handle) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn get_max_age_ns(&mut self, _handle: cache::Handle) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn get_stale_while_revalidate_ns( + &mut self, + _handle: cache::Handle, + ) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn get_age_ns(&mut self, _handle: cache::Handle) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } + + async fn get_hits(&mut self, _handle: cache::Handle) -> Result { + Err(Error::Unsupported { + msg: "Cache API primitives not yet supported", + } + .into()) + } +} diff --git a/lib/src/component/dictionary.rs b/lib/src/component/dictionary.rs new file mode 100644 index 00000000..0d9c8026 --- /dev/null +++ b/lib/src/component/dictionary.rs @@ -0,0 +1,40 @@ +use { + super::fastly::api::{dictionary, types}, + super::FastlyError, + crate::{error, session::Session}, +}; + +#[async_trait::async_trait] +impl dictionary::Host for Session { + async fn open(&mut self, name: String) -> Result { + let handle = self.dictionary_handle(name.as_str())?; + Ok(handle.into()) + } + + async fn get( + &mut self, + h: dictionary::Handle, + key: String, + max_len: u64, + ) -> Result, FastlyError> { + let dict = self + .dictionary(h.into())? + .contents() + .map_err(|err| error::Error::Other(err.into()))?; + + let key = key.as_str(); + let item = dict + .get(key) + .ok_or_else(|| FastlyError::from(types::Error::OptionalNone))?; + + if item.len() > usize::try_from(max_len).unwrap() { + return Err(error::Error::BufferLengthError { + buf: "item_out", + len: "item_max_len", + } + .into()); + } + + Ok(Some(item.clone())) + } +} diff --git a/lib/src/component/error.rs b/lib/src/component/error.rs new file mode 100644 index 00000000..12594435 --- /dev/null +++ b/lib/src/component/error.rs @@ -0,0 +1,210 @@ +use { + super::fastly::api::types, + super::FastlyError, + crate::{ + config::ClientCertError, + error::{self, HandleError}, + object_store::{KeyValidationError, ObjectStoreError}, + wiggle_abi::{DictionaryError, SecretStoreError}, + }, + http::{ + header::{InvalidHeaderName, InvalidHeaderValue, ToStrError}, + method::InvalidMethod, + status::InvalidStatusCode, + uri::InvalidUri, + }, +}; + +impl From for FastlyError { + fn from(err: types::Error) -> Self { + Self::FastlyError(err.into()) + } +} + +impl From for FastlyError { + fn from(_: HandleError) -> Self { + types::Error::BadHandle.into() + } +} + +impl From for FastlyError { + fn from(_: ClientCertError) -> Self { + types::Error::GenericError.into() + } +} + +impl From for FastlyError { + fn from(_: InvalidStatusCode) -> Self { + types::Error::InvalidArgument.into() + } +} + +impl From for FastlyError { + fn from(_: InvalidHeaderName) -> Self { + types::Error::GenericError.into() + } +} + +impl From for FastlyError { + fn from(_: InvalidHeaderValue) -> Self { + types::Error::GenericError.into() + } +} + +impl From for FastlyError { + fn from(_: std::str::Utf8Error) -> Self { + types::Error::GenericError.into() + } +} + +impl From for FastlyError { + fn from(_: std::io::Error) -> Self { + types::Error::GenericError.into() + } +} + +impl From for FastlyError { + fn from(_: ToStrError) -> Self { + types::Error::GenericError.into() + } +} + +impl From for FastlyError { + fn from(_: InvalidMethod) -> Self { + types::Error::GenericError.into() + } +} + +impl From for FastlyError { + fn from(_: InvalidUri) -> Self { + types::Error::GenericError.into() + } +} + +impl From for FastlyError { + fn from(_: http::Error) -> Self { + types::Error::GenericError.into() + } +} + +impl From for FastlyError { + fn from(err: wiggle::GuestError) -> Self { + use wiggle::GuestError::*; + match err { + PtrNotAligned { .. } => types::Error::BadAlign.into(), + // We may want to expand the FastlyStatus enum to distinguish between more of these + // values. + InvalidFlagValue { .. } + | InvalidEnumValue { .. } + | PtrOutOfBounds { .. } + | PtrBorrowed { .. } + | PtrOverflow { .. } + | InvalidUtf8 { .. } + | TryFromIntError { .. } => types::Error::InvalidArgument.into(), + // These errors indicate either a pathological user input or an internal programming + // error + BorrowCheckerOutOfHandles | SliceLengthsDiffer => types::Error::UnknownError.into(), + // Recursive case: InFunc wraps a GuestError with some context which + // doesn't determine what sort of FastlyStatus we return. + InFunc { err, .. } => Self::from(*err), + } + } +} + +impl From for FastlyError { + fn from(err: ObjectStoreError) -> Self { + use ObjectStoreError::*; + match err { + MissingObject => types::Error::OptionalNone.into(), + PoisonedLock => panic!("{}", err), + UnknownObjectStore(_) => types::Error::InvalidArgument.into(), + } + } +} + +impl From for FastlyError { + fn from(_: KeyValidationError) -> FastlyError { + types::Error::GenericError.into() + } +} + +impl From for FastlyError { + fn from(err: SecretStoreError) -> Self { + use SecretStoreError::*; + match err { + UnknownSecretStore(_) => types::Error::OptionalNone.into(), + UnknownSecret(_) => types::Error::OptionalNone.into(), + InvalidSecretStoreHandle(_) => types::Error::InvalidArgument.into(), + InvalidSecretHandle(_) => types::Error::InvalidArgument.into(), + } + } +} + +impl From for FastlyError { + fn from(err: DictionaryError) -> Self { + use DictionaryError::*; + match err { + UnknownDictionaryItem(_) => types::Error::OptionalNone.into(), + UnknownDictionary(_) => types::Error::InvalidArgument.into(), + } + } +} + +impl From for FastlyError { + fn from(err: error::Error) -> Self { + use error::Error; + match err { + Error::BufferLengthError { .. } => types::Error::BufferLen.into(), + Error::InvalidArgument => types::Error::InvalidArgument.into(), + Error::Unsupported { .. } => types::Error::Unsupported.into(), + Error::HandleError { .. } => types::Error::BadHandle.into(), + Error::InvalidStatusCode { .. } => types::Error::InvalidArgument.into(), + // Map specific kinds of `hyper::Error` into their respective error codes. + Error::HyperError(e) if e.is_parse() => types::Error::HttpInvalid.into(), + Error::HyperError(e) if e.is_user() => types::Error::HttpUser.into(), + Error::HyperError(e) if e.is_incomplete_message() => { + types::Error::HttpIncomplete.into() + } + Error::HyperError(_) => types::Error::UnknownError.into(), + // Destructuring a GuestError is recursive, so we use a helper function: + Error::GuestError(e) => e.into(), + // We delegate to some error types' own implementation of `to_fastly_status`. + Error::DictionaryError(e) => e.into(), + Error::ObjectStoreError(e) => e.into(), + Error::SecretStoreError(e) => e.into(), + // All other hostcall errors map to a generic `ERROR` value. + Error::AbiVersionMismatch + | Error::BackendUrl(_) + | Error::BadCerts(_) + | Error::DownstreamRequestError(_) + | Error::DownstreamRespSending + | Error::FastlyConfig(_) + | Error::FatalError(_) + | Error::FileFormat + | Error::Infallible(_) + | Error::InvalidClientCert(_) + | Error::InvalidHeaderName(_) + | Error::InvalidHeaderValue(_) + | Error::InvalidMethod(_) + | Error::InvalidUri(_) + | Error::IoError(_) + | Error::NotAvailable(_) + | Error::Other(_) + | Error::ProfilingStrategy + | Error::StreamingChunkSend + | Error::UnknownBackend(_) + | Error::Utf8Expected(_) + | Error::BackendNameRegistryError(_) + | Error::HttpError(_) + | Error::UnknownObjectStore(_) + | Error::ObjectStoreKeyValidationError(_) + | Error::UnfinishedStreamingBody + | Error::ValueAbsent + | Error::ToStr(_) + | Error::InvalidAlpnRepsonse { .. } + | Error::DeviceDetectionError(_) + | Error::Again + | Error::SharedMemory => types::Error::GenericError.into(), + } + } +} diff --git a/lib/src/component/geo.rs b/lib/src/component/geo.rs new file mode 100644 index 00000000..fd31a5c3 --- /dev/null +++ b/lib/src/component/geo.rs @@ -0,0 +1,35 @@ +use { + super::fastly::api::geo, + super::FastlyError, + crate::{error, session::Session}, + std::net::{IpAddr, Ipv4Addr, Ipv6Addr}, +}; + +#[async_trait::async_trait] +impl geo::Host for Session { + async fn lookup(&mut self, octets: Vec, max_len: u64) -> Result { + let ip_addr: IpAddr = match octets.len() { + 4 => IpAddr::V4(Ipv4Addr::from( + TryInto::<[u8; 4]>::try_into(octets).unwrap(), + )), + 16 => IpAddr::V6(Ipv6Addr::from( + TryInto::<[u8; 16]>::try_into(octets).unwrap(), + )), + _ => return Err(error::Error::InvalidArgument.into()), + }; + + let json = self + .geolocation_lookup(&ip_addr) + .ok_or(geo::Error::UnknownError)?; + + if json.len() > usize::try_from(max_len).unwrap() { + return Err(error::Error::BufferLengthError { + buf: "geo_out", + len: "geo_max_len", + } + .into()); + } + + Ok(json) + } +} diff --git a/lib/src/component/headers.rs b/lib/src/component/headers.rs new file mode 100644 index 00000000..f2fbbc0e --- /dev/null +++ b/lib/src/component/headers.rs @@ -0,0 +1,38 @@ +type MultiValueCursor = u32; + +/// Write multiple values out to a single buffer, until the iterator is exhausted, or `max_len` +/// bytes have been written. In the case that there are still values remaining, the second value of +/// the returned tuple will be `Some`. +pub fn write_values( + iter: I, + terminator: u8, + max_len: usize, + mut cursor: MultiValueCursor, +) -> (Vec, Option) +where + I: Iterator, + T: AsRef<[u8]>, +{ + let mut buf = Vec::with_capacity(max_len); + + let mut finished = true; + let skip_amt = usize::try_from(cursor).expect("u32 can fit in usize"); + for item in iter.skip(skip_amt) { + let bytes = item.as_ref(); + + let needed = buf.len() + bytes.len() + 1; + if needed > max_len { + finished = false; + break; + } + + buf.extend(bytes); + buf.push(terminator); + + cursor += 1 + } + + let cursor = if finished { None } else { Some(cursor) }; + + (buf, cursor) +} diff --git a/lib/src/component/http_body.rs b/lib/src/component/http_body.rs new file mode 100644 index 00000000..c1acb947 --- /dev/null +++ b/lib/src/component/http_body.rs @@ -0,0 +1,252 @@ +use { + super::{ + fastly::api::{http_body, http_types}, + headers, FastlyError, + }, + crate::{body::Body, error::Error, session::Session}, + ::http_body::Body as HttpBody, + http::header::{HeaderName, HeaderValue}, +}; + +/// This constant reflects a similar constant within Hyper, which will panic +/// if given header names longer than this value. +pub const MAX_HEADER_NAME_LEN: usize = (1 << 16) - 1; + +#[async_trait::async_trait] +impl http_body::Host for Session { + async fn new(&mut self) -> Result { + Ok(self.insert_body(Body::empty()).into()) + } + + async fn write( + &mut self, + h: http_types::BodyHandle, + buf: Vec, + end: http_body::WriteEnd, + ) -> Result { + // Validate the body handle and the buffer. + let buf = buf.as_slice(); + + // Push the buffer onto the front or back of the body based on the `BodyWriteEnd` flag. + match end { + http_body::WriteEnd::Front => { + // Only normal bodies can be front-written + let body = self.body_mut(h.into())?; + body.push_front(buf); + } + http_body::WriteEnd::Back => { + if self.is_streaming_body(h.into()) { + let body = self.streaming_body_mut(h.into())?; + body.send_chunk(buf).await?; + } else { + let body = self.body_mut(h.into())?; + body.push_back(buf); + } + } + } + + // Finally, return the number of bytes written, which is _always_ the full buffer + Ok(buf + .len() + .try_into() + .expect("the buffer length must fit into a u32")) + } + + async fn append( + &mut self, + dest: http_types::BodyHandle, + src: http_types::BodyHandle, + ) -> Result<(), FastlyError> { + // Take the `src` body out of the session, and get a mutable reference + // to the `dest` body we will append to. + let src = self.take_body(src.into())?; + + if self.is_streaming_body(dest.into()) { + let dest = self.streaming_body_mut(dest.into())?; + for chunk in src { + dest.send_chunk(chunk).await?; + } + } else { + let dest = self.body_mut(dest.into())?; + dest.append(src); + } + Ok(()) + } + + async fn read( + &mut self, + h: http_types::BodyHandle, + chunk_size: u32, + ) -> Result, FastlyError> { + // only normal bodies (not streaming bodies) can be read from + let body = self.body_mut(h.into())?; + + if let Some(chunk) = body.data().await { + // pass up any error encountered when reading a chunk + let mut chunk = chunk?; + // split the chunk, saving any bytes that don't fit into the destination buffer + let extra_bytes = chunk.split_off(std::cmp::min(chunk_size as usize, chunk.len())); + // `chunk.len()` is now the smaller of (1) the destination buffer and (2) the available data. + let chunk = chunk.to_vec(); + // if there are leftover bytes, put them back at the front of the body + if !extra_bytes.is_empty() { + body.push_front(extra_bytes); + } + + Ok(chunk) + } else { + Ok(Vec::new()) + } + } + + async fn close(&mut self, h: http_types::BodyHandle) -> Result<(), FastlyError> { + // Drop the body and pass up an error if the handle does not exist + if self.is_streaming_body(h.into()) { + // Make sure a streaming body gets a `finish` message + self.take_streaming_body(h.into())?.finish()?; + Ok(()) + } else { + Ok(self.drop_body(h.into())?) + } + } + + async fn known_length(&mut self, h: http_types::BodyHandle) -> Result { + if self.is_streaming_body(h.into()) { + Err(Error::ValueAbsent.into()) + } else if let Some(len) = self.body_mut(h.into())?.len() { + Ok(len) + } else { + Err(Error::ValueAbsent.into()) + } + } + + async fn trailer_append( + &mut self, + h: http_types::BodyHandle, + name: String, + value: Vec, + ) -> Result<(), FastlyError> { + // Appending trailers is always allowed for bodies and streaming bodies. + if self.is_streaming_body(h.into()) { + let body = self.streaming_body_mut(h.into())?; + let name = HeaderName::from_bytes(name.as_bytes())?; + let value = HeaderValue::from_bytes(value.as_slice())?; + body.append_trailer(name, value); + Ok(()) + } else { + let trailers = &mut self.body_mut(h.into())?.trailers; + if name.len() > MAX_HEADER_NAME_LEN { + return Err(Error::InvalidArgument.into()); + } + + let name = HeaderName::from_bytes(name.as_bytes())?; + let value = HeaderValue::from_bytes(value.as_slice())?; + trailers.append(name, value); + Ok(()) + } + } + + async fn trailer_names_get( + &mut self, + h: http_types::BodyHandle, + max_len: u64, + cursor: u32, + ) -> Result, Option)>, FastlyError> { + // Read operations are not allowed on streaming bodies. + if self.is_streaming_body(h.into()) { + return Err(Error::InvalidArgument.into()); + } + + let body = self.body_mut(h.into())?; + if !body.trailers_ready { + return Err(Error::Again.into()); + } + + let trailers = &body.trailers; + let (buf, next) = headers::write_values( + trailers.keys(), + b'\0', + usize::try_from(max_len).unwrap(), + cursor, + ); + if buf.is_empty() && next.is_none() { + return Ok(None); + } + + Ok(Some((buf, next))) + } + + async fn trailer_value_get( + &mut self, + h: http_types::BodyHandle, + name: String, + max_len: u64, + ) -> Result>, FastlyError> { + // Read operations are not allowed on streaming bodies. + if self.is_streaming_body(h.into()) { + return Err(Error::InvalidArgument.into()); + } + + let body = &mut self.body_mut(h.into())?; + if !body.trailers_ready { + return Err(Error::Again.into()); + } + + let trailers = &mut body.trailers; + if name.len() > MAX_HEADER_NAME_LEN { + return Err(Error::InvalidArgument.into()); + } + + let value = { + let name = HeaderName::from_bytes(name.as_bytes())?; + if let Some(value) = trailers.get(&name) { + value + } else { + return Ok(None); + } + }; + + if value.len() > max_len as usize { + return Err(Error::BufferLengthError { + buf: "value", + len: "value_max_len", + } + .into()); + } + + Ok(Some(value.as_bytes().to_owned())) + } + + async fn trailer_values_get( + &mut self, + h: http_types::BodyHandle, + name: String, + max_len: u64, + cursor: u32, + ) -> Result, Option)>, FastlyError> { + // Read operations are not allowed on streaming bodies. + if self.is_streaming_body(h.into()) { + return Err(Error::InvalidArgument.into()); + } + + let body = &mut self.body_mut(h.into())?; + if !body.trailers_ready { + return Err(Error::Again.into()); + } + + let trailers = &mut body.trailers; + let name = HeaderName::from_bytes(name.as_bytes())?; + let (buf, next) = headers::write_values( + trailers.get_all(&name).into_iter(), + b'\0', + usize::try_from(max_len).unwrap(), + cursor, + ); + + if buf.is_empty() && next.is_none() { + return Ok(None); + } + + Ok(Some((buf, next))) + } +} diff --git a/lib/src/component/http_req.rs b/lib/src/component/http_req.rs new file mode 100644 index 00000000..c003034e --- /dev/null +++ b/lib/src/component/http_req.rs @@ -0,0 +1,821 @@ +use { + super::{ + fastly::api::{http_req, http_types, types}, + headers::write_values, + FastlyError, + }, + crate::{ + config::{Backend, ClientCertInfo}, + error::Error, + secret_store::SecretLookup, + session::{AsyncItem, AsyncItemHandle, PeekableTask, Session, ViceroyRequestMetadata}, + upstream, + wiggle_abi::SecretStoreError, + }, + fastly_shared::{INVALID_BODY_HANDLE, INVALID_RESPONSE_HANDLE}, + http::{ + header::{HeaderName, HeaderValue}, + request::Request, + Method, Uri, + }, + std::str::FromStr, +}; + +// NOTE [error-detail]: +// +// The v2 apis return additional error through an send-error-detail outparam, but this is a little +// bit awkward in the context of wit, which lacks the notion of an outparam. As the presence of +// this value is optional, and only serves to augment additional error context, we instead +// represent this as a different error result in compute.wit: +// +// ``` +// result, error>> +// ``` +// +// The effect of this is that we can no longer rely on the `trappable_error_types` option to +// `component::bindgen!` to give us a type that represents both an error and a trap. Instead, we +// get the following translated return type: +// +// ``` +// Result, Error)>, anyhow::Error> +// ``` +// +// Where the outer result is for managing errors that should be considered traps, and the inner +// result is for managing successful return values, or application-level errors that might include +// additional details. We could wrap up the tuple into an additional error type and declare it as a +// trappable error, but that's a bit more overhead for only four functions that currently don't +// populate the send-error-detail. + +const MAX_HEADER_NAME_LEN: usize = (1 << 16) - 1; + +#[async_trait::async_trait] +impl http_req::Host for Session { + async fn method_get( + &mut self, + h: http_types::RequestHandle, + max_len: u64, + ) -> Result { + let req = self.request_parts(h.into())?; + let req_method = &req.method; + + if req_method.as_str().len() > usize::try_from(max_len).unwrap() { + return Err(Error::BufferLengthError { + buf: "method", + len: "method_max_len", + } + .into()); + } + + Ok(req_method.to_string()) + } + + async fn uri_get( + &mut self, + h: http_types::RequestHandle, + max_len: u64, + ) -> Result { + let req = self.request_parts(h.into())?; + let req_uri = &req.uri; + let res = req_uri.to_string(); + + if res.len() > usize::try_from(max_len).unwrap() { + return Err(Error::BufferLengthError { + buf: "reqid_out", + len: "reqid_max_len", + } + .into()); + } + + Ok(res) + } + + async fn cache_override_set( + &mut self, + _h: http_types::RequestHandle, + _tag: http_req::CacheOverrideTag, + _ttl: u32, + _stale_while_revalidate: u32, + ) -> Result<(), FastlyError> { + // For now, we ignore caching directives because we never cache anything + Ok(()) + } + + async fn cache_override_v2_set( + &mut self, + _h: http_types::RequestHandle, + _tag: http_req::CacheOverrideTag, + _ttl: u32, + _stale_while_revalidate: u32, + _sk: Option, + ) -> Result<(), FastlyError> { + // For now, we ignore caching directives because we never cache anything + Ok(()) + } + + async fn downstream_client_ip_addr(&mut self) -> Result, FastlyError> { + use std::net::IpAddr; + match self.downstream_client_ip() { + IpAddr::V4(addr) => { + let octets = addr.octets(); + debug_assert_eq!(octets.len(), 4); + Ok(Vec::from(octets)) + } + IpAddr::V6(addr) => { + let octets = addr.octets(); + debug_assert_eq!(octets.len(), 16); + Ok(Vec::from(octets)) + } + } + } + + async fn downstream_server_ip_addr(&mut self) -> Result, FastlyError> { + Err(Error::NotAvailable("Downstream server ip address").into()) + } + + async fn downstream_tls_cipher_openssl_name( + &mut self, + _max_len: u64, + ) -> Result { + Err(Error::NotAvailable("Client TLS data").into()) + } + + async fn downstream_tls_protocol(&mut self, _max_len: u64) -> Result { + Err(Error::NotAvailable("Client TLS data").into()) + } + + async fn downstream_tls_client_hello(&mut self, _max_len: u64) -> Result, FastlyError> { + Err(Error::NotAvailable("Client TLS data").into()) + } + + async fn downstream_tls_raw_client_certificate( + &mut self, + _max_len: u64, + ) -> Result, FastlyError> { + Err(Error::NotAvailable("Client TLS data").into()) + } + + async fn downstream_tls_client_cert_verify_result( + &mut self, + ) -> Result { + Err(Error::NotAvailable("Client TLS data").into()) + } + + async fn downstream_tls_ja3_md5(&mut self) -> Result, FastlyError> { + Err(Error::NotAvailable("Client TLS JA3 hash").into()) + } + + async fn new(&mut self) -> Result { + let (parts, _) = Request::new(()).into_parts(); + Ok(self.insert_request_parts(parts).into()) + } + + async fn header_names_get( + &mut self, + h: http_types::RequestHandle, + max_len: u64, + cursor: u32, + ) -> Result, Option)>, FastlyError> { + let headers = &self.request_parts(h.into())?.headers; + + let (buf, next) = write_values( + headers.keys(), + b'\0', + usize::try_from(max_len).unwrap(), + cursor, + ); + + if buf.is_empty() && next.is_none() { + return Ok(None); + } + + Ok(Some((buf, next))) + } + + async fn header_value_get( + &mut self, + h: http_types::RequestHandle, + name: String, + max_len: u64, + ) -> Result>, FastlyError> { + if name.len() > MAX_HEADER_NAME_LEN { + return Err(Error::InvalidArgument.into()); + } + + let headers = &self.request_parts(h.into())?.headers; + let value = if let Some(value) = headers.get(&name) { + value + } else { + return Ok(None); + }; + + if value.len() > usize::try_from(max_len).unwrap() { + return Err(Error::BufferLengthError { + buf: "value", + len: "value_max_len", + } + .into()); + } + + Ok(Some(value.as_bytes().to_owned())) + } + + async fn header_values_get( + &mut self, + h: http_types::RequestHandle, + name: String, + max_len: u64, + cursor: u32, + ) -> Result, Option)>, FastlyError> { + let headers = &self.request_parts(h.into())?.headers; + + let values = headers.get_all(HeaderName::from_str(&name)?); + + let (buf, next) = write_values( + values.into_iter(), + b'\0', + usize::try_from(max_len).unwrap(), + cursor, + ); + + if buf.is_empty() && next.is_none() { + return Ok(None); + } + + Ok(Some((buf, next))) + } + + async fn header_values_set( + &mut self, + h: http_types::RequestHandle, + name: String, + values: Vec, + ) -> Result<(), FastlyError> { + if name.len() > MAX_HEADER_NAME_LEN { + return Err(Error::InvalidArgument.into()); + } + + let headers = &mut self.request_parts_mut(h.into())?.headers; + + let name = HeaderName::from_bytes(name.as_bytes())?; + + // Remove any values if they exist + if let http::header::Entry::Occupied(e) = headers.entry(&name) { + e.remove_entry_mult(); + } + + // Add all the new values + for value in values.split(|b| *b == 0) { + headers.append(&name, HeaderValue::from_bytes(value)?); + } + + Ok(()) + } + + async fn header_insert( + &mut self, + h: http_types::RequestHandle, + name: String, + value: Vec, + ) -> Result<(), FastlyError> { + if name.len() > MAX_HEADER_NAME_LEN { + return Err(Error::InvalidArgument.into()); + } + + let headers = &mut self.request_parts_mut(h.into())?.headers; + let name = HeaderName::from_bytes(name.as_bytes())?; + let value = HeaderValue::from_bytes(value.as_slice())?; + headers.insert(name, value); + + Ok(()) + } + + async fn header_append( + &mut self, + h: http_types::RequestHandle, + name: String, + value: Vec, + ) -> Result<(), FastlyError> { + if name.len() > MAX_HEADER_NAME_LEN { + return Err(Error::InvalidArgument.into()); + } + + let headers = &mut self.request_parts_mut(h.into())?.headers; + let name = HeaderName::from_bytes(name.as_bytes())?; + let value = HeaderValue::from_bytes(value.as_slice())?; + headers.append(name, value); + + Ok(()) + } + + async fn header_remove( + &mut self, + h: http_types::RequestHandle, + name: String, + ) -> Result<(), FastlyError> { + if name.len() > MAX_HEADER_NAME_LEN { + return Err(Error::InvalidArgument.into()); + } + + let headers = &mut self.request_parts_mut(h.into())?.headers; + let name = HeaderName::from_bytes(name.as_bytes())?; + headers + .remove(name) + .ok_or(FastlyError::from(types::Error::InvalidArgument))?; + + Ok(()) + } + + async fn method_set( + &mut self, + h: http_types::RequestHandle, + method: String, + ) -> Result<(), FastlyError> { + let method_ref = &mut self.request_parts_mut(h.into())?.method; + *method_ref = Method::from_bytes(method.as_bytes())?; + Ok(()) + } + + async fn uri_set( + &mut self, + h: http_types::RequestHandle, + uri: String, + ) -> Result<(), FastlyError> { + let uri_ref = &mut self.request_parts_mut(h.into())?.uri; + *uri_ref = Uri::try_from(uri.as_bytes())?; + Ok(()) + } + + async fn version_get( + &mut self, + h: http_types::RequestHandle, + ) -> Result { + let req = self.request_parts(h.into())?; + let version = http_types::HttpVersion::try_from(req.version)?; + Ok(version) + } + + async fn version_set( + &mut self, + h: http_types::RequestHandle, + version: http_types::HttpVersion, + ) -> Result<(), FastlyError> { + let req = self.request_parts_mut(h.into())?; + req.version = hyper::Version::from(version); + Ok(()) + } + + async fn send( + &mut self, + h: http_types::RequestHandle, + b: http_types::BodyHandle, + backend_name: String, + ) -> Result { + // prepare the request + let req_parts = self.take_request_parts(h.into())?; + let req_body = self.take_body(b.into())?; + let req = Request::from_parts(req_parts, req_body); + let backend = self + .backend(&backend_name) + .ok_or(FastlyError::from(types::Error::UnknownError))?; + + // synchronously send the request + let resp = upstream::send_request(req, backend, self.tls_config()).await?; + let (resp_handle, body_handle) = self.insert_response(resp); + Ok((resp_handle.into(), body_handle.into())) + } + + async fn send_v2( + &mut self, + h: http_types::RequestHandle, + b: http_types::BodyHandle, + backend_name: String, + ) -> Result< + // This return type is a little surprising. Please see the [error-detail] note at the top + // of the file for an explanation. + Result, types::Error)>, + anyhow::Error, + > { + // This initial implementation ignores the error detail field + match self.send(h, b, backend_name).await { + Ok(r) => Ok(Ok(r)), + Err(e) => e.with_empty_detail(), + } + } + + async fn send_async( + &mut self, + h: http_types::RequestHandle, + b: http_types::BodyHandle, + backend_name: String, + ) -> Result { + // prepare the request + let req_parts = self.take_request_parts(h.into())?; + let req_body = self.take_body(b.into())?; + let req = Request::from_parts(req_parts, req_body); + let backend = self + .backend(&backend_name) + .ok_or(FastlyError::from(types::Error::UnknownError))?; + + // asynchronously send the request + let task = + PeekableTask::spawn(upstream::send_request(req, backend, self.tls_config())).await; + + // return a handle to the pending request + Ok(self.insert_pending_request(task).into()) + } + + async fn send_async_streaming( + &mut self, + h: http_types::RequestHandle, + b: http_types::BodyHandle, + backend_name: String, + ) -> Result { + // prepare the request + let req_parts = self.take_request_parts(h.into())?; + let req_body = self.begin_streaming(b.into())?; + let req = Request::from_parts(req_parts, req_body); + let backend = self + .backend(&backend_name) + .ok_or(FastlyError::from(types::Error::UnknownError))?; + + // asynchronously send the request + let task = + PeekableTask::spawn(upstream::send_request(req, backend, self.tls_config())).await; + + // return a handle to the pending request + Ok(self.insert_pending_request(task).into()) + } + + async fn pending_req_poll( + &mut self, + h: http_types::PendingRequestHandle, + ) -> Result, FastlyError> { + if self + .async_item_mut(AsyncItemHandle::from_u32(h))? + .is_ready() + { + let resp = self.take_pending_request(h.into())?.recv().await?; + let (resp_handle, resp_body_handle) = self.insert_response(resp); + Ok(Some((resp_handle.into(), resp_body_handle.into()))) + } else { + Ok(None) + } + } + + async fn pending_req_poll_v2( + &mut self, + h: http_types::PendingRequestHandle, + ) -> Result< + // This return type is a little surprising. Please see the [error-detail] note at the top + // of the file for an explanation. + Result, (Option, types::Error)>, + anyhow::Error, + > { + match self.pending_req_poll(h).await { + Ok(r) => Ok(Ok(r)), + Err(e) => e.with_empty_detail(), + } + } + + async fn pending_req_wait( + &mut self, + h: http_types::PendingRequestHandle, + ) -> Result { + let pending_req = self.take_pending_request(h.into())?.recv().await?; + let (resp_handle, body_handle) = self.insert_response(pending_req); + Ok((resp_handle.into(), body_handle.into())) + } + + async fn pending_req_wait_v2( + &mut self, + h: http_types::PendingRequestHandle, + ) -> Result< + // This return type is a little surprising. Please see the [error-detail] note at the top + // of the file for an explanation. + Result, types::Error)>, + anyhow::Error, + > { + match self.pending_req_wait(h).await { + Ok(r) => Ok(Ok(r)), + Err(e) => e.with_empty_detail(), + } + } + + async fn pending_req_select( + &mut self, + h: Vec, + ) -> Result<(u32, http_types::Response), FastlyError> { + use crate::wiggle_abi::types; + + if h.is_empty() { + return Err(Error::InvalidArgument.into()); + } + + // perform the select operation + let done_index = self + .select_impl( + h.iter() + .map(|handle| types::PendingRequestHandle::from(*handle).into()), + ) + .await?; + + let item = self.take_async_item( + types::PendingRequestHandle::from(h.get(done_index).cloned().unwrap()).into(), + )?; + + let outcome = match item { + AsyncItem::PendingReq(res) => match res { + PeekableTask::Complete(resp) => match resp { + Ok(resp) => { + let (resp_handle, body_handle) = self.insert_response(resp); + (done_index as u32, (resp_handle.into(), body_handle.into())) + } + // Unfortunately, the ABI provides no means of returning error information + // from completed `select`. + Err(_) => ( + done_index as u32, + (INVALID_RESPONSE_HANDLE, INVALID_BODY_HANDLE), + ), + }, + _ => panic!("Pending request was not completed"), + }, + _ => panic!("AsyncItem was not a pending request"), + }; + + Ok(outcome) + } + + async fn pending_req_select_v2( + &mut self, + h: Vec, + ) -> Result< + // This return type is a little surprising. Please see the [error-detail] note at the top + // of the file for an explanation. + Result<(u32, http_types::Response), (Option, types::Error)>, + anyhow::Error, + > { + match self.pending_req_select(h).await { + Ok(r) => Ok(Ok(r)), + Err(e) => e.with_empty_detail(), + } + } + + async fn fastly_key_is_valid(&mut self) -> Result { + Err(Error::NotAvailable("FASTLY_KEY is valid").into()) + } + + async fn close(&mut self, h: http_types::RequestHandle) -> Result<(), FastlyError> { + // We don't do anything with the parts, but we do pass the error up if + // the handle given doesn't exist + self.take_request_parts(h.into())?; + Ok(()) + } + + async fn auto_decompress_response_set( + &mut self, + h: http_types::RequestHandle, + encodings: http_types::ContentEncodings, + ) -> Result<(), FastlyError> { + use crate::wiggle_abi::types; + + // NOTE: We're going to hide this flag in the extensions of the request in order to decrease + // the book-keeping burden inside Session. The flag will get picked up later, in `send_request`. + let extensions = &mut self.request_parts_mut(h.into())?.extensions; + + let encodings = types::ContentEncodings::try_from(encodings.as_array()[0])?; + + match extensions.get_mut::() { + None => { + extensions.insert(ViceroyRequestMetadata { + auto_decompress_encodings: encodings, + // future note: at time of writing, this is the only field of + // this structure, but there is an intention to add more fields. + // When we do, and if/when an error appears, what you're looking + // for is: + // ..Default::default() + }); + } + Some(vrm) => { + vrm.auto_decompress_encodings = encodings; + } + } + + Ok(()) + } + + async fn upgrade_websocket(&mut self, _backend: String) -> Result<(), FastlyError> { + Err(Error::NotAvailable("WebSocket upgrade").into()) + } + + async fn redirect_to_websocket_proxy(&mut self, _backend: String) -> Result<(), FastlyError> { + Err(Error::NotAvailable("Redirect to WebSocket proxy").into()) + } + + async fn redirect_to_websocket_proxy_v2( + &mut self, + _handle: http_req::RequestHandle, + _backend: String, + ) -> Result<(), FastlyError> { + Err(Error::NotAvailable("Redirect to WebSocket proxy").into()) + } + + async fn redirect_to_grip_proxy(&mut self, _backend: String) -> Result<(), FastlyError> { + Err(Error::NotAvailable("Redirect to Fanout/GRIP proxy").into()) + } + + async fn redirect_to_grip_proxy_v2( + &mut self, + _handle: http_req::RequestHandle, + _backend: String, + ) -> Result<(), FastlyError> { + Err(Error::NotAvailable("Redirect to Fanout/GRIP proxy").into()) + } + + async fn framing_headers_mode_set( + &mut self, + _h: http_types::RequestHandle, + mode: http_types::FramingHeadersMode, + ) -> Result<(), FastlyError> { + match mode { + http_types::FramingHeadersMode::ManuallyFromHeaders => { + Err(Error::NotAvailable("Manual framing headers").into()) + } + http_types::FramingHeadersMode::Automatic => Ok(()), + } + } + + async fn register_dynamic_backend( + &mut self, + prefix: String, + target: String, + options: http_types::BackendConfigOptions, + config: http_types::DynamicBackendConfig, + ) -> Result<(), FastlyError> { + let name = prefix.as_str(); + let origin_name = target.as_str(); + + let override_host = if options.contains(http_types::BackendConfigOptions::HOST_OVERRIDE) { + if config.host_override.is_empty() { + return Err(types::Error::InvalidArgument.into()); + } + + if config.host_override.len() > 1024 { + return Err(types::Error::InvalidArgument.into()); + } + + Some(HeaderValue::from_bytes(config.host_override.as_bytes())?) + } else { + None + }; + + let scheme = if options.contains(http_types::BackendConfigOptions::USE_SSL) { + "https" + } else { + "http" + }; + + let mut cert_host = if options.contains(http_types::BackendConfigOptions::CERT_HOSTNAME) { + if config.cert_hostname.is_empty() { + return Err(types::Error::InvalidArgument.into()); + } + + if config.cert_hostname.len() > 1024 { + return Err(types::Error::InvalidArgument.into()); + } + + Some(config.cert_hostname) + } else { + None + }; + + let use_sni = if options.contains(http_types::BackendConfigOptions::SNI_HOSTNAME) { + if config.sni_hostname.len() > 1024 { + return Err(types::Error::InvalidArgument.into()); + } + + if config.sni_hostname.is_empty() { + false + } else { + if let Some(cert_host) = &cert_host { + if cert_host != &config.sni_hostname { + // because we're using rustls, we cannot support distinct SNI and cert hostnames + return Err(types::Error::InvalidArgument.into()); + } + } else { + cert_host = Some(config.sni_hostname); + } + + true + } + } else { + true + }; + + let client_cert = if options.contains(http_types::BackendConfigOptions::CLIENT_CERT) { + let key_lookup = + self.secret_lookup(config.client_key.into()) + .ok_or(Error::SecretStoreError( + SecretStoreError::InvalidSecretHandle(config.client_key.into()), + ))?; + let key = match &key_lookup { + SecretLookup::Standard { + store_name, + secret_name, + } => self + .secret_stores() + .get_store(store_name) + .ok_or(Error::SecretStoreError( + SecretStoreError::InvalidSecretHandle(config.client_key.into()), + ))? + .get_secret(secret_name) + .ok_or(Error::SecretStoreError( + SecretStoreError::InvalidSecretHandle(config.client_key.into()), + ))? + .plaintext(), + + SecretLookup::Injected { plaintext } => plaintext, + }; + + Some(ClientCertInfo::new(config.client_cert.as_bytes(), key)?) + } else { + None + }; + + let grpc = false; + + let new_backend = Backend { + uri: Uri::builder() + .scheme(scheme) + .authority(origin_name) + .path_and_query("/") + .build()?, + override_host, + cert_host, + use_sni, + grpc, + client_cert, + ca_certs: Vec::new(), + }; + + if !self.add_backend(name, new_backend) { + return Err(Error::BackendNameRegistryError(name.to_string()).into()); + } + + Ok(()) + } + + async fn downstream_client_h2_fingerprint( + &mut self, + _max_len: u64, + ) -> Result, FastlyError> { + Err(Error::NotAvailable("Client H2 fingerprint").into()) + } + + async fn downstream_client_request_id(&mut self, max_len: u64) -> Result { + let result = format!("{:032x}", self.req_id()); + + if result.len() > usize::try_from(max_len).unwrap() { + return Err(Error::BufferLengthError { + buf: "reqid_out", + len: "reqid_max_len", + } + .into()); + } + + Ok(result) + } + + async fn downstream_client_oh_fingerprint( + &mut self, + _max_len: u64, + ) -> Result, FastlyError> { + Err(Error::NotAvailable("Client original header fingerprint").into()) + } + + async fn downstream_tls_ja4(&mut self, _max_len: u64) -> Result, FastlyError> { + Err(Error::NotAvailable("Client TLS JA4 hash").into()) + } + + async fn downstream_compliance_region( + &mut self, + _max_len: u64, + ) -> Result, FastlyError> { + Err(Error::NotAvailable("Client TLS JA4 hash").into()) + } + + async fn original_header_names_get( + &mut self, + _max_len: u64, + _cursor: u32, + ) -> Result, Option)>, FastlyError> { + Err(Error::NotAvailable("Client Compliance Region").into()) + } + + async fn original_header_count(&mut self) -> Result { + Ok(self + .downstream_original_headers() + .len() + .try_into() + .expect("More than u32::MAX headers")) + } +} diff --git a/lib/src/component/http_resp.rs b/lib/src/component/http_resp.rs new file mode 100644 index 00000000..2d04a8e6 --- /dev/null +++ b/lib/src/component/http_resp.rs @@ -0,0 +1,265 @@ +use { + super::fastly::api::{http_resp, http_types, types}, + super::{headers::write_values, FastlyError}, + crate::{error::Error, session::Session}, + http::{HeaderName, HeaderValue}, + hyper::http::response::Response, + std::str::FromStr, +}; + +const MAX_HEADER_NAME_LEN: usize = (1 << 16) - 1; + +#[async_trait::async_trait] +impl http_resp::Host for Session { + async fn new(&mut self) -> Result { + let (parts, _) = Response::new(()).into_parts(); + Ok(self.insert_response_parts(parts).into()) + } + + async fn status_get( + &mut self, + h: http_types::ResponseHandle, + ) -> Result { + let parts = self.response_parts(h.into())?; + Ok(parts.status.as_u16()) + } + + async fn status_set( + &mut self, + h: http_types::ResponseHandle, + status: http_types::HttpStatus, + ) -> Result<(), FastlyError> { + let resp = self.response_parts_mut(h.into())?; + let status = hyper::StatusCode::from_u16(status)?; + resp.status = status; + Ok(()) + } + + async fn header_append( + &mut self, + h: http_types::ResponseHandle, + name: String, + value: Vec, + ) -> Result<(), FastlyError> { + if name.len() > MAX_HEADER_NAME_LEN { + Err(types::Error::InvalidArgument)?; + } + + let headers = &mut self.response_parts_mut(h.into())?.headers; + let name = HeaderName::from_bytes(name.as_bytes())?; + let value = HeaderValue::from_bytes(value.as_slice())?; + headers.append(name, value); + Ok(()) + } + + async fn send_downstream( + &mut self, + h: http_types::ResponseHandle, + b: http_types::BodyHandle, + streaming: bool, + ) -> Result<(), FastlyError> { + let resp = { + // Take the response parts and body from the session, and use them to build a response. + // Return an `FastlyStatus::Badf` error code if either of the given handles are invalid. + let resp_parts = self.take_response_parts(h.into())?; + let body = if streaming { + self.begin_streaming(b.into())? + } else { + self.take_body(b.into())? + }; + Response::from_parts(resp_parts, body) + }; // Set the downstream response, and return. + self.send_downstream_response(resp)?; + Ok(()) + } + + async fn header_names_get( + &mut self, + h: http_types::ResponseHandle, + max_len: u64, + cursor: u32, + ) -> Result, Option)>, FastlyError> { + let headers = &self.response_parts(h.into())?.headers; + + let (buf, next) = write_values( + headers.keys(), + b'\0', + usize::try_from(max_len).unwrap(), + cursor, + ); + + if buf.is_empty() && next.is_none() { + return Ok(None); + } + + Ok(Some((buf, next))) + } + + async fn header_value_get( + &mut self, + h: http_types::ResponseHandle, + name: String, + max_len: u64, + ) -> Result>, FastlyError> { + if name.len() > MAX_HEADER_NAME_LEN { + return Err(Error::InvalidArgument.into()); + } + + let headers = &self.response_parts(h.into())?.headers; + let value = if let Some(value) = headers.get(&name) { + value + } else { + return Ok(None); + }; + + if value.len() > usize::try_from(max_len).unwrap() { + return Err(Error::BufferLengthError { + buf: "value", + len: "value_max_len", + } + .into()); + } + + Ok(Some(value.as_bytes().to_owned())) + } + + async fn header_values_get( + &mut self, + h: http_types::ResponseHandle, + name: String, + max_len: u64, + cursor: u32, + ) -> Result, Option)>, FastlyError> { + let headers = &self.response_parts(h.into())?.headers; + + let values = headers.get_all(HeaderName::from_str(&name)?); + + let (buf, next) = write_values( + values.into_iter(), + b'\0', + usize::try_from(max_len).unwrap(), + cursor, + ); + + if buf.is_empty() && next.is_none() { + return Ok(None); + } + + Ok(Some((buf, next))) + } + + async fn header_values_set( + &mut self, + h: http_types::ResponseHandle, + name: String, + values: Vec, + ) -> Result<(), FastlyError> { + if name.len() > MAX_HEADER_NAME_LEN { + return Err(Error::InvalidArgument.into()); + } + + let headers = &mut self.response_parts_mut(h.into())?.headers; + + let name = HeaderName::from_bytes(name.as_bytes())?; + + // Remove any values if they exist + if let http::header::Entry::Occupied(e) = headers.entry(&name) { + e.remove_entry_mult(); + } + + // Add all the new values + for value in values.split(|b| *b == 0) { + headers.append(&name, HeaderValue::from_bytes(value)?); + } + + Ok(()) + } + + async fn header_insert( + &mut self, + h: http_types::ResponseHandle, + name: String, + value: Vec, + ) -> Result<(), FastlyError> { + if name.len() > MAX_HEADER_NAME_LEN { + return Err(Error::InvalidArgument.into()); + } + + let headers = &mut self.response_parts_mut(h.into())?.headers; + let name = HeaderName::from_bytes(name.as_bytes())?; + let value = HeaderValue::from_bytes(value.as_slice())?; + headers.insert(name, value); + + Ok(()) + } + + async fn header_remove( + &mut self, + h: http_types::ResponseHandle, + name: String, + ) -> Result<(), FastlyError> { + if name.len() > MAX_HEADER_NAME_LEN { + return Err(Error::InvalidArgument.into()); + } + + let headers = &mut self.response_parts_mut(h.into())?.headers; + let name = HeaderName::from_bytes(name.as_bytes())?; + headers + .remove(name) + .ok_or(FastlyError::from(types::Error::InvalidArgument))?; + + Ok(()) + } + + async fn version_get( + &mut self, + h: http_types::ResponseHandle, + ) -> Result { + let req = self.response_parts(h.into())?; + let version = http_types::HttpVersion::try_from(req.version)?; + Ok(version) + } + + async fn version_set( + &mut self, + h: http_types::ResponseHandle, + version: http_types::HttpVersion, + ) -> Result<(), FastlyError> { + let req = self.response_parts_mut(h.into())?; + req.version = hyper::Version::from(version); + Ok(()) + } + + async fn close(&mut self, h: http_types::ResponseHandle) -> Result<(), FastlyError> { + // We don't do anything with the parts, but we do pass the error up if + // the handle given doesn't exist + self.take_response_parts(h.into())?; + Ok(()) + } + + async fn framing_headers_mode_set( + &mut self, + _h: http_types::ResponseHandle, + mode: http_types::FramingHeadersMode, + ) -> Result<(), FastlyError> { + match mode { + http_types::FramingHeadersMode::ManuallyFromHeaders => { + Err(Error::NotAvailable("Manual framing headers").into()) + } + http_types::FramingHeadersMode::Automatic => Ok(()), + } + } + + async fn http_keepalive_mode_set( + &mut self, + _: http_types::ResponseHandle, + mode: http_resp::KeepaliveMode, + ) -> Result<(), FastlyError> { + match mode { + http_resp::KeepaliveMode::NoKeepalive => { + Err(Error::NotAvailable("No Keepalive").into()) + } + http_resp::KeepaliveMode::Automatic => Ok(()), + } + } +} diff --git a/lib/src/component/http_types.rs b/lib/src/component/http_types.rs new file mode 100644 index 00000000..042a9540 --- /dev/null +++ b/lib/src/component/http_types.rs @@ -0,0 +1,34 @@ +use { + super::fastly::api::{http_types, types}, + crate::session::Session, +}; + +impl http_types::Host for Session {} + +// The http crate's `Version` is a struct that has a bunch of +// associated constants, not an enum; this is only a partial conversion. +impl TryFrom for http_types::HttpVersion { + type Error = types::Error; + fn try_from(v: http::version::Version) -> Result { + match v { + http::version::Version::HTTP_09 => Ok(http_types::HttpVersion::Http09), + http::version::Version::HTTP_10 => Ok(http_types::HttpVersion::Http10), + http::version::Version::HTTP_11 => Ok(http_types::HttpVersion::Http11), + http::version::Version::HTTP_2 => Ok(http_types::HttpVersion::H2), + http::version::Version::HTTP_3 => Ok(http_types::HttpVersion::H3), + _ => Err(types::Error::Unsupported), + } + } +} + +impl From for http::version::Version { + fn from(v: http_types::HttpVersion) -> http::version::Version { + match v { + http_types::HttpVersion::Http09 => http::version::Version::HTTP_09, + http_types::HttpVersion::Http10 => http::version::Version::HTTP_10, + http_types::HttpVersion::Http11 => http::version::Version::HTTP_11, + http_types::HttpVersion::H2 => http::version::Version::HTTP_2, + http_types::HttpVersion::H3 => http::version::Version::HTTP_3, + } + } +} diff --git a/lib/src/component/kv_store.rs b/lib/src/component/kv_store.rs new file mode 100644 index 00000000..6f2ccb47 --- /dev/null +++ b/lib/src/component/kv_store.rs @@ -0,0 +1,145 @@ +use { + super::fastly::api::{http_types, kv_store}, + super::FastlyError, + crate::{ + body::Body, + error, + object_store::{ObjectKey, ObjectStoreError}, + session::{ + PeekableTask, PendingKvDeleteTask, PendingKvInsertTask, PendingKvLookupTask, Session, + }, + }, +}; + +#[async_trait::async_trait] +impl kv_store::Host for Session { + async fn open(&mut self, name: String) -> Result { + if self.object_store.store_exists(&name)? { + let handle = self.obj_store_handle(&name)?; + Ok(handle.into()) + } else { + Err( + error::Error::ObjectStoreError(ObjectStoreError::UnknownObjectStore(name.clone())) + .into(), + ) + } + } + + async fn lookup( + &mut self, + store: kv_store::Handle, + key: String, + ) -> Result, FastlyError> { + let store = self.get_obj_store_key(store.into()).unwrap(); + let key = ObjectKey::new(&key)?; + match self.obj_lookup(store, &key) { + Ok(obj) => { + let new_handle = self.insert_body(Body::from(obj)); + Ok(Some(new_handle.into())) + } + // Don't write to the invalid handle as the SDK will return Ok(None) + // if the object does not exist. We need to return `Ok(())` here to + // make sure Viceroy does not crash + Err(ObjectStoreError::MissingObject) => Ok(None), + Err(err) => Err(err.into()), + } + } + + async fn lookup_async( + &mut self, + store: kv_store::Handle, + key: String, + ) -> Result { + let store = self.get_obj_store_key(store.into()).unwrap(); + let key = ObjectKey::new(key)?; + // just create a future that's already ready + let fut = futures::future::ok(self.obj_lookup(store, &key)); + let task = PendingKvLookupTask::new(PeekableTask::spawn(fut).await); + Ok(self.insert_pending_kv_lookup(task).into()) + } + + async fn pending_lookup_wait( + &mut self, + pending: kv_store::PendingLookupHandle, + ) -> Result, FastlyError> { + let pending_obj = self + .take_pending_kv_lookup(pending.into())? + .task() + .recv() + .await?; + // proceed with the normal match from lookup() + match pending_obj { + Ok(obj) => Ok(Some(self.insert_body(Body::from(obj)).into())), + Err(ObjectStoreError::MissingObject) => Ok(None), + Err(err) => Err(err.into()), + } + } + + async fn insert( + &mut self, + store: kv_store::Handle, + key: String, + body_handle: http_types::BodyHandle, + ) -> Result<(), FastlyError> { + let store = self.get_obj_store_key(store.into()).unwrap().clone(); + let key = ObjectKey::new(&key)?; + let bytes = self.take_body(body_handle.into())?.read_into_vec().await?; + self.obj_insert(store, key, bytes)?; + + Ok(()) + } + + async fn insert_async( + &mut self, + store: kv_store::Handle, + key: String, + body_handle: http_types::BodyHandle, + ) -> Result { + let store = self.get_obj_store_key(store.into()).unwrap().clone(); + let key = ObjectKey::new(&key)?; + let bytes = self.take_body(body_handle.into())?.read_into_vec().await?; + let fut = futures::future::ok(self.obj_insert(store, key, bytes)); + let task = PeekableTask::spawn(fut).await; + + Ok(self + .insert_pending_kv_insert(PendingKvInsertTask::new(task)) + .into()) + } + + async fn pending_insert_wait( + &mut self, + handle: kv_store::PendingInsertHandle, + ) -> Result<(), FastlyError> { + Ok((self + .take_pending_kv_insert(handle.into())? + .task() + .recv() + .await?)?) + } + + async fn delete_async( + &mut self, + store: kv_store::Handle, + key: String, + ) -> Result { + let store = self.get_obj_store_key(store.into()).unwrap().clone(); + let key = ObjectKey::new(&key)?; + let fut = futures::future::ok(self.obj_delete(store, key)); + let task = PeekableTask::spawn(fut).await; + + Ok(self + .insert_pending_kv_delete(PendingKvDeleteTask::new(task)) + .into()) + } + + async fn pending_delete_wait( + &mut self, + handle: kv_store::PendingDeleteHandle, + ) -> Result<(), FastlyError> { + Ok((self + .take_pending_kv_delete(handle.into())? + .task() + .recv() + .await?)?) + } +} diff --git a/lib/src/component/log.rs b/lib/src/component/log.rs new file mode 100644 index 00000000..18065f14 --- /dev/null +++ b/lib/src/component/log.rs @@ -0,0 +1,38 @@ +use { + super::fastly::api::{log, types}, + super::FastlyError, + crate::session::Session, + lazy_static::lazy_static, +}; + +fn is_reserved_endpoint(name: &[u8]) -> bool { + use regex::bytes::{RegexSet, RegexSetBuilder}; + const RESERVED_ENDPOINTS: &[&str] = &["^stdout$", "^stderr$", "^fst_managed_"]; + lazy_static! { + static ref RESERVED_ENDPOINT_RE: RegexSet = RegexSetBuilder::new(RESERVED_ENDPOINTS) + .case_insensitive(true) + .build() + .unwrap(); + } + RESERVED_ENDPOINT_RE.is_match(name) +} + +#[async_trait::async_trait] +impl log::Host for Session { + async fn endpoint_get(&mut self, name: String) -> Result { + let name = name.as_bytes(); + + if is_reserved_endpoint(name) { + return Err(types::Error::InvalidArgument.into()); + } + + Ok(self.log_endpoint_handle(name).into()) + } + + async fn write(&mut self, h: log::Handle, msg: String) -> Result { + let endpoint = self.log_endpoint(h.into())?; + let msg = msg.as_bytes(); + endpoint.write_entry(&msg)?; + Ok(u32::try_from(msg.len()).unwrap()) + } +} diff --git a/lib/src/component/mod.rs b/lib/src/component/mod.rs new file mode 100644 index 00000000..28800e38 --- /dev/null +++ b/lib/src/component/mod.rs @@ -0,0 +1,113 @@ +use {crate::linking::ComponentCtx, wasmtime::component}; + +/// This error type is used to classify two errors that can arise in a host-side implementation of +/// the fastly api: +/// +/// * Application errors that are recoverable, and returned to the guest, and +/// * Traps that are expected to cause the guest to tear down immediately. +/// +/// So a return type of `Result` is morally equivalent to +/// `Result, TrapError>`, but the former is much more pleasant to +/// program with. +/// +/// We write explicit `From` impls for errors that we raise throughout the implementation of the +/// compute apis, so that we're able to make the choice between an application error and a trap. +pub enum FastlyError { + /// An application error, that will be communicated back to the guest through the + /// `fastly:api/types/error` type. + FastlyError(anyhow::Error), + + /// An trap, which will cause wasmtime to immediately terminate the guest. + Trap(anyhow::Error), +} + +impl FastlyError { + pub fn with_empty_detail( + self, + ) -> wasmtime::Result< + Result< + T, + ( + Option, + fastly::api::types::Error, + ), + >, + > { + match self { + Self::FastlyError(e) => match e.downcast() { + Ok(e) => Ok(Err((None, e))), + Err(e) => Err(e), + }, + Self::Trap(e) => Err(e), + } + } +} + +component::bindgen!({ + path: "wit", + world: "fastly:api/compute", + async: true, + with: { + "fastly:api/uap/user-agent": uap::UserAgent, + + "wasi:clocks": wasmtime_wasi::bindings::clocks, + "wasi:random": wasmtime_wasi::bindings::random, + "wasi:io": wasmtime_wasi::bindings::io, + "wasi:cli": wasmtime_wasi::bindings::cli, + }, + trappable_error_type: { + "fastly:api/types/error" => FastlyError + }, +}); + +pub fn link_host_functions(linker: &mut component::Linker) -> anyhow::Result<()> { + wasmtime_wasi::bindings::clocks::wall_clock::add_to_linker(linker, |x| x)?; + wasmtime_wasi::bindings::clocks::monotonic_clock::add_to_linker(linker, |x| x)?; + wasmtime_wasi::bindings::random::random::add_to_linker(linker, |x| x)?; + wasmtime_wasi::bindings::filesystem::types::add_to_linker(linker, |x| x)?; + wasmtime_wasi::bindings::filesystem::preopens::add_to_linker(linker, |x| x)?; + wasmtime_wasi::bindings::io::error::add_to_linker(linker, |x| x)?; + wasmtime_wasi::bindings::io::streams::add_to_linker(linker, |x| x)?; + wasmtime_wasi::bindings::io::poll::add_to_linker(linker, |x| x)?; + wasmtime_wasi::bindings::cli::environment::add_to_linker(linker, |x| x)?; + wasmtime_wasi::bindings::cli::exit::add_to_linker(linker, |x| x)?; + wasmtime_wasi::bindings::cli::stdin::add_to_linker(linker, |x| x)?; + wasmtime_wasi::bindings::cli::stdout::add_to_linker(linker, |x| x)?; + wasmtime_wasi::bindings::cli::stderr::add_to_linker(linker, |x| x)?; + + fastly::api::async_io::add_to_linker(linker, |x| x.session())?; + fastly::api::backend::add_to_linker(linker, |x| x.session())?; + fastly::api::cache::add_to_linker(linker, |x| x.session())?; + fastly::api::dictionary::add_to_linker(linker, |x| x.session())?; + fastly::api::geo::add_to_linker(linker, |x| x.session())?; + fastly::api::http_body::add_to_linker(linker, |x| x.session())?; + fastly::api::http_req::add_to_linker(linker, |x| x.session())?; + fastly::api::http_resp::add_to_linker(linker, |x| x.session())?; + fastly::api::http_types::add_to_linker(linker, |x| x.session())?; + fastly::api::log::add_to_linker(linker, |x| x.session())?; + fastly::api::kv_store::add_to_linker(linker, |x| x.session())?; + fastly::api::purge::add_to_linker(linker, |x| x.session())?; + fastly::api::secret_store::add_to_linker(linker, |x| x.session())?; + fastly::api::types::add_to_linker(linker, |x| x.session())?; + fastly::api::uap::add_to_linker(linker, |x| x.session())?; + + Ok(()) +} + +pub mod async_io; +pub mod backend; +pub mod cache; +pub mod dictionary; +pub mod error; +pub mod geo; +pub mod headers; +pub mod http_body; +pub mod http_req; +pub mod http_resp; +pub mod http_types; +pub mod kv_store; +pub mod log; +pub mod purge; +pub mod secret_store; +pub mod types; +pub mod uap; diff --git a/lib/src/component/purge.rs b/lib/src/component/purge.rs new file mode 100644 index 00000000..8d42c358 --- /dev/null +++ b/lib/src/component/purge.rs @@ -0,0 +1,17 @@ +use { + super::fastly::api::purge, + super::FastlyError, + crate::{error::Error, session::Session}, +}; + +#[async_trait::async_trait] +impl purge::Host for Session { + async fn purge_surrogate_key( + &mut self, + _surrogate_key: String, + _options: purge::PurgeOptionsMask, + _max_len: u64, + ) -> Result, FastlyError> { + Err(Error::NotAvailable("FastlyPurge").into()) + } +} diff --git a/lib/src/component/secret_store.rs b/lib/src/component/secret_store.rs new file mode 100644 index 00000000..ac2406d4 --- /dev/null +++ b/lib/src/component/secret_store.rs @@ -0,0 +1,80 @@ +use { + super::fastly::api::secret_store, + super::FastlyError, + crate::{ + error::Error, secret_store::SecretLookup, session::Session, wiggle_abi::SecretStoreError, + }, +}; + +#[async_trait::async_trait] +impl secret_store::Host for Session { + async fn open(&mut self, name: String) -> Result { + let handle = self + .secret_store_handle(&name) + .ok_or(Error::SecretStoreError( + SecretStoreError::UnknownSecretStore(name.to_string()), + ))?; + Ok(handle.into()) + } + + async fn get( + &mut self, + store: secret_store::StoreHandle, + key: String, + ) -> Result, FastlyError> { + let store_name = self.secret_store_name(store.into()).ok_or_else(|| { + FastlyError::from(SecretStoreError::InvalidSecretStoreHandle(store.into())) + })?; + Ok(self + .secret_handle(&store_name, &key) + .map(secret_store::SecretHandle::from)) + } + + async fn plaintext( + &mut self, + secret: secret_store::SecretHandle, + max_len: u64, + ) -> Result, FastlyError> { + let lookup = self + .secret_lookup(secret.into()) + .ok_or(Error::SecretStoreError( + SecretStoreError::InvalidSecretHandle(secret.into()), + ))?; + + let plaintext = match &lookup { + SecretLookup::Standard { + store_name, + secret_name, + } => self + .secret_stores() + .get_store(store_name) + .ok_or(Error::SecretStoreError( + SecretStoreError::InvalidSecretHandle(secret.into()), + ))? + .get_secret(secret_name) + .ok_or(Error::SecretStoreError( + SecretStoreError::InvalidSecretHandle(secret.into()), + ))? + .plaintext(), + + SecretLookup::Injected { plaintext } => plaintext, + }; + + if plaintext.len() > usize::try_from(max_len).unwrap() { + return Err(Error::BufferLengthError { + buf: "plaintext", + len: "plaintext_max_len", + } + .into()); + } + + Ok(Some(String::from(std::str::from_utf8(plaintext)?))) + } + + async fn from_bytes( + &mut self, + plaintext: String, + ) -> Result { + Ok(self.add_secret(Vec::from(plaintext.as_bytes())).into()) + } +} diff --git a/lib/src/component/types.rs b/lib/src/component/types.rs new file mode 100644 index 00000000..84760fbc --- /dev/null +++ b/lib/src/component/types.rs @@ -0,0 +1,15 @@ +use {super::fastly::api::types, crate::session::Session}; + +pub(crate) use super::FastlyError; + +impl super::fastly::api::types::Host for Session { + fn convert_error(&mut self, err: FastlyError) -> wasmtime::Result { + match err { + FastlyError::FastlyError(e) => match e.downcast() { + Ok(e) => wasmtime::Result::Ok(e), + Err(e) => wasmtime::Result::Err(e), + }, + FastlyError::Trap(e) => wasmtime::Result::Err(e), + } + } +} diff --git a/lib/src/component/uap.rs b/lib/src/component/uap.rs new file mode 100644 index 00000000..4c99c5ba --- /dev/null +++ b/lib/src/component/uap.rs @@ -0,0 +1,56 @@ +use { + super::fastly::api::uap, + super::FastlyError, + crate::{error::Error, session::Session}, + wasmtime::component::Resource, +}; + +#[derive(Debug)] +pub struct UserAgent; + +#[async_trait::async_trait] +impl uap::HostUserAgent for Session { + async fn family( + &mut self, + _agent: Resource, + _max_len: u64, + ) -> wasmtime::Result { + anyhow::bail!("UserAgent resource is unimplemented") + } + + async fn major( + &mut self, + _agent: Resource, + _max_len: u64, + ) -> wasmtime::Result { + anyhow::bail!("UserAgent resource is unimplemented") + } + + async fn minor( + &mut self, + _agent: Resource, + _max_len: u64, + ) -> wasmtime::Result { + anyhow::bail!("UserAgent resource is unimplemented") + } + + async fn patch( + &mut self, + _agent: Resource, + _max_len: u64, + ) -> wasmtime::Result { + anyhow::bail!("UserAgent resource is unimplemented") + } + + fn drop(&mut self, _agent: Resource) -> wasmtime::Result<()> { + anyhow::bail!("UserAgent resource is unimplemented") + } +} + +#[async_trait::async_trait] +impl uap::Host for Session { + async fn parse(&mut self, _user_agent: String) -> Result, FastlyError> { + // not available + Err(Error::NotAvailable("User-agent parsing is not available").into()) + } +} diff --git a/lib/src/execute.rs b/lib/src/execute.rs index 0cd56abc..a4e4cf27 100644 --- a/lib/src/execute.rs +++ b/lib/src/execute.rs @@ -9,10 +9,11 @@ use crate::config::UnknownImportBehavior; use { crate::{ body::Body, + component as compute, config::{Backends, DeviceDetection, Dictionaries, ExperimentalModule, Geolocation}, downstream::prepare_request, error::ExecutionError, - linking::{create_store, link_host_functions, WasmCtx}, + linking::{create_store, link_host_functions, ComponentCtx, WasmCtx}, object_store::ObjectStores, secret_store::SecretStores, session::Session, @@ -22,6 +23,7 @@ use { hyper::{Request, Response}, std::{ collections::HashSet, + fs, net::{IpAddr, Ipv4Addr}, path::{Path, PathBuf}, sync::atomic::{AtomicBool, AtomicU64, Ordering}, @@ -30,12 +32,30 @@ use { time::{Duration, Instant}, }, tokio::sync::oneshot::{self, Sender}, - tracing::{event, info, info_span, Instrument, Level}, - wasmtime::{Engine, InstancePre, Linker, Module, ProfilingStrategy}, + tracing::{event, info, info_span, warn, Instrument, Level}, + wasmtime::{ + component::{self, Component}, + Engine, InstancePre, Linker, Module, ProfilingStrategy, + }, wasmtime_wasi::I32Exit, }; pub const EPOCH_INTERRUPTION_PERIOD: Duration = Duration::from_micros(50); + +enum Instance { + Module(Module, InstancePre), + Component(component::InstancePre), +} + +impl Instance { + fn unwrap_module(&self) -> (&Module, &InstancePre) { + match self { + Instance::Module(m, i) => (m, i), + Instance::Component(_) => panic!("unwrap_module called on a component"), + } + } +} + /// Execution context used by a [`ViceroyService`](struct.ViceroyService.html). /// /// This is all of the state needed to instantiate a module, in order to respond to an HTTP @@ -46,9 +66,7 @@ pub struct ExecuteCtx { /// A reference to the global context for Wasm compilation. engine: Engine, /// An almost-linked Instance: each import function is linked, just needs a Store - instance_pre: Arc>, - /// The module to run - module: Module, + instance_pre: Arc, /// The backends for this execution. backends: Arc, /// The device detection mappings for this execution. @@ -89,19 +107,76 @@ impl ExecuteCtx { guest_profile_path: Option, unknown_import_behavior: UnknownImportBehavior, ) -> Result { - let config = &configure_wasmtime(profiling_strategy); + let input = fs::read(&module_path)?; + + let is_wat = module_path + .as_ref() + .extension() + .map(|str| str == "wat") + .unwrap_or(false); + + let is_component = matches!( + wasmparser::Parser::new(0).parse(&input, true), + Ok(wasmparser::Chunk::Parsed { + payload: wasmparser::Payload::Version { + encoding: wasmparser::Encoding::Component, + .. + }, + .. + }) + ); + + let config = &configure_wasmtime(is_component, profiling_strategy); let engine = Engine::new(config)?; - let mut linker = Linker::new(&engine); - link_host_functions(&mut linker, &wasi_modules)?; - let module = Module::from_file(&engine, module_path)?; - match unknown_import_behavior { - UnknownImportBehavior::LinkError => (), - UnknownImportBehavior::Trap => linker.define_unknown_imports_as_traps(&module)?, - UnknownImportBehavior::ZeroOrNull => { - linker.define_unknown_imports_as_default_values(&module)? + let instance_pre = if is_component { + if unknown_import_behavior != UnknownImportBehavior::LinkError { + return Err(Error::Other(anyhow::anyhow!( + "Wasm components do not support unknown import behaviors other than link-time errors" + ))); } - } - let instance_pre = linker.instantiate_pre(&module)?; + + warn!( + " + + +------------------------------------------------------------------------+ + | | + | Wasm Component support in viceroy is in active development, and is not | + | supported for general consumption. | + | | + +------------------------------------------------------------------------+ + + " + ); + + let mut linker: component::Linker = component::Linker::new(&engine); + compute::link_host_functions(&mut linker)?; + let component = if is_wat { + Component::from_file(&engine, &module_path)? + } else { + Component::from_binary(&engine, &input)? + }; + let instance_pre = linker.instantiate_pre(&component)?; + Instance::Component(instance_pre) + } else { + let mut linker = Linker::new(&engine); + link_host_functions(&mut linker, &wasi_modules)?; + let module = if is_wat { + Module::from_file(&engine, &module_path)? + } else { + Module::from_binary(&engine, &input)? + }; + + match unknown_import_behavior { + UnknownImportBehavior::LinkError => (), + UnknownImportBehavior::Trap => linker.define_unknown_imports_as_traps(&module)?, + UnknownImportBehavior::ZeroOrNull => { + linker.define_unknown_imports_as_default_values(&module)? + } + } + + let instance_pre = linker.instantiate_pre(&module)?; + Instance::Module(module, instance_pre) + }; // Create the epoch-increment thread. @@ -118,7 +193,6 @@ impl ExecuteCtx { Ok(Self { engine, instance_pre: Arc::new(instance_pre), - module, backends: Arc::new(Backends::default()), device_detection: Arc::new(DeviceDetection::default()), geolocation: Arc::new(Geolocation::default()), @@ -351,71 +425,119 @@ impl ExecuteCtx { .as_secs(); path.join(format!("{}-{}.json", now, req_id)) }); - let profiler = guest_profile_path.is_some().then(|| { - let program_name = "main"; - GuestProfiler::new( - program_name, - EPOCH_INTERRUPTION_PERIOD, - vec![(program_name.to_string(), self.module.clone())], - ) - }); - // We currently have to postpone linking and instantiation to the guest task - // due to wasmtime limitations, in particular the fact that `Instance` is not `Send`. - // However, the fact that the module itself is created within `ExecuteCtx::new` - // means that the heavy lifting happens only once. - let mut store = - create_store(&self, session, profiler, |_| {}).map_err(ExecutionError::Context)?; + match self.instance_pre.as_ref() { + Instance::Component(instance_pre) => { + if self.guest_profile_path.is_some() { + warn!("Components do not currently support the guest profiler"); + } - let instance = self - .instance_pre - .instantiate_async(&mut store) - .await - .map_err(ExecutionError::Instantiation)?; + let req = session.downstream_request(); + let body = session.downstream_request_body(); - // Pull out the `_start` function, which by convention with WASI is the main entry point for - // an application. - let main_func = instance - .get_typed_func::<(), ()>(&mut store, "_start") - .map_err(ExecutionError::Typechecking)?; + let mut store = ComponentCtx::create_store(&self, session, None, |_| {}) + .map_err(ExecutionError::Context)?; + + let (compute, _instance) = + compute::Compute::instantiate_pre(&mut store, instance_pre) + .await + .map_err(ExecutionError::Instantiation)?; - // Invoke the entrypoint function, which may or may not send a downstream response. - let outcome = match main_func.call_async(&mut store, ()).await { - Ok(_) => Ok(()), - Err(e) => { - if let Some(exit) = e.downcast_ref::() { - if exit.0 == 0 { - Ok(()) - } else { - event!(Level::ERROR, "WebAssembly exited with error: {:?}", e); - Err(ExecutionError::WasmTrap(e)) + let result = compute + .fastly_api_reactor() + .call_serve(&mut store, req.into(), body.into()) + .await + .map_err(ExecutionError::Typechecking)?; + + let outcome = match result { + Ok(()) => Ok(()), + Err(()) => { + event!(Level::ERROR, "WebAssembly exited with an error"); + Err(ExecutionError::WasmTrap(anyhow::Error::msg("failed"))) } - } else { - event!(Level::ERROR, "WebAssembly trapped: {:?}", e); - Err(ExecutionError::WasmTrap(e)) - } + }; + + // Ensure the downstream response channel is closed, whether or not a response was + // sent during execution. + store.data_mut().close_downstream_response_sender(); + + let request_duration = Instant::now().duration_since(start_timestamp); + + info!( + "request completed using {} of WebAssembly heap", + bytesize::ByteSize::b(store.data().limiter().memory_allocated as u64), + ); + + info!("request completed in {:.0?}", request_duration); + + outcome } - }; - // If we collected a profile, write it to the file - write_profile(&mut store, guest_profile_path.as_ref()); + Instance::Module(module, instance_pre) => { + let profiler = self.guest_profile_path.is_some().then(|| { + let program_name = "main"; + GuestProfiler::new( + program_name, + EPOCH_INTERRUPTION_PERIOD, + vec![(program_name.to_string(), module.clone())], + ) + }); + + // We currently have to postpone linking and instantiation to the guest task + // due to wasmtime limitations, in particular the fact that `Instance` is not `Send`. + // However, the fact that the module itself is created within `ExecuteCtx::new` + // means that the heavy lifting happens only once. + let mut store = create_store(&self, session, profiler, |_| {}) + .map_err(ExecutionError::Context)?; + + let instance = instance_pre + .instantiate_async(&mut store) + .await + .map_err(ExecutionError::Instantiation)?; + + // Pull out the `_start` function, which by convention with WASI is the main entry point for + // an application. + let main_func = instance + .get_typed_func::<(), ()>(&mut store, "_start") + .map_err(ExecutionError::Typechecking)?; + + // Invoke the entrypoint function, which may or may not send a downstream response. + let outcome = match main_func.call_async(&mut store, ()).await { + Ok(_) => Ok(()), + Err(e) => { + if let Some(exit) = e.downcast_ref::() { + if exit.0 == 0 { + Ok(()) + } else { + event!(Level::ERROR, "WebAssembly exited with error: {:?}", e); + Err(ExecutionError::WasmTrap(e)) + } + } else { + event!(Level::ERROR, "WebAssembly trapped: {:?}", e); + Err(ExecutionError::WasmTrap(e)) + } + } + }; - // Ensure the downstream response channel is closed, whether or not a response was - // sent during execution. - store.data_mut().close_downstream_response_sender(); + // If we collected a profile, write it to the file + write_profile(&mut store, guest_profile_path.as_ref()); - let heap_bytes = store.data().limiter().memory_allocated; + // Ensure the downstream response channel is closed, whether or not a response was + // sent during execution. + store.data_mut().close_downstream_response_sender(); - let request_duration = Instant::now().duration_since(start_timestamp); + let request_duration = Instant::now().duration_since(start_timestamp); - info!( - "request completed using {} of WebAssembly heap", - bytesize::ByteSize::b(heap_bytes as u64) - ); + info!( + "request completed using {} of WebAssembly heap", + bytesize::ByteSize::b(store.data().limiter().memory_allocated as u64) + ); - info!("request completed in {:.0?}", request_duration); + info!("request completed in {:.0?}", request_duration); - outcome + outcome + } + } } pub async fn run_main(self, program_name: &str, args: &[String]) -> Result<(), anyhow::Error> { @@ -440,11 +562,17 @@ impl ExecuteCtx { self.secret_stores.clone(), ); + if let Instance::Component(_) = self.instance_pre.as_ref() { + panic!("components not currently supported with `run`"); + } + + let (module, instance_pre) = self.instance_pre.unwrap_module(); + let profiler = self.guest_profile_path.is_some().then(|| { GuestProfiler::new( program_name, EPOCH_INTERRUPTION_PERIOD, - vec![(program_name.to_string(), self.module.clone())], + vec![(program_name.to_string(), module.clone())], ) }); let mut store = create_store(&self, session, profiler, |builder| { @@ -455,8 +583,7 @@ impl ExecuteCtx { }) .map_err(ExecutionError::Context)?; - let instance = self - .instance_pre + let instance = instance_pre .instantiate_async(&mut store) .await .map_err(ExecutionError::Instantiation)?; @@ -520,7 +647,10 @@ impl Drop for ExecuteCtx { } } -fn configure_wasmtime(profiling_strategy: ProfilingStrategy) -> wasmtime::Config { +fn configure_wasmtime( + allow_components: bool, + profiling_strategy: ProfilingStrategy, +) -> wasmtime::Config { use wasmtime::{ Config, InstanceAllocationStrategy, PoolingAllocationConfig, WasmBacktraceDetails, }; @@ -568,5 +698,9 @@ fn configure_wasmtime(profiling_strategy: ProfilingStrategy) -> wasmtime::Config pooling_allocation_config, )); + if allow_components { + config.wasm_component_model(true); + } + config } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index a2383aff..db972744 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -20,6 +20,7 @@ pub mod logging; pub mod session; mod async_io; +pub mod component; mod downstream; mod execute; mod headers; @@ -29,7 +30,7 @@ mod secret_store; mod service; mod streaming_body; mod upstream; -mod wiggle_abi; +pub mod wiggle_abi; pub use { error::Error, execute::ExecuteCtx, service::ViceroyService, upstream::BackendConnector, diff --git a/lib/src/linking.rs b/lib/src/linking.rs index 9915d10d..24675aac 100644 --- a/lib/src/linking.rs +++ b/lib/src/linking.rs @@ -41,6 +41,80 @@ impl wasmtime::ResourceLimiter for Limiter { } } +#[allow(unused)] +pub struct ComponentCtx { + table: wasmtime_wasi::ResourceTable, + wasi: wasmtime_wasi::WasiCtx, + session: Session, + guest_profiler: Option>, + limiter: Limiter, +} + +impl ComponentCtx { + pub fn wasi(&mut self) -> &mut wasmtime_wasi::WasiCtx { + &mut self.wasi + } + + pub fn session(&mut self) -> &mut Session { + &mut self.session + } + + pub fn take_guest_profiler(&mut self) -> Option> { + self.guest_profiler.take() + } + + pub fn limiter(&self) -> &Limiter { + &self.limiter + } + + pub fn close_downstream_response_sender(&mut self) { + self.session.close_downstream_response_sender() + } + + /// Initialize a new [`Store`][store], given an [`ExecuteCtx`][ctx]. + /// + /// [ctx]: ../wiggle_abi/struct.ExecuteCtx.html + /// [store]: https://docs.rs/wasmtime/latest/wasmtime/struct.Store.html + pub(crate) fn create_store( + ctx: &ExecuteCtx, + session: Session, + guest_profiler: Option, + extra_init: impl FnOnce(&mut WasiCtxBuilder), + ) -> Result, anyhow::Error> { + let mut builder = make_wasi_ctx(ctx, &session); + + extra_init(&mut builder); + + let wasm_ctx = Self { + table: wasmtime_wasi::ResourceTable::new(), + wasi: builder.build(), + session, + guest_profiler: guest_profiler.map(Box::new), + limiter: Limiter::default(), + }; + let mut store = Store::new(ctx.engine(), wasm_ctx); + store.set_epoch_deadline(1); + store.epoch_deadline_callback(|mut store| { + if let Some(mut prof) = store.data_mut().guest_profiler.take() { + prof.sample(&store, std::time::Duration::ZERO); + store.data_mut().guest_profiler = Some(prof); + } + Ok(UpdateDeadline::Yield(1)) + }); + store.limiter(|ctx| &mut ctx.limiter); + Ok(store) + } +} + +impl wasmtime_wasi::WasiView for ComponentCtx { + fn table(&mut self) -> &mut wasmtime_wasi::ResourceTable { + &mut self.table + } + fn ctx(&mut self) -> &mut wasmtime_wasi::WasiCtx { + &mut self.wasi + } +} + pub struct WasmCtx { wasi: WasiP1Ctx, wasi_nn: WasiNnCtx, @@ -160,6 +234,7 @@ pub fn link_host_functions( wasmtime_wasi_nn::witx::add_to_linker(linker, WasmCtx::wasi_nn) } })?; + wasmtime_wasi::preview1::add_to_linker_async(linker, WasmCtx::wasi)?; wiggle_abi::fastly_abi::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_cache::add_to_linker(linker, WasmCtx::session)?; diff --git a/lib/src/upstream.rs b/lib/src/upstream.rs index e0882402..add4e2d5 100644 --- a/lib/src/upstream.rs +++ b/lib/src/upstream.rs @@ -325,6 +325,7 @@ pub fn send_request( /// The type ultimately yielded by a `PendingRequest`. /// An asynchronous request awaiting a response. +#[allow(unused)] #[derive(Debug)] pub enum PendingRequest { // NB: we use channels rather than a `JoinHandle` in order to support the `poll` API. diff --git a/lib/wit/deps/cli/command.wit b/lib/wit/deps/cli/command.wit new file mode 100644 index 00000000..d8005bd3 --- /dev/null +++ b/lib/wit/deps/cli/command.wit @@ -0,0 +1,7 @@ +package wasi:cli@0.2.0; + +world command { + include imports; + + export run; +} diff --git a/lib/wit/deps/cli/environment.wit b/lib/wit/deps/cli/environment.wit new file mode 100644 index 00000000..70065233 --- /dev/null +++ b/lib/wit/deps/cli/environment.wit @@ -0,0 +1,18 @@ +interface environment { + /// Get the POSIX-style environment variables. + /// + /// Each environment variable is provided as a pair of string variable names + /// and string value. + /// + /// Morally, these are a value import, but until value imports are available + /// in the component model, this import function should return the same + /// values each time it is called. + get-environment: func() -> list>; + + /// Get the POSIX-style arguments to the program. + get-arguments: func() -> list; + + /// Return a path that programs should use as their initial current working + /// directory, interpreting `.` as shorthand for this. + initial-cwd: func() -> option; +} diff --git a/lib/wit/deps/cli/exit.wit b/lib/wit/deps/cli/exit.wit new file mode 100644 index 00000000..d0c2b82a --- /dev/null +++ b/lib/wit/deps/cli/exit.wit @@ -0,0 +1,4 @@ +interface exit { + /// Exit the current instance and any linked instances. + exit: func(status: result); +} diff --git a/lib/wit/deps/cli/imports.wit b/lib/wit/deps/cli/imports.wit new file mode 100644 index 00000000..083b84a0 --- /dev/null +++ b/lib/wit/deps/cli/imports.wit @@ -0,0 +1,20 @@ +package wasi:cli@0.2.0; + +world imports { + include wasi:clocks/imports@0.2.0; + include wasi:filesystem/imports@0.2.0; + include wasi:sockets/imports@0.2.0; + include wasi:random/imports@0.2.0; + include wasi:io/imports@0.2.0; + + import environment; + import exit; + import stdin; + import stdout; + import stderr; + import terminal-input; + import terminal-output; + import terminal-stdin; + import terminal-stdout; + import terminal-stderr; +} diff --git a/lib/wit/deps/cli/run.wit b/lib/wit/deps/cli/run.wit new file mode 100644 index 00000000..a70ee8c0 --- /dev/null +++ b/lib/wit/deps/cli/run.wit @@ -0,0 +1,4 @@ +interface run { + /// Run the program. + run: func() -> result; +} diff --git a/lib/wit/deps/cli/stdio.wit b/lib/wit/deps/cli/stdio.wit new file mode 100644 index 00000000..31ef35b5 --- /dev/null +++ b/lib/wit/deps/cli/stdio.wit @@ -0,0 +1,17 @@ +interface stdin { + use wasi:io/streams@0.2.0.{input-stream}; + + get-stdin: func() -> input-stream; +} + +interface stdout { + use wasi:io/streams@0.2.0.{output-stream}; + + get-stdout: func() -> output-stream; +} + +interface stderr { + use wasi:io/streams@0.2.0.{output-stream}; + + get-stderr: func() -> output-stream; +} diff --git a/lib/wit/deps/cli/terminal.wit b/lib/wit/deps/cli/terminal.wit new file mode 100644 index 00000000..38c724ef --- /dev/null +++ b/lib/wit/deps/cli/terminal.wit @@ -0,0 +1,49 @@ +/// Terminal input. +/// +/// In the future, this may include functions for disabling echoing, +/// disabling input buffering so that keyboard events are sent through +/// immediately, querying supported features, and so on. +interface terminal-input { + /// The input side of a terminal. + resource terminal-input; +} + +/// Terminal output. +/// +/// In the future, this may include functions for querying the terminal +/// size, being notified of terminal size changes, querying supported +/// features, and so on. +interface terminal-output { + /// The output side of a terminal. + resource terminal-output; +} + +/// An interface providing an optional `terminal-input` for stdin as a +/// link-time authority. +interface terminal-stdin { + use terminal-input.{terminal-input}; + + /// If stdin is connected to a terminal, return a `terminal-input` handle + /// allowing further interaction with it. + get-terminal-stdin: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stdout as a +/// link-time authority. +interface terminal-stdout { + use terminal-output.{terminal-output}; + + /// If stdout is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + get-terminal-stdout: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stderr as a +/// link-time authority. +interface terminal-stderr { + use terminal-output.{terminal-output}; + + /// If stderr is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + get-terminal-stderr: func() -> option; +} diff --git a/lib/wit/deps/clocks/monotonic-clock.wit b/lib/wit/deps/clocks/monotonic-clock.wit new file mode 100644 index 00000000..4e4dc3a1 --- /dev/null +++ b/lib/wit/deps/clocks/monotonic-clock.wit @@ -0,0 +1,45 @@ +package wasi:clocks@0.2.0; +/// WASI Monotonic Clock is a clock API intended to let users measure elapsed +/// time. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A monotonic clock is a clock which has an unspecified initial value, and +/// successive reads of the clock will produce non-decreasing values. +/// +/// It is intended for measuring elapsed time. +interface monotonic-clock { + use wasi:io/poll@0.2.0.{pollable}; + + /// An instant in time, in nanoseconds. An instant is relative to an + /// unspecified initial value, and can only be compared to instances from + /// the same monotonic-clock. + type instant = u64; + + /// A duration of time, in nanoseconds. + type duration = u64; + + /// Read the current value of the clock. + /// + /// The clock is monotonic, therefore calling this function repeatedly will + /// produce a sequence of non-decreasing values. + now: func() -> instant; + + /// Query the resolution of the clock. Returns the duration of time + /// corresponding to a clock tick. + resolution: func() -> duration; + + /// Create a `pollable` which will resolve once the specified instant + /// occured. + subscribe-instant: func( + when: instant, + ) -> pollable; + + /// Create a `pollable` which will resolve once the given duration has + /// elapsed, starting at the time at which this function was called. + /// occured. + subscribe-duration: func( + when: duration, + ) -> pollable; +} diff --git a/lib/wit/deps/clocks/wall-clock.wit b/lib/wit/deps/clocks/wall-clock.wit new file mode 100644 index 00000000..440ca0f3 --- /dev/null +++ b/lib/wit/deps/clocks/wall-clock.wit @@ -0,0 +1,42 @@ +package wasi:clocks@0.2.0; +/// WASI Wall Clock is a clock API intended to let users query the current +/// time. The name "wall" makes an analogy to a "clock on the wall", which +/// is not necessarily monotonic as it may be reset. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A wall clock is a clock which measures the date and time according to +/// some external reference. +/// +/// External references may be reset, so this clock is not necessarily +/// monotonic, making it unsuitable for measuring elapsed time. +/// +/// It is intended for reporting the current date and time for humans. +interface wall-clock { + /// A time and date in seconds plus nanoseconds. + record datetime { + seconds: u64, + nanoseconds: u32, + } + + /// Read the current value of the clock. + /// + /// This clock is not monotonic, therefore calling this function repeatedly + /// will not necessarily produce a sequence of non-decreasing values. + /// + /// The returned timestamps represent the number of seconds since + /// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch], + /// also known as [Unix Time]. + /// + /// The nanoseconds field of the output is always less than 1000000000. + /// + /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 + /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time + now: func() -> datetime; + + /// Query the resolution of the clock. + /// + /// The nanoseconds field of the output is always less than 1000000000. + resolution: func() -> datetime; +} diff --git a/lib/wit/deps/clocks/world.wit b/lib/wit/deps/clocks/world.wit new file mode 100644 index 00000000..c0224572 --- /dev/null +++ b/lib/wit/deps/clocks/world.wit @@ -0,0 +1,6 @@ +package wasi:clocks@0.2.0; + +world imports { + import monotonic-clock; + import wall-clock; +} diff --git a/lib/wit/deps/fastly/compute.wit b/lib/wit/deps/fastly/compute.wit new file mode 100644 index 00000000..9a77d5d5 --- /dev/null +++ b/lib/wit/deps/fastly/compute.wit @@ -0,0 +1,1069 @@ +package fastly:api; + +interface types { + // TODO: split this up into function-specific error enums + enum error { + /// Unknown error value. + /// It should be an internal error if this is returned. + unknown-error, + /// Generic error value. + /// This means that some unexpected error occurred during a hostcall. + generic-error, + /// Invalid argument. + invalid-argument, + /// Invalid handle. + /// Thrown when a handle is not valid. E.G. No dictionary exists with the given name. + bad-handle, + /// Buffer length error. + /// Thrown when a buffer is too long. + buffer-len, + /// Unsupported operation error. + /// This error is thrown when some operation cannot be performed, because it is not supported. + unsupported, + /// Alignment error. + /// This is thrown when a pointer does not point to a properly aligned slice of memory. + bad-align, + /// Invalid HTTP error. + /// This can be thrown when a method, URI, header, or status is not valid. This can also + /// be thrown if a message head is too large. + http-invalid, + /// HTTP user error. + /// This is thrown in cases where user code caused an HTTP error. For example, attempt to send + /// a 1xx response code, or a request with a non-absolute URI. This can also be caused by + /// an unexpected header: both `content-length` and `transfer-encoding`, for example. + http-user, + /// HTTP incomplete message error. + /// This can be thrown when a stream ended unexpectedly. + http-incomplete, + /// A `None` error. + /// This status code is used to indicate when an optional value did not exist, as opposed to + /// an empty value. + /// Note, this value should no longer be used, as we have explicit optional types now. + optional-none, + /// Message head too large. + http-head-too-large, + /// Invalid HTTP status. + http-invalid-status, + /// Limit exceeded + /// + /// This is returned when an attempt to allocate a resource has exceeded the maximum number of + /// resources permitted. For example, creating too many response handles. + limit-exceeded + } + + type secret-handle = u32; +} + +interface http-types { + + use types.{secret-handle}; + + type body-handle = u32; + + type request-handle = u32; + type pending-request-handle = u32; + type response-handle = u32; + type request = tuple; + type response = tuple; + + enum http-version { + http09, + http10, + http11, + h2, + h3 + } + + flags content-encodings { + gzip + } + + /// Adjust how this requests's framing headers are determined. + enum framing-headers-mode { + automatic, + manually-from-headers + } + + enum tls-version { + tls1, + tls11, + tls12, + tls13 + } + + flags backend-config-options { + reserved, + host-override, + connect-timeout, + first-byte-timeout, + between-bytes-timeout, + use-ssl, + ssl-min-version, + ssl-max-version, + cert-hostname, + ca-cert, + ciphers, + sni-hostname, + dont-pool, + client-cert, + grpc, + } + + /// Create a backend for later use + record dynamic-backend-config { + host-override: string, + connect-timeout: u32, + first-byte-timeout: u32, + between-bytes-timeout: u32, + ssl-min-version: option, + ssl-max-version: option, + cert-hostname: string, + ca-cert: string, + ciphers: string, + sni-hostname: string, + client-cert: string, + client-key: secret-handle, + } + + type http-status = u16; +} + +/* + * Fastly UAP + */ +interface uap { + + use types.{error}; + + resource user-agent { + family: func(max-len: u64) -> string; + major: func(max-len: u64) -> string; + minor: func(max-len: u64) -> string; + patch: func(max-len: u64) -> string; + } + + parse: func(user-agent: string) -> result; +} + +/* + * Fastly HTTP Body + */ +interface http-body { + + use types.{error}; + use http-types.{body-handle}; + + enum write-end { + back, + front + } + + append: func(dest: body-handle, src: body-handle) -> result<_, error>; + + new: func() -> result; + + read: func(h: body-handle, chunk-size: u32) -> result, error>; + + write: func(h: body-handle, buf: list, end: write-end) -> result; + + close: func(h: body-handle) -> result<_, error>; + + known-length: func(h: body-handle) -> result; + + trailer-append: func( + h: body-handle, + name: string, + value: list, + ) -> result<_, error>; + + trailer-names-get: func( + h: body-handle, + max-len: u64, + cursor: u32, + ) -> result, option>>, error>; + + trailer-value-get: func( + h: body-handle, + name: string, + max-len: u64, + ) -> result>, error>; + + trailer-values-get: func( + h: body-handle, + name: string, + max-len: u64, + cursor: u32 + ) -> result, option>>, error>; +} + +/* + * Fastly Log + */ +interface log { + + use types.{error}; + + type handle = u32; + + endpoint-get: func(name: string) -> result; + + write: func(h: handle, msg: string) -> result; +} + +/* + * Fastly HTTP Req + */ +interface http-req { + + use types.{error}; + use http-types.{ + body-handle, request-handle, http-version, response, pending-request-handle, + content-encodings, framing-headers-mode, backend-config-options, + dynamic-backend-config + }; + + flags cache-override-tag { + /// Do not cache the response to this request, regardless of the origin response's headers. + pass, + ttl, + stale-while-revalidate, + pci, + } + + enum client-cert-verify-result { + ok, + bad-certificate, + certificate-revoked, + certificate-expired, + unknown-ca, + certificate-missing, + certificate-unknown, + } + + enum send-error-detail-tag { + uninitialized, + ok, + dns-timeout, + dns-error, + destination-not-found, + destination-unavailable, + destination-ip-unroutable, + connection-refused, + connection-terminated, + connection-timeout, + connection-limit-reached, + tls-certificate-error, + tls-configuration-error, + http-incomplete-response, + http-response-header-section-too-large, + http-response-body-too-large, + http-response-timeout, + http-response-status-invalid, + http-upgrade-failed, + http-protocol-error, + http-request-cache-key-invalid, + http-request-uri-invalid, + internal-error, + tls-alert-received, + tls-protocol-error, + } + + flags send-error-detail-mask { + reserved, + dns-error-rcode, + dns-error-info-code, + tls-alert-id, + } + + record send-error-detail { + tag: send-error-detail-tag, + mask: send-error-detail-mask, + dns-error-rcode: u16, + dns-error-info-code: u16, + tls-alert-id: u8, + } + + cache-override-set: func( + h: request-handle, + tag: cache-override-tag, + ttl: u32, + stale-while-revalidate: u32, + ) -> result<_, error>; + + cache-override-v2-set: func( + h: request-handle, + tag: cache-override-tag, + ttl: u32, + stale-while-revalidate: u32, + sk: option + ) -> result<_, error>; + + downstream-client-ip-addr: func() -> result, error>; + + downstream-server-ip-addr: func() -> result, error>; + + downstream-client-h2-fingerprint: func(max-len: u64) -> result, error>; + + downstream-client-request-id: func(max-len: u64) -> result; + + downstream-client-oh-fingerprint: func(max-len: u64) -> result, error>; + + downstream-tls-cipher-openssl-name: func(max-len: u64) -> result; + + downstream-tls-protocol: func(max-len: u64) -> result; + + downstream-tls-client-hello: func(max-len: u64) -> result, error>; + + downstream-tls-raw-client-certificate: func(max-len: u64) -> result, error>; + + downstream-tls-client-cert-verify-result: func() -> result; + + downstream-tls-ja3-md5: func() -> result, error>; + + downstream-tls-ja4: func(max-len: u64) -> result, error>; + + downstream-compliance-region: func(max-len: u64) -> result, error>; + + new: func() -> result; + + header-names-get: func( + h: request-handle, + max-len: u64, + cursor: u32, + ) -> result, option>>, error>; + + original-header-names-get: func( + max-len: u64, + cursor: u32, + ) -> result, option>>, error>; + + original-header-count: func() -> result; + + header-value-get: func( + h: request-handle, + name: string, + max-len: u64, + ) -> result>, error>; + + header-values-get: func( + h: request-handle, + name: string, + max-len: u64, + cursor: u32 + ) -> result, option>>, error>; + + header-values-set: func( + h: request-handle, + name: string, + values: list + ) -> result<_, error>; + + header-insert: func(h: request-handle, name: string, value: list) -> result<_, error>; + + header-append: func( + h: request-handle, + name: string, + value: list, + ) -> result<_, error>; + + header-remove: func(h: request-handle, name: string) -> result<_, error>; + + method-get: func(h: request-handle, max-len: u64) -> result; + + method-set: func(h: request-handle, method: string) -> result<_, error>; + + uri-get: func(h: request-handle, max-len: u64) -> result; + + uri-set: func(h: request-handle, uri: string) -> result<_, error>; + + version-get: func(h: request-handle) -> result; + + version-set: func(h: request-handle, version: http-version) -> result<_, error>; + + send: func( + h: request-handle, + b: body-handle, + backend: string, + ) -> result; + + send-v2: func( + h: request-handle, + b: body-handle, + backend: string, + ) -> result, error>>; + + send-async: func(h: request-handle, b: body-handle, backend: string) -> +result; + + send-async-streaming: func(h: request-handle, b: body-handle, backend: string) +-> result; + + pending-req-poll: func( + h: pending-request-handle, + ) -> result, error>; + + pending-req-poll-v2: func( + h: pending-request-handle, + ) -> result, tuple, error>>; + + pending-req-wait: func(h: pending-request-handle) -> result; + + pending-req-wait-v2: func( + h: pending-request-handle + ) -> result, error>>; + + pending-req-select: func( + h: list + ) -> result, error>; + + pending-req-select-v2: func( + h: list + ) -> result, tuple, error>>; + + fastly-key-is-valid: func() -> result; + + close: func(h: request-handle) -> result<_, error>; + + auto-decompress-response-set: func( + h: request-handle, + encodings: content-encodings, + ) -> result<_, error>; + + upgrade-websocket: func(backend: string) -> result<_, error>; + + redirect-to-websocket-proxy: func(backend: string) -> result<_, error>; + + redirect-to-websocket-proxy-v2: func( + h: request-handle, + backend: string, + ) -> result<_, error>; + + redirect-to-grip-proxy: func(backend: string) -> result<_, error>; + + redirect-to-grip-proxy-v2: func( + h: request-handle, + backend: string, + ) -> result<_, error>; + + framing-headers-mode-set: func( + h: request-handle, + mode: framing-headers-mode, + ) -> result<_, error>; + + register-dynamic-backend: func( + prefix: string, + target: string, + options: backend-config-options, + config: dynamic-backend-config, + ) -> result<_, error>; + +} + +/* + * Fastly HTTP Resp + */ +interface http-resp { + use types.{error}; + + use http-types.{ + response-handle, body-handle, http-version, http-status, + framing-headers-mode + }; + + new: func() -> result; + + header-names-get: func( + h: response-handle, + max-len: u64, + cursor: u32, + ) -> result, option>>, error>; + + header-value-get: func( + h: response-handle, + name: string, + max-len: u64, + ) -> result>, error>; + + header-values-get: func( + h: response-handle, + name: string, + max-len: u64, + cursor: u32 + ) -> result, option>>, error>; + + header-values-set: func( + h: response-handle, + name: string, + values: list + ) -> result<_, error>; + + header-insert: func( + h: response-handle, + name: string, + value: list, + ) -> result<_, error>; + + header-append: func( + h: response-handle, + name: string, + value: list, + ) -> result<_, error>; + + header-remove: func( + h: response-handle, + name: string, + ) -> result<_, error>; + + version-get: func(h: response-handle) -> result; + + version-set: func( + h: response-handle, + version: http-version, + ) -> result<_, error>; + + send-downstream: func( + h: response-handle, + b: body-handle, + streaming: bool, + ) -> result<_, error>; + + status-get: func(h: response-handle) -> result; + + status-set: func(h: response-handle, status: http-status) -> result<_, error>; + + close: func(h: response-handle) -> result<_, error>; + + /// Adjust how this response's framing headers are determined. + framing-headers-mode-set: func(h: response-handle, mode: framing-headers-mode) +-> result<_, error>; + + enum keepalive-mode { + automatic, + no-keepalive, + } + + http-keepalive-mode-set: func(h: response-handle, mode: keepalive-mode) -> +result<_, error>; +} + +/* + * Fastly Dictionary + */ +interface dictionary { + + use types.{error}; + + type handle = u32; + + open: func(name: string) -> result; + + get: func( + h: handle, + key: string, + max-len: u64, + ) -> result, error>; +} + +/* + * Fastly Geo + */ +interface geo { + use types.{error}; + + lookup: func(addr-octets: list, max-len: u64) -> result; +} + +/* + * Fastly device detection + */ +interface device-detection { + use types.{error}; + + lookup: func(user-agent: string, max-len: u64) -> result; +} + +/* + * Fastly edge-rate-limiter + */ +interface erl { + use types.{error}; + + check-rate: func( + rc: string, + entry: string, + delta: u32, + window: u32, + limit: u32, + pb: string, + ttl: u32, + ) -> result; + + ratecounter-increment: func( + rc: string, + entry: string, + delta: u32, + ) -> result<_, error>; + + ratecounter-lookup-rate: func( + rc: string, + entry: string, + window: u32, + ) -> result; + + ratecounter-lookup-count: func( + rc: string, + entry: string, + duration: u32, + ) -> result; + + penaltybox-add: func( + pb: string, + entry: string, + ttl: u32, + ) -> result<_, error>; + + penaltybox-has: func( + pb: string, + entry: string, + ) -> result; +} + +/* + * Fastly KV Store + */ +interface kv-store { + + use types.{error}; + use http-types.{body-handle}; + + type handle = u32; + type pending-lookup-handle = u32; + type pending-insert-handle = u32; + type pending-delete-handle = u32; + + open: func(name: string) -> result; + + lookup: func( + store: handle, + key: string, + ) -> result, error>; + + lookup-async: func( + store: handle, + key: string, + ) -> result; + + pending-lookup-wait: func( + handle: pending-lookup-handle, + ) -> result, error>; + + insert: func( + store: handle, + key: string, + body-handle: body-handle, + ) -> result<_, error>; + + insert-async: func( + store: handle, + key: string, + body-handle: body-handle, + ) -> result; + + pending-insert-wait: func( + handle: pending-insert-handle, + ) -> result<_, error>; + + delete-async: func( + store: handle, + key: string, + ) -> result; + + pending-delete-wait: func( + handle: pending-delete-handle, + ) -> result<_, error>; +} + +/* + * Fastly Secret Store + */ +interface secret-store { + + use types.{error, secret-handle}; + + type store-handle = u32; + + open: func(name: string) -> result; + + get: func( + store: store-handle, + key: string, + ) -> result, error>; + + plaintext: func( + secret: secret-handle, + max-len: u64 + ) -> result, error>; + + from-bytes: func(bytes: string) -> result; +} + +/* + * Fastly backend + */ +interface backend { + use types.{error}; + use http-types.{tls-version}; + + exists: func(backend: string) -> result; + + enum backend-health { + unknown, + healthy, + unhealthy, + } + + is-healthy: func(backend: string) -> result; + + /// Returns `true` if the backend is a "dynamic" backend. + is-dynamic: func(backend: string) -> result; + + /// Get the host of this backend. + get-host: func(backend: string, max-len: u64) -> result; + + /// Get the "override host" for this backend. + /// + /// This is used to change the `Host` header sent to the backend. See the + /// Fastly documentation oh this topic here: https://docs.fastly.com/en/guides/specifying-an-override-host + get-override-host: func( + backend: string, + max-len: u64, + ) -> result, error>; + + /// Get the remote TCP port of the backend connection for the request. + get-port: func(backend: string) -> result; + + /// Get the connection timeout of the backend. + get-connect-timeout-ms: func(backend: string) -> result; + + /// Get the first byte timeout of the backend. + get-first-byte-timeout-ms: func(backend: string) -> result; + + /// Get the between byte timeout of the backend. + get-between-bytes-timeout-ms: func(backend: string) -> result; + + /// Returns `true` if the backend is configured to use SSL. + is-ssl: func(backend: string) -> result; + + /// Get the minimum SSL version this backend will use. + get-ssl-min-version: func(backend: string) -> result; + + /// Get the maximum SSL version this backend will use. + get-ssl-max-version: func(backend: string) -> result; +} + +/* + * Fastly Async IO + */ +interface async-io { + use types.{error}; + + /// A handle to an object supporting generic async operations. + /// Can be either a `BodyHandle` or a `PendingRequestHandle`. + /// + /// Each async item has an associated I/O action: + /// + /// * Pending requests: awaiting the response headers / `Response` object + /// * Normal bodies: reading bytes from the body + /// * Streaming bodies: writing bytes to the body + /// + /// For writing bytes, note that there is a large host-side buffer that bytes can eagerly be written + /// into, even before the origin itself consumes that data. + type handle = u32; + + /// Blocks until one of the given objects is ready for I/O, or the optional timeout expires. + /// + /// Valid object handles includes bodies and pending requests. See the `async_item_handle` + /// definition for more details, including what I/O actions are associated with each handle + /// type. + /// + /// The timeout is specified in milliseconds, or 0 if no timeout is desired. + /// + /// Returns the _index_ (not handle!) of the first object that is ready, or + /// none if the timeout expires before any objects are ready for I/O. + select: func(hs: list, timeout-ms: u32) -> result, error>; + + /// Returns 1 if the given async item is "ready" for its associated I/O action, 0 otherwise. + /// + /// If an object is ready, the I/O action is guaranteed to complete without blocking. + /// + /// Valid object handles includes bodies and pending requests. See the `async_item_handle` + /// definition for more details, including what I/O actions are associated with each handle + /// type. + is-ready: func(handle: handle) -> result; +} + +/* + * Fastly Purge + */ +interface purge { + + use types.{error}; + + flags purge-options-mask { + soft-purge, + ret-buf + } + + /* + * A surrogate key can be a max of 1024 characters. + * A surrogate key must contain only printable ASCII characters (those between `0x21` and `0x7E`, inclusive). + */ + purge-surrogate-key: func( + surrogate-keys: string, + purge-options: purge-options-mask, + max-len: u64, + ) -> result, error>; +} + +/* + * Fastly Cache + */ +interface cache { + + use types.{error}; + use http-types.{body-handle, request-handle}; + + /// The outcome of a cache lookup (either bare or as part of a cache transaction) + type handle = u32; + type object-length = u64; + type duration-ns = u64; + type cache-hit-count = u64; + + flags lookup-options-mask { + request-headers, + } + + /// Extensible options for cache lookup operations; currently used for both `lookup` and `transaction_lookup`. + record lookup-options { + /** + * A full request handle, but used only for its headers + */ + request-headers: request-handle, + } + + flags write-options-mask { + reserved, + request-headers, + vary-rule, + initial-age-ns, + stale-while-revalidate-ns, + surrogate-keys, + length, + user-metadata, + sensitive-data, + } + + /// Configuration for several hostcalls that write to the cache: + /// - `insert` + /// - `transaction-insert` + /// - `transaction-insert-and-stream-back` + /// - `transaction-update` + /// + /// Some options are only allowed for certain of these hostcalls; see `cache-write-options-mask`. + record write-options { + /// this is a required field; there's no flag for it + max-age-ns: duration-ns, + /// a full request handle, but used only for its headers + request-headers: request-handle, + /// a list of header names separated by spaces + vary-rule: string, + /// The initial age of the object in nanoseconds (default: 0). + /// + /// This age is used to determine the freshness lifetime of the object as well as to + /// prioritize which variant to return if a subsequent lookup matches more than one vary rule + initial-age-ns: duration-ns, + stale-while-revalidate-ns: duration-ns, + /// a list of surrogate keys separated by spaces + surrogate-keys: string, + length: object-length, + user-metadata: list, + } + + flags get-body-options-mask { + reserved, + %from, + to, + } + + record get-body-options { + %from: u64, + to: u64, + } + + /// The status of this lookup (and potential transaction) + flags lookup-state { + /// a cached object was found + found, + /// the cached object is valid to use (implies found) + usable, + /// the cached object is stale (but may or may not be valid to use) + stale, + /// this client is requested to insert or revalidate an object + must-insert-or-update, + } + + /// Performs a non-request-collapsing cache lookup. + /// + /// Returns a result without waiting for any request collapsing that may be ongoing. + lookup: func( + key: string, + mask: lookup-options-mask, + options: lookup-options, + ) -> result; + + /// Performs a non-request-collapsing cache insertion (or update). + /// + /// The returned handle is to a streaming body that is used for writing the object into + /// the cache. + insert: func( + key: string, + options-mask: write-options-mask, + options: write-options, + ) -> result; + + /// The entrypoint to the request-collapsing cache transaction API. + /// + /// This operation always participates in request collapsing and may return stale objects. To bypass + /// request collapsing, use `lookup` and `insert` instead. + transaction-lookup: func( + key: string, + mask: lookup-options-mask, + options: lookup-options, + ) -> result; + + /// Insert an object into the cache with the given metadata. + /// + /// Can only be used in if the cache handle state includes the `must-insert-or-update` flag. + /// + /// The returned handle is to a streaming body that is used for writing the object into + /// the cache. + transaction-insert: func( + handle: handle, + mask: write-options-mask, + options: write-options, + ) -> result; + + /// Insert an object into the cache with the given metadata, and return a readable stream of the + /// bytes as they are stored. + /// + /// This helps avoid the "slow reader" problem on a teed stream, for example when a program wishes + /// to store a backend request in the cache while simultaneously streaming to a client in an HTTP + /// response. + /// + /// The returned body handle is to a streaming body that is used for writing the object _into_ + /// the cache. The returned cache handle provides a separate transaction for reading out the + /// newly cached object to send elsewhere. + transaction-insert-and-stream-back: func( + handle: handle, + mask: write-options-mask, + options: write-options, + ) -> result, error>; + + /// Update the metadata of an object in the cache without changing its data. + /// + /// Can only be used in if the cache handle state includes both of the flags: + /// - `found` + /// - `must-insert-or-update` + transaction-update: func( + handle: handle, + mask: write-options-mask, + options: write-options, + ) -> result<_, error>; + + /// Cancel an obligation to provide an object to the cache. + /// + /// Useful if there is an error before streaming is possible, e.g. if a backend is unreachable. + transaction-cancel: func(handle: handle) -> result<_, error>; + + close: func(handle: handle) -> result<_, error>; + + get-state: func(handle: handle) -> result; + + /// Gets the user metadata of the found object, returning the `optional-none` error if there + /// was no found object. + get-user-metadata: func(handle: handle) -> result; + + /// Gets a range of the found object body, returning the `optional-none` error if there + /// was no found object. + /// + /// The returned `body_handle` must be closed before calling this function again on the same + /// `cache_handle`. + /// + /// Note: until the CacheD protocol is adjusted to fully support this functionality, + /// the body of objects that are past the stale-while-revalidate period will not + /// be available, even when other metadata is. + get-body: func( + handle: handle, + mask: get-body-options-mask, + options: get-body-options, + ) -> result; + + /// Gets the content length of the found object, returning the `$none` error if there + /// was no found object, or no content length was provided. + get-length: func(handle: handle) -> result; + + /// Gets the configured max age of the found object, returning the `$none` error if there + /// was no found object. + get-max-age-ns: func(handle: handle) -> result; + + /// Gets the configured stale-while-revalidate period of the found object, returning the + /// `$none` error if there was no found object. + get-stale-while-revalidate-ns: func(handle: handle) -> result; + + /// Gets the age of the found object, returning the `$none` error if there + /// was no found object. + get-age-ns: func(handle: handle) -> result; + + /// Gets the number of cache hits for the found object, returning the `$none` error if there + /// was no found object. + get-hits: func(handle: handle) -> result; +} + +interface reactor { + use http-types.{request-handle, body-handle}; + + /// Serve the given request + /// + /// response handle not currently returned, because in the case of a streamed response + /// send downstream must be fully streamed due to the run to completion semantics. + serve: func(req: request-handle, body: body-handle) -> result; +} + +world compute { + import wasi:clocks/wall-clock@0.2.0; + import wasi:clocks/monotonic-clock@0.2.0; + import wasi:io/error@0.2.0; + import wasi:io/streams@0.2.0; + import wasi:random/random@0.2.0; + import wasi:cli/stdout@0.2.0; + import wasi:cli/stderr@0.2.0; + import wasi:cli/stdin@0.2.0; + + import async-io; + import backend; + import cache; + import dictionary; + import geo; + import device-detection; + import erl; + import http-body; + import http-req; + import http-resp; + import log; + import kv-store; + import purge; + import secret-store; + import uap; + + export reactor; +} diff --git a/lib/wit/deps/filesystem/preopens.wit b/lib/wit/deps/filesystem/preopens.wit new file mode 100644 index 00000000..da801f6d --- /dev/null +++ b/lib/wit/deps/filesystem/preopens.wit @@ -0,0 +1,8 @@ +package wasi:filesystem@0.2.0; + +interface preopens { + use types.{descriptor}; + + /// Return the set of preopened directories, and their path. + get-directories: func() -> list>; +} diff --git a/lib/wit/deps/filesystem/types.wit b/lib/wit/deps/filesystem/types.wit new file mode 100644 index 00000000..11108fcd --- /dev/null +++ b/lib/wit/deps/filesystem/types.wit @@ -0,0 +1,634 @@ +package wasi:filesystem@0.2.0; +/// WASI filesystem is a filesystem API primarily intended to let users run WASI +/// programs that access their files on their existing filesystems, without +/// significant overhead. +/// +/// It is intended to be roughly portable between Unix-family platforms and +/// Windows, though it does not hide many of the major differences. +/// +/// Paths are passed as interface-type `string`s, meaning they must consist of +/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain +/// paths which are not accessible by this API. +/// +/// The directory separator in WASI is always the forward-slash (`/`). +/// +/// All paths in WASI are relative paths, and are interpreted relative to a +/// `descriptor` referring to a base directory. If a `path` argument to any WASI +/// function starts with `/`, or if any step of resolving a `path`, including +/// `..` and symbolic link steps, reaches a directory outside of the base +/// directory, or reaches a symlink to an absolute or rooted path in the +/// underlying filesystem, the function fails with `error-code::not-permitted`. +/// +/// For more information about WASI path resolution and sandboxing, see +/// [WASI filesystem path resolution]. +/// +/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md +interface types { + use wasi:io/streams@0.2.0.{input-stream, output-stream, error}; + use wasi:clocks/wall-clock@0.2.0.{datetime}; + + /// File size or length of a region within a file. + type filesize = u64; + + /// The type of a filesystem object referenced by a descriptor. + /// + /// Note: This was called `filetype` in earlier versions of WASI. + enum descriptor-type { + /// The type of the descriptor or file is unknown or is different from + /// any of the other types specified. + unknown, + /// The descriptor refers to a block device inode. + block-device, + /// The descriptor refers to a character device inode. + character-device, + /// The descriptor refers to a directory inode. + directory, + /// The descriptor refers to a named pipe. + fifo, + /// The file refers to a symbolic link inode. + symbolic-link, + /// The descriptor refers to a regular file inode. + regular-file, + /// The descriptor refers to a socket. + socket, + } + + /// Descriptor flags. + /// + /// Note: This was called `fdflags` in earlier versions of WASI. + flags descriptor-flags { + /// Read mode: Data can be read. + read, + /// Write mode: Data can be written to. + write, + /// Request that writes be performed according to synchronized I/O file + /// integrity completion. The data stored in the file and the file's + /// metadata are synchronized. This is similar to `O_SYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + file-integrity-sync, + /// Request that writes be performed according to synchronized I/O data + /// integrity completion. Only the data stored in the file is + /// synchronized. This is similar to `O_DSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + data-integrity-sync, + /// Requests that reads be performed at the same level of integrety + /// requested for writes. This is similar to `O_RSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + requested-write-sync, + /// Mutating directories mode: Directory contents may be mutated. + /// + /// When this flag is unset on a descriptor, operations using the + /// descriptor which would create, rename, delete, modify the data or + /// metadata of filesystem objects, or obtain another handle which + /// would permit any of those, shall fail with `error-code::read-only` if + /// they would otherwise succeed. + /// + /// This may only be set on directories. + mutate-directory, + } + + /// File attributes. + /// + /// Note: This was called `filestat` in earlier versions of WASI. + record descriptor-stat { + /// File type. + %type: descriptor-type, + /// Number of hard links to the file. + link-count: link-count, + /// For regular files, the file size in bytes. For symbolic links, the + /// length in bytes of the pathname contained in the symbolic link. + size: filesize, + /// Last data access timestamp. + /// + /// If the `option` is none, the platform doesn't maintain an access + /// timestamp for this file. + data-access-timestamp: option, + /// Last data modification timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// modification timestamp for this file. + data-modification-timestamp: option, + /// Last file status-change timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// status-change timestamp for this file. + status-change-timestamp: option, + } + + /// Flags determining the method of how paths are resolved. + flags path-flags { + /// As long as the resolved path corresponds to a symbolic link, it is + /// expanded. + symlink-follow, + } + + /// Open flags used by `open-at`. + flags open-flags { + /// Create file if it does not exist, similar to `O_CREAT` in POSIX. + create, + /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. + directory, + /// Fail if file already exists, similar to `O_EXCL` in POSIX. + exclusive, + /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. + truncate, + } + + /// Number of hard links to an inode. + type link-count = u64; + + /// When setting a timestamp, this gives the value to set it to. + variant new-timestamp { + /// Leave the timestamp set to its previous value. + no-change, + /// Set the timestamp to the current time of the system clock associated + /// with the filesystem. + now, + /// Set the timestamp to the given value. + timestamp(datetime), + } + + /// A directory entry. + record directory-entry { + /// The type of the file referred to by this directory entry. + %type: descriptor-type, + + /// The name of the object. + name: string, + } + + /// Error codes returned by functions, similar to `errno` in POSIX. + /// Not all of these error codes are returned by the functions provided by this + /// API; some are used in higher-level library layers, and others are provided + /// merely for alignment with POSIX. + enum error-code { + /// Permission denied, similar to `EACCES` in POSIX. + access, + /// Resource unavailable, or operation would block, similar to `EAGAIN` and `EWOULDBLOCK` in POSIX. + would-block, + /// Connection already in progress, similar to `EALREADY` in POSIX. + already, + /// Bad descriptor, similar to `EBADF` in POSIX. + bad-descriptor, + /// Device or resource busy, similar to `EBUSY` in POSIX. + busy, + /// Resource deadlock would occur, similar to `EDEADLK` in POSIX. + deadlock, + /// Storage quota exceeded, similar to `EDQUOT` in POSIX. + quota, + /// File exists, similar to `EEXIST` in POSIX. + exist, + /// File too large, similar to `EFBIG` in POSIX. + file-too-large, + /// Illegal byte sequence, similar to `EILSEQ` in POSIX. + illegal-byte-sequence, + /// Operation in progress, similar to `EINPROGRESS` in POSIX. + in-progress, + /// Interrupted function, similar to `EINTR` in POSIX. + interrupted, + /// Invalid argument, similar to `EINVAL` in POSIX. + invalid, + /// I/O error, similar to `EIO` in POSIX. + io, + /// Is a directory, similar to `EISDIR` in POSIX. + is-directory, + /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. + loop, + /// Too many links, similar to `EMLINK` in POSIX. + too-many-links, + /// Message too large, similar to `EMSGSIZE` in POSIX. + message-size, + /// Filename too long, similar to `ENAMETOOLONG` in POSIX. + name-too-long, + /// No such device, similar to `ENODEV` in POSIX. + no-device, + /// No such file or directory, similar to `ENOENT` in POSIX. + no-entry, + /// No locks available, similar to `ENOLCK` in POSIX. + no-lock, + /// Not enough space, similar to `ENOMEM` in POSIX. + insufficient-memory, + /// No space left on device, similar to `ENOSPC` in POSIX. + insufficient-space, + /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. + not-directory, + /// Directory not empty, similar to `ENOTEMPTY` in POSIX. + not-empty, + /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. + not-recoverable, + /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. + unsupported, + /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. + no-tty, + /// No such device or address, similar to `ENXIO` in POSIX. + no-such-device, + /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. + overflow, + /// Operation not permitted, similar to `EPERM` in POSIX. + not-permitted, + /// Broken pipe, similar to `EPIPE` in POSIX. + pipe, + /// Read-only file system, similar to `EROFS` in POSIX. + read-only, + /// Invalid seek, similar to `ESPIPE` in POSIX. + invalid-seek, + /// Text file busy, similar to `ETXTBSY` in POSIX. + text-file-busy, + /// Cross-device link, similar to `EXDEV` in POSIX. + cross-device, + } + + /// File or memory access pattern advisory information. + enum advice { + /// The application has no advice to give on its behavior with respect + /// to the specified data. + normal, + /// The application expects to access the specified data sequentially + /// from lower offsets to higher offsets. + sequential, + /// The application expects to access the specified data in a random + /// order. + random, + /// The application expects to access the specified data in the near + /// future. + will-need, + /// The application expects that it will not access the specified data + /// in the near future. + dont-need, + /// The application expects to access the specified data once and then + /// not reuse it thereafter. + no-reuse, + } + + /// A 128-bit hash value, split into parts because wasm doesn't have a + /// 128-bit integer type. + record metadata-hash-value { + /// 64 bits of a 128-bit hash value. + lower: u64, + /// Another 64 bits of a 128-bit hash value. + upper: u64, + } + + /// A descriptor is a reference to a filesystem object, which may be a file, + /// directory, named pipe, special file, or other object on which filesystem + /// calls may be made. + resource descriptor { + /// Return a stream for reading from a file, if available. + /// + /// May fail with an error-code describing why the file cannot be read. + /// + /// Multiple read, write, and append streams may be active on the same open + /// file and they do not interfere with each other. + /// + /// Note: This allows using `read-stream`, which is similar to `read` in POSIX. + read-via-stream: func( + /// The offset within the file at which to start reading. + offset: filesize, + ) -> result; + + /// Return a stream for writing to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be written. + /// + /// Note: This allows using `write-stream`, which is similar to `write` in + /// POSIX. + write-via-stream: func( + /// The offset within the file at which to start writing. + offset: filesize, + ) -> result; + + /// Return a stream for appending to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be appended. + /// + /// Note: This allows using `write-stream`, which is similar to `write` with + /// `O_APPEND` in in POSIX. + append-via-stream: func() -> result; + + /// Provide file advisory information on a descriptor. + /// + /// This is similar to `posix_fadvise` in POSIX. + advise: func( + /// The offset within the file to which the advisory applies. + offset: filesize, + /// The length of the region to which the advisory applies. + length: filesize, + /// The advice. + advice: advice + ) -> result<_, error-code>; + + /// Synchronize the data of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fdatasync` in POSIX. + sync-data: func() -> result<_, error-code>; + + /// Get flags associated with a descriptor. + /// + /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX. + /// + /// Note: This returns the value that was the `fs_flags` value returned + /// from `fdstat_get` in earlier versions of WASI. + get-flags: func() -> result; + + /// Get the dynamic type of a descriptor. + /// + /// Note: This returns the same value as the `type` field of the `fd-stat` + /// returned by `stat`, `stat-at` and similar. + /// + /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided + /// by `fstat` in POSIX. + /// + /// Note: This returns the value that was the `fs_filetype` value returned + /// from `fdstat_get` in earlier versions of WASI. + get-type: func() -> result; + + /// Adjust the size of an open file. If this increases the file's size, the + /// extra bytes are filled with zeros. + /// + /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. + set-size: func(size: filesize) -> result<_, error-code>; + + /// Adjust the timestamps of an open file or directory. + /// + /// Note: This is similar to `futimens` in POSIX. + /// + /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. + set-times: func( + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Read from a descriptor, without using and updating the descriptor's offset. + /// + /// This function returns a list of bytes containing the data that was + /// read, along with a bool which, when true, indicates that the end of the + /// file was reached. The returned list will contain up to `length` bytes; it + /// may return fewer than requested, if the end of the file is reached or + /// if the I/O operation is interrupted. + /// + /// In the future, this may change to return a `stream`. + /// + /// Note: This is similar to `pread` in POSIX. + read: func( + /// The maximum number of bytes to read. + length: filesize, + /// The offset within the file at which to read. + offset: filesize, + ) -> result, bool>, error-code>; + + /// Write to a descriptor, without using and updating the descriptor's offset. + /// + /// It is valid to write past the end of a file; the file is extended to the + /// extent of the write, with bytes between the previous end and the start of + /// the write set to zero. + /// + /// In the future, this may change to take a `stream`. + /// + /// Note: This is similar to `pwrite` in POSIX. + write: func( + /// Data to write + buffer: list, + /// The offset within the file at which to write. + offset: filesize, + ) -> result; + + /// Read directory entries from a directory. + /// + /// On filesystems where directories contain entries referring to themselves + /// and their parents, often named `.` and `..` respectively, these entries + /// are omitted. + /// + /// This always returns a new stream which starts at the beginning of the + /// directory. Multiple streams may be active on the same directory, and they + /// do not interfere with each other. + read-directory: func() -> result; + + /// Synchronize the data and metadata of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fsync` in POSIX. + sync: func() -> result<_, error-code>; + + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + create-directory-at: func( + /// The relative path at which to create the directory. + path: string, + ) -> result<_, error-code>; + + /// Return the attributes of an open file or directory. + /// + /// Note: This is similar to `fstat` in POSIX, except that it does not return + /// device and inode information. For testing whether two descriptors refer to + /// the same underlying filesystem object, use `is-same-object`. To obtain + /// additional data that can be used do determine whether a file has been + /// modified, use `metadata-hash`. + /// + /// Note: This was called `fd_filestat_get` in earlier versions of WASI. + stat: func() -> result; + + /// Return the attributes of a file or directory. + /// + /// Note: This is similar to `fstatat` in POSIX, except that it does not + /// return device and inode information. See the `stat` description for a + /// discussion of alternatives. + /// + /// Note: This was called `path_filestat_get` in earlier versions of WASI. + stat-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + + /// Adjust the timestamps of a file or directory. + /// + /// Note: This is similar to `utimensat` in POSIX. + /// + /// Note: This was called `path_filestat_set_times` in earlier versions of + /// WASI. + set-times-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to operate on. + path: string, + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Create a hard link. + /// + /// Note: This is similar to `linkat` in POSIX. + link-at: func( + /// Flags determining the method of how the path is resolved. + old-path-flags: path-flags, + /// The relative source path from which to link. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path at which to create the hard link. + new-path: string, + ) -> result<_, error-code>; + + /// Open a file or directory. + /// + /// The returned descriptor is not guaranteed to be the lowest-numbered + /// descriptor not currently open/ it is randomized to prevent applications + /// from depending on making assumptions about indexes, since this is + /// error-prone in multi-threaded contexts. The returned descriptor is + /// guaranteed to be less than 2**31. + /// + /// If `flags` contains `descriptor-flags::mutate-directory`, and the base + /// descriptor doesn't have `descriptor-flags::mutate-directory` set, + /// `open-at` fails with `error-code::read-only`. + /// + /// If `flags` contains `write` or `mutate-directory`, or `open-flags` + /// contains `truncate` or `create`, and the base descriptor doesn't have + /// `descriptor-flags::mutate-directory` set, `open-at` fails with + /// `error-code::read-only`. + /// + /// Note: This is similar to `openat` in POSIX. + open-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the object to open. + path: string, + /// The method by which to open the file. + open-flags: open-flags, + /// Flags to use for the resulting descriptor. + %flags: descriptor-flags, + ) -> result; + + /// Read the contents of a symbolic link. + /// + /// If the contents contain an absolute or rooted path in the underlying + /// filesystem, this function fails with `error-code::not-permitted`. + /// + /// Note: This is similar to `readlinkat` in POSIX. + readlink-at: func( + /// The relative path of the symbolic link from which to read. + path: string, + ) -> result; + + /// Remove a directory. + /// + /// Return `error-code::not-empty` if the directory is not empty. + /// + /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + remove-directory-at: func( + /// The relative path to a directory to remove. + path: string, + ) -> result<_, error-code>; + + /// Rename a filesystem object. + /// + /// Note: This is similar to `renameat` in POSIX. + rename-at: func( + /// The relative source path of the file or directory to rename. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path to which to rename the file or directory. + new-path: string, + ) -> result<_, error-code>; + + /// Create a symbolic link (also known as a "symlink"). + /// + /// If `old-path` starts with `/`, the function fails with + /// `error-code::not-permitted`. + /// + /// Note: This is similar to `symlinkat` in POSIX. + symlink-at: func( + /// The contents of the symbolic link. + old-path: string, + /// The relative destination path at which to create the symbolic link. + new-path: string, + ) -> result<_, error-code>; + + /// Unlink a filesystem object that is not a directory. + /// + /// Return `error-code::is-directory` if the path refers to a directory. + /// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + unlink-file-at: func( + /// The relative path to a file to unlink. + path: string, + ) -> result<_, error-code>; + + /// Test whether two descriptors refer to the same filesystem object. + /// + /// In POSIX, this corresponds to testing whether the two descriptors have the + /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers. + /// wasi-filesystem does not expose device and inode numbers, so this function + /// may be used instead. + is-same-object: func(other: borrow) -> bool; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a descriptor. + /// + /// This returns a hash of the last-modification timestamp and file size, and + /// may also include the inode number, device number, birth timestamp, and + /// other metadata fields that may change when the file is modified or + /// replaced. It may also include a secret value chosen by the + /// implementation and not otherwise exposed. + /// + /// Implementations are encourated to provide the following properties: + /// + /// - If the file is not modified or replaced, the computed hash value should + /// usually not change. + /// - If the object is modified or replaced, the computed hash value should + /// usually change. + /// - The inputs to the hash should not be easily computable from the + /// computed hash. + /// + /// However, none of these is required. + metadata-hash: func() -> result; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a directory descriptor and a relative path. + /// + /// This performs the same hash computation as `metadata-hash`. + metadata-hash-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + } + + /// A stream of directory entries. + resource directory-entry-stream { + /// Read a single directory entry from a `directory-entry-stream`. + read-directory-entry: func() -> result, error-code>; + } + + /// Attempts to extract a filesystem-related `error-code` from the stream + /// `error` provided. + /// + /// Stream operations which return `stream-error::last-operation-failed` + /// have a payload with more information about the operation that failed. + /// This payload can be passed through to this function to see if there's + /// filesystem-related information about the error to return. + /// + /// Note that this function is fallible because not all stream-related + /// errors are filesystem-related errors. + filesystem-error-code: func(err: borrow) -> option; +} diff --git a/lib/wit/deps/filesystem/world.wit b/lib/wit/deps/filesystem/world.wit new file mode 100644 index 00000000..663f5792 --- /dev/null +++ b/lib/wit/deps/filesystem/world.wit @@ -0,0 +1,6 @@ +package wasi:filesystem@0.2.0; + +world imports { + import types; + import preopens; +} diff --git a/lib/wit/deps/http/handler.wit b/lib/wit/deps/http/handler.wit new file mode 100644 index 00000000..a34a0649 --- /dev/null +++ b/lib/wit/deps/http/handler.wit @@ -0,0 +1,43 @@ +/// This interface defines a handler of incoming HTTP Requests. It should +/// be exported by components which can respond to HTTP Requests. +interface incoming-handler { + use types.{incoming-request, response-outparam}; + + /// This function is invoked with an incoming HTTP Request, and a resource + /// `response-outparam` which provides the capability to reply with an HTTP + /// Response. The response is sent by calling the `response-outparam.set` + /// method, which allows execution to continue after the response has been + /// sent. This enables both streaming to the response body, and performing other + /// work. + /// + /// The implementor of this function must write a response to the + /// `response-outparam` before returning, or else the caller will respond + /// with an error on its behalf. + handle: func( + request: incoming-request, + response-out: response-outparam + ); +} + +/// This interface defines a handler of outgoing HTTP Requests. It should be +/// imported by components which wish to make HTTP Requests. +interface outgoing-handler { + use types.{ + outgoing-request, request-options, future-incoming-response, error-code + }; + + /// This function is invoked with an outgoing HTTP Request, and it returns + /// a resource `future-incoming-response` which represents an HTTP Response + /// which may arrive in the future. + /// + /// The `options` argument accepts optional parameters for the HTTP + /// protocol's transport layer. + /// + /// This function may return an error if the `outgoing-request` is invalid + /// or not allowed to be made. Otherwise, protocol errors are reported + /// through the `future-incoming-response`. + handle: func( + request: outgoing-request, + options: option + ) -> result; +} diff --git a/lib/wit/deps/http/proxy.wit b/lib/wit/deps/http/proxy.wit new file mode 100644 index 00000000..687c24d2 --- /dev/null +++ b/lib/wit/deps/http/proxy.wit @@ -0,0 +1,32 @@ +package wasi:http@0.2.0; + +/// The `wasi:http/proxy` world captures a widely-implementable intersection of +/// hosts that includes HTTP forward and reverse proxies. Components targeting +/// this world may concurrently stream in and out any number of incoming and +/// outgoing HTTP requests. +world proxy { + /// HTTP proxies have access to time and randomness. + include wasi:clocks/imports@0.2.0; + import wasi:random/random@0.2.0; + + /// Proxies have standard output and error streams which are expected to + /// terminate in a developer-facing console provided by the host. + import wasi:cli/stdout@0.2.0; + import wasi:cli/stderr@0.2.0; + + /// TODO: this is a temporary workaround until component tooling is able to + /// gracefully handle the absence of stdin. Hosts must return an eof stream + /// for this import, which is what wasi-libc + tooling will do automatically + /// when this import is properly removed. + import wasi:cli/stdin@0.2.0; + + /// This is the default handler to use when user code simply wants to make an + /// HTTP request (e.g., via `fetch()`). + import outgoing-handler; + + /// The host delivers incoming HTTP requests to a component by calling the + /// `handle` function of this exported interface. A host may arbitrarily reuse + /// or not reuse component instance when delivering incoming HTTP requests and + /// thus a component must be able to handle 0..N calls to `handle`. + export incoming-handler; +} diff --git a/lib/wit/deps/http/types.wit b/lib/wit/deps/http/types.wit new file mode 100644 index 00000000..755ac6a6 --- /dev/null +++ b/lib/wit/deps/http/types.wit @@ -0,0 +1,570 @@ +/// This interface defines all of the types and methods for implementing +/// HTTP Requests and Responses, both incoming and outgoing, as well as +/// their headers, trailers, and bodies. +interface types { + use wasi:clocks/monotonic-clock@0.2.0.{duration}; + use wasi:io/streams@0.2.0.{input-stream, output-stream}; + use wasi:io/error@0.2.0.{error as io-error}; + use wasi:io/poll@0.2.0.{pollable}; + + /// This type corresponds to HTTP standard Methods. + variant method { + get, + head, + post, + put, + delete, + connect, + options, + trace, + patch, + other(string) + } + + /// This type corresponds to HTTP standard Related Schemes. + variant scheme { + HTTP, + HTTPS, + other(string) + } + + /// These cases are inspired by the IANA HTTP Proxy Error Types: + /// https://www.iana.org/assignments/http-proxy-status/http-proxy-status.xhtml#table-http-proxy-error-types + variant error-code { + DNS-timeout, + DNS-error(DNS-error-payload), + destination-not-found, + destination-unavailable, + destination-IP-prohibited, + destination-IP-unroutable, + connection-refused, + connection-terminated, + connection-timeout, + connection-read-timeout, + connection-write-timeout, + connection-limit-reached, + TLS-protocol-error, + TLS-certificate-error, + TLS-alert-received(TLS-alert-received-payload), + HTTP-request-denied, + HTTP-request-length-required, + HTTP-request-body-size(option), + HTTP-request-method-invalid, + HTTP-request-URI-invalid, + HTTP-request-URI-too-long, + HTTP-request-header-section-size(option), + HTTP-request-header-size(option), + HTTP-request-trailer-section-size(option), + HTTP-request-trailer-size(field-size-payload), + HTTP-response-incomplete, + HTTP-response-header-section-size(option), + HTTP-response-header-size(field-size-payload), + HTTP-response-body-size(option), + HTTP-response-trailer-section-size(option), + HTTP-response-trailer-size(field-size-payload), + HTTP-response-transfer-coding(option), + HTTP-response-content-coding(option), + HTTP-response-timeout, + HTTP-upgrade-failed, + HTTP-protocol-error, + loop-detected, + configuration-error, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. It also includes an optional string for an + /// unstructured description of the error. Users should not depend on the + /// string for diagnosing errors, as it's not required to be consistent + /// between implementations. + internal-error(option) + } + + /// Defines the case payload type for `DNS-error` above: + record DNS-error-payload { + rcode: option, + info-code: option + } + + /// Defines the case payload type for `TLS-alert-received` above: + record TLS-alert-received-payload { + alert-id: option, + alert-message: option + } + + /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: + record field-size-payload { + field-name: option, + field-size: option + } + + /// Attempts to extract a http-related `error` from the wasi:io `error` + /// provided. + /// + /// Stream operations which return + /// `wasi:io/stream/stream-error::last-operation-failed` have a payload of + /// type `wasi:io/error/error` with more information about the operation + /// that failed. This payload can be passed through to this function to see + /// if there's http-related information about the error to return. + /// + /// Note that this function is fallible because not all io-errors are + /// http-related errors. + http-error-code: func(err: borrow) -> option; + + /// This type enumerates the different kinds of errors that may occur when + /// setting or appending to a `fields` resource. + variant header-error { + /// This error indicates that a `field-key` or `field-value` was + /// syntactically invalid when used with an operation that sets headers in a + /// `fields`. + invalid-syntax, + + /// This error indicates that a forbidden `field-key` was used when trying + /// to set a header in a `fields`. + forbidden, + + /// This error indicates that the operation on the `fields` was not + /// permitted because the fields are immutable. + immutable, + } + + /// Field keys are always strings. + type field-key = string; + + /// Field values should always be ASCII strings. However, in + /// reality, HTTP implementations often have to interpret malformed values, + /// so they are provided as a list of bytes. + type field-value = list; + + /// This following block defines the `fields` resource which corresponds to + /// HTTP standard Fields. Fields are a common representation used for both + /// Headers and Trailers. + /// + /// A `fields` may be mutable or immutable. A `fields` created using the + /// constructor, `from-list`, or `clone` will be mutable, but a `fields` + /// resource given by other means (including, but not limited to, + /// `incoming-request.headers`, `outgoing-request.headers`) might be be + /// immutable. In an immutable fields, the `set`, `append`, and `delete` + /// operations will fail with `header-error.immutable`. + resource fields { + + /// Construct an empty HTTP Fields. + /// + /// The resulting `fields` is mutable. + constructor(); + + /// Construct an HTTP Fields. + /// + /// The resulting `fields` is mutable. + /// + /// The list represents each key-value pair in the Fields. Keys + /// which have multiple values are represented by multiple entries in this + /// list with the same key. + /// + /// The tuple is a pair of the field key, represented as a string, and + /// Value, represented as a list of bytes. In a valid Fields, all keys + /// and values are valid UTF-8 strings. However, values are not always + /// well-formed, so they are represented as a raw list of bytes. + /// + /// An error result will be returned if any header or value was + /// syntactically invalid, or if a header was forbidden. + from-list: static func( + entries: list> + ) -> result; + + /// Get all of the values corresponding to a key. If the key is not present + /// in this `fields`, an empty list is returned. However, if the key is + /// present but empty, this is represented by a list with one or more + /// empty field-values present. + get: func(name: field-key) -> list; + + /// Returns `true` when the key is present in this `fields`. If the key is + /// syntactically invalid, `false` is returned. + has: func(name: field-key) -> bool; + + /// Set all of the values for a key. Clears any existing values for that + /// key, if they have been set. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + set: func(name: field-key, value: list) -> result<_, header-error>; + + /// Delete all values for a key. Does nothing if no values for the key + /// exist. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + delete: func(name: field-key) -> result<_, header-error>; + + /// Append a value for a key. Does not change or delete any existing + /// values for that key. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + append: func(name: field-key, value: field-value) -> result<_, header-error>; + + /// Retrieve the full set of keys and values in the Fields. Like the + /// constructor, the list represents each key-value pair. + /// + /// The outer list represents each key-value pair in the Fields. Keys + /// which have multiple values are represented by multiple entries in this + /// list with the same key. + entries: func() -> list>; + + /// Make a deep copy of the Fields. Equivelant in behavior to calling the + /// `fields` constructor on the return value of `entries`. The resulting + /// `fields` is mutable. + clone: func() -> fields; + } + + /// Headers is an alias for Fields. + type headers = fields; + + /// Trailers is an alias for Fields. + type trailers = fields; + + /// Represents an incoming HTTP Request. + resource incoming-request { + + /// Returns the method of the incoming request. + method: func() -> method; + + /// Returns the path with query parameters from the request, as a string. + path-with-query: func() -> option; + + /// Returns the protocol scheme from the request. + scheme: func() -> option; + + /// Returns the authority from the request, if it was present. + authority: func() -> option; + + /// Get the `headers` associated with the request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// The `headers` returned are a child resource: it must be dropped before + /// the parent `incoming-request` is dropped. Dropping this + /// `incoming-request` before all children are dropped will trap. + headers: func() -> headers; + + /// Gives the `incoming-body` associated with this request. Will only + /// return success at most once, and subsequent calls will return error. + consume: func() -> result; + } + + /// Represents an outgoing HTTP Request. + resource outgoing-request { + + /// Construct a new `outgoing-request` with a default `method` of `GET`, and + /// `none` values for `path-with-query`, `scheme`, and `authority`. + /// + /// * `headers` is the HTTP Headers for the Request. + /// + /// It is possible to construct, or manipulate with the accessor functions + /// below, an `outgoing-request` with an invalid combination of `scheme` + /// and `authority`, or `headers` which are not permitted to be sent. + /// It is the obligation of the `outgoing-handler.handle` implementation + /// to reject invalid constructions of `outgoing-request`. + constructor( + headers: headers + ); + + /// Returns the resource corresponding to the outgoing Body for this + /// Request. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-request` can be retrieved at most once. Subsequent + /// calls will return error. + body: func() -> result; + + /// Get the Method for the Request. + method: func() -> method; + /// Set the Method for the Request. Fails if the string present in a + /// `method.other` argument is not a syntactically valid method. + set-method: func(method: method) -> result; + + /// Get the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. + path-with-query: func() -> option; + /// Set the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. Fails is the + /// string given is not a syntactically valid path and query uri component. + set-path-with-query: func(path-with-query: option) -> result; + + /// Get the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. + scheme: func() -> option; + /// Set the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. Fails if the + /// string given is not a syntactically valid uri scheme. + set-scheme: func(scheme: option) -> result; + + /// Get the HTTP Authority for the Request. A value of `none` may be used + /// with Related Schemes which do not require an Authority. The HTTP and + /// HTTPS schemes always require an authority. + authority: func() -> option; + /// Set the HTTP Authority for the Request. A value of `none` may be used + /// with Related Schemes which do not require an Authority. The HTTP and + /// HTTPS schemes always require an authority. Fails if the string given is + /// not a syntactically valid uri authority. + set-authority: func(authority: option) -> result; + + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transfered to + /// another component by e.g. `outgoing-handler.handle`. + headers: func() -> headers; + } + + /// Parameters for making an HTTP Request. Each of these parameters is + /// currently an optional timeout applicable to the transport layer of the + /// HTTP protocol. + /// + /// These timeouts are separate from any the user may use to bound a + /// blocking call to `wasi:io/poll.poll`. + resource request-options { + /// Construct a default `request-options` value. + constructor(); + + /// The timeout for the initial connect to the HTTP Server. + connect-timeout: func() -> option; + + /// Set the timeout for the initial connect to the HTTP Server. An error + /// return value indicates that this timeout is not supported. + set-connect-timeout: func(duration: option) -> result; + + /// The timeout for receiving the first byte of the Response body. + first-byte-timeout: func() -> option; + + /// Set the timeout for receiving the first byte of the Response body. An + /// error return value indicates that this timeout is not supported. + set-first-byte-timeout: func(duration: option) -> result; + + /// The timeout for receiving subsequent chunks of bytes in the Response + /// body stream. + between-bytes-timeout: func() -> option; + + /// Set the timeout for receiving subsequent chunks of bytes in the Response + /// body stream. An error return value indicates that this timeout is not + /// supported. + set-between-bytes-timeout: func(duration: option) -> result; + } + + /// Represents the ability to send an HTTP Response. + /// + /// This resource is used by the `wasi:http/incoming-handler` interface to + /// allow a Response to be sent corresponding to the Request provided as the + /// other argument to `incoming-handler.handle`. + resource response-outparam { + + /// Set the value of the `response-outparam` to either send a response, + /// or indicate an error. + /// + /// This method consumes the `response-outparam` to ensure that it is + /// called at most once. If it is never called, the implementation + /// will respond with an error. + /// + /// The user may provide an `error` to `response` to allow the + /// implementation determine how to respond with an HTTP error response. + set: static func( + param: response-outparam, + response: result, + ); + } + + /// This type corresponds to the HTTP standard Status Code. + type status-code = u16; + + /// Represents an incoming HTTP Response. + resource incoming-response { + + /// Returns the status code from the incoming response. + status: func() -> status-code; + + /// Returns the headers from the incoming response. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `incoming-response` is dropped. + headers: func() -> headers; + + /// Returns the incoming body. May be called at most once. Returns error + /// if called additional times. + consume: func() -> result; + } + + /// Represents an incoming HTTP Request or Response's Body. + /// + /// A body has both its contents - a stream of bytes - and a (possibly + /// empty) set of trailers, indicating that the full contents of the + /// body have been received. This resource represents the contents as + /// an `input-stream` and the delivery of trailers as a `future-trailers`, + /// and ensures that the user of this interface may only be consuming either + /// the body contents or waiting on trailers at any given time. + resource incoming-body { + + /// Returns the contents of the body, as a stream of bytes. + /// + /// Returns success on first call: the stream representing the contents + /// can be retrieved at most once. Subsequent calls will return error. + /// + /// The returned `input-stream` resource is a child: it must be dropped + /// before the parent `incoming-body` is dropped, or consumed by + /// `incoming-body.finish`. + /// + /// This invariant ensures that the implementation can determine whether + /// the user is consuming the contents of the body, waiting on the + /// `future-trailers` to be ready, or neither. This allows for network + /// backpressure is to be applied when the user is consuming the body, + /// and for that backpressure to not inhibit delivery of the trailers if + /// the user does not read the entire body. + %stream: func() -> result; + + /// Takes ownership of `incoming-body`, and returns a `future-trailers`. + /// This function will trap if the `input-stream` child is still alive. + finish: static func(this: incoming-body) -> future-trailers; + } + + /// Represents a future which may eventaully return trailers, or an error. + /// + /// In the case that the incoming HTTP Request or Response did not have any + /// trailers, this future will resolve to the empty set of trailers once the + /// complete Request or Response body has been received. + resource future-trailers { + + /// Returns a pollable which becomes ready when either the trailers have + /// been received, or an error has occured. When this pollable is ready, + /// the `get` method will return `some`. + subscribe: func() -> pollable; + + /// Returns the contents of the trailers, or an error which occured, + /// once the future is ready. + /// + /// The outer `option` represents future readiness. Users can wait on this + /// `option` to become `some` using the `subscribe` method. + /// + /// The outer `result` is used to retrieve the trailers or error at most + /// once. It will be success on the first call in which the outer option + /// is `some`, and error on subsequent calls. + /// + /// The inner `result` represents that either the HTTP Request or Response + /// body, as well as any trailers, were received successfully, or that an + /// error occured receiving them. The optional `trailers` indicates whether + /// or not trailers were present in the body. + /// + /// When some `trailers` are returned by this method, the `trailers` + /// resource is immutable, and a child. Use of the `set`, `append`, or + /// `delete` methods will return an error, and the resource must be + /// dropped before the parent `future-trailers` is dropped. + get: func() -> option, error-code>>>; + } + + /// Represents an outgoing HTTP Response. + resource outgoing-response { + + /// Construct an `outgoing-response`, with a default `status-code` of `200`. + /// If a different `status-code` is needed, it must be set via the + /// `set-status-code` method. + /// + /// * `headers` is the HTTP Headers for the Response. + constructor(headers: headers); + + /// Get the HTTP Status Code for the Response. + status-code: func() -> status-code; + + /// Set the HTTP Status Code for the Response. Fails if the status-code + /// given is not a valid http status code. + set-status-code: func(status-code: status-code) -> result; + + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transfered to + /// another component by e.g. `outgoing-handler.handle`. + headers: func() -> headers; + + /// Returns the resource corresponding to the outgoing Body for this Response. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-response` can be retrieved at most once. Subsequent + /// calls will return error. + body: func() -> result; + } + + /// Represents an outgoing HTTP Request or Response's Body. + /// + /// A body has both its contents - a stream of bytes - and a (possibly + /// empty) set of trailers, inducating the full contents of the body + /// have been sent. This resource represents the contents as an + /// `output-stream` child resource, and the completion of the body (with + /// optional trailers) with a static function that consumes the + /// `outgoing-body` resource, and ensures that the user of this interface + /// may not write to the body contents after the body has been finished. + /// + /// If the user code drops this resource, as opposed to calling the static + /// method `finish`, the implementation should treat the body as incomplete, + /// and that an error has occured. The implementation should propogate this + /// error to the HTTP protocol by whatever means it has available, + /// including: corrupting the body on the wire, aborting the associated + /// Request, or sending a late status code for the Response. + resource outgoing-body { + + /// Returns a stream for writing the body contents. + /// + /// The returned `output-stream` is a child resource: it must be dropped + /// before the parent `outgoing-body` resource is dropped (or finished), + /// otherwise the `outgoing-body` drop or `finish` will trap. + /// + /// Returns success on the first call: the `output-stream` resource for + /// this `outgoing-body` may be retrieved at most once. Subsequent calls + /// will return error. + write: func() -> result; + + /// Finalize an outgoing body, optionally providing trailers. This must be + /// called to signal that the response is complete. If the `outgoing-body` + /// is dropped without calling `outgoing-body.finalize`, the implementation + /// should treat the body as corrupted. + /// + /// Fails if the body's `outgoing-request` or `outgoing-response` was + /// constructed with a Content-Length header, and the contents written + /// to the body (via `write`) does not match the value given in the + /// Content-Length. + finish: static func( + this: outgoing-body, + trailers: option + ) -> result<_, error-code>; + } + + /// Represents a future which may eventaully return an incoming HTTP + /// Response, or an error. + /// + /// This resource is returned by the `wasi:http/outgoing-handler` interface to + /// provide the HTTP Response corresponding to the sent Request. + resource future-incoming-response { + /// Returns a pollable which becomes ready when either the Response has + /// been received, or an error has occured. When this pollable is ready, + /// the `get` method will return `some`. + subscribe: func() -> pollable; + + /// Returns the incoming HTTP Response, or an error, once one is ready. + /// + /// The outer `option` represents future readiness. Users can wait on this + /// `option` to become `some` using the `subscribe` method. + /// + /// The outer `result` is used to retrieve the response or error at most + /// once. It will be success on the first call in which the outer option + /// is `some`, and error on subsequent calls. + /// + /// The inner `result` represents that either the incoming HTTP Response + /// status and headers have recieved successfully, or that an error + /// occured. Errors may also occur while consuming the response body, + /// but those will be reported by the `incoming-body` and its + /// `output-stream` child. + get: func() -> option>>; + + } +} diff --git a/lib/wit/deps/io/error.wit b/lib/wit/deps/io/error.wit new file mode 100644 index 00000000..22e5b648 --- /dev/null +++ b/lib/wit/deps/io/error.wit @@ -0,0 +1,34 @@ +package wasi:io@0.2.0; + + +interface error { + /// A resource which represents some error information. + /// + /// The only method provided by this resource is `to-debug-string`, + /// which provides some human-readable information about the error. + /// + /// In the `wasi:io` package, this resource is returned through the + /// `wasi:io/streams/stream-error` type. + /// + /// To provide more specific error information, other interfaces may + /// provide functions to further "downcast" this error into more specific + /// error information. For example, `error`s returned in streams derived + /// from filesystem types to be described using the filesystem's own + /// error-code type, using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a parameter + /// `borrow` and returns + /// `option`. + /// + /// The set of functions which can "downcast" an `error` into a more + /// concrete type is open. + resource error { + /// Returns a string that is suitable to assist humans in debugging + /// this error. + /// + /// WARNING: The returned string should not be consumed mechanically! + /// It may change across platforms, hosts, or other implementation + /// details. Parsing this string is a major platform-compatibility + /// hazard. + to-debug-string: func() -> string; + } +} diff --git a/lib/wit/deps/io/poll.wit b/lib/wit/deps/io/poll.wit new file mode 100644 index 00000000..ddc67f8b --- /dev/null +++ b/lib/wit/deps/io/poll.wit @@ -0,0 +1,41 @@ +package wasi:io@0.2.0; + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +interface poll { + /// `pollable` represents a single I/O event which may be ready, or not. + resource pollable { + + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + ready: func() -> bool; + + /// `block` returns immediately if the pollable is ready, and otherwise + /// blocks until ready. + /// + /// This function is equivalent to calling `poll.poll` on a list + /// containing only this pollable. + block: func(); + } + + /// Poll for completion on a set of pollables. + /// + /// This function takes a list of pollables, which identify I/O sources of + /// interest, and waits until one or more of the events is ready for I/O. + /// + /// The result `list` contains one or more indices of handles in the + /// argument list that is ready for I/O. + /// + /// If the list contains more elements than can be indexed with a `u32` + /// value, this function traps. + /// + /// A timeout can be implemented by adding a pollable from the + /// wasi-clocks API to the list. + /// + /// This function does not return a `result`; polling in itself does not + /// do any I/O so it doesn't fail. If any of the I/O sources identified by + /// the pollables has an error, it is indicated by marking the source as + /// being reaedy for I/O. + poll: func(in: list>) -> list; +} diff --git a/lib/wit/deps/io/streams.wit b/lib/wit/deps/io/streams.wit new file mode 100644 index 00000000..6d2f871e --- /dev/null +++ b/lib/wit/deps/io/streams.wit @@ -0,0 +1,262 @@ +package wasi:io@0.2.0; + +/// WASI I/O is an I/O abstraction API which is currently focused on providing +/// stream types. +/// +/// In the future, the component model is expected to add built-in stream types; +/// when it does, they are expected to subsume this API. +interface streams { + use error.{error}; + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + last-operation-failed(error), + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed + } + + /// An input bytestream. + /// + /// `input-stream`s are *non-blocking* to the extent practical on underlying + /// platforms. I/O operations always return promptly; if fewer bytes are + /// promptly available than requested, they return the number of bytes promptly + /// available, which could even be zero. To wait for data to be available, + /// use the `subscribe` function to obtain a `pollable` which can be polled + /// for using `wasi:io/poll`. + resource input-stream { + /// Perform a non-blocking read from the stream. + /// + /// When the source of a `read` is binary data, the bytes from the source + /// are returned verbatim. When the source of a `read` is known to the + /// implementation to be text, bytes containing the UTF-8 encoding of the + /// text are returned. + /// + /// This function returns a list of bytes containing the read data, + /// when successful. The returned list will contain up to `len` bytes; + /// it may return fewer than requested, but not more. The list is + /// empty when no bytes are available for reading at this time. The + /// pollable given by `subscribe` will be ready when more bytes are + /// available. + /// + /// This function fails with a `stream-error` when the operation + /// encounters an error, giving `last-operation-failed`, or when the + /// stream is closed, giving `closed`. + /// + /// When the caller gives a `len` of 0, it represents a request to + /// read 0 bytes. If the stream is still open, this call should + /// succeed and return an empty list, or otherwise fail with `closed`. + /// + /// The `len` parameter is a `u64`, which could represent a list of u8 which + /// is not possible to allocate in wasm32, or not desirable to allocate as + /// as a return value by the callee. The callee may return a list of bytes + /// less than `len` in size while more bytes are available for reading. + read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, behavior is identical to `read`. + blocking-read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Skip bytes from a stream. Returns number of bytes skipped. + /// + /// Behaves identical to `read`, except instead of returning a list + /// of bytes, returns the number of bytes consumed from the stream. + skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + blocking-skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Create a `pollable` which will resolve once either the specified stream + /// has bytes available to read or the other end of the stream has been + /// closed. + /// The created `pollable` is a child resource of the `input-stream`. + /// Implementations may trap if the `input-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + subscribe: func() -> pollable; + } + + + /// An output bytestream. + /// + /// `output-stream`s are *non-blocking* to the extent practical on + /// underlying platforms. Except where specified otherwise, I/O operations also + /// always return promptly, after the number of bytes that can be written + /// promptly, which could even be zero. To wait for the stream to be ready to + /// accept data, the `subscribe` function to obtain a `pollable` which can be + /// polled for using `wasi:io/poll`. + resource output-stream { + /// Check readiness for writing. This function never blocks. + /// + /// Returns the number of bytes permitted for the next call to `write`, + /// or an error. Calling `write` with more bytes than this function has + /// permitted will trap. + /// + /// When this function returns 0 bytes, the `subscribe` pollable will + /// become ready when this function will report at least 1 byte, or an + /// error. + check-write: func() -> result; + + /// Perform a write. This function never blocks. + /// + /// When the destination of a `write` is binary data, the bytes from + /// `contents` are written verbatim. When the destination of a `write` is + /// known to the implementation to be text, the bytes of `contents` are + /// transcoded from UTF-8 into the encoding of the destination and then + /// written. + /// + /// Precondition: check-write gave permit of Ok(n) and contents has a + /// length of less than or equal to n. Otherwise, this function will trap. + /// + /// returns Err(closed) without writing if the stream has closed since + /// the last call to check-write provided a permit. + write: func( + contents: list + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write`, and `flush`, and is implemented with the + /// following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while !contents.is_empty() { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, contents.len()); + /// let (chunk, rest) = contents.split_at(len); + /// this.write(chunk ); // eliding error handling + /// contents = rest; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + blocking-write-and-flush: func( + contents: list + ) -> result<_, stream-error>; + + /// Request to flush buffered output. This function never blocks. + /// + /// This tells the output-stream that the caller intends any buffered + /// output to be flushed. the output which is expected to be flushed + /// is all that has been passed to `write` prior to this call. + /// + /// Upon calling this function, the `output-stream` will not accept any + /// writes (`check-write` will return `ok(0)`) until the flush has + /// completed. The `subscribe` pollable will become ready when the + /// flush has completed and the stream can accept more writes. + flush: func() -> result<_, stream-error>; + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + blocking-flush: func() -> result<_, stream-error>; + + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occured. When this + /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an + /// error. + /// + /// If the stream is closed, this pollable is always ready immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + subscribe: func() -> pollable; + + /// Write zeroes to a stream. + /// + /// This should be used precisely like `write` with the exact same + /// preconditions (must use check-write first), but instead of + /// passing a list of bytes, you simply pass the number of zero-bytes + /// that should be written. + write-zeroes: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 zeroes, and then flush the stream. + /// Block until all of these operations are complete, or an error + /// occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with + /// the following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while num_zeroes != 0 { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, num_zeroes); + /// this.write-zeroes(len); // eliding error handling + /// num_zeroes -= len; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + blocking-write-zeroes-and-flush: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Read from one stream and write to another. + /// + /// The behavior of splice is equivelant to: + /// 1. calling `check-write` on the `output-stream` + /// 2. calling `read` on the `input-stream` with the smaller of the + /// `check-write` permitted length and the `len` provided to `splice` + /// 3. calling `write` on the `output-stream` with that read data. + /// + /// Any error reported by the call to `check-write`, `read`, or + /// `write` ends the splice and reports that error. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until the + /// `output-stream` is ready for writing, and the `input-stream` + /// is ready for reading, before performing the `splice`. + blocking-splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + } +} diff --git a/lib/wit/deps/io/world.wit b/lib/wit/deps/io/world.wit new file mode 100644 index 00000000..5f0b43fe --- /dev/null +++ b/lib/wit/deps/io/world.wit @@ -0,0 +1,6 @@ +package wasi:io@0.2.0; + +world imports { + import streams; + import poll; +} diff --git a/lib/wit/deps/random/insecure-seed.wit b/lib/wit/deps/random/insecure-seed.wit new file mode 100644 index 00000000..47210ac6 --- /dev/null +++ b/lib/wit/deps/random/insecure-seed.wit @@ -0,0 +1,25 @@ +package wasi:random@0.2.0; +/// The insecure-seed interface for seeding hash-map DoS resistance. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +interface insecure-seed { + /// Return a 128-bit value that may contain a pseudo-random value. + /// + /// The returned value is not required to be computed from a CSPRNG, and may + /// even be entirely deterministic. Host implementations are encouraged to + /// provide pseudo-random values to any program exposed to + /// attacker-controlled content, to enable DoS protection built into many + /// languages' hash-map implementations. + /// + /// This function is intended to only be called once, by a source language + /// to initialize Denial Of Service (DoS) protection in its hash-map + /// implementation. + /// + /// # Expected future evolution + /// + /// This will likely be changed to a value import, to prevent it from being + /// called multiple times and potentially used for purposes other than DoS + /// protection. + insecure-seed: func() -> tuple; +} diff --git a/lib/wit/deps/random/insecure.wit b/lib/wit/deps/random/insecure.wit new file mode 100644 index 00000000..c58f4ee8 --- /dev/null +++ b/lib/wit/deps/random/insecure.wit @@ -0,0 +1,22 @@ +package wasi:random@0.2.0; +/// The insecure interface for insecure pseudo-random numbers. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +interface insecure { + /// Return `len` insecure pseudo-random bytes. + /// + /// This function is not cryptographically secure. Do not use it for + /// anything related to security. + /// + /// There are no requirements on the values of the returned bytes, however + /// implementations are encouraged to return evenly distributed values with + /// a long period. + get-insecure-random-bytes: func(len: u64) -> list; + + /// Return an insecure pseudo-random `u64` value. + /// + /// This function returns the same type of pseudo-random data as + /// `get-insecure-random-bytes`, represented as a `u64`. + get-insecure-random-u64: func() -> u64; +} diff --git a/lib/wit/deps/random/random.wit b/lib/wit/deps/random/random.wit new file mode 100644 index 00000000..0c017f09 --- /dev/null +++ b/lib/wit/deps/random/random.wit @@ -0,0 +1,26 @@ +package wasi:random@0.2.0; +/// WASI Random is a random data API. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +interface random { + /// Return `len` cryptographically-secure random or pseudo-random bytes. + /// + /// This function must produce data at least as cryptographically secure and + /// fast as an adequately seeded cryptographically-secure pseudo-random + /// number generator (CSPRNG). It must not block, from the perspective of + /// the calling program, under any circumstances, including on the first + /// request and on requests for numbers of bytes. The returned data must + /// always be unpredictable. + /// + /// This function must always return fresh data. Deterministic environments + /// must omit this function, rather than implementing it with deterministic + /// data. + get-random-bytes: func(len: u64) -> list; + + /// Return a cryptographically-secure random or pseudo-random `u64` value. + /// + /// This function returns the same type of data as `get-random-bytes`, + /// represented as a `u64`. + get-random-u64: func() -> u64; +} diff --git a/lib/wit/deps/random/world.wit b/lib/wit/deps/random/world.wit new file mode 100644 index 00000000..3da34914 --- /dev/null +++ b/lib/wit/deps/random/world.wit @@ -0,0 +1,7 @@ +package wasi:random@0.2.0; + +world imports { + import random; + import insecure; + import insecure-seed; +} diff --git a/lib/wit/deps/sockets/instance-network.wit b/lib/wit/deps/sockets/instance-network.wit new file mode 100644 index 00000000..e455d0ff --- /dev/null +++ b/lib/wit/deps/sockets/instance-network.wit @@ -0,0 +1,9 @@ + +/// This interface provides a value-export of the default network handle.. +interface instance-network { + use network.{network}; + + /// Get a handle to the default network. + instance-network: func() -> network; + +} diff --git a/lib/wit/deps/sockets/ip-name-lookup.wit b/lib/wit/deps/sockets/ip-name-lookup.wit new file mode 100644 index 00000000..8e639ec5 --- /dev/null +++ b/lib/wit/deps/sockets/ip-name-lookup.wit @@ -0,0 +1,51 @@ + +interface ip-name-lookup { + use wasi:io/poll@0.2.0.{pollable}; + use network.{network, error-code, ip-address}; + + + /// Resolve an internet host name to a list of IP addresses. + /// + /// Unicode domain names are automatically converted to ASCII using IDNA encoding. + /// If the input is an IP address string, the address is parsed and returned + /// as-is without making any external requests. + /// + /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. + /// + /// This function never blocks. It either immediately fails or immediately + /// returns successfully with a `resolve-address-stream` that can be used + /// to (asynchronously) fetch the results. + /// + /// # Typical errors + /// - `invalid-argument`: `name` is a syntactically invalid domain name or IP address. + /// + /// # References: + /// - + /// - + /// - + /// - + resolve-addresses: func(network: borrow, name: string) -> result; + + resource resolve-address-stream { + /// Returns the next address from the resolver. + /// + /// This function should be called multiple times. On each call, it will + /// return the next address in connection order preference. If all + /// addresses have been exhausted, this function returns `none`. + /// + /// This function never returns IPv4-mapped IPv6 addresses. + /// + /// # Typical errors + /// - `name-unresolvable`: Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY) + /// - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN) + /// - `permanent-resolver-failure`: A permanent failure in name resolution occurred. (EAI_FAIL) + /// - `would-block`: A result is not available yet. (EWOULDBLOCK, EAGAIN) + resolve-next-address: func() -> result, error-code>; + + /// Create a `pollable` which will resolve once the stream is ready for I/O. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func() -> pollable; + } +} diff --git a/lib/wit/deps/sockets/network.wit b/lib/wit/deps/sockets/network.wit new file mode 100644 index 00000000..9cadf065 --- /dev/null +++ b/lib/wit/deps/sockets/network.wit @@ -0,0 +1,145 @@ + +interface network { + /// An opaque resource that represents access to (a subset of) the network. + /// This enables context-based security for networking. + /// There is no need for this to map 1:1 to a physical network interface. + resource network; + + /// Error codes. + /// + /// In theory, every API can return any error code. + /// In practice, API's typically only return the errors documented per API + /// combined with a couple of errors that are always possible: + /// - `unknown` + /// - `access-denied` + /// - `not-supported` + /// - `out-of-memory` + /// - `concurrency-conflict` + /// + /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. + enum error-code { + /// Unknown error + unknown, + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + + /// The operation is not supported. + /// + /// POSIX equivalent: EOPNOTSUPP + not-supported, + + /// One of the arguments is invalid. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + + /// Not enough memory to complete the operation. + /// + /// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY + out-of-memory, + + /// The operation timed out before it could finish completely. + timeout, + + /// This operation is incompatible with another asynchronous operation that is already in progress. + /// + /// POSIX equivalent: EALREADY + concurrency-conflict, + + /// Trying to finish an asynchronous operation that: + /// - has not been started yet, or: + /// - was already finished by a previous `finish-*` call. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + not-in-progress, + + /// The operation has been aborted because it could not be completed immediately. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + would-block, + + + /// The operation is not valid in the socket's current state. + invalid-state, + + /// A new socket resource could not be created because of a system limit. + new-socket-limit, + + /// A bind operation failed because the provided address is not an address that the `network` can bind to. + address-not-bindable, + + /// A bind operation failed because the provided address is already in use or because there are no ephemeral ports available. + address-in-use, + + /// The remote address is not reachable + remote-unreachable, + + + /// The TCP connection was forcefully rejected + connection-refused, + + /// The TCP connection was reset. + connection-reset, + + /// A TCP connection was aborted. + connection-aborted, + + + /// The size of a datagram sent to a UDP socket exceeded the maximum + /// supported size. + datagram-too-large, + + + /// Name does not exist or has no suitable associated IP addresses. + name-unresolvable, + + /// A temporary failure in name resolution occurred. + temporary-resolver-failure, + + /// A permanent failure in name resolution occurred. + permanent-resolver-failure, + } + + enum ip-address-family { + /// Similar to `AF_INET` in POSIX. + ipv4, + + /// Similar to `AF_INET6` in POSIX. + ipv6, + } + + type ipv4-address = tuple; + type ipv6-address = tuple; + + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + record ipv4-socket-address { + /// sin_port + port: u16, + /// sin_addr + address: ipv4-address, + } + + record ipv6-socket-address { + /// sin6_port + port: u16, + /// sin6_flowinfo + flow-info: u32, + /// sin6_addr + address: ipv6-address, + /// sin6_scope_id + scope-id: u32, + } + + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } + +} diff --git a/lib/wit/deps/sockets/tcp-create-socket.wit b/lib/wit/deps/sockets/tcp-create-socket.wit new file mode 100644 index 00000000..c7ddf1f2 --- /dev/null +++ b/lib/wit/deps/sockets/tcp-create-socket.wit @@ -0,0 +1,27 @@ + +interface tcp-create-socket { + use network.{network, error-code, ip-address-family}; + use tcp.{tcp-socket}; + + /// Create a new TCP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`connect` + /// is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References + /// - + /// - + /// - + /// - + create-tcp-socket: func(address-family: ip-address-family) -> result; +} diff --git a/lib/wit/deps/sockets/tcp.wit b/lib/wit/deps/sockets/tcp.wit new file mode 100644 index 00000000..5902b9ee --- /dev/null +++ b/lib/wit/deps/sockets/tcp.wit @@ -0,0 +1,353 @@ + +interface tcp { + use wasi:io/streams@0.2.0.{input-stream, output-stream}; + use wasi:io/poll@0.2.0.{pollable}; + use wasi:clocks/monotonic-clock@0.2.0.{duration}; + use network.{network, error-code, ip-socket-address, ip-address-family}; + + enum shutdown-type { + /// Similar to `SHUT_RD` in POSIX. + receive, + + /// Similar to `SHUT_WR` in POSIX. + send, + + /// Similar to `SHUT_RDWR` in POSIX. + both, + } + + /// A TCP socket resource. + /// + /// The socket can be in one of the following states: + /// - `unbound` + /// - `bind-in-progress` + /// - `bound` (See note below) + /// - `listen-in-progress` + /// - `listening` + /// - `connect-in-progress` + /// - `connected` + /// - `closed` + /// See + /// for a more information. + /// + /// Note: Except where explicitly mentioned, whenever this documentation uses + /// the term "bound" without backticks it actually means: in the `bound` state *or higher*. + /// (i.e. `bound`, `listen-in-progress`, `listening`, `connect-in-progress` or `connected`) + /// + /// In addition to the general error codes documented on the + /// `network::error-code` type, TCP socket methods may always return + /// `error(invalid-state)` when in the `closed` state. + resource tcp-socket { + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the TCP/UDP port is zero, the socket will be bound to a random free port. + /// + /// Bind can be attempted multiple times on the same socket, even with + /// different arguments on each iteration. But never concurrently and + /// only as long as the previous bind failed. Once a bind succeeds, the + /// binding can't be changed anymore. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) + /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT + /// state of a recently closed socket on the same local address. In practice this means that the SO_REUSEADDR + /// socket option should be set implicitly on all platforms, except on Windows where this is the default behavior + /// and SO_REUSEADDR performs something different entirely. + /// + /// Unlike in POSIX, in WASI the bind operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `bind` as part of either `start-bind` or `finish-bind`. + /// + /// # References + /// - + /// - + /// - + /// - + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + finish-bind: func() -> result<_, error-code>; + + /// Connect to a remote endpoint. + /// + /// On success: + /// - the socket is transitioned into the `connection` state. + /// - a pair of streams is returned that can be used to read & write to the connection + /// + /// After a failed connection attempt, the socket will be in the `closed` + /// state and the only valid action left is to `drop` the socket. A single + /// socket can not be used to connect more than once. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) + /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN) + /// - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows) + /// - `timeout`: Connection timed out. (ETIMEDOUT) + /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `not-in-progress`: A connect operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// The POSIX equivalent of `start-connect` is the regular `connect` syscall. + /// Because all WASI sockets are non-blocking this is expected to return + /// EINPROGRESS, which should be translated to `ok()` in WASI. + /// + /// The POSIX equivalent of `finish-connect` is a `poll` for event `POLLOUT` + /// with a timeout of 0 on the socket descriptor. Followed by a check for + /// the `SO_ERROR` socket option, in case the poll signaled readiness. + /// + /// # References + /// - + /// - + /// - + /// - + start-connect: func(network: borrow, remote-address: ip-socket-address) -> result<_, error-code>; + finish-connect: func() -> result, error-code>; + + /// Start listening for new connections. + /// + /// Transitions the socket into the `listening` state. + /// + /// Unlike POSIX, the socket must already be explicitly bound. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. (EDESTADDRREQ) + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD) + /// - `invalid-state`: The socket is already in the `listening` state. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// - `not-in-progress`: A listen operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// Unlike in POSIX, in WASI the listen operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `listen` as part of either `start-listen` or `finish-listen`. + /// + /// # References + /// - + /// - + /// - + /// - + start-listen: func() -> result<_, error-code>; + finish-listen: func() -> result<_, error-code>; + + /// Accept a new client socket. + /// + /// The returned socket is bound and in the `connected` state. The following properties are inherited from the listener socket: + /// - `address-family` + /// - `keep-alive-enabled` + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// - `hop-limit` + /// - `receive-buffer-size` + /// - `send-buffer-size` + /// + /// On success, this function returns the newly accepted client socket along with + /// a pair of streams that can be used to read & write to the connection. + /// + /// # Typical errors + /// - `invalid-state`: Socket is not in the `listening` state. (EINVAL) + /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) + /// - `connection-aborted`: An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References + /// - + /// - + /// - + /// - + accept: func() -> result, error-code>; + + /// Get the bound local address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + local-address: func() -> result; + + /// Get the remote address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + remote-address: func() -> result; + + /// Whether the socket is in the `listening` state. + /// + /// Equivalent to the SO_ACCEPTCONN socket option. + is-listening: func() -> bool; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + address-family: func() -> ip-address-family; + + /// Hints the desired listen queue size. Implementations are free to ignore this. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// + /// # Typical errors + /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. + /// - `invalid-argument`: (set) The provided value was 0. + /// - `invalid-state`: (set) The socket is in the `connect-in-progress` or `connected` state. + set-listen-backlog-size: func(value: u64) -> result<_, error-code>; + + /// Enables or disables keepalive. + /// + /// The keepalive behavior can be adjusted using: + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true. + /// + /// Equivalent to the SO_KEEPALIVE socket option. + keep-alive-enabled: func() -> result; + set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; + + /// Amount of time the connection has to be idle before TCP starts sending keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS) + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + keep-alive-idle-time: func() -> result; + set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; + + /// The time between keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPINTVL socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + keep-alive-interval: func() -> result; + set-keep-alive-interval: func(value: duration) -> result<_, error-code>; + + /// The maximum amount of keepalive packets TCP should send before aborting the connection. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPCNT socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + keep-alive-count: func() -> result; + set-keep-alive-count: func(value: u32) -> result<_, error-code>; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + hop-limit: func() -> result; + set-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + receive-buffer-size: func() -> result; + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + send-buffer-size: func() -> result; + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + + /// Create a `pollable` which can be used to poll for, or block on, + /// completion of any of the asynchronous operations of this socket. + /// + /// When `finish-bind`, `finish-listen`, `finish-connect` or `accept` + /// return `error(would-block)`, this pollable can be used to wait for + /// their success or failure, after which the method can be retried. + /// + /// The pollable is not limited to the async operation that happens to be + /// in progress at the time of calling `subscribe` (if any). Theoretically, + /// `subscribe` only has to be called once per socket and can then be + /// (re)used for the remainder of the socket's lifetime. + /// + /// See + /// for a more information. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func() -> pollable; + + /// Initiate a graceful shutdown. + /// + /// - `receive`: The socket is not expecting to receive any data from + /// the peer. The `input-stream` associated with this socket will be + /// closed. Any data still in the receive queue at time of calling + /// this method will be discarded. + /// - `send`: The socket has no more data to send to the peer. The `output-stream` + /// associated with this socket will be closed and a FIN packet will be sent. + /// - `both`: Same effect as `receive` & `send` combined. + /// + /// This function is idempotent. Shutting a down a direction more than once + /// has no effect and returns `ok`. + /// + /// The shutdown function does not close (drop) the socket. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + shutdown: func(shutdown-type: shutdown-type) -> result<_, error-code>; + } +} diff --git a/lib/wit/deps/sockets/udp-create-socket.wit b/lib/wit/deps/sockets/udp-create-socket.wit new file mode 100644 index 00000000..0482d1fe --- /dev/null +++ b/lib/wit/deps/sockets/udp-create-socket.wit @@ -0,0 +1,27 @@ + +interface udp-create-socket { + use network.{network, error-code, ip-address-family}; + use udp.{udp-socket}; + + /// Create a new UDP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind` is called, + /// the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References: + /// - + /// - + /// - + /// - + create-udp-socket: func(address-family: ip-address-family) -> result; +} diff --git a/lib/wit/deps/sockets/udp.wit b/lib/wit/deps/sockets/udp.wit new file mode 100644 index 00000000..d987a0a9 --- /dev/null +++ b/lib/wit/deps/sockets/udp.wit @@ -0,0 +1,266 @@ + +interface udp { + use wasi:io/poll@0.2.0.{pollable}; + use network.{network, error-code, ip-socket-address, ip-address-family}; + + /// A received datagram. + record incoming-datagram { + /// The payload. + /// + /// Theoretical max size: ~64 KiB. In practice, typically less than 1500 bytes. + data: list, + + /// The source address. + /// + /// This field is guaranteed to match the remote address the stream was initialized with, if any. + /// + /// Equivalent to the `src_addr` out parameter of `recvfrom`. + remote-address: ip-socket-address, + } + + /// A datagram to be sent out. + record outgoing-datagram { + /// The payload. + data: list, + + /// The destination address. + /// + /// The requirements on this field depend on how the stream was initialized: + /// - with a remote address: this field must be None or match the stream's remote address exactly. + /// - without a remote address: this field is required. + /// + /// If this value is None, the send operation is equivalent to `send` in POSIX. Otherwise it is equivalent to `sendto`. + remote-address: option, + } + + + + /// A UDP socket handle. + resource udp-socket { + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the port is zero, the socket will be bound to a random free port. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// Unlike in POSIX, in WASI the bind operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `bind` as part of either `start-bind` or `finish-bind`. + /// + /// # References + /// - + /// - + /// - + /// - + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + finish-bind: func() -> result<_, error-code>; + + /// Set up inbound & outbound communication channels, optionally to a specific peer. + /// + /// This function only changes the local socket configuration and does not generate any network traffic. + /// On success, the `remote-address` of the socket is updated. The `local-address` may be updated as well, + /// based on the best network path to `remote-address`. + /// + /// When a `remote-address` is provided, the returned streams are limited to communicating with that specific peer: + /// - `send` can only be used to send to this destination. + /// - `receive` will only return datagrams sent from the provided `remote-address`. + /// + /// This method may be called multiple times on the same socket to change its association, but + /// only the most recently returned pair of streams will be operational. Implementations may trap if + /// the streams returned by a previous invocation haven't been dropped yet before calling `stream` again. + /// + /// The POSIX equivalent in pseudo-code is: + /// ```text + /// if (was previously connected) { + /// connect(s, AF_UNSPEC) + /// } + /// if (remote_address is Some) { + /// connect(s, remote_address) + /// } + /// ``` + /// + /// Unlike in POSIX, the socket must already be explicitly bound. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-state`: The socket is not bound. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + %stream: func(remote-address: option) -> result, error-code>; + + /// Get the current bound address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + local-address: func() -> result; + + /// Get the address the socket is currently streaming to. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not streaming to a specific remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + remote-address: func() -> result; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + address-family: func() -> ip-address-family; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + unicast-hop-limit: func() -> result; + set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + receive-buffer-size: func() -> result; + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + send-buffer-size: func() -> result; + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + + /// Create a `pollable` which will resolve once the socket is ready for I/O. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func() -> pollable; + } + + resource incoming-datagram-stream { + /// Receive messages on the socket. + /// + /// This function attempts to receive up to `max-results` datagrams on the socket without blocking. + /// The returned list may contain fewer elements than requested, but never more. + /// + /// This function returns successfully with an empty list when either: + /// - `max-results` is 0, or: + /// - `max-results` is greater than 0, but no results are immediately available. + /// This function never returns `error(would-block)`. + /// + /// # Typical errors + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + receive: func(max-results: u64) -> result, error-code>; + + /// Create a `pollable` which will resolve once the stream is ready to receive again. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func() -> pollable; + } + + resource outgoing-datagram-stream { + /// Check readiness for sending. This function never blocks. + /// + /// Returns the number of datagrams permitted for the next call to `send`, + /// or an error. Calling `send` with more datagrams than this function has + /// permitted will trap. + /// + /// When this function returns ok(0), the `subscribe` pollable will + /// become ready when this function will report at least ok(1), or an + /// error. + /// + /// Never returns `would-block`. + check-send: func() -> result; + + /// Send messages on the socket. + /// + /// This function attempts to send all provided `datagrams` on the socket without blocking and + /// returns how many messages were actually sent (or queued for sending). This function never + /// returns `error(would-block)`. If none of the datagrams were able to be sent, `ok(0)` is returned. + /// + /// This function semantically behaves the same as iterating the `datagrams` list and sequentially + /// sending each individual datagram until either the end of the list has been reached or the first error occurred. + /// If at least one datagram has been sent successfully, this function never returns an error. + /// + /// If the input list is empty, the function returns `ok(0)`. + /// + /// Each call to `send` must be permitted by a preceding `check-send`. Implementations must trap if + /// either `check-send` was not called or `datagrams` contains more items than `check-send` permitted. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `stream`. (EISCONN) + /// - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + send: func(datagrams: list) -> result; + + /// Create a `pollable` which will resolve once the stream is ready to send again. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func() -> pollable; + } +} diff --git a/lib/wit/deps/sockets/world.wit b/lib/wit/deps/sockets/world.wit new file mode 100644 index 00000000..f8bb92ae --- /dev/null +++ b/lib/wit/deps/sockets/world.wit @@ -0,0 +1,11 @@ +package wasi:sockets@0.2.0; + +world imports { + import instance-network; + import network; + import udp; + import udp-create-socket; + import tcp; + import tcp-create-socket; + import ip-name-lookup; +} diff --git a/lib/wit/viceroy.wit b/lib/wit/viceroy.wit new file mode 100644 index 00000000..646dd328 --- /dev/null +++ b/lib/wit/viceroy.wit @@ -0,0 +1,5 @@ +package fastly:viceroy; + +world compute { + include fastly:api/compute; +} \ No newline at end of file diff --git a/test-fixtures/Cargo.lock b/test-fixtures/Cargo.lock index 70c2d590..8aa058bc 100644 --- a/test-fixtures/Cargo.lock +++ b/test-fixtures/Cargo.lock @@ -230,18 +230,18 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", @@ -250,9 +250,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu",