From ce8bdcf09d8f479442a9f64ed1ee64af26ae3675 Mon Sep 17 00:00:00 2001 From: Steve Biedermann Date: Tue, 29 Sep 2026 14:21:04 +0200 Subject: [PATCH] step --- .gitignore | 3 +- Cargo.lock | 1352 ++++++++++++++++++++++++++++++++- Cargo.toml | 12 + README.md | 161 ++++ example-wasm-agent/Cargo.lock | 390 ++++++++++ example-wasm-agent/Cargo.toml | 18 + example-wasm-agent/src/lib.rs | 65 ++ src/cli.rs | 220 ++++++ src/lib.rs | 2 + src/main.rs | 27 +- src/server.rs | 1232 ++++++++++++++++++++++++++++++ src/wasm_agent.rs | 4 +- 12 files changed, 3448 insertions(+), 38 deletions(-) create mode 100644 README.md create mode 100644 example-wasm-agent/Cargo.lock create mode 100644 example-wasm-agent/Cargo.toml create mode 100644 example-wasm-agent/src/lib.rs create mode 100644 src/cli.rs create mode 100644 src/server.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..eeb63bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/target +target/ +/data \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c948e25..ca789fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -53,6 +103,54 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.21", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure 0.13.2", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "async-trait" version = "0.1.92" @@ -64,18 +162,116 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "base64 0.22.1", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-server" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc" +dependencies = [ + "arc-swap", + "bytes", + "either", + "fs-err", + "http", + "http-body", + "hyper", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.13.2" @@ -158,7 +354,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ec6a5b75f54547c579a6b117c6fdd5f04f4ab7598de747b9f440a53592b3a4a" dependencies = [ "ambient-authority", - "rand", + "rand 0.8.8", ] [[package]] @@ -223,6 +419,46 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + [[package]] name = "cobs" version = "0.3.0" @@ -232,6 +468,32 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -448,6 +710,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + [[package]] name = "debugid" version = "0.8.0" @@ -457,6 +725,26 @@ dependencies = [ "uuid", ] +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "digest" version = "0.10.7" @@ -580,7 +868,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad19858c4c462309a8f3a20abec53e8603bda1eefda26c8bfab51d5516b40cbb" dependencies = [ "anyhow", - "base64", + "base64 0.22.1", "bytemuck", "extism-convert-macros", "prost", @@ -608,11 +896,23 @@ version = "1.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2f59c8dadb5e0bde9a48c6ed45312e6ef625cbcd5f67c28459dbc8fe8bc0383" dependencies = [ - "base64", + "base64 0.22.1", "serde", "serde_json", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.5.0" @@ -663,6 +963,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" +dependencies = [ + "autocfg", + "tokio", +] + [[package]] name = "fs-set-times" version = "0.20.3" @@ -773,6 +1083,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -781,7 +1103,7 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", ] [[package]] @@ -802,6 +1124,25 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -818,6 +1159,18 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb" +dependencies = [ + "hashbrown 0.17.1", +] [[package]] name = "heck" @@ -825,6 +1178,112 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -966,7 +1425,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" dependencies = [ "bitmaps", - "rand_core", + "rand_core 0.6.4", "rand_xoshiro", "sized-chunks", "typenum", @@ -1007,6 +1466,12 @@ version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -1042,6 +1507,55 @@ dependencies = [ "cc", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + [[package]] name = "jobserver" version = "0.1.35" @@ -1102,6 +1616,17 @@ dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1120,6 +1645,15 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.34" @@ -1167,6 +1701,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "maybe-owned" version = "0.3.4" @@ -1188,12 +1728,45 @@ dependencies = [ "rustix 1.1.5", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "multiversion_no_op" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1203,6 +1776,31 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1224,12 +1822,66 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" +dependencies = [ + "base64 0.23.1", + "serde_core", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1279,6 +1931,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1372,6 +2030,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1385,8 +2049,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1396,7 +2070,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1408,13 +2092,22 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_xoshiro" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1437,6 +2130,29 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.14.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -1479,6 +2195,54 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rmp" version = "0.8.15" @@ -1498,6 +2262,31 @@ dependencies = [ "serde", ] +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.21", +] + +[[package]] +name = "rusqlite" +version = "0.40.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + [[package]] name = "rustc-demangle" version = "0.1.28" @@ -1510,6 +2299,24 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.38.44" @@ -1546,6 +2353,79 @@ dependencies = [ "rustix 1.1.5", ] +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.23" @@ -1558,12 +2438,53 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -1617,6 +2538,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_spanned" version = "1.1.1" @@ -1626,6 +2558,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -1639,6 +2583,17 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -1665,6 +2620,26 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -1696,12 +2671,46 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.119" @@ -1724,6 +2733,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "synstructure" version = "0.14.0" @@ -1828,6 +2857,36 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.4" @@ -1838,6 +2897,70 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.9.12+spec-1.1.0" @@ -1898,6 +3021,52 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" @@ -1960,6 +3129,28 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.5", + "sha1", + "thiserror 2.0.21", +] + [[package]] name = "typenum" version = "1.20.1" @@ -1990,6 +3181,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -2008,6 +3205,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.26.1" @@ -2025,12 +3228,37 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2063,6 +3291,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.129" @@ -2076,6 +3313,17 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.129" @@ -2274,7 +3522,7 @@ version = "43.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ed398988226d7aa0505ac6bb576e09532ad722d702ec4e66365d78ed695c95f" dependencies = [ - "base64", + "base64 0.22.1", "directories-next", "log", "postcard", @@ -2472,6 +3720,25 @@ dependencies = [ "wast 259.0.0", ] +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "wiggle" version = "43.0.2" @@ -2622,6 +3889,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -2729,6 +4005,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-parser" version = "0.245.1" @@ -2764,9 +4046,21 @@ dependencies = [ name = "wolloseum" version = "0.1.0" dependencies = [ + "axum", + "axum-server", + "base64 0.22.1", + "clap", "extism", + "rcgen", + "reqwest", + "ring", + "rusqlite", + "rustls", "serde", "serde_json", + "tempfile", + "tokio", + "uuid", "wat", ] @@ -2776,6 +4070,34 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.21", + "time", +] + +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + [[package]] name = "yoke" version = "0.8.3" @@ -2796,7 +4118,7 @@ dependencies = [ "proc-macro2", "quote", "syn 3.0.6", - "synstructure", + "synstructure 0.14.0", ] [[package]] @@ -2837,9 +4159,15 @@ dependencies = [ "proc-macro2", "quote", "syn 3.0.6", - "synstructure", + "synstructure 0.14.0", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zerotrie" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index a677518..1ffbc47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,9 +4,21 @@ version = "0.1.0" edition = "2024" [dependencies] +axum = { version = "0.8", features = ["ws"] } +axum-server = { version = "0.8", default-features = false, features = ["tls-rustls-no-provider"] } +base64 = "0.22" +clap = { version = "4.6.7", features = ["derive", "env"] } extism = { version = "1.30", default-features = false, features = ["wasmtime-default-features"] } +rcgen = { version = "0.14", features = ["x509-parser"] } +reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider"] } +ring = "0.17" +rusqlite = { version = "0.40", features = ["bundled"] } +rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +tokio = { version = "1", features = ["full"] } +uuid = { version = "1", features = ["v4"] } [dev-dependencies] +tempfile = "3" wat = "1" diff --git a/README.md b/README.md new file mode 100644 index 0000000..da8eb8d --- /dev/null +++ b/README.md @@ -0,0 +1,161 @@ +# Wolloseum + +Wolloseum is a deterministic, procedurally generated game server. Players +upload untrusted WASM agents (run in a sandboxed Extism/Wasmtime runtime with +fuel and memory limits) that control spawned agents in a tick-based +simulation. All identity and request authentication is certificate-based +(Ed25519 client certificates signed by a locally issued root CA); there are no +passwords or bearer tokens. + +## Building the server + +```sh +cargo build --release +``` + +The binary is produced at `target/release/wolloseum`. + +## Configuration + +The server takes its configuration from CLI flags (each with an environment +variable fallback), defined in [src/cli.rs](src/cli.rs): + +| Flag | Env var | Default | Purpose | +| ------------ | -------------------- | ---------------- | -------------------------------------------------- | +| `--data-dir` | `WOLLOSEUM_DATA_DIR` | `data` | Directory for certificates and the SQLite database | +| `--bind` | `WOLLOSEUM_BIND` | `127.0.0.1:8080` | HTTP listener address | + +On first start the server bootstraps a PKI under `/certificates/`: + +- `root-ca.pem` / `root-ca-key.pem` — self-signed root CA +- `server.pem` / `server-key.pem` — HTTP server certificate (issued for `localhost`) +- `admin-client.pem` / `admin-client-key.pem` — the initial admin client identity + +The SQLite database is created at `/wolloseum.sqlite3` and stores +identities, used request nonces, per-player agent WASM, and the core +simulation WASM module. Private key files and the certificate directory are +written with restrictive (owner-only) permissions on Unix. + +## Running the server + +```sh +cargo run --release -- serve --data-dir data --bind 127.0.0.1:8080 +``` + +On startup the server prints the admin certificate ID and the paths to the +admin client certificate/key and root CA certificate — you'll need these to +issue authenticated CLI/API requests. + +## API endpoints + +All `/api/*` endpoints except `/api/status` and `/api/events` require request +signing. Each signed request must include: + +``` +X-Client-Id: +X-Request-Timestamp: +X-Request-Nonce: +X-Request-Signature: +``` + +The signed message is `METHOD\nPATH\nTIMESTAMP\nNONCE\n` followed by the raw +request body bytes. + +| Method | Path | Auth | Description | +| ------ | ------------------- | ------------------- | ----------------------------------------------------------- | +| GET | `/` | none | Player web UI | +| GET | `/admin` | none | Admin web UI | +| GET | `/api/status` | none | Current game running state | +| GET | `/api/events` | none (WebSocket) | Live game/agent/core state change events | +| POST | `/api/create_user` | admin | Create a player identity, returns certificate + private key | +| POST | `/api/delete_user` | admin | Delete a player identity and their uploaded agent | +| POST | `/api/start_game` | admin | Resume the simulation | +| POST | `/api/stop_game` | admin | Pause the simulation | +| POST | `/api/update_agent` | any active identity | Upload/replace the caller's agent WASM module | +| POST | `/api/upload_core` | admin | Upload/replace the core simulation WASM module | + +### `/api/upload_core` + +Replaces the core simulation logic module. The request body is the raw WASM +bytes (not JSON), following the same conventions as `/api/update_agent`: + +- Body limit: 1 MiB (`Content-Length` over the limit is rejected) +- The module is sandboxed-loaded (no WASI imports, bounded memory) to confirm + it is valid before being persisted +- Requires admin authentication (signed request headers as above) +- On success the module is stored in the `core_module` table (single active + row) and a `core_uploaded` event is published over `/api/events` + +```sh +curl -k http://127.0.0.1:8080/api/upload_core \ + -X POST \ + --data-binary @core_simulation.wasm \ + -H "x-client-id: $ADMIN_ID" \ + -H "x-request-timestamp: $TS" \ + -H "x-request-nonce: $NONCE" \ + -H "x-request-signature: $SIGNATURE" +``` + +In practice, use the CLI (below) rather than constructing signatures by hand. + +## Using the CLI client + +The same binary provides a signing HTTP client: + +```sh +cargo run --release -- cli \ + --server http://127.0.0.1:84080\ + --root-ca data/certificates/root-ca.pem \ + --client-cert data/certificates/admin-client.pem \ + --client-key data/certificates/admin-client-key.pem \ + status +``` + +Available subcommands (see [src/cli.rs](src/cli.rs)): `status`, +`create-user `, `delete-user `, `stop-game`, +`start-game`, `update-agent `. + +## Building the example WASM agent + +[example-wasm-agent/](example-wasm-agent) is a standalone crate implementing +a minimal Extism plugin agent matching the ABI expected by +[src/wasm_agent.rs](src/wasm_agent.rs) (a JSON `decide` call taking +`{abi_version, tick, x, y, adjacent_terrain}` and returning +`{abi_version, action}`). Movement is a deterministic function of the tick +and position — no RNG, clock, or host access is used, so replays are +reproducible. + +Build it to a `.wasm` file: + +```sh +cd example-wasm-agent +rustup target add wasm32-unknown-unknown # if not already installed +cargo build --release --target wasm32-unknown-unknown +``` + +The compiled module is at +`example-wasm-agent/target/wasm32-unknown-unknown/release/example_wasm_agent.wasm`. + +## Uploading the example agent + +First create a player identity (as admin), then upload the agent WASM signed +with that player's own certificate: + +```sh +# As admin, create a player and save the returned certificate/private key: +cargo run --release -- cli \ + --client-cert data/certificates/admin-client.pem \ + --client-key data/certificates/admin-client-key.pem \ + create-user alice +# -> write the returned certificate_pem/private_key_pem to alice.pem / alice-key.pem + +# As alice, upload the compiled agent: +cargo run --release -- cli \ + --client-cert alice.pem \ + --client-key alice-key.pem \ + update-agent example-wasm-agent/target/wasm32-unknown-unknown/release/example_wasm_agent.wasm +``` + +Core simulation WASM can be uploaded the same way by an admin using +`POST /api/upload_core` (no dedicated CLI subcommand yet — use `curl` as shown +above, or extend the CLI client following the same pattern as `update-agent`). diff --git a/example-wasm-agent/Cargo.lock b/example-wasm-agent/Cargo.lock new file mode 100644 index 0000000..783bb87 --- /dev/null +++ b/example-wasm-agent/Cargo.lock @@ -0,0 +1,390 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "example-wasm-agent" +version = "0.1.0" +dependencies = [ + "extism-pdk", + "serde", +] + +[[package]] +name = "extism-convert" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad19858c4c462309a8f3a20abec53e8603bda1eefda26c8bfab51d5516b40cbb" +dependencies = [ + "anyhow", + "base64", + "bytemuck", + "extism-convert-macros", + "prost", + "rmp-serde", + "serde", + "serde_json", +] + +[[package]] +name = "extism-convert-macros" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2932799f6d9f9646f97b65287f6bb2addc75a0ee61e40fb24559a7540dd928" +dependencies = [ + "manyhow", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "extism-manifest" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2f59c8dadb5e0bde9a48c6ed45312e6ef625cbcd5f67c28459dbc8fe8bc0383" +dependencies = [ + "base64", + "serde", + "serde_json", +] + +[[package]] +name = "extism-pdk" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352fcb5a66eb74145a1c4a01f2bd15d59c62c85be73aac8471880c65b26b798f" +dependencies = [ + "anyhow", + "base64", + "extism-convert", + "extism-manifest", + "extism-pdk-derive", + "serde", + "serde_json", +] + +[[package]] +name = "extism-pdk-derive" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d086daea5fd844e3c5ac69ddfe36df4a9a43e7218cf7d1f888182b089b09806c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/example-wasm-agent/Cargo.toml b/example-wasm-agent/Cargo.toml new file mode 100644 index 0000000..b004281 --- /dev/null +++ b/example-wasm-agent/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "example-wasm-agent" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +extism-pdk = "1" +serde = { version = "1", features = ["derive"] } + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = true diff --git a/example-wasm-agent/src/lib.rs b/example-wasm-agent/src/lib.rs new file mode 100644 index 0000000..ea42286 --- /dev/null +++ b/example-wasm-agent/src/lib.rs @@ -0,0 +1,65 @@ +use extism_pdk::*; +use serde::{Deserialize, Serialize}; + +const ABI_VERSION: u16 = 1; +const BOUNDARY_TERRAIN: i32 = 3; + +#[derive(Deserialize)] +struct DecisionInput { + #[allow(dead_code)] + abi_version: u16, + tick: u64, + x: u16, + y: u16, + adjacent_terrain: [i32; 4], +} + +#[derive(Serialize)] +struct DecisionOutput { + abi_version: u16, + action: i32, +} + +/// Deterministic decision function called once per simulation tick. +/// +/// The host provides the full observation as JSON input and expects a JSON +/// decision back; no filesystem, network, clock, or RNG access is available. +/// Movement here is a reproducible function of (tick, x, y) alone, so replay +/// with the same seed and inputs always yields the same actions. +#[plugin_fn] +pub fn decide(input: Json) -> FnResult> { + let input = input.0; + + // Action codes: 1=North, 2=East, 3=South, 4=West, matching adjacent_terrain order. + let open_directions: Vec = input + .adjacent_terrain + .iter() + .enumerate() + .filter(|(_, terrain)| **terrain != BOUNDARY_TERRAIN) + .map(|(index, _)| (index + 1) as i32) + .collect(); + + let action = if open_directions.is_empty() { + 0 // Wait + } else { + let seed = deterministic_seed(input.tick, input.x, input.y); + open_directions[(seed as usize) % open_directions.len()] + }; + + Ok(Json(DecisionOutput { + abi_version: ABI_VERSION, + action, + })) +} + +/// SplitMix64-style mix so the same (tick, x, y) always yields the same choice. +fn deterministic_seed(tick: u64, x: u16, y: u16) -> u64 { + let mut state = tick + .wrapping_mul(0x9E37_79B9_7F4A_7C15) + .wrapping_add(u64::from(x).wrapping_mul(0xBF58_476D_1CE4_E5B9)) + .wrapping_add(u64::from(y).wrapping_mul(0x94D0_49BB_1331_11EB)); + state ^= state >> 33; + state = state.wrapping_mul(0xFF51_AFD7_ED55_8CCD); + state ^= state >> 33; + state +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..1b4e858 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,220 @@ +use std::error::Error; +use std::fs; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; + +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD as BASE64; +use clap::{Args, Parser, Subcommand}; +use rcgen::KeyPair; +use reqwest::{Client, Method}; +use ring::signature::Ed25519KeyPair; +use serde::Serialize; + +#[derive(Debug, Parser)] +#[command( + name = "wolloseum", + version, + about = "Wolloseum game server and client" +)] +pub struct Arguments { + #[command(subcommand)] + command: TopLevelCommand, +} + +#[derive(Debug, Subcommand)] +enum TopLevelCommand { + /// Start the HTTP game server. + Serve(ServeArguments), + /// Send authenticated requests to a Wolloseum server. + Cli(ClientArguments), +} + +#[derive(Debug, Args)] +struct ServeArguments { + /// Directory for certificates and the SQLite database. + #[arg(long, env = "WOLLOSEUM_DATA_DIR", default_value = "data")] + data_dir: PathBuf, + /// HTTP listener address. + #[arg(long, env = "WOLLOSEUM_BIND", default_value = "127.0.0.1:8080")] + bind: SocketAddr, +} + +#[derive(Debug, Args)] +struct ClientArguments { + /// HTTP base URL for the server. + #[arg( + long, + global = true, + env = "WOLLOSEUM_SERVER", + default_value = "http://127.0.0.1:8080" + )] + server: String, + /// Client certificate whose PEM fingerprint is the identity ID. + #[arg( + long, + global = true, + env = "WOLLOSEUM_CLIENT_CERT", + default_value = "data/certificates/admin-client.pem" + )] + client_cert: PathBuf, + /// Private key for the selected client certificate. + #[arg( + long, + global = true, + env = "WOLLOSEUM_CLIENT_KEY", + default_value = "data/certificates/admin-client-key.pem" + )] + client_key: PathBuf, + #[command(subcommand)] + command: ClientCommand, +} + +#[derive(Debug, Subcommand)] +enum ClientCommand { + /// Read the current game status. + Status, + /// Create a user and print their certificate and private key as JSON. + CreateUser { username: String }, + /// Delete a player by certificate ID. + DeleteUser { client_id: String }, + /// Pause the game. + StopGame, + /// Resume the game. + StartGame, + /// Upload or update this client identity's Extism WASM agent. + UpdateAgent { path: PathBuf }, +} + +pub async fn run(arguments: Arguments) -> Result<(), Box> { + match arguments.command { + TopLevelCommand::Serve(arguments) => { + crate::server::run(arguments.data_dir, arguments.bind).await + } + TopLevelCommand::Cli(arguments) => run_client(arguments).await, + } +} + +async fn run_client(arguments: ClientArguments) -> Result<(), Box> { + let client = Client::builder().build()?; + + let (method, path, body, needs_signature) = match arguments.command { + ClientCommand::Status => (Method::GET, "/api/status", Vec::new(), false), + ClientCommand::CreateUser { username } => ( + Method::POST, + "/api/create_user", + serde_json::to_vec(&CreateUserBody { username })?, + true, + ), + ClientCommand::DeleteUser { client_id } => ( + Method::POST, + "/api/delete_user", + serde_json::to_vec(&DeleteUserBody { client_id })?, + true, + ), + ClientCommand::StopGame => (Method::POST, "/api/stop_game", Vec::new(), true), + ClientCommand::StartGame => (Method::POST, "/api/start_game", Vec::new(), true), + ClientCommand::UpdateAgent { path } => { + (Method::POST, "/api/update_agent", fs::read(path)?, true) + } + }; + + let url = format!("{}{}", arguments.server.trim_end_matches('/'), path); + let mut request = client.request(method.clone(), url).body(body.clone()); + if needs_signature { + let certificate_pem = fs::read_to_string(&arguments.client_cert)?; + let key = KeyPair::from_pem(&fs::read_to_string(&arguments.client_key)?)?; + let signer = Ed25519KeyPair::from_pkcs8(&key.serialize_der()) + .map_err(|_| "client private key is not a valid Ed25519 PKCS#8 key")?; + let client_id = certificate_id(&certificate_pem); + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let nonce = uuid::Uuid::new_v4().to_string(); + let message = signature_message(method.as_str(), path, timestamp, &nonce, &body); + let signature = BASE64.encode(signer.sign(&message).as_ref()); + request = request + .header("x-client-id", client_id) + .header("x-request-timestamp", timestamp.to_string()) + .header("x-request-nonce", nonce) + .header("x-request-signature", signature); + } + + let response = request.send().await?; + let status = response.status(); + let body = response.bytes().await?; + if !status.is_success() { + return Err(format!( + "server returned {status}: {}", + String::from_utf8_lossy(&body) + ) + .into()); + } + if body.is_empty() { + println!("HTTP {status}"); + } else if let Ok(json) = serde_json::from_slice::(&body) { + println!("{}", serde_json::to_string_pretty(&json)?); + } else { + println!("{}", String::from_utf8_lossy(&body)); + } + Ok(()) +} + +#[derive(Serialize)] +struct CreateUserBody { + username: String, +} + +#[derive(Serialize)] +struct DeleteUserBody { + client_id: String, +} + +fn signature_message( + method: &str, + path: &str, + timestamp: u64, + nonce: &str, + body: &[u8], +) -> Vec { + let mut message = format!("{method}\n{path}\n{timestamp}\n{nonce}\n").into_bytes(); + message.extend_from_slice(body); + message +} + +fn certificate_id(certificate_pem: &str) -> String { + ring::digest::digest(&ring::digest::SHA256, certificate_pem.as_bytes()) + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +#[cfg(test)] +mod tests { + use super::Arguments; + use clap::Parser; + + #[test] + fn serve_and_cli_are_explicit_top_level_commands() { + assert!(Arguments::try_parse_from(["wolloseum"]).is_err()); + assert!(Arguments::try_parse_from(["wolloseum", "serve"]).is_ok()); + assert!(Arguments::try_parse_from(["wolloseum", "cli", "status"]).is_ok()); + } + + #[test] + fn cli_can_select_user_credentials_for_upload() { + assert!( + Arguments::try_parse_from([ + "wolloseum", + "cli", + "--client-cert", + "player.pem", + "--client-key", + "player-key.pem", + "update-agent", + "agent.wasm", + ]) + .is_ok() + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 0a887f2..7f8316a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,4 @@ +pub mod cli; +pub mod server; pub mod simulation; pub mod wasm_agent; diff --git a/src/main.rs b/src/main.rs index 3de5779..a899b73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,6 @@ -use std::collections::BTreeMap; -use wolloseum::simulation::{Action, AgentId, World}; +use clap::Parser; -fn main() -> Result<(), Box> { - let mut world = World::generate(0x574F4C4C, 32, 32)?; - let agent_id = AgentId(1); - world.spawn_agent(agent_id)?; - world.step(&BTreeMap::from([(agent_id, Action::Wait)]))?; - - let (grass, water, rock) = world.terrain_counts(); - let agent = world.agent(agent_id).expect("agent was just spawned"); - println!( - "Wolloseum seed={} map={}x{} tick={} terrain=({} grass, {} water, {} rock) agent={} at {:?}", - world.seed(), - world.dimensions().0, - world.dimensions().1, - world.tick(), - grass, - water, - rock, - agent.id().0, - agent.position(), - ); - Ok(()) +#[tokio::main] +async fn main() -> Result<(), Box> { + wolloseum::cli::run(wolloseum::cli::Arguments::parse()).await } diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..84b57fc --- /dev/null +++ b/src/server.rs @@ -0,0 +1,1232 @@ +use std::error::Error; +use std::fs::{self, OpenOptions}; +use std::io::Write; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use axum::body::Bytes; +use axum::extract::State; +use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade}; +use axum::http::{HeaderMap, Method, StatusCode}; +use axum::response::{Html, IntoResponse, Response}; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use axum_server::tls_rustls::RustlsConfig; +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD as BASE64; +use rcgen::{ + BasicConstraints, Certificate, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, + Issuer, KeyPair, KeyUsagePurpose, PKCS_ED25519, SanType, +}; +use ring::signature::{ED25519, UnparsedPublicKey}; +use rusqlite::{Connection, OptionalExtension, params}; +use serde::{Deserialize, Serialize}; +use tokio::sync::broadcast; + +use crate::simulation::{AgentObservation, ObservedTerrain}; +use crate::wasm_agent::WasmAgent; + +const MAX_WASM_BYTES: usize = 1_048_576; +const MAX_REQUEST_SKEW_SECONDS: u64 = 60; +const REQUEST_NONCE_MAX_BYTES: usize = 128; + +#[derive(Clone)] +struct AppState { + database: Arc>, + root_certificate_pem: Arc, + root_key_pem: Arc, + game_running: Arc, + event_sender: broadcast::Sender, + event_sequence: Arc>, +} + +#[derive(Clone, Debug, Serialize)] +struct StateEvent { + sequence: u64, + event: String, + data: serde_json::Value, +} + +struct Pki { + root_certificate_pem: String, + root_key_pem: String, + admin_id: String, + admin_public_key: Vec, + admin_certificate_pem: String, +} + +impl Pki { + fn initialize(directory: &Path) -> Result> { + fs::create_dir_all(directory)?; + set_private_directory_permissions(directory)?; + + let root_certificate_path = directory.join("root-ca.pem"); + let root_key_path = directory.join("root-ca-key.pem"); + let admin_certificate_path = directory.join("admin-client.pem"); + let admin_key_path = directory.join("admin-client-key.pem"); + let server_certificate_path = directory.join("server.pem"); + let server_key_path = directory.join("server-key.pem"); + + let root_certificate_exists = root_certificate_path.exists(); + let root_key_exists = root_key_path.exists(); + if root_certificate_exists != root_key_exists { + return Err("root CA certificate/key pair is incomplete".into()); + } + + if !root_certificate_exists { + if admin_certificate_path.exists() + || admin_key_path.exists() + || server_certificate_path.exists() + || server_key_path.exists() + { + return Err( + "certificate files exist without a root CA; refusing partial bootstrap".into(), + ); + } + let root_key = KeyPair::generate_for(&PKCS_ED25519)?; + let mut root_params = CertificateParams::default(); + root_params + .distinguished_name + .push(DnType::CommonName, "Wolloseum Root CA"); + root_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + root_params.key_usages = vec![ + KeyUsagePurpose::DigitalSignature, + KeyUsagePurpose::KeyCertSign, + KeyUsagePurpose::CrlSign, + ]; + let root_certificate = root_params.self_signed(&root_key)?; + let root_certificate_pem = root_certificate.pem(); + let root_key_pem = root_key.serialize_pem(); + write_new_file( + &root_certificate_path, + root_certificate_pem.as_bytes(), + false, + )?; + write_new_file(&root_key_path, root_key_pem.as_bytes(), true)?; + + let issuer = + Issuer::from_ca_cert_pem(&root_certificate_pem, KeyPair::from_pem(&root_key_pem)?)?; + let (admin_certificate, admin_key) = + issue_client_certificate("Wolloseum Admin", &issuer)?; + write_new_file( + &admin_certificate_path, + admin_certificate.pem().as_bytes(), + false, + )?; + write_new_file(&admin_key_path, admin_key.serialize_pem().as_bytes(), true)?; + } else if !admin_certificate_path.exists() || !admin_key_path.exists() { + return Err( + "admin certificate/key is missing; refusing to generate a replacement".into(), + ); + } + + let root_certificate_pem = fs::read_to_string(&root_certificate_path)?; + let root_key_pem = fs::read_to_string(&root_key_path)?; + let issuer = + Issuer::from_ca_cert_pem(&root_certificate_pem, KeyPair::from_pem(&root_key_pem)?)?; + + let server_certificate_exists = server_certificate_path.exists(); + let server_key_exists = server_key_path.exists(); + if server_certificate_exists != server_key_exists { + return Err("HTTPS server certificate/key pair is incomplete".into()); + } + if !server_certificate_exists { + let (certificate, key) = issue_server_certificate(&issuer)?; + write_new_file( + &server_certificate_path, + certificate.pem().as_bytes(), + false, + )?; + write_new_file(&server_key_path, key.serialize_pem().as_bytes(), true)?; + } + + let admin_certificate_pem = fs::read_to_string(&admin_certificate_path)?; + let admin_key = KeyPair::from_pem(&fs::read_to_string(&admin_key_path)?)?; + let admin_id = certificate_id(&admin_certificate_pem); + + Ok(Self { + root_certificate_pem, + root_key_pem, + admin_id, + admin_public_key: admin_key.public_key_raw().to_vec(), + admin_certificate_pem, + }) + } +} + +pub async fn run(data_directory: PathBuf, address: SocketAddr) -> Result<(), Box> { + let certificate_directory = data_directory.join("certificates"); + let pki = Pki::initialize(&certificate_directory)?; + let database_path = data_directory.join("wolloseum.sqlite3"); + let database = initialize_database( + &database_path, + &pki.admin_id, + &pki.admin_public_key, + &pki.admin_certificate_pem, + )?; + + eprintln!("Admin certificate ID: {}", pki.admin_id); + eprintln!( + "Admin client certificate: {}", + certificate_directory.join("admin-client.pem").display() + ); + eprintln!( + "Admin private key: {}", + certificate_directory.join("admin-client-key.pem").display() + ); + eprintln!( + "Root CA certificate: {}", + certificate_directory.join("root-ca.pem").display() + ); + + let state = AppState { + database: Arc::new(Mutex::new(database)), + root_certificate_pem: Arc::new(pki.root_certificate_pem), + root_key_pem: Arc::new(pki.root_key_pem), + game_running: Arc::new(AtomicBool::new(true)), + event_sender: broadcast::channel(256).0, + event_sequence: Arc::new(Mutex::new(0)), + }; + let app = Router::new() + .route("/", get(user_ui)) + .route("/admin", get(admin_ui)) + .route("/api/status", get(game_status)) + .route("/api/events", get(websocket_events)) + .route("/api/create_user", post(create_user)) + .route("/api/delete_user", post(delete_user)) + .route("/api/start_game", post(start_game)) + .route("/api/stop_game", post(stop_game)) + .route("/api/update_agent", post(update_agent)) + .route("/api/upload_core", post(upload_core)) + .layer(axum::extract::DefaultBodyLimit::max(MAX_WASM_BYTES)) + .with_state(state); + + eprintln!("Wolloseum HTTP listening on http://{address}"); + axum_server::bind(address) + .serve(app.into_make_service()) + .await?; + Ok(()) +} + +fn initialize_database( + path: &Path, + admin_id: &str, + admin_public_key: &[u8], + admin_certificate_pem: &str, +) -> Result> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + set_private_directory_permissions(parent)?; + } + let connection = Connection::open(path)?; + connection.pragma_update(None, "foreign_keys", "ON")?; + connection.pragma_update(None, "journal_mode", "WAL")?; + connection.execute_batch( + "CREATE TABLE IF NOT EXISTS identities ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + public_key BLOB NOT NULL, + certificate_pem TEXT NOT NULL, + is_admin INTEGER NOT NULL DEFAULT 0, + active INTEGER NOT NULL DEFAULT 1 + ); + CREATE TABLE IF NOT EXISTS used_nonces ( + identity_id TEXT NOT NULL REFERENCES identities(id) ON DELETE CASCADE, + nonce TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (identity_id, nonce) + ); + CREATE TABLE IF NOT EXISTS agents ( + identity_id TEXT PRIMARY KEY REFERENCES identities(id) ON DELETE CASCADE, + wasm BLOB NOT NULL, + sha256 TEXT NOT NULL, + uploaded_at INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS core_module ( + id INTEGER PRIMARY KEY CHECK (id = 1), + wasm BLOB NOT NULL, + sha256 TEXT NOT NULL, + uploaded_at INTEGER NOT NULL + );", + )?; + connection.execute( + "INSERT INTO identities (id, username, public_key, certificate_pem, is_admin, active) + VALUES (?1, 'admin', ?2, ?3, 1, 1) + ON CONFLICT(id) DO UPDATE SET public_key = excluded.public_key, + certificate_pem = excluded.certificate_pem, is_admin = 1, active = 1", + params![admin_id, admin_public_key, admin_certificate_pem], + )?; + Ok(connection) +} + +#[derive(Debug)] +struct ApiError(StatusCode, String); + +impl ApiError { + fn bad_request(message: impl Into) -> Self { + Self(StatusCode::BAD_REQUEST, message.into()) + } + + fn unauthorized() -> Self { + Self( + StatusCode::UNAUTHORIZED, + "invalid or missing request signature".to_string(), + ) + } + + fn forbidden() -> Self { + Self( + StatusCode::FORBIDDEN, + "admin certificate required".to_string(), + ) + } + + fn internal(error: impl std::fmt::Display) -> Self { + Self(StatusCode::INTERNAL_SERVER_ERROR, error.to_string()) + } +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + (self.0, Json(serde_json::json!({ "error": self.1 }))).into_response() + } +} + +#[derive(Serialize)] +struct ApiIdentity { + client_id: String, + certificate_pem: String, + private_key_pem: String, +} + +#[derive(Deserialize)] +struct CreateUserRequest { + username: String, +} + +#[derive(Deserialize)] +struct DeleteUserRequest { + client_id: String, +} + +#[derive(Serialize)] +struct GameStatus { + running: bool, +} + +fn authenticate( + state: &AppState, + headers: &HeaderMap, + method: &Method, + path: &str, + body: &[u8], + require_admin: bool, +) -> Result { + let client_id = header(headers, "x-client-id")?; + let timestamp = header(headers, "x-request-timestamp")? + .parse::() + .map_err(|_| ApiError::unauthorized())?; + let nonce = header(headers, "x-request-nonce")?; + let signature = BASE64 + .decode(header(headers, "x-request-signature")?) + .map_err(|_| ApiError::unauthorized())?; + let now = unix_time(); + if timestamp.abs_diff(now) > MAX_REQUEST_SKEW_SECONDS + || nonce.is_empty() + || nonce.len() > REQUEST_NONCE_MAX_BYTES + || !nonce + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || b"-_".contains(&byte)) + { + return Err(ApiError::unauthorized()); + } + + let (public_key, is_admin, active) = { + let database = state.database.lock().map_err(ApiError::internal)?; + database + .query_row( + "SELECT public_key, is_admin, active FROM identities WHERE id = ?1", + [client_id], + |row| { + Ok(( + row.get::<_, Vec>(0)?, + row.get::<_, bool>(1)?, + row.get::<_, bool>(2)?, + )) + }, + ) + .optional() + .map_err(ApiError::internal)? + .ok_or_else(ApiError::unauthorized)? + }; + if !active { + return Err(ApiError::unauthorized()); + } + if require_admin && !is_admin { + return Err(ApiError::forbidden()); + } + + let signed_message = request_signature_message(method, path, timestamp, nonce, body); + if !verify_signature(&public_key, &signed_message, &signature) { + return Err(ApiError::unauthorized()); + } + + let mut database = state.database.lock().map_err(ApiError::internal)?; + let transaction = database.transaction().map_err(ApiError::internal)?; + transaction + .execute( + "DELETE FROM used_nonces WHERE created_at < ?1", + [now.saturating_sub(120) as i64], + ) + .map_err(ApiError::internal)?; + transaction + .execute( + "INSERT INTO used_nonces (identity_id, nonce, created_at) VALUES (?1, ?2, ?3)", + params![client_id, nonce, now as i64], + ) + .map_err(|_| ApiError::unauthorized())?; + transaction.commit().map_err(ApiError::internal)?; + Ok(client_id.to_string()) +} + +fn header<'a>(headers: &'a HeaderMap, name: &'static str) -> Result<&'a str, ApiError> { + headers + .get(name) + .and_then(|value| value.to_str().ok()) + .ok_or_else(ApiError::unauthorized) +} + +pub fn request_signature_message( + method: &Method, + path: &str, + timestamp: u64, + nonce: &str, + body: &[u8], +) -> Vec { + let mut message = + format!("{}\n{}\n{}\n{}\n", method.as_str(), path, timestamp, nonce).into_bytes(); + message.extend_from_slice(body); + message +} + +async fn create_user( + State(state): State, + headers: HeaderMap, + body: Bytes, +) -> Result, ApiError> { + authenticate( + &state, + &headers, + &Method::POST, + "/api/create_user", + &body, + true, + )?; + let request: CreateUserRequest = + serde_json::from_slice(&body).map_err(|error| ApiError::bad_request(error.to_string()))?; + let username = request.username.trim(); + if username.is_empty() + || username.len() > 32 + || !username + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || b"-_".contains(&byte)) + { + return Err(ApiError::bad_request( + "username must be 1-32 ASCII letters, numbers, '-' or '_'", + )); + } + + let root_key = KeyPair::from_pem(&state.root_key_pem).map_err(ApiError::internal)?; + let issuer = Issuer::from_ca_cert_pem(&state.root_certificate_pem, root_key) + .map_err(ApiError::internal)?; + let (certificate, key) = + issue_client_certificate(username, &issuer).map_err(ApiError::internal)?; + let certificate_pem = certificate.pem(); + let client_id = certificate_id(&certificate_pem); + let public_key = key.public_key_raw().to_vec(); + let private_key_pem = key.serialize_pem(); + + let database = state.database.lock().map_err(ApiError::internal)?; + database + .execute( + "INSERT INTO identities (id, username, public_key, certificate_pem) VALUES (?1, ?2, ?3, ?4)", + params![client_id, username, public_key, certificate_pem], + ) + .map_err(|error| { + if error.sqlite_error_code() == Some(rusqlite::ErrorCode::ConstraintViolation) { + ApiError(StatusCode::CONFLICT, "username already exists".to_string()) + } else { + ApiError::internal(error) + } + })?; + publish_state_event(&state, "user_created", serde_json::json!({})); + + Ok(Json(ApiIdentity { + client_id, + certificate_pem, + private_key_pem, + })) +} + +async fn delete_user( + State(state): State, + headers: HeaderMap, + body: Bytes, +) -> Result { + authenticate( + &state, + &headers, + &Method::POST, + "/api/delete_user", + &body, + true, + )?; + let request: DeleteUserRequest = + serde_json::from_slice(&body).map_err(|error| ApiError::bad_request(error.to_string()))?; + let database = state.database.lock().map_err(ApiError::internal)?; + let changed = database + .execute( + "DELETE FROM identities WHERE id = ?1 AND is_admin = 0", + [&request.client_id], + ) + .map_err(ApiError::internal)?; + if changed == 0 { + return Err(ApiError( + StatusCode::NOT_FOUND, + "player not found".to_string(), + )); + } + publish_state_event(&state, "user_deleted", serde_json::json!({})); + Ok(StatusCode::NO_CONTENT) +} + +async fn stop_game( + State(state): State, + headers: HeaderMap, + body: Bytes, +) -> Result, ApiError> { + authenticate( + &state, + &headers, + &Method::POST, + "/api/stop_game", + &body, + true, + )?; + set_game_running(&state, false); + Ok(Json(GameStatus { running: false })) +} + +async fn start_game( + State(state): State, + headers: HeaderMap, + body: Bytes, +) -> Result, ApiError> { + authenticate( + &state, + &headers, + &Method::POST, + "/api/start_game", + &body, + true, + )?; + set_game_running(&state, true); + Ok(Json(GameStatus { running: true })) +} + +async fn update_agent( + State(state): State, + headers: HeaderMap, + body: Bytes, +) -> Result, ApiError> { + let client_id = authenticate( + &state, + &headers, + &Method::POST, + "/api/update_agent", + &body, + false, + )?; + if body.len() > MAX_WASM_BYTES { + return Err(ApiError( + StatusCode::PAYLOAD_TOO_LARGE, + "WASM module exceeds 1 MiB".to_string(), + )); + } + let mut agent = + WasmAgent::load(&body).map_err(|error| ApiError::bad_request(error.to_string()))?; + agent + .decide(AgentObservation { + tick: 0, + x: 0, + y: 0, + adjacent: [ObservedTerrain::Boundary; 4], + }) + .map_err(|error| ApiError::bad_request(error.to_string()))?; + + let digest = ring::digest::digest(&ring::digest::SHA256, &body); + let sha256 = digest + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + let database = state.database.lock().map_err(ApiError::internal)?; + database + .execute( + "INSERT INTO agents (identity_id, wasm, sha256, uploaded_at) VALUES (?1, ?2, ?3, ?4) + ON CONFLICT(identity_id) DO UPDATE SET wasm = excluded.wasm, + sha256 = excluded.sha256, uploaded_at = excluded.uploaded_at", + params![client_id, body.as_ref(), sha256, unix_time() as i64], + ) + .map_err(ApiError::internal)?; + publish_state_event( + &state, + "agent_uploaded", + serde_json::json!({ "bytes": body.len() }), + ); + + Ok(Json( + serde_json::json!({ "sha256": sha256, "bytes": body.len() }), + )) +} + +async fn upload_core( + State(state): State, + headers: HeaderMap, + body: Bytes, +) -> Result, ApiError> { + authenticate( + &state, + &headers, + &Method::POST, + "/api/upload_core", + &body, + true, + )?; + if body.len() > MAX_WASM_BYTES { + return Err(ApiError( + StatusCode::PAYLOAD_TOO_LARGE, + "WASM module exceeds 1 MiB".to_string(), + )); + } + // Sandboxed load validates the module without requiring agent-specific exports. + WasmAgent::load(&body).map_err(|error| ApiError::bad_request(error.to_string()))?; + + let digest = ring::digest::digest(&ring::digest::SHA256, &body); + let sha256 = digest + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + let database = state.database.lock().map_err(ApiError::internal)?; + database + .execute( + "INSERT INTO core_module (id, wasm, sha256, uploaded_at) VALUES (1, ?1, ?2, ?3) + ON CONFLICT(id) DO UPDATE SET wasm = excluded.wasm, + sha256 = excluded.sha256, uploaded_at = excluded.uploaded_at", + params![body.as_ref(), sha256, unix_time() as i64], + ) + .map_err(ApiError::internal)?; + publish_state_event( + &state, + "core_uploaded", + serde_json::json!({ "bytes": body.len() }), + ); + + Ok(Json( + serde_json::json!({ "sha256": sha256, "bytes": body.len() }), + )) +} + +async fn game_status(State(state): State) -> Json { + Json(GameStatus { + running: state.game_running.load(Ordering::SeqCst), + }) +} + +async fn websocket_events(State(state): State, upgrade: WebSocketUpgrade) -> Response { + let receiver = state.event_sender.subscribe(); + upgrade.on_upgrade(move |socket| stream_events(socket, receiver, state)) +} + +async fn stream_events( + mut socket: WebSocket, + mut receiver: broadcast::Receiver, + state: AppState, +) { + let snapshot = snapshot_event(&state); + let mut last_sequence = snapshot.sequence; + if send_event(&mut socket, &snapshot).await.is_err() { + return; + } + + loop { + tokio::select! { + incoming = socket.recv() => match incoming { + Some(Ok(Message::Close(_))) | None | Some(Err(_)) => break, + Some(Ok(Message::Ping(payload))) => { + if socket.send(Message::Pong(payload)).await.is_err() { + break; + } + } + Some(Ok(_)) => {} + }, + event = receiver.recv() => match event { + Ok(event) => { + if event.sequence <= last_sequence { + continue; + } + last_sequence = event.sequence; + if send_event(&mut socket, &event).await.is_err() { + break; + } + } + Err(broadcast::error::RecvError::Lagged(_)) => { + let snapshot = snapshot_event(&state); + last_sequence = snapshot.sequence; + if send_event(&mut socket, &snapshot).await.is_err() { + break; + } + } + Err(broadcast::error::RecvError::Closed) => break, + } + } + } +} + +async fn send_event(socket: &mut WebSocket, event: &StateEvent) -> Result<(), axum::Error> { + let message = serde_json::to_string(event).expect("state events are serializable"); + socket.send(Message::Text(message.into())).await +} + +fn snapshot_event(state: &AppState) -> StateEvent { + let sequence = *state + .event_sequence + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + StateEvent { + sequence, + event: "snapshot".to_string(), + data: serde_json::json!({ + "running": state.game_running.load(Ordering::SeqCst), + "sequence": sequence, + }), + } +} + +fn publish_state_event(state: &AppState, event: &str, data: serde_json::Value) { + let mut sequence = state + .event_sequence + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + publish_state_event_locked(state, &mut sequence, event, data); +} + +fn set_game_running(state: &AppState, running: bool) { + let mut sequence = state + .event_sequence + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if state.game_running.swap(running, Ordering::SeqCst) == running { + return; + } + publish_state_event_locked( + state, + &mut sequence, + "game_state_changed", + serde_json::json!({ "running": running }), + ); +} + +fn publish_state_event_locked( + state: &AppState, + sequence: &mut u64, + event: &str, + data: serde_json::Value, +) { + *sequence = sequence.saturating_add(1); + let _ = state.event_sender.send(StateEvent { + sequence: *sequence, + event: event.to_string(), + data, + }); +} + +async fn user_ui() -> Html<&'static str> { + Html(USER_UI) +} + +async fn admin_ui() -> Html<&'static str> { + Html(ADMIN_UI) +} + +const USER_UI: &str = r#" + +Wolloseum +

Wolloseum

Connecting to live updates...

Administration

+

Agent upload

Authenticate with your issued certificate and sign the exact WASM request body.

+
POST /api/update_agent
+X-Client-Id, X-Request-Timestamp, X-Request-Nonce, X-Request-Signature
+

Live state changes

    + +"#; + +const ADMIN_UI: &str = r#" + +Wolloseum Admin +

    Wolloseum administration

    Player view

    Connecting to live updates...

    +

    Sign each API request with the admin client key. The signed message is method, path, timestamp, nonce, then the exact body bytes.

    +
    • POST /api/create_user
    • POST /api/delete_user
    • POST /api/stop_game
    • POST /api/start_game
    • POST /api/upload_core
    +
    Headers: X-Client-Id, X-Request-Timestamp, X-Request-Nonce, X-Request-Signature
    +

    Live state changes

      + +"#; + +fn issue_client_certificate( + name: &str, + issuer: &Issuer<'_, KeyPair>, +) -> Result<(Certificate, KeyPair), Box> { + let key = KeyPair::generate_for(&PKCS_ED25519)?; + let mut params = CertificateParams::new(Vec::::new())?; + params.distinguished_name.push(DnType::CommonName, name); + params.use_authority_key_identifier_extension = true; + params.key_usages.push(KeyUsagePurpose::DigitalSignature); + params + .extended_key_usages + .push(ExtendedKeyUsagePurpose::ClientAuth); + let certificate = params.signed_by(&key, issuer)?; + Ok((certificate, key)) +} + +fn issue_server_certificate( + issuer: &Issuer<'_, KeyPair>, +) -> Result<(Certificate, KeyPair), Box> { + let key = KeyPair::generate_for(&PKCS_ED25519)?; + let mut params = CertificateParams::new(vec!["localhost".to_string()])?; + params + .distinguished_name + .push(DnType::CommonName, "localhost"); + params + .subject_alt_names + .push(SanType::IpAddress(IpAddr::V4(Ipv4Addr::LOCALHOST))); + params.use_authority_key_identifier_extension = true; + params.key_usages.push(KeyUsagePurpose::DigitalSignature); + params + .extended_key_usages + .push(ExtendedKeyUsagePurpose::ServerAuth); + let certificate = params.signed_by(&key, issuer)?; + Ok((certificate, key)) +} + +fn certificate_id(certificate_pem: &str) -> String { + let digest = ring::digest::digest(&ring::digest::SHA256, certificate_pem.as_bytes()); + digest + .as_ref() + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn unix_time() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + +fn write_new_file(path: &Path, contents: &[u8], private: bool) -> Result<(), Box> { + let mut options = OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + if private { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let mut file = options.open(path)?; + file.write_all(contents)?; + file.sync_all()?; + Ok(()) +} + +#[cfg(unix)] +fn set_private_directory_permissions(path: &Path) -> std::io::Result<()> { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(path, fs::Permissions::from_mode(0o700)) +} + +#[cfg(not(unix))] +fn set_private_directory_permissions(_path: &Path) -> std::io::Result<()> { + Ok(()) +} + +fn verify_signature(public_key: &[u8], message: &[u8], signature: &[u8]) -> bool { + UnparsedPublicKey::new(&ED25519, public_key) + .verify(message, signature) + .is_ok() +} + +#[cfg(test)] +mod tests { + use super::{ + AppState, Pki, authenticate, certificate_id, create_user, delete_user, + request_signature_message, start_game, stop_game, unix_time, update_agent, + verify_signature, + }; + use axum::Json; + use axum::body::Bytes; + use axum::extract::State; + use axum::http::{HeaderMap, HeaderValue, Method}; + use base64::Engine as _; + use base64::engine::general_purpose::STANDARD as BASE64; + use rcgen::{KeyPair, PKCS_ED25519}; + use ring::signature::Ed25519KeyPair; + use std::fs; + use std::path::Path; + use std::sync::atomic::AtomicBool; + use std::sync::{Arc, Mutex}; + + fn test_state(directory: &Path) -> (AppState, String, KeyPair) { + let cert_directory = directory.join("certificates"); + let pki = Pki::initialize(&cert_directory).unwrap(); + let admin_id = pki.admin_id.clone(); + let admin_key = KeyPair::from_pem( + &fs::read_to_string(cert_directory.join("admin-client-key.pem")).unwrap(), + ) + .unwrap(); + let database = super::initialize_database( + &directory.join("wolloseum.sqlite3"), + &pki.admin_id, + &pki.admin_public_key, + &pki.admin_certificate_pem, + ) + .unwrap(); + let state = AppState { + database: Arc::new(Mutex::new(database)), + root_certificate_pem: Arc::new(pki.root_certificate_pem), + root_key_pem: Arc::new(pki.root_key_pem), + game_running: Arc::new(AtomicBool::new(true)), + event_sender: tokio::sync::broadcast::channel(256).0, + event_sequence: Arc::new(Mutex::new(0)), + }; + (state, admin_id, admin_key) + } + + fn signed_headers( + client_id: &str, + key: &KeyPair, + method: &Method, + path: &str, + body: &[u8], + nonce: &str, + ) -> HeaderMap { + let timestamp = unix_time(); + let message = super::request_signature_message(method, path, timestamp, nonce, body); + let key = Ed25519KeyPair::from_pkcs8(&key.serialize_der()).unwrap(); + let signature = BASE64.encode(key.sign(&message).as_ref()); + let mut headers = HeaderMap::new(); + headers.insert("x-client-id", HeaderValue::from_str(client_id).unwrap()); + headers.insert( + "x-request-timestamp", + HeaderValue::from_str(×tamp.to_string()).unwrap(), + ); + headers.insert("x-request-nonce", HeaderValue::from_str(nonce).unwrap()); + headers.insert( + "x-request-signature", + HeaderValue::from_str(&signature).unwrap(), + ); + headers + } + + fn valid_agent_wasm() -> Vec { + let output = br#"{"abi_version":1,"action":0}"#; + let stores = output + .chunks(8) + .enumerate() + .map(|(index, chunk)| { + let mut bytes = [0; 8]; + bytes[..chunk.len()].copy_from_slice(chunk); + format!( + "(call $store_u64 (i64.add (local.get $output) (i64.const {})) (i64.const {}))", + index * 8, + i64::from_le_bytes(bytes) + ) + }) + .collect::(); + let source = format!( + r#"(module + (import "extism:host/env" "alloc" (func $alloc (param i64) (result i64))) + (import "extism:host/env" "store_u64" (func $store_u64 (param i64 i64))) + (import "extism:host/env" "output_set" (func $output_set (param i64 i64))) + (func (export "decide") (result i32) + (local $output i64) + (local.set $output (call $alloc (i64.const {}))) + {stores} + (call $output_set (local.get $output) (i64.const {})) + (i32.const 0) + ) + )"#, + output.len(), + output.len() + ); + wat::parse_str(source).unwrap() + } + + #[test] + fn signatures_bind_the_exact_request_and_nonce() { + let key = KeyPair::generate_for(&PKCS_ED25519).unwrap(); + let signer = Ed25519KeyPair::from_pkcs8(&key.serialize_der()).unwrap(); + let message = request_signature_message( + &axum::http::Method::POST, + "/api/update_agent", + 1_700_000_000, + "nonce-1", + b"wasm blob", + ); + let signature = signer.sign(&message); + + assert!(verify_signature( + key.public_key_raw(), + &message, + signature.as_ref() + )); + assert!(!verify_signature( + key.public_key_raw(), + &request_signature_message( + &axum::http::Method::POST, + "/api/update_agent", + 1_700_000_000, + "nonce-2", + b"wasm blob", + ), + signature.as_ref(), + )); + } + + #[tokio::test] + async fn create_user_registers_a_signing_identity_and_game_controls_are_admin_only() { + let directory = tempfile::tempdir().unwrap(); + let (state, admin_id, admin_key) = test_state(directory.path()); + let mut events = state.event_sender.subscribe(); + let snapshot = super::snapshot_event(&state); + assert_eq!(snapshot.sequence, 0); + assert_eq!(snapshot.event, "snapshot"); + let body = br#"{"username":"player1"}"#; + let headers = signed_headers( + &admin_id, + &admin_key, + &Method::POST, + "/api/create_user", + body, + "admin-create-1", + ); + let Json(identity) = + create_user(State(state.clone()), headers, Bytes::copy_from_slice(body)) + .await + .unwrap(); + + assert!(identity.certificate_pem.contains("BEGIN CERTIFICATE")); + let user_key = KeyPair::from_pem(&identity.private_key_pem).unwrap(); + let upload_body = b"signed wasm bytes"; + let user_headers = signed_headers( + &identity.client_id, + &user_key, + &Method::POST, + "/api/update_agent", + upload_body, + "player-upload-1", + ); + assert_eq!( + authenticate( + &state, + &user_headers, + &Method::POST, + "/api/update_agent", + upload_body, + false, + ) + .unwrap(), + identity.client_id + ); + assert!( + authenticate( + &state, + &user_headers, + &Method::POST, + "/api/update_agent", + upload_body, + false, + ) + .is_err() + ); + + let stored_public_key: Vec = state + .database + .lock() + .unwrap() + .query_row( + "SELECT public_key FROM identities WHERE id = ?1", + [&identity.client_id], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(stored_public_key, user_key.public_key_raw()); + + let wasm = valid_agent_wasm(); + let upload_headers = signed_headers( + &identity.client_id, + &user_key, + &Method::POST, + "/api/update_agent", + &wasm, + "player-upload-2", + ); + let Json(upload_result) = update_agent( + State(state.clone()), + upload_headers, + Bytes::from(wasm.clone()), + ) + .await + .unwrap(); + assert_eq!(upload_result["bytes"], wasm.len()); + let stored_wasm: Vec = state + .database + .lock() + .unwrap() + .query_row( + "SELECT wasm FROM agents WHERE identity_id = ?1", + [&identity.client_id], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(stored_wasm, wasm); + + let stop_body = Bytes::new(); + let stop_headers = signed_headers( + &admin_id, + &admin_key, + &Method::POST, + "/api/stop_game", + &stop_body, + "admin-stop-1", + ); + let Json(status) = stop_game(State(state.clone()), stop_headers, stop_body) + .await + .unwrap(); + assert!(!status.running); + + let start_body = Bytes::new(); + let start_headers = signed_headers( + &admin_id, + &admin_key, + &Method::POST, + "/api/start_game", + &start_body, + "admin-start-1", + ); + let Json(status) = start_game(State(state.clone()), start_headers, start_body) + .await + .unwrap(); + assert!(status.running); + + let user_stop_body = Bytes::new(); + let user_stop_headers = signed_headers( + &identity.client_id, + &user_key, + &Method::POST, + "/api/stop_game", + &user_stop_body, + "player-stop-1", + ); + assert!( + stop_game(State(state.clone()), user_stop_headers, user_stop_body) + .await + .is_err() + ); + + let delete_body = serde_json::to_vec(&serde_json::json!({ + "client_id": identity.client_id, + })) + .unwrap(); + let delete_headers = signed_headers( + &admin_id, + &admin_key, + &Method::POST, + "/api/delete_user", + &delete_body, + "admin-delete-1", + ); + delete_user( + State(state.clone()), + delete_headers, + Bytes::from(delete_body), + ) + .await + .unwrap(); + let player_count: i64 = state + .database + .lock() + .unwrap() + .query_row( + "SELECT COUNT(*) FROM identities WHERE id = ?1", + [&identity.client_id], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(player_count, 0); + let agent_count: i64 = state + .database + .lock() + .unwrap() + .query_row( + "SELECT COUNT(*) FROM agents WHERE identity_id = ?1", + [&identity.client_id], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(agent_count, 0); + + for (sequence, expected_event) in [ + "user_created", + "agent_uploaded", + "game_state_changed", + "game_state_changed", + "user_deleted", + ] + .into_iter() + .enumerate() + { + let event = events.try_recv().unwrap(); + assert_eq!(event.sequence, sequence as u64 + 1); + assert_eq!(event.event, expected_event); + assert!(!event.data.to_string().contains(&identity.client_id)); + } + assert!(matches!( + events.try_recv(), + Err(tokio::sync::broadcast::error::TryRecvError::Empty) + )); + } + + #[test] + fn certificate_id_is_stable() { + let cert = "-----BEGIN CERTIFICATE-----\nfixture\n-----END CERTIFICATE-----\n"; + assert_eq!(certificate_id(cert), certificate_id(cert)); + } + + #[test] + fn key_permissions_are_private_on_unix() { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("private.pem"); + super::write_new_file(&path, b"secret", true).unwrap(); + assert_eq!( + fs::metadata(path).unwrap().permissions().mode() & 0o777, + 0o600 + ); + } + } +} diff --git a/src/wasm_agent.rs b/src/wasm_agent.rs index c1201f4..1a9fbce 100644 --- a/src/wasm_agent.rs +++ b/src/wasm_agent.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::simulation::{Action, AgentObservation, Direction, ObservedTerrain}; const MAX_MODULE_BYTES: usize = 1_048_576; -const MAX_MEMORY_PAGES: u32 = 16; +const MAX_MEMORY_PAGES: u32 = 32; pub const FUEL_PER_DECISION: u64 = 100_000; pub const AGENT_ABI_VERSION: u16 = 1; @@ -147,7 +147,7 @@ mod tests { }) .collect::(); let memory_check = if check_memory_limit { - "(local.set $previous (memory.grow (i32.const 16))) (local.get $previous) (i32.const -1) (i32.ne) (if (then (unreachable)))" + "(local.set $previous (memory.grow (i32.const 64))) (local.get $previous) (i32.const -1) (i32.ne) (if (then (unreachable)))" } else { "" };