diff --git a/Cargo.lock b/Cargo.lock index d4f089eca..014ded2a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -517,7 +517,7 @@ dependencies = [ "aws-smithy-json", "aws-smithy-types", "aws-types", - "bytes 1.1.0", + "bytes 1.2.1", "hex 0.4.3", "http 0.2.8", "hyper", @@ -551,7 +551,7 @@ dependencies = [ "aws-smithy-http", "aws-smithy-types", "aws-types", - "bytes 1.1.0", + "bytes 1.2.1", "http 0.2.8", "http-body", "lazy_static", @@ -577,7 +577,7 @@ dependencies = [ "aws-smithy-types", "aws-smithy-xml", "aws-types", - "bytes 1.1.0", + "bytes 1.2.1", "http 0.2.8", "tokio-stream", "tower", @@ -599,7 +599,7 @@ dependencies = [ "aws-smithy-json", "aws-smithy-types", "aws-types", - "bytes 1.1.0", + "bytes 1.2.1", "http 0.2.8", "tokio-stream", "tower", @@ -622,7 +622,7 @@ dependencies = [ "aws-smithy-types", "aws-smithy-xml", "aws-types", - "bytes 1.1.0", + "bytes 1.2.1", "http 0.2.8", "tower", ] @@ -680,7 +680,7 @@ dependencies = [ "aws-smithy-http", "aws-smithy-http-tower", "aws-smithy-types", - "bytes 1.1.0", + "bytes 1.2.1", "fastrand", "http 0.2.8", "http-body", @@ -700,7 +700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cc1af50eac644ab6f58e5bae29328ba3092851fc2ce648ad139134699b2b66f" dependencies = [ "aws-smithy-types", - "bytes 1.1.0", + "bytes 1.2.1", "bytes-utils", "futures-core", "http 0.2.8", @@ -721,7 +721,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1bf4c4664dff2febf91f8796505c5bc8f38a0bff0d1397d1d3fdda17bd5c5d1" dependencies = [ "aws-smithy-http", - "bytes 1.1.0", + "bytes 1.2.1", "http 0.2.8", "http-body", "pin-project-lite 0.2.9", @@ -795,7 +795,7 @@ dependencies = [ "axum-core", "base64 0.13.0", "bitflags", - "bytes 1.1.0", + "bytes 1.2.1", "futures-util", "headers", "http 0.2.8", @@ -827,7 +827,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4f44a0e6200e9d11a1cdc989e4b358f6e3d354fbf48478f345a17f4e43f8635" dependencies = [ "async-trait", - "bytes 1.1.0", + "bytes 1.2.1", "futures-util", "http 0.2.8", "http-body", @@ -943,7 +943,7 @@ checksum = "d82e7850583ead5f8bbef247e2a3c37a19bd576e8420cd262a6711921827e1e5" dependencies = [ "base64 0.13.0", "bollard-stubs", - "bytes 1.1.0", + "bytes 1.2.1", "futures-core", "futures-util", "hex 0.4.3", @@ -1064,9 +1064,9 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "bytes" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" [[package]] name = "bytes-utils" @@ -1074,7 +1074,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1934a3ef9cac8efde4966a92781e77713e1ba329f1d42e446c7d7eba340d8ef1" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.1", "either", ] @@ -1513,7 +1513,7 @@ version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.1", "memchr", ] @@ -2784,7 +2784,7 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.1", "fnv", "futures-core", "futures-sink", @@ -2841,7 +2841,7 @@ checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" dependencies = [ "base64 0.13.0", "bitflags", - "bytes 1.1.0", + "bytes 1.2.1", "headers-core", "http 0.2.8", "httpdate", @@ -2985,7 +2985,7 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.1", "fnv", "itoa 1.0.2", ] @@ -2996,7 +2996,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.1", "http 0.2.8", "pin-project-lite 0.2.9", ] @@ -3019,6 +3019,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" +[[package]] +name = "http-serde" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e272971f774ba29341db2f686255ff8a979365a26fb9e4277f6b6d9ec0cdd5e" +dependencies = [ + "http 0.2.8", + "serde", +] + [[package]] name = "http-types" version = "2.12.0" @@ -3043,9 +3053,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" @@ -3061,11 +3071,11 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.20" +version = "0.14.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.1", "futures-channel", "futures-core", "futures-util", @@ -3153,7 +3163,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.1", "hyper", "native-tls", "tokio", @@ -3760,7 +3770,7 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f8f35e687561d5c1667590911e6698a8cb714a134a7505718a182e7bc9d3836" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.1", "encoding_rs", "futures-util", "http 0.2.8", @@ -4091,7 +4101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "449048140ee61e28f57abe6e9975eedc1f3a29855c7407bd6c12b18578863379" dependencies = [ "async-trait", - "bytes 1.1.0", + "bytes 1.2.1", "http 0.2.8", "opentelemetry", "reqwest", @@ -4321,7 +4331,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16d0fec4acc8779b696e3ff25527884fb17cda6cf59a249c57aa1af1e2f65b36" dependencies = [ "async-trait", - "bytes 1.1.0", + "bytes 1.2.1", "futures-util", "headers", "http 0.2.8", @@ -4507,7 +4517,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "399c3c31cdec40583bb68f0b18403400d01ec4289c383aa047560439952c4dd7" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.1", "prost-derive", ] @@ -4517,7 +4527,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f835c582e6bd972ba8347313300219fed5bfa52caf175298d860b61ff6069bb" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.1", "heck", "itertools", "lazy_static", @@ -4550,7 +4560,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dfaa718ad76a44b3415e6c4d53b17c8f99160dcb3a99b10470fce8ad43f6e3e" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.1", "prost", ] @@ -4919,7 +4929,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" dependencies = [ "base64 0.13.0", - "bytes 1.1.0", + "bytes 1.2.1", "encoding_rs", "futures-core", "futures-util", @@ -5048,6 +5058,17 @@ dependencies = [ "paste", ] +[[package]] +name = "rmp-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "rocket" version = "0.5.0-rc.2" @@ -5059,7 +5080,7 @@ dependencies = [ "atomic", "atty", "binascii", - "bytes 1.1.0", + "bytes 1.2.1", "either", "figment", "futures", @@ -5317,7 +5338,7 @@ dependencies = [ "async-compression", "async-trait", "base64 0.13.0", - "bytes 1.1.0", + "bytes 1.2.1", "cookie 0.16.0", "encoding_rs", "enumflags2", @@ -5463,9 +5484,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.143" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e8e5d5b70924f74ff5c6d64d9a5acd91422117c60f48c4e07855238a254553" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" dependencies = [ "serde_derive", ] @@ -5491,9 +5512,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.143" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" dependencies = [ "proc-macro2 1.0.43", "quote 1.0.21", @@ -5585,7 +5606,7 @@ dependencies = [ "async-tungstenite", "base64 0.13.0", "bitflags", - "bytes 1.1.0", + "bytes 1.2.1", "cfg-if 1.0.0", "flate2", "futures", @@ -5721,7 +5742,10 @@ dependencies = [ "comfy-table", "crossterm", "http 0.2.8", + "http-serde", + "hyper", "once_cell", + "rmp-serde", "rustrict", "serde", "serde_json", @@ -5737,7 +5761,7 @@ dependencies = [ "anyhow", "async-trait", "axum", - "bytes 1.1.0", + "bytes 1.2.1", "cargo", "cargo_metadata", "chrono", @@ -5858,6 +5882,8 @@ dependencies = [ "async-trait", "cap-std", "clap 4.0.18", + "hyper", + "rmp-serde", "serenity", "shuttle-common", "shuttle-proto", @@ -6012,9 +6038,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ "libc", "winapi", @@ -6087,7 +6113,7 @@ dependencies = [ "atoi 0.4.0", "bitflags", "byteorder", - "bytes 1.1.0", + "bytes 1.2.1", "crc 2.1.0", "crossbeam-queue", "either", @@ -6135,7 +6161,7 @@ dependencies = [ "base64 0.13.0", "bitflags", "byteorder", - "bytes 1.1.0", + "bytes 1.2.1", "chrono", "crc 3.0.0", "crossbeam-queue", @@ -6607,7 +6633,7 @@ checksum = "4bc22b1c2267be6d1769c6d787936201341f03c915456ed8a8db8d40d665215f" dependencies = [ "async-trait", "bytes 0.5.6", - "bytes 1.1.0", + "bytes 1.2.1", "fnv", "futures", "http 0.1.21", @@ -6757,7 +6783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" dependencies = [ "autocfg 1.1.0", - "bytes 1.1.0", + "bytes 1.2.1", "libc", "memchr", "mio", @@ -6842,7 +6868,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53474327ae5e166530d17f2d956afcb4f8a004de581b3cae10f12006bc8163e3" dependencies = [ "async-stream", - "bytes 1.1.0", + "bytes 1.2.1", "futures-core", "tokio", "tokio-stream", @@ -6881,7 +6907,7 @@ version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.1", "futures-core", "futures-io", "futures-sink", @@ -6897,7 +6923,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.1", "futures-core", "futures-sink", "pin-project-lite 0.2.9", @@ -6954,7 +6980,7 @@ dependencies = [ "async-trait", "axum", "base64 0.13.0", - "bytes 1.1.0", + "bytes 1.2.1", "futures-core", "futures-util", "h2", @@ -7016,7 +7042,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aba3f3efabf7fb41fae8534fc20a817013dd1c12cb45441efb6c82e6556b4cd8" dependencies = [ "bitflags", - "bytes 1.1.0", + "bytes 1.2.1", "futures-core", "futures-util", "http 0.2.8", @@ -7035,7 +7061,7 @@ checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba" dependencies = [ "base64 0.13.0", "bitflags", - "bytes 1.1.0", + "bytes 1.2.1", "futures-core", "futures-util", "http 0.2.8", @@ -7221,7 +7247,7 @@ checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5" dependencies = [ "base64 0.13.0", "byteorder", - "bytes 1.1.0", + "bytes 1.2.1", "http 0.2.8", "httparse", "log", @@ -7240,7 +7266,7 @@ checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" dependencies = [ "base64 0.13.0", "byteorder", - "bytes 1.1.0", + "bytes 1.2.1", "http 0.2.8", "httparse", "log", @@ -7559,7 +7585,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cef4e1e9114a4b7f1ac799f16ce71c14de5778500c5450ec6b7b920c55b587e" dependencies = [ - "bytes 1.1.0", + "bytes 1.2.1", "futures-channel", "futures-util", "headers", @@ -8270,3 +8296,15 @@ dependencies = [ "cc", "libc", ] + +[[patch.unused]] +name = "shuttle-aws-rds" +version = "0.7.0" + +[[patch.unused]] +name = "shuttle-persist" +version = "0.7.0" + +[[patch.unused]] +name = "shuttle-shared-db" +version = "0.7.0" diff --git a/common/Cargo.toml b/common/Cargo.toml index 8b10d111a..c54827c57 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -11,7 +11,10 @@ chrono = { version = "0.4.22", features = ["serde"] } comfy-table = { version = "6.1.0", optional = true } crossterm = { version = "0.25.0", optional = true } http = { version = "0.2.8", optional = true } +http-serde = { version = "1.1.2", optional = true } +hyper = { version = "0.14.23", optional = true } once_cell = "1.13.1" +rmp-serde = { version = "1.1.1", optional = true } rustrict = "0.5.0" serde = { version = "1.0.143", features = ["derive"] } serde_json = { version = "1.0.85", optional = true } @@ -24,3 +27,4 @@ default = ["models"] models = ["display", "serde_json", "http"] display = ["comfy-table", "crossterm"] +axum-wasm = ["http-serde", "hyper", "rmp-serde"] diff --git a/common/src/lib.rs b/common/src/lib.rs index bd7e50afc..6d44adf67 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -4,6 +4,8 @@ pub mod log; #[cfg(feature = "models")] pub mod models; pub mod project; +#[cfg(feature = "axum-wasm")] +pub mod wasm; use serde::{Deserialize, Serialize}; use uuid::Uuid; diff --git a/common/src/wasm.rs b/common/src/wasm.rs new file mode 100644 index 000000000..9a3512c9b --- /dev/null +++ b/common/src/wasm.rs @@ -0,0 +1,160 @@ +use hyper::http::{HeaderMap, Method, Request, Response, StatusCode, Uri, Version}; +use rmps::Serializer; +use serde::{Deserialize, Serialize}; + +extern crate rmp_serde as rmps; + +// todo: add http extensions field +#[derive(Serialize, Deserialize, Debug)] +pub struct RequestWrapper { + #[serde(with = "http_serde::method")] + pub method: Method, + + #[serde(with = "http_serde::uri")] + pub uri: Uri, + + #[serde(with = "http_serde::version")] + pub version: Version, + + #[serde(with = "http_serde::header_map")] + pub headers: HeaderMap, +} + +impl From for RequestWrapper { + fn from(parts: hyper::http::request::Parts) -> Self { + RequestWrapper { + method: parts.method, + uri: parts.uri, + version: parts.version, + headers: parts.headers, + } + } +} + +impl RequestWrapper { + /// Serialize a RequestWrapper to the Rust MessagePack data format + pub fn into_rmp(self) -> Vec { + let mut buf = Vec::new(); + self.serialize(&mut Serializer::new(&mut buf)).unwrap(); + + buf + } + + /// Consume the wrapper and return a request builder with `Parts` set + pub fn into_request_builder(self) -> hyper::http::request::Builder { + let mut request = Request::builder() + .method(self.method) + .version(self.version) + .uri(self.uri); + + request + .headers_mut() + .unwrap() + .extend(self.headers.into_iter()); + + request + } +} + +// todo: add http extensions field +#[derive(Serialize, Deserialize, Debug)] +pub struct ResponseWrapper { + #[serde(with = "http_serde::status_code")] + pub status: StatusCode, + + #[serde(with = "http_serde::version")] + pub version: Version, + + #[serde(with = "http_serde::header_map")] + pub headers: HeaderMap, +} + +impl From for ResponseWrapper { + fn from(parts: hyper::http::response::Parts) -> Self { + ResponseWrapper { + status: parts.status, + version: parts.version, + headers: parts.headers, + } + } +} + +impl ResponseWrapper { + /// Serialize a ResponseWrapper into the Rust MessagePack data format + pub fn into_rmp(self) -> Vec { + let mut buf = Vec::new(); + self.serialize(&mut Serializer::new(&mut buf)).unwrap(); + + buf + } + + /// Consume the wrapper and return a response builder with `Parts` set + pub fn into_response_builder(self) -> hyper::http::response::Builder { + let mut response = Response::builder() + .status(self.status) + .version(self.version); + + response + .headers_mut() + .unwrap() + .extend(self.headers.into_iter()); + + response + } +} + +#[cfg(test)] +mod test { + use super::*; + use hyper::body::Body; + use hyper::http::HeaderValue; + + #[test] + fn request_roundtrip() { + let request: Request = Request::builder() + .method(Method::PUT) + .version(Version::HTTP_11) + .header("test", HeaderValue::from_static("request")) + .uri(format!("https://axum-wasm.example/hello")) + .body(Body::empty()) + .unwrap(); + + let (parts, _) = request.into_parts(); + let rmp = RequestWrapper::from(parts).into_rmp(); + + let back: RequestWrapper = rmps::from_slice(&rmp).unwrap(); + + assert_eq!( + back.headers.get("test").unwrap(), + HeaderValue::from_static("request") + ); + assert_eq!(back.method, Method::PUT); + assert_eq!(back.version, Version::HTTP_11); + assert_eq!( + back.uri.to_string(), + "https://axum-wasm.example/hello".to_string() + ); + } + + #[test] + fn response_roundtrip() { + let response: Response = Response::builder() + .version(Version::HTTP_11) + .header("test", HeaderValue::from_static("response")) + .status(StatusCode::NOT_MODIFIED) + .body(Body::empty()) + .unwrap(); + + let (parts, _) = response.into_parts(); + let rmp = ResponseWrapper::from(parts).into_rmp(); + + let back: ResponseWrapper = rmps::from_slice(&rmp).unwrap(); + + assert_eq!( + back.headers.get("test").unwrap(), + HeaderValue::from_static("response") + ); + assert_eq!(back.status, StatusCode::NOT_MODIFIED); + assert_eq!(back.version, Version::HTTP_11); + } +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 04e03b72d..5ea9e6003 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -10,6 +10,8 @@ anyhow = "1.0.62" async-trait = "0.1.58" cap-std = "0.26.0" clap ={ version = "4.0.18", features = ["derive"] } +hyper = { version = "0.14.23", features = ["full"] } +rmp-serde = { version = "1.1.1" } serenity = { version = "0.11.5", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } thiserror = "1.0.37" tokio = { version = "=1.20.1", features = ["full"] } @@ -35,3 +37,7 @@ version = "0.7.0" default-features = false features = ["loader"] path = "../service" + +[features] +shuttle-axum = ["shuttle-common/axum-wasm"] + diff --git a/runtime/Makefile b/runtime/Makefile index 18dac0caa..458f6ad3b 100644 --- a/runtime/Makefile +++ b/runtime/Makefile @@ -6,6 +6,10 @@ wasm: cd ../tmp/wasm; cargo build --target wasm32-wasi cp ../tmp/wasm/target/wasm32-wasi/debug/shuttle_serenity.wasm bot.wasm +axum: + cd ../tmp/axum-wasm; cargo build --target wasm32-wasi + cp ../tmp/axum-wasm/target/wasm32-wasi/debug/shuttle_axum.wasm axum.wasm + test: wasm cargo test -- --nocapture diff --git a/runtime/README.md b/runtime/README.md index 193ee80f7..f38778c44 100644 --- a/runtime/README.md +++ b/runtime/README.md @@ -1,5 +1,6 @@ -## How to run +# How to run +## shuttle-next ```bash $ make wasm $ DISCORD_TOKEN=xxx cargo run @@ -8,9 +9,47 @@ $ DISCORD_TOKEN=xxx cargo run In another terminal: ``` bash -grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "path": "runtime/bot.wasm"}' localhost:8000 runtime.Runtime/Load -grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic"}' localhost:8000 runtime.Runtime/Start -grpcurl -plaintext -import-path ../proto -proto runtime.proto localhost:8000 runtime.Runtime/SubscribeLogs +grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "path": "runtime/bot.wasm"}' localhost:6001 runtime.Runtime/Load +grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic"}' localhost:6001 runtime.Runtime/Start +grpcurl -plaintext -import-path ../proto -proto runtime.proto localhost:6001 runtime.Runtime/SubscribeLogs +``` + +## axum-wasm + +Compile the wasm axum router: + +```bash +make axum +``` + +Run the test: + +```bash +cargo test axum --features shuttle-axum -- --nocapture +``` + +Load and run: + +```bash +cargo run --features shuttle-axum -- --axum --provisioner-address http://localhost:8000 +``` + +In another terminal: + +``` bash +# a full, absolute path from home was needed for me in the load request +grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "path": "runtime/axum.wasm"}' localhost:6001 runtime.Runtime/Load + +grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic"}' localhost:6001 runtime.Runtime/Start + +# grpcurl -plaintext -import-path ../proto -proto runtime.proto localhost:6001 runtime.Runtime/SubscribeLogs +``` + +Curl the service: +```bash +curl localhost:7002/hello + +curl localhost:7002/goodbye ``` ## shuttle-legacy @@ -33,16 +72,16 @@ Or directly (this is the path hardcoded in `deployer::start`): # first, make sure the shuttle-runtime binary is built cargo build # then -/home//target/debug/shuttle-runtime --legacy --provisioner-address http://localhost:8000 +/home//target/debug/shuttle-runtime --legacy --provisioner-address http://localhost:6001 ``` Pass the path to `deployer::start` Then in another shell, load a `.so` file and start it up: ``` bash -grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "path": "examples/rocket/hello-world/target/debug/libhello_world.so"}' localhost:8000 runtime.Runtime/Load -grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic"}' localhost:8000 runtime.Runtime/Start -grpcurl -plaintext -import-path ../proto -proto runtime.proto localhost:8000 runtime.Runtime/SubscribeLogs +grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "path": "examples/rocket/hello-world/target/debug/libhello_world.so"}' localhost:6001 runtime.Runtime/Load +grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic"}' localhost:6001 runtime.Runtime/Start +grpcurl -plaintext -import-path ../proto -proto runtime.proto localhost:6001 runtime.Runtime/SubscribeLogs ``` ## Running the tests diff --git a/runtime/src/args.rs b/runtime/src/args.rs index 084304364..2e123f4e8 100644 --- a/runtime/src/args.rs +++ b/runtime/src/args.rs @@ -8,6 +8,10 @@ pub struct Args { pub provisioner_address: Endpoint, /// Is this runtime for a legacy service - #[clap(long)] + #[clap(long, conflicts_with("axum"))] pub legacy: bool, + + /// Is this runtime for an axum-wasm service + #[clap(long, conflicts_with("legacy"))] + pub axum: bool, } diff --git a/runtime/src/axum/mod.rs b/runtime/src/axum/mod.rs new file mode 100644 index 000000000..40aaa06db --- /dev/null +++ b/runtime/src/axum/mod.rs @@ -0,0 +1,324 @@ +use std::convert::Infallible; +use std::fs::File; +use std::io::{BufReader, Read, Write}; +use std::net::{Ipv4Addr, SocketAddr}; +use std::os::unix::prelude::RawFd; +use std::path::Path; +use std::sync::{Arc, Mutex}; + +use async_trait::async_trait; +use cap_std::os::unix::net::UnixStream; +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Body, Request, Response}; +use shuttle_common::wasm::{RequestWrapper, ResponseWrapper}; +use shuttle_proto::runtime::runtime_server::Runtime; +use shuttle_proto::runtime::{ + self, LoadRequest, LoadResponse, StartRequest, StartResponse, SubscribeLogsRequest, +}; +use tokio_stream::wrappers::ReceiverStream; +use tonic::Status; +use tracing::info; +use wasi_common::file::FileCaps; +use wasmtime::{Engine, Linker, Module, Store}; +use wasmtime_wasi::sync::net::UnixStream as WasiUnixStream; +use wasmtime_wasi::{WasiCtx, WasiCtxBuilder}; + +extern crate rmp_serde as rmps; + +pub struct AxumWasm { + router: std::sync::Mutex>, + port: Mutex>, +} + +impl AxumWasm { + pub fn new() -> Self { + Self { + router: std::sync::Mutex::new(None), + port: std::sync::Mutex::new(None), + } + } +} + +#[async_trait] +impl Runtime for AxumWasm { + async fn load( + &self, + request: tonic::Request, + ) -> Result, Status> { + let wasm_path = request.into_inner().path; + info!(wasm_path, "loading"); + + let router = Router::new(wasm_path); + + *self.router.lock().unwrap() = Some(router); + + let message = LoadResponse { success: true }; + + Ok(tonic::Response::new(message)) + } + + async fn start( + &self, + _request: tonic::Request, + ) -> Result, Status> { + let port = 7002; + let address = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), port); + + let router = self.router.lock().unwrap().take().unwrap().inner; + + let make_service = make_service_fn(move |_conn| { + let router = router.clone(); + async move { + Ok::<_, Infallible>(service_fn(move |req: Request| { + let router = router.clone(); + async move { + Ok::<_, Infallible>(router.lock().await.send_request(req).await.unwrap()) + } + })) + } + }); + + info!("starting hyper server on: {}", &address); + let server = hyper::Server::bind(&address).serve(make_service); + + _ = tokio::spawn(server); + + *self.port.lock().unwrap() = Some(port); + + let message = StartResponse { + success: true, + port: port as u32, + }; + + Ok(tonic::Response::new(message)) + } + + type SubscribeLogsStream = ReceiverStream>; + + async fn subscribe_logs( + &self, + _request: tonic::Request, + ) -> Result, Status> { + todo!() + } +} + +struct RouterBuilder { + engine: Engine, + store: Store, + linker: Linker, + src: Option, +} + +impl RouterBuilder { + pub fn new() -> Self { + let engine = Engine::default(); + + let mut linker: Linker = Linker::new(&engine); + wasmtime_wasi::add_to_linker(&mut linker, |s| s).unwrap(); + + let wasi = WasiCtxBuilder::new() + .inherit_stdio() + .inherit_args() + .unwrap() + .build(); + + let store = Store::new(&engine, wasi); + + Self { + engine, + store, + linker, + src: None, + } + } + + pub fn src>(mut self, src: P) -> Self { + self.src = Some(File::open(src).unwrap()); + self + } + + pub fn build(mut self) -> Router { + let mut buf = Vec::new(); + self.src.unwrap().read_to_end(&mut buf).unwrap(); + let module = Module::new(&self.engine, buf).unwrap(); + + for export in module.exports() { + println!("export: {}", export.name()); + } + + self.linker + .module(&mut self.store, "axum", &module) + .unwrap(); + let inner = RouterInner { + store: self.store, + linker: self.linker, + }; + Router { + inner: Arc::new(tokio::sync::Mutex::new(inner)), + } + } +} + +struct RouterInner { + store: Store, + linker: Linker, +} + +impl RouterInner { + /// Send a HTTP request with body to given endpoint on the axum-wasm router and return the response + pub async fn send_request( + &mut self, + req: hyper::Request, + ) -> Result, Infallible> { + let (mut parts_stream, parts_client) = UnixStream::pair().unwrap(); + let (mut body_stream, body_client) = UnixStream::pair().unwrap(); + + let parts_client = WasiUnixStream::from_cap_std(parts_client); + let body_client = WasiUnixStream::from_cap_std(body_client); + + self.store + .data_mut() + .insert_file(3, Box::new(parts_client), FileCaps::all()); + + self.store + .data_mut() + .insert_file(4, Box::new(body_client), FileCaps::all()); + + let (parts, body) = req.into_parts(); + + // serialise request parts to rmp + let request_rmp = RequestWrapper::from(parts).into_rmp(); + + // write request parts + parts_stream.write_all(&request_rmp).unwrap(); + + // write body + body_stream + .write_all(hyper::body::to_bytes(body).await.unwrap().as_ref()) + .unwrap(); + // signal to the receiver that end of file has been reached + body_stream.write_all(&[0]).unwrap(); + + println!("calling inner Router"); + self.linker + .get(&mut self.store, "axum", "__SHUTTLE_Axum_call") + .unwrap() + .into_func() + .unwrap() + .typed::<(RawFd, RawFd), (), _>(&self.store) + .unwrap() + .call(&mut self.store, (3, 4)) + .unwrap(); + + // read response parts from host + let reader = BufReader::new(&mut parts_stream); + + // deserialize response parts from rust messagepack + let wrapper: ResponseWrapper = rmps::from_read(reader).unwrap(); + + // read response body from wasm router + let mut body_buf = Vec::new(); + let mut c_buf: [u8; 1] = [0; 1]; + loop { + body_stream.read_exact(&mut c_buf).unwrap(); + if c_buf[0] == 0 { + break; + } else { + body_buf.push(c_buf[0]); + } + } + + let response: Response = wrapper + .into_response_builder() + .body(body_buf.into()) + .unwrap(); + + Ok(response) + } +} + +#[derive(Clone)] +struct Router { + inner: Arc>, +} + +impl Router { + pub fn builder() -> RouterBuilder { + RouterBuilder::new() + } + + pub fn new>(src: P) -> Self { + Self::builder().src(src).build() + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use hyper::{http::HeaderValue, Method, Request, StatusCode, Version}; + + #[tokio::test] + async fn axum() { + let axum = Router::new("axum.wasm"); + let mut inner = axum.inner.lock().await; + + // GET /hello + let request: Request = Request::builder() + .method(Method::GET) + .version(Version::HTTP_11) + .uri(format!("https://axum-wasm.example/hello")) + .body(Body::empty()) + .unwrap(); + + let res = inner.send_request(request).await.unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + &hyper::body::to_bytes(res.into_body()) + .await + .unwrap() + .iter() + .cloned() + .collect::>() + .as_ref(), + b"Hello, World!" + ); + + // GET /goodbye + let request: Request = Request::builder() + .method(Method::GET) + .version(Version::HTTP_11) + .header("test", HeaderValue::from_static("goodbye")) + .uri(format!("https://axum-wasm.example/goodbye")) + .body(Body::from("Goodbye world body")) + .unwrap(); + + let res = inner.send_request(request).await.unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + &hyper::body::to_bytes(res.into_body()) + .await + .unwrap() + .iter() + .cloned() + .collect::>() + .as_ref(), + b"Goodbye, World!" + ); + + // GET /invalid + let request: Request = Request::builder() + .method(Method::GET) + .version(Version::HTTP_11) + .header("test", HeaderValue::from_static("invalid")) + .uri(format!("https://axum-wasm.example/invalid")) + .body(Body::empty()) + .unwrap(); + + let res = inner.send_request(request).await.unwrap(); + + assert_eq!(res.status(), StatusCode::NOT_FOUND); + } +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 7ca426d3a..1119bf890 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1,8 +1,10 @@ mod args; +mod axum; mod legacy; mod next; pub mod provisioner_factory; pub use args::Args; +pub use axum::AxumWasm; pub use legacy::Legacy; pub use next::Next; diff --git a/runtime/src/main.rs b/runtime/src/main.rs index d2eae6557..f7a5dc597 100644 --- a/runtime/src/main.rs +++ b/runtime/src/main.rs @@ -5,7 +5,7 @@ use std::{ use clap::Parser; use shuttle_proto::runtime::runtime_server::RuntimeServer; -use shuttle_runtime::{Args, Legacy, Next}; +use shuttle_runtime::{Args, AxumWasm, Legacy, Next}; use tonic::transport::Server; use tracing::trace; use tracing_subscriber::{fmt, prelude::*, EnvFilter}; @@ -36,6 +36,10 @@ async fn main() { let legacy = Legacy::new(provisioner_address); let svc = RuntimeServer::new(legacy); server_builder.add_service(svc) + } else if args.axum { + let axum = AxumWasm::new(); + let svc = RuntimeServer::new(axum); + server_builder.add_service(svc) } else { let next = Next::new(); let svc = RuntimeServer::new(next); diff --git a/tmp/axum-wasm/Cargo.toml b/tmp/axum-wasm/Cargo.toml new file mode 100644 index 000000000..52c5418cf --- /dev/null +++ b/tmp/axum-wasm/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "shuttle-axum" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = [ "cdylib" ] + +[dependencies] +# most axum features can be enabled, but "tokio" and "ws" depend on socket2 +# via "hyper/tcp" which is not compatible with wasi +axum = { version = "0.6.0-rc.4", default-features = false } +futures-executor = "0.3.21" +http = "0.2.7" +tower-service = "0.3.1" +rmp-serde = { version = "1.1.1" } + +[dependencies.shuttle-common] +path = "../../common" +default-features = false +features = ["axum-wasm"] +version = "0.7.0" diff --git a/tmp/axum-wasm/src/lib.rs b/tmp/axum-wasm/src/lib.rs new file mode 100644 index 000000000..e0a8b6542 --- /dev/null +++ b/tmp/axum-wasm/src/lib.rs @@ -0,0 +1,93 @@ +use axum::body::{Body, HttpBody}; +use axum::{response::Response, routing::get, Router}; +use futures_executor::block_on; +use http::Request; +use shuttle_common::wasm::{RequestWrapper, ResponseWrapper}; +use std::fs::File; +use std::io::BufReader; +use std::io::{Read, Write}; +use std::os::wasi::prelude::*; +use tower_service::Service; + +extern crate rmp_serde as rmps; + +pub fn handle_request(req: Request) -> Response +where + B: HttpBody + Send + 'static, +{ + block_on(app(req)) +} + +async fn app(request: Request) -> Response +where + B: HttpBody + Send + 'static, +{ + let mut router = Router::new() + .route("/hello", get(hello)) + .route("/goodbye", get(goodbye)) + .into_service(); + + let response = router.call(request).await.unwrap(); + + response +} + +async fn hello() -> &'static str { + "Hello, World!" +} + +async fn goodbye() -> &'static str { + "Goodbye, World!" +} + +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn __SHUTTLE_Axum_call(fd_3: RawFd, fd_4: RawFd) { + println!("inner handler awoken; interacting with fd={fd_3},{fd_4}"); + + // file descriptor 3 for reading and writing http parts + let mut parts_fd = unsafe { File::from_raw_fd(fd_3) }; + + let reader = BufReader::new(&mut parts_fd); + + // deserialize request parts from rust messagepack + let wrapper: RequestWrapper = rmps::from_read(reader).unwrap(); + + // file descriptor 4 for reading and writing http body + let mut body_fd = unsafe { File::from_raw_fd(fd_4) }; + + // read body from host + let mut body_buf = Vec::new(); + let mut c_buf: [u8; 1] = [0; 1]; + loop { + body_fd.read(&mut c_buf).unwrap(); + if c_buf[0] == 0 { + break; + } else { + body_buf.push(c_buf[0]); + } + } + + let request: Request = wrapper + .into_request_builder() + .body(body_buf.into()) + .unwrap(); + + println!("inner router received request: {:?}", &request); + let res = handle_request(request); + + let (parts, mut body) = res.into_parts(); + + // wrap and serialize response parts as rmp + let response_parts = ResponseWrapper::from(parts).into_rmp(); + + // write response parts + parts_fd.write_all(&response_parts).unwrap(); + + // write body if there is one + if let Some(body) = block_on(body.data()) { + body_fd.write_all(body.unwrap().as_ref()).unwrap(); + } + // signal to the reader that end of file has been reached + body_fd.write(&[0]).unwrap(); +}