diff --git a/Cargo.lock b/Cargo.lock index 0629e42..0e976d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -16,7 +25,7 @@ checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" dependencies = [ "cipher", "cpubits", - "cpufeatures", + "cpufeatures 0.3.1", ] [[package]] @@ -28,6 +37,30 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + [[package]] name = "arc-swap" version = "1.9.2" @@ -49,6 +82,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -160,6 +204,9 @@ name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] [[package]] name = "bytes" @@ -170,6 +217,24 @@ dependencies = [ "serde", ] +[[package]] +name = "cap-primitives" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes 3.0.1", + "ipnet", + "maybe-owned", + "rustix", + "rustix-linux-procfs", + "windows-sys 0.61.2", + "winx", +] + [[package]] name = "cc" version = "1.4.5" @@ -177,6 +242,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -208,7 +275,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.1", "rand_core", ] @@ -228,6 +295,15 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.20", +] + [[package]] name = "colored" version = "3.1.1" @@ -285,12 +361,36 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpp_demangle" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpubits" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "cpufeatures" version = "0.3.1" @@ -300,6 +400,139 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-assembler-x64" +version = "0.136.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.136.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.136.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.136.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.136.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.17.1", + "libm", + "log", + "postcard", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "serde_derive", + "sha2 0.10.9", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.136.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.136.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" + +[[package]] +name = "cranelift-control" +version = "0.136.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.136.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.136.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "cranelift-codegen", + "hashbrown 0.17.1", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.136.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" + +[[package]] +name = "cranelift-native" +version = "0.136.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.136.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" + [[package]] name = "crc-fast" version = "1.10.0" @@ -440,7 +673,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.1", "curve25519-dalek-derive", "digest 0.11.3", "fiat-crypto", @@ -506,6 +739,27 @@ dependencies = [ "ctutils", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "displaydoc" version = "0.2.7" @@ -549,7 +803,7 @@ checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ "curve25519-dalek", "ed25519", - "sha2", + "sha2 0.11.0", "subtle", ] @@ -579,6 +833,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a7a45518d2863d18aa47f4a0cf9faec2aa4304cc09df5e41299f276b3ad135e" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -640,6 +921,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -649,6 +936,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes 2.0.4", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "futures" version = "0.3.34" @@ -758,6 +1056,18 @@ dependencies = [ "rand_core", ] +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + [[package]] name = "group" version = "0.14.0" @@ -794,11 +1104,22 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] [[package]] name = "heck" @@ -1020,6 +1341,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -1049,6 +1376,8 @@ checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] @@ -1060,12 +1389,43 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "io-extras" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" +dependencies = [ + "io-lifetimes 3.0.1", + "windows-sys 0.52.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "io-lifetimes" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" + [[package]] name = "ipnet" version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.15.0" @@ -1093,7 +1453,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror", + "thiserror 2.0.20", "walkdir", "windows-link", ] @@ -1130,6 +1490,16 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.105" @@ -1147,12 +1517,33 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed" +dependencies = [ + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1196,6 +1587,12 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + [[package]] name = "matchers" version = "0.2.0" @@ -1211,6 +1608,12 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "md-5" version = "0.10.6" @@ -1233,6 +1636,15 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memfd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" +dependencies = [ + "rustix", +] + [[package]] name = "mime" version = "0.3.17" @@ -1260,6 +1672,34 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multiversion" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edb7f0ff51249dfda9ab96b5823695e15a052dc15074c9dbf3d118afaf2c201" +dependencies = [ + "multiversion-macros", + "target-features", +] + +[[package]] +name = "multiversion-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b093064383341eb3271f42e381cb8f10a01459478446953953c75d24bd339fc0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "target-features", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "ntapi" version = "0.4.3" @@ -1336,6 +1776,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "object" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" +dependencies = [ + "crc32fast", + "hashbrown 0.17.1", + "indexmap", + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1359,7 +1811,7 @@ dependencies = [ "fiat-crypto", "primefield", "primeorder", - "sha2", + "sha2 0.11.0", ] [[package]] @@ -1469,6 +1921,24 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.6" @@ -1535,6 +2005,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulley-interpreter" +version = "49.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "49.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pumpkin-codecs" version = "0.1.0+26.2-26.45" @@ -1601,11 +2092,22 @@ dependencies = [ "cesu8", "flate2", "pumpkin-codecs", - "thiserror", + "thiserror 2.0.20", "tracing", "uuid", ] +[[package]] +name = "pumpkin-plugin-runtime" +version = "0.1.0+26.2-26.45" +dependencies = [ + "futures", + "thiserror 2.0.20", + "tokio", + "tracing", + "wasmtime", +] + [[package]] name = "pumpkin-protocol" version = "0.1.0+26.2-26.45" @@ -1625,7 +2127,7 @@ dependencies = [ "pumpkin-world", "serde", "serde_json", - "thiserror", + "thiserror 2.0.20", "tokio", "uuid", "xxhash-rust", @@ -1652,8 +2154,8 @@ dependencies = [ "rustls", "serde", "serde_json", - "sha2", - "thiserror", + "sha2 0.11.0", + "thiserror 2.0.20", "uuid", "webpki-root-certs", ] @@ -1669,7 +2171,7 @@ dependencies = [ "dashmap", "flate2", "futures", - "itertools", + "itertools 0.15.0", "lz4-java-wrc", "pumpkin-config", "pumpkin-data", @@ -1681,9 +2183,9 @@ dependencies = [ "ruzstd", "serde", "serde_json", - "sha2", + "sha2 0.11.0", "slotmap", - "thiserror", + "thiserror 2.0.20", "tokio", "tokio-util", "tracing", @@ -1752,6 +2254,32 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "serde", + "smallvec", +] + [[package]] name = "regex-automata" version = "0.4.18" @@ -1848,6 +2376,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + [[package]] name = "rustc-hash" version = "2.1.3" @@ -1876,6 +2410,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix", +] + [[package]] name = "rustls" version = "0.23.44" @@ -2028,6 +2572,10 @@ name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -2098,10 +2646,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.1", "digest 0.11.3", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.11.0" @@ -2109,7 +2668,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.1", "digest 0.11.3", ] @@ -2196,6 +2755,9 @@ name = "smallvec" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -2297,6 +2859,18 @@ dependencies = [ "windows", ] +[[package]] +name = "target-features" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1bbb9f3c5c463a01705937a24fdabc5047929ac764b2d5b9cf681c1f5041ed5" + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tempfile" version = "3.27.0" @@ -2310,13 +2884,42 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -2636,6 +3239,7 @@ dependencies = [ "hmac", "num-bigint", "pumpkin-command", + "pumpkin-plugin-runtime", "pumpkin-protocol", "pumpkin-util", "rand", @@ -2644,16 +3248,18 @@ dependencies = [ "serde", "serde_json", "sha1", - "sha2", + "sha2 0.11.0", "socket2", "sysinfo", "tempfile", - "thiserror", + "thiserror 2.0.20", "tokio", "toml", "tracing", "tracing-subscriber", "uuid", + "wasmtime", + "wasmtime-wasi", ] [[package]] @@ -2736,6 +3342,298 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e974fe6821a8cf64575d51ea2194e2c8f77e7b66e9afe7419ce8a97f9ee0d251" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a11585adb92fe9b55ad1d760e8d8fb5d87e0d2e303cb8eed57f078d54293a2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a61719f93a87b16d325921e251800c4833f8fab50fa21c7de73aed50086313" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmprinter" +version = "0.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a65e30fb2cd3cc5cb7761a6064787b2abf017e6566d8d0dedf2bb7f55ad5383" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser", +] + +[[package]] +name = "wasmtime" +version = "49.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "addr2line", + "async-trait", + "bitflags", + "bumpalo", + "bytes", + "cc", + "encoding_rs", + "futures", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rustix", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasmparser", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-environ" +version = "49.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.17.1", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2 0.10.9", + "smallvec", + "target-lexicon", + "wasm-encoder", + "wasmparser", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-cache" +version = "49.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "base64 0.22.1", + "directories-next", + "log", + "postcard", + "rustix", + "serde", + "serde_derive", + "sha2 0.10.9", + "toml", + "wasmtime-environ", + "windows-sys 0.61.2", + "zstd", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "49.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "49.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" + +[[package]] +name = "wasmtime-internal-core" +version = "49.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "hashbrown 0.17.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "49.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools 0.14.0", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.20", + "wasmparser", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "49.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "cc", + "libc", + "rustix", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "49.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "cc", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "49.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "49.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "49.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "49.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "anyhow", + "bitflags", + "heck", + "indexmap", + "wit-component", + "wit-parser", +] + +[[package]] +name = "wasmtime-wasi" +version = "49.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "async-trait", + "bitflags", + "bytes", + "cap-primitives", + "futures", + "rand", + "rustix", + "rustix-linux-procfs", + "thiserror 2.0.20", + "tokio", + "tracing", + "url", + "wasmtime", + "wasmtime-wasi-io", + "windows-sys 0.61.2", + "winx", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "49.0.0-dev" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=f6b3140dbb1d9b436ef523d14740292c956b0364#f6b3140dbb1d9b436ef523d14740292c956b0364" +dependencies = [ + "async-trait", + "bytes", + "futures", + "tracing", + "wasmtime", +] + [[package]] name = "web-sys" version = "0.3.105" @@ -2984,6 +3882,54 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags", + "windows-sys 0.52.0", +] + +[[package]] +name = "wit-component" +version = "0.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "481b5c47b2ecce0389b5e08a05557d6a190c9cd761773b8880a8017ee04dc7ef" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4daaa3cd97ae49ecd0a99dc009d453f93e0f083dd3be38c0f24a83a93e37ac" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser", +] + [[package]] name = "wnaf" version = "0.14.1" @@ -3102,3 +4048,31 @@ name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.1.0+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index eb8a67a..522966c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,9 @@ license = "GPL-3.0" pumpkin-protocol = { path = "../Pumpkin/crates/pumpkin-protocol" } pumpkin-util = { path = "../Pumpkin/crates/pumpkin-util" } pumpkin-command = { path = "../Pumpkin/crates/pumpkin-command" } +pumpkin-plugin-runtime = { path = "../Pumpkin/crates/pumpkin-plugin-runtime" } +wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "f6b3140dbb1d9b436ef523d14740292c956b0364", default-features = false, features = ["runtime", "component-model", "component-model-async", "async", "cache", "cranelift", "gc", "gc-drc", "threads", "std"] } +wasmtime-wasi = { git = "https://github.com/bytecodealliance/wasmtime", rev = "f6b3140dbb1d9b436ef523d14740292c956b0364", default-features = false, features = ["p2", "p3"] } tokio = { version = "1.53", features = ["full"] } serde = { version = "1.0", features = ["derive"] } diff --git a/src/backend/bridge.rs b/src/backend/bridge.rs index dfea634..5679f0f 100644 --- a/src/backend/bridge.rs +++ b/src/backend/bridge.rs @@ -17,7 +17,9 @@ use pumpkin_protocol::java::packet_decoder::TCPNetworkDecoder; use pumpkin_protocol::java::packet_encoder::TCPNetworkEncoder; use pumpkin_protocol::java::server::handshake::SHandShake; use pumpkin_protocol::java::server::login::{SLoginPluginResponse, SLoginStart}; -use pumpkin_protocol::java::server::play::{SChatCommand, SCommandSuggestion}; +use pumpkin_protocol::java::server::play::{ + SChatCommand, SChatMessage, SCommandSuggestion, SCustomPayload, +}; use pumpkin_protocol::packet::MultiVersionJavaPacket; use pumpkin_protocol::ser::{NetworkReadExt, NetworkReadSliceExt}; use pumpkin_protocol::{ClientPacket, ConnectionState, ServerPacket}; @@ -37,6 +39,11 @@ use crate::backend::forwarding::{ use crate::command::{CommandDispatcher, CommandSender, ProxyCommandSource}; use crate::config::{BackendConfig, Config, ForwardingMode, NetworkConfig}; use crate::network; +use crate::plugin::PluginManager; +use crate::plugin::api::{ + PlayerChatEvent, PlayerCommandEvent, PlayerDisconnectEvent, PlayerJoinEvent, + PluginMessageEvent, ServerConnectEvent, ServerConnectedEvent, +}; use crate::security::PacketRateLimiter; use crate::session::{PlayerAction, PlayerSession, SessionManager}; @@ -87,6 +94,7 @@ pub struct BackendBridge { session_manager: Arc, command_dispatcher: Arc, proxy_config: Arc, + plugin_manager: Arc, } impl BackendBridge { @@ -106,6 +114,7 @@ impl BackendBridge { session_manager: Arc, command_dispatcher: Arc, proxy_config: Arc, + plugin_manager: Arc, ) -> Self { Self { server_name, @@ -122,6 +131,7 @@ impl BackendBridge { session_manager, command_dispatcher, proxy_config, + plugin_manager, } } @@ -413,8 +423,39 @@ impl BackendBridge { let mut current_server_name = self.server_name.clone(); + let mut connect_event = ServerConnectEvent { + uuid: self.player.id.to_string(), + username: self.player.username.clone(), + current_server: None, + target_server: current_server_name.clone(), + cancelled: false, + cancel_reason: None, + }; + self.plugin_manager + .fire_server_connect(&mut connect_event) + .await; + if connect_event.cancelled { + let reason = connect_event + .cancel_reason + .unwrap_or_else(|| "Connection cancelled by plugin".to_string()); + warn!( + "Player '{}' connection to '{}' cancelled by plugin: {}", + self.player.username, connect_event.target_server, reason + ); + return Err(BridgeError::VineError(reason)); + } + current_server_name = connect_event.target_server; + + let initial_backend_cfg = if current_server_name == self.server_name { + self.config.clone() + } else if let Some(cfg) = self.proxy_config.servers.get(¤t_server_name) { + cfg.clone() + } else { + self.config.clone() + }; + let (mut backend_decoder, mut backend_encoder, success_bytes) = match self - .login_backend(¤t_server_name, &self.config) + .login_backend(¤t_server_name, &initial_backend_cfg) .await { Ok(res) => res, @@ -454,12 +495,31 @@ impl BackendBridge { self.online_players.load(Ordering::Relaxed) ); + let connected_event = ServerConnectedEvent { + uuid: self.player.id.to_string(), + username: self.player.username.clone(), + server_name: current_server_name.clone(), + }; + self.plugin_manager + .fire_server_connected(&connected_event) + .await; + + let join_event = PlayerJoinEvent { + uuid: self.player.id.to_string(), + username: self.player.username.clone(), + client_ip: self.client_ip.to_string(), + initial_server: current_server_name.clone(), + }; + self.plugin_manager.fire_player_join(&join_event).await; + let (action_tx, mut action_rx) = tokio::sync::mpsc::unbounded_channel(); self.session_manager.register(PlayerSession { username: self.player.username.clone(), uuid: self.player.id, current_server: current_server_name.clone(), action_tx: action_tx.clone(), + client_ip: self.client_ip.to_string(), + protocol_version: self.client_version.protocol_version() as u32, }); loop { @@ -467,8 +527,8 @@ impl BackendBridge { &self.player.username, &mut client_decoder, &mut client_encoder, - backend_decoder, - backend_encoder, + &mut backend_decoder, + &mut backend_encoder, self.max_packets_per_sec, self.client_version, &mut action_rx, @@ -476,6 +536,9 @@ impl BackendBridge { self.command_dispatcher.clone(), self.proxy_config.clone(), self.session_manager.clone(), + self.plugin_manager.clone(), + self.player.id, + current_server_name.clone(), ) .await; @@ -518,6 +581,32 @@ impl BackendBridge { self.player.username, server_name ); + let mut connect_event = ServerConnectEvent { + uuid: self.player.id.to_string(), + username: self.player.username.clone(), + current_server: Some(current_server_name.clone()), + target_server: server_name.clone(), + cancelled: false, + cancel_reason: None, + }; + self.plugin_manager + .fire_server_connect(&mut connect_event) + .await; + if connect_event.cancelled { + let reason = connect_event + .cancel_reason + .as_deref() + .unwrap_or("Server switch cancelled by plugin"); + let msg = TextComponent::text(format!("§c{}", reason)); + let chat_msg = CSystemChatMessage::new(&msg, false); + if let Ok(bytes) = chat_msg.serialize_packet(&self.client_version) { + let _ = client_encoder.write_packet(bytes).await; + let _ = client_encoder.flush().await; + } + continue; + } + let server_name = connect_event.target_server; + let target_config = match self.proxy_config.servers.get(&server_name) { Some(cfg) => cfg.clone(), None => { @@ -552,6 +641,14 @@ impl BackendBridge { "Player '{}' successfully switched to '{}'", self.player.username, current_server_name ); + let connected_event = ServerConnectedEvent { + uuid: self.player.id.to_string(), + username: self.player.username.clone(), + server_name: current_server_name.clone(), + }; + self.plugin_manager + .fire_server_connected(&connected_event) + .await; } Err(e) => { warn!( @@ -695,6 +792,15 @@ impl BackendBridge { } } + let disconnect_event = PlayerDisconnectEvent { + uuid: self.player.id.to_string(), + username: self.player.username.clone(), + last_server: Some(current_server_name.clone()), + }; + self.plugin_manager + .fire_player_disconnect(&disconnect_event) + .await; + self.session_manager.unregister(&self.player.username); self.online_players.fetch_sub(1, Ordering::Relaxed); info!( @@ -907,8 +1013,8 @@ impl BackendBridge { username: &str, client_decoder: &mut TCPNetworkDecoder, client_encoder: &mut TCPNetworkEncoder, - mut backend_decoder: TCPNetworkDecoder, - mut backend_encoder: TCPNetworkEncoder, + backend_decoder: &mut TCPNetworkDecoder, + backend_encoder: &mut TCPNetworkEncoder, max_packets_per_sec: u32, client_version: JavaMinecraftVersion, action_rx: &mut tokio::sync::mpsc::UnboundedReceiver, @@ -916,6 +1022,9 @@ impl BackendBridge { command_dispatcher: Arc, proxy_config: Arc, session_manager: Arc, + plugin_manager: Arc, + player_uuid: uuid::Uuid, + current_server_name: String, ) -> Result where R1: AsyncRead + Unpin, @@ -925,6 +1034,8 @@ impl BackendBridge { { let mut client_rate_limiter = PacketRateLimiter::new(max_packets_per_sec); let chat_command_id = SChatCommand::to_id(client_version); + let chat_message_id = SChatMessage::to_id(client_version); + let custom_payload_id = SCustomPayload::to_id(client_version); let command_suggestion_id = SCommandSuggestion::to_id(client_version); let command_suggestions_clientbound_id = CCommandSuggestions::to_id(client_version); let commands_packet_id = CCommands::to_id(client_version); @@ -967,8 +1078,22 @@ impl BackendBridge { if let Ok(chat_cmd) = SChatCommand::read(&mut payload, &client_version) { let cmd_text = chat_cmd.command; - let first_word = - cmd_text.split_whitespace().next().unwrap_or(cmd_text); + let mut cmd_event = PlayerCommandEvent { + uuid: player_uuid.to_string(), + username: username.to_string(), + command: cmd_text.to_string(), + cancelled: false, + }; + plugin_manager.fire_player_command(&mut cmd_event).await; + if cmd_event.cancelled { + continue; + } + + let first_word = cmd_event + .command + .split_whitespace() + .next() + .unwrap_or(&cmd_event.command); if command_dispatcher.has_command(first_word) { let source = ProxyCommandSource { sender: CommandSender::Player { @@ -978,7 +1103,43 @@ impl BackendBridge { config: proxy_config.clone(), session_manager: session_manager.clone(), }; - command_dispatcher.handle_command(&source, cmd_text); + command_dispatcher.handle_command(&source, &cmd_event.command); + continue; + } + } + } + + if raw_packet.id == chat_message_id { + let mut payload = &raw_packet.payload[..]; + if let Ok(chat_msg) = SChatMessage::read(&mut payload, &client_version) + { + let mut chat_event = PlayerChatEvent { + uuid: player_uuid.to_string(), + username: username.to_string(), + message: chat_msg.message.to_string(), + cancelled: false, + }; + plugin_manager.fire_player_chat(&mut chat_event).await; + if chat_event.cancelled { + continue; + } + } + } + + if raw_packet.id == custom_payload_id { + let mut payload = &raw_packet.payload[..]; + if let Ok(custom_payload) = + SCustomPayload::read(&mut payload, &client_version) + { + let mut msg_event = PluginMessageEvent { + channel: custom_payload.channel.to_string(), + data: custom_payload.data.to_vec(), + player_uuid: Some(player_uuid.to_string()), + server_name: Some(current_server_name.clone()), + cancelled: false, + }; + plugin_manager.fire_plugin_message(&mut msg_event).await; + if msg_event.cancelled { continue; } } @@ -1032,9 +1193,8 @@ impl BackendBridge { // 2. Root command name completion (no space, e.g. "/", "/s", "/ser", "/server") if !clean_cmd.contains(' ') { let clean_lower = clean_cmd.to_ascii_lowercase(); - let permitted = command_dispatcher - .raw() - .get_all_permitted_commands(&source); + let permitted = + command_dispatcher.get_all_permitted_commands(&source); let proxy_matches: Vec<(String, Option)> = permitted .into_iter() .filter(|(cmd_name, _)| cmd_name.starts_with(&clean_lower)) @@ -1171,9 +1331,11 @@ impl BackendBridge { config: proxy_config.clone(), session_manager: session_manager.clone(), }; - let permitted = - command_dispatcher.raw().get_all_permitted_commands(&source); - let permitted_refs: Vec<(&str, &str)> = permitted.into_iter().collect(); + let permitted = command_dispatcher.get_all_permitted_commands(&source); + let permitted_refs: Vec<(&str, &str)> = permitted + .iter() + .map(|(k, v)| (k.as_str(), v.as_str())) + .collect(); if let Some(modified_payload) = inject_proxy_commands( &raw_packet.payload, @@ -1225,6 +1387,25 @@ impl BackendBridge { let _ = client_tx.send(bytes); } } + PlayerAction::Disconnect(reason) => { + let text = TextComponent::text(reason.clone()); + let disconnect_pkt = CPlayDisconnect::new(&text); + if let Ok(bytes) = disconnect_pkt.serialize_packet(&client_version) { + let _ = client_tx.send(bytes); + } + return TunnelExit::BackendDisconnected { + reason: Some(reason), + }; + } + PlayerAction::PluginMessage { channel, data } => { + let custom_payload = + pumpkin_protocol::java::client::play::CCustomPayload::new( + &channel, &data, + ); + if let Ok(bytes) = custom_payload.serialize_packet(&client_version) { + let _ = client_tx.send(bytes); + } + } } } TunnelExit::ClientDisconnected diff --git a/src/client.rs b/src/client.rs index 62d24de..4c33ba9 100644 --- a/src/client.rs +++ b/src/client.rs @@ -25,6 +25,8 @@ use crate::backend::{BackendBridge, Router}; use crate::command::CommandDispatcher; use crate::config::Config; use crate::network::PrefixedRead; +use crate::plugin::PluginManager; +use crate::plugin::api::{PlayerLoginEvent, PlayerPreLoginEvent, ProxyPingEvent}; use crate::security::Sanitizer; use crate::session::SessionManager; use crate::status::StatusHandler; @@ -55,6 +57,7 @@ pub struct ClientHandler { online_players: Arc, session_manager: Arc, command_dispatcher: Arc, + plugin_manager: Arc, } impl ClientHandler { @@ -69,6 +72,7 @@ impl ClientHandler { online_players: Arc, session_manager: Arc, command_dispatcher: Arc, + plugin_manager: Arc, ) -> Self { Self { peer_addr, @@ -80,6 +84,7 @@ impl ClientHandler { online_players, session_manager, command_dispatcher, + plugin_manager, } } @@ -161,9 +166,29 @@ impl ClientHandler { .map_err(|e| ClientError::DecodeError(e.to_string()))?; if raw_status.id == 0x00 { - let status_pkt = self - .status_handler - .create_status_packet(client_version, Some(virtual_host)); + let initial_motd = self.status_handler.resolve_motd_text(Some(virtual_host)); + let initial_online = self + .online_players + .load(std::sync::atomic::Ordering::Relaxed) as u32; + let mut ping_event = ProxyPingEvent { + client_ip: self.peer_addr.ip().to_string(), + protocol_version: client_version.protocol_version() as u32, + virtual_host: virtual_host.to_string(), + motd: initial_motd, + max_players: self.config.motd.max_players, + online_players: initial_online, + version_name: self.config.motd.version_name.clone(), + }; + self.plugin_manager.fire_proxy_ping(&mut ping_event).await; + + let status_pkt = self.status_handler.create_custom_status_packet( + client_version, + &ping_event.motd, + ping_event.max_players, + ping_event.online_players, + &ping_event.version_name, + Some(virtual_host), + ); let status_bytes = status_pkt .serialize_packet(&client_version) .map_err(|e| ClientError::EncodeError(e.to_string()))?; @@ -215,6 +240,30 @@ impl ClientHandler { let username = login_start.name.to_string(); + let mut pre_login_event = PlayerPreLoginEvent { + client_ip: client_ip.to_string(), + username: username.clone(), + protocol_version: client_version.protocol_version() as u32, + virtual_host: virtual_host.clone(), + cancelled: false, + cancel_reason: None, + }; + self.plugin_manager + .fire_player_pre_login(&mut pre_login_event) + .await; + if pre_login_event.cancelled { + let reason = pre_login_event + .cancel_reason + .as_deref() + .unwrap_or("Connection cancelled by plugin"); + warn!( + "[{}] Player '{}' pre-login cancelled by plugin: {}", + self.peer_addr, username, reason + ); + Self::kick(&mut encoder, client_version, reason).await?; + return Ok(()); + } + if self.config.security.validate_usernames && !Sanitizer::is_valid_username(&username) { warn!( "[{}] Rejecting invalid username '{}' (failed sanitization)", @@ -347,6 +396,29 @@ impl ClientHandler { AuthenticatedPlayer::offline(&username) }; + let mut login_event = PlayerLoginEvent { + uuid: authenticated_player.id.to_string(), + username: authenticated_player.username.clone(), + client_ip: client_ip.to_string(), + cancelled: false, + cancel_reason: None, + }; + self.plugin_manager + .fire_player_login(&mut login_event) + .await; + if login_event.cancelled { + let reason = login_event + .cancel_reason + .as_deref() + .unwrap_or("Login cancelled by plugin"); + warn!( + "[{}] Player '{}' login cancelled by plugin: {}", + self.peer_addr, authenticated_player.username, reason + ); + Self::kick(&mut encoder, client_version, reason).await?; + return Ok(()); + } + let comp_threshold = self.config.server.compression_threshold; if comp_threshold > 0 { let set_comp = CSetCompression::new(VarInt(comp_threshold)); @@ -416,6 +488,7 @@ impl ClientHandler { self.session_manager.clone(), self.command_dispatcher.clone(), self.config.clone(), + self.plugin_manager.clone(), ); bridge.run(decoder, encoder).await?; diff --git a/src/command/glist.rs b/src/command/glist.rs index 10dd84c..86d34e3 100644 --- a/src/command/glist.rs +++ b/src/command/glist.rs @@ -158,12 +158,16 @@ mod tests { uuid: Uuid::new_v4(), current_server: "lobby".to_string(), action_tx: tx1, + client_ip: "127.0.0.1".to_string(), + protocol_version: 765, }); session_manager.register(PlayerSession { username: "Alex".to_string(), uuid: Uuid::new_v4(), current_server: "survival".to_string(), action_tx: tx2, + client_ip: "127.0.0.1".to_string(), + protocol_version: 765, }); let (action_tx, mut action_rx) = mpsc::unbounded_channel(); diff --git a/src/command/mod.rs b/src/command/mod.rs index 45bfa94..3a304bc 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -215,9 +215,11 @@ impl CommandExecutor for ListCommandExecutor { } } +use std::sync::RwLock; + /// Central registry and dispatcher for commands using `pumpkin-command` pub struct CommandDispatcher { - inner: RawCommandDispatcher, + inner: RwLock>, } impl Default for CommandDispatcher { @@ -246,18 +248,36 @@ impl CommandDispatcher { .executes(CommandSpecificHelpExecutor), ), ); - Self { inner: dispatcher } + Self { + inner: RwLock::new(dispatcher), + } + } + + /// Registers a new command builder into the dispatcher + pub fn register( + &self, + builder: pumpkin_command::argument_builder::CommandArgumentBuilder, + ) { + if let Ok(mut lock) = self.inner.write() { + lock.register(builder); + } } /// Returns true if a command with the given name is registered pub fn has_command(&self, name: &str) -> bool { let clean = name.trim_start_matches('/').to_ascii_lowercase(); - self.inner.has_command(&clean) + if let Ok(lock) = self.inner.read() { + lock.has_command(&clean) + } else { + false + } } /// Dispatches a command for execution, reporting any error messages back to the source pub fn handle_command(&self, source: &ProxyCommandSource, input: &str) { - self.inner.handle_command(source, input); + if let Ok(lock) = self.inner.read() { + lock.handle_command(source, input); + } } /// Executes raw input directly, returning the execution result @@ -268,7 +288,23 @@ impl CommandDispatcher { ) -> Result { let trimmed = input.trim(); let clean = trimmed.strip_prefix('/').unwrap_or(trimmed); - self.inner.execute_input(clean, source) + if let Ok(lock) = self.inner.read() { + lock.execute_input(clean, source) + } else { + Err(command_error("Command dispatcher lock poisoned")) + } + } + + /// Returns all permitted commands for the given source + pub fn get_all_permitted_commands(&self, source: &ProxyCommandSource) -> Vec<(String, String)> { + if let Ok(lock) = self.inner.read() { + lock.get_all_permitted_commands(source) + .into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + } else { + Vec::new() + } } /// Returns suggestions with range information for any command input. @@ -278,14 +314,16 @@ impl CommandDispatcher { let clean = input.strip_prefix('/').unwrap_or(input); if clean.contains(' ') { let first_word = clean.split_whitespace().next().unwrap_or(clean); - if self.has_command(first_word) { - return self.inner.suggest_with_range(clean, source); + if self.has_command(first_word) + && let Ok(lock) = self.inner.read() + { + return lock.suggest_with_range(clean, source); } return Suggestions::empty(); } let clean_lower = clean.to_ascii_lowercase(); - let permitted = self.inner.get_all_permitted_commands(source); + let permitted = self.get_all_permitted_commands(source); let mut suggestions = Vec::new(); for (cmd_name, description) in permitted { if cmd_name.starts_with(&clean_lower) { @@ -316,16 +354,6 @@ impl CommandDispatcher { pub fn suggest_with_range(&self, input: &str, source: &ProxyCommandSource) -> Suggestions { self.suggest_command(input, source) } - - /// Exposes the inner pumpkin command dispatcher - pub fn raw(&self) -> &RawCommandDispatcher { - &self.inner - } - - /// Exposes a mutable reference to the inner pumpkin command dispatcher - pub fn raw_mut(&mut self) -> &mut RawCommandDispatcher { - &mut self.inner - } } #[cfg(test)] diff --git a/src/command/send.rs b/src/command/send.rs index ef86694..eca6273 100644 --- a/src/command/send.rs +++ b/src/command/send.rs @@ -205,6 +205,8 @@ mod tests { uuid: Uuid::new_v4(), current_server: "lobby".to_string(), action_tx: tx, + client_ip: "127.0.0.1".to_string(), + protocol_version: 765, }); let source = ProxyCommandSource { @@ -333,6 +335,8 @@ mod tests { uuid: Uuid::new_v4(), current_server: "lobby".to_string(), action_tx: tx, + client_ip: "127.0.0.1".to_string(), + protocol_version: 765, }); let source = ProxyCommandSource { diff --git a/src/command/server.rs b/src/command/server.rs index 4de06c0..5de99a2 100644 --- a/src/command/server.rs +++ b/src/command/server.rs @@ -182,6 +182,8 @@ mod tests { uuid: Uuid::new_v4(), current_server: "lobby".to_string(), action_tx: action_tx.clone(), + client_ip: "127.0.0.1".to_string(), + protocol_version: 765, }; session_manager.register(session); diff --git a/src/config.rs b/src/config.rs index e23fc12..66ac428 100644 --- a/src/config.rs +++ b/src/config.rs @@ -60,6 +60,29 @@ pub struct Config { pub bedrock: crate::bedrock::BedrockConfig, #[serde(default)] pub permissions: PermissionsConfig, + #[serde(default)] + pub plugins: PluginConfig, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PluginConfig { + #[serde(default = "default_true")] + pub enabled: bool, + #[serde(default = "default_plugins_dir")] + pub plugin_dir: String, +} + +fn default_plugins_dir() -> String { + "./plugins".to_string() +} + +impl Default for PluginConfig { + fn default() -> Self { + Self { + enabled: true, + plugin_dir: default_plugins_dir(), + } + } } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -361,6 +384,7 @@ impl Default for Config { telemetry: TelemetryConfig::default(), bedrock: crate::bedrock::BedrockConfig::default(), permissions: PermissionsConfig::default(), + plugins: PluginConfig::default(), } } } diff --git a/src/lib.rs b/src/lib.rs index c55a0bd..1e3733f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub mod command; pub mod config; pub mod network; pub mod permissions; +pub mod plugin; pub mod proxy; pub mod security; pub mod session; diff --git a/src/plugin/api.rs b/src/plugin/api.rs new file mode 100644 index 0000000..eda6b08 --- /dev/null +++ b/src/plugin/api.rs @@ -0,0 +1,151 @@ +use std::future::Future; +use std::path::PathBuf; +use std::pin::Pin; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub type PluginFuture<'a, T> = Pin + Send + 'a>>; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PluginMetadata { + pub name: String, + pub version: String, + pub authors: Vec, + pub description: Option, + pub dependencies: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum EventPriority { + Lowest = 0, + Low = 1, + Normal = 2, + High = 3, + Highest = 4, + Monitor = 5, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum EventType { + ProxyPing, + PlayerPreLogin, + PlayerLogin, + PlayerJoin, + ServerConnect, + ServerConnected, + PlayerDisconnect, + PlayerChat, + PlayerCommand, + PluginMessage, +} + +#[derive(Debug, Clone)] +pub struct ProxyPingEvent { + pub client_ip: String, + pub protocol_version: u32, + pub virtual_host: String, + pub motd: String, + pub max_players: u32, + pub online_players: u32, + pub version_name: String, +} + +#[derive(Debug, Clone)] +pub struct PlayerPreLoginEvent { + pub client_ip: String, + pub username: String, + pub protocol_version: u32, + pub virtual_host: String, + pub cancelled: bool, + pub cancel_reason: Option, +} + +#[derive(Debug, Clone)] +pub struct PlayerLoginEvent { + pub uuid: String, + pub username: String, + pub client_ip: String, + pub cancelled: bool, + pub cancel_reason: Option, +} + +#[derive(Debug, Clone)] +pub struct PlayerJoinEvent { + pub uuid: String, + pub username: String, + pub client_ip: String, + pub initial_server: String, +} + +#[derive(Debug, Clone)] +pub struct ServerConnectEvent { + pub uuid: String, + pub username: String, + pub current_server: Option, + pub target_server: String, + pub cancelled: bool, + pub cancel_reason: Option, +} + +#[derive(Debug, Clone)] +pub struct ServerConnectedEvent { + pub uuid: String, + pub username: String, + pub server_name: String, +} + +#[derive(Debug, Clone)] +pub struct PlayerDisconnectEvent { + pub uuid: String, + pub username: String, + pub last_server: Option, +} + +#[derive(Debug, Clone)] +pub struct PlayerChatEvent { + pub uuid: String, + pub username: String, + pub message: String, + pub cancelled: bool, +} + +#[derive(Debug, Clone)] +pub struct PlayerCommandEvent { + pub uuid: String, + pub username: String, + pub command: String, + pub cancelled: bool, +} + +#[derive(Debug, Clone)] +pub struct PluginMessageEvent { + pub channel: String, + pub data: Vec, + pub player_uuid: Option, + pub server_name: Option, + pub cancelled: bool, +} + +pub struct PluginContext { + pub metadata: PluginMetadata, + pub data_folder: PathBuf, + pub registered_commands: Mutex>, + pub registered_events: Mutex>, +} + +impl PluginContext { + #[must_use] + pub fn new(metadata: PluginMetadata, data_folder: PathBuf) -> Self { + Self { + metadata, + data_folder, + registered_commands: Mutex::new(Vec::new()), + registered_events: Mutex::new(Vec::new()), + } + } +} + +pub trait Plugin: Send + Sync { + fn on_load(&self, context: Arc) -> PluginFuture<'_, Result<(), String>>; + fn on_unload(&self, context: Arc) -> PluginFuture<'_, Result<(), String>>; +} diff --git a/src/plugin/loader/mod.rs b/src/plugin/loader/mod.rs new file mode 100644 index 0000000..ce1d9f8 --- /dev/null +++ b/src/plugin/loader/mod.rs @@ -0,0 +1 @@ +pub mod wasm; diff --git a/src/plugin/loader/wasm/mod.rs b/src/plugin/loader/wasm/mod.rs new file mode 100644 index 0000000..da24076 --- /dev/null +++ b/src/plugin/loader/wasm/mod.rs @@ -0,0 +1,34 @@ +pub mod wasm_host; + +use std::path::Path; +use std::sync::Arc; + +use crate::plugin::api::PluginMetadata; +use wasm_host::state::ProxyPluginContext; +pub use wasm_host::{PluginRuntime, WasmPlugin}; + +pub struct WasmPluginLoader { + runtime: PluginRuntime, +} + +impl WasmPluginLoader { + pub fn new() -> Result { + let runtime = PluginRuntime::new()?; + Ok(Self { runtime }) + } + + pub fn can_load(&self, path: &Path) -> bool { + path.extension() + .and_then(|e| e.to_str()) + .map(|ext| ext.eq_ignore_ascii_case("wasm")) + .unwrap_or(false) + } + + pub async fn load( + &self, + path: &Path, + proxy: Option, + ) -> Result<(Arc, PluginMetadata), wasm_host::PluginInitError> { + self.runtime.init_plugin(path, proxy).await + } +} diff --git a/src/plugin/loader/wasm/wasm_host/concurrent_store.rs b/src/plugin/loader/wasm/wasm_host/concurrent_store.rs new file mode 100644 index 0000000..40f6e41 --- /dev/null +++ b/src/plugin/loader/wasm/wasm_host/concurrent_store.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; + +use pumpkin_plugin_runtime::{RuntimeSpawner, SpawnError, SpawnFuture, StoreExecutor}; +use wasmtime::Store; + +use super::state::PluginHostState; + +pub use pumpkin_plugin_runtime::LegacySyncReentry; + +pub type LegacyStore = StoreExecutor; + +pub struct TokioSpawner { + runtime: tokio::runtime::Handle, +} + +impl TokioSpawner { + #[must_use] + pub const fn new(runtime: tokio::runtime::Handle) -> Self { + Self { runtime } + } +} + +impl RuntimeSpawner for TokioSpawner { + fn spawn(&self, task: SpawnFuture) -> Result<(), SpawnError> { + drop(self.runtime.spawn(task)); + Ok(()) + } + + fn spawn_blocking(&self, task: Box) -> Result<(), SpawnError> { + drop(self.runtime.spawn_blocking(task)); + Ok(()) + } +} + +pub async fn start_legacy_store( + store: Store, + policy: LegacySyncReentry, + spawner: Arc, +) -> wasmtime::Result { + StoreExecutor::start(store, policy, spawner).await +} diff --git a/src/plugin/loader/wasm/wasm_host/mod.rs b/src/plugin/loader/wasm/wasm_host/mod.rs new file mode 100644 index 0000000..7327f16 --- /dev/null +++ b/src/plugin/loader/wasm/wasm_host/mod.rs @@ -0,0 +1,267 @@ +pub mod concurrent_store; +pub mod state; +pub mod wit; + +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use thiserror::Error; +use wasmtime::component::{Component, HasSelf, Linker}; +use wasmtime::{Engine, Store}; + +use crate::plugin::api::{Plugin, PluginContext, PluginFuture, PluginMetadata}; +use concurrent_store::{LegacyStore, LegacySyncReentry, TokioSpawner}; +use state::{PluginHostState, ProxyPluginContext}; + +#[derive(Error, Debug)] +pub enum PluginInitError { + #[error("Failed to read plugin file '{0}': {1}")] + FileReadFailed(PathBuf, std::io::Error), + #[error("Failed to compile WebAssembly component: {0}")] + ComponentCompilationFailed(wasmtime::Error), + #[error("Failed to setup linker: {0}")] + LinkerSetupFailed(wasmtime::Error), + #[error("Failed to instantiate plugin: {0}")] + InstantiationFailed(wasmtime::Error), + #[error("Failed to initialize plugin: {0}")] + InitPluginFailed(wasmtime::Error), + #[error("Failed to retrieve metadata: {0}")] + GetMetadataFailed(wasmtime::Error), + #[error("Engine creation failed: {0}")] + EngineCreationFailed(wasmtime::Error), +} + +pub fn setup_linker(engine: &Engine) -> wasmtime::Result> { + let mut linker = Linker::::new(engine); + wasmtime_wasi::p2::add_to_linker_async(&mut linker)?; + wit::Plugin::add_to_linker::<_, HasSelf<_>>(&mut linker, |state: &mut PluginHostState| state)?; + Ok(linker) +} + +pub struct WasmPlugin { + pub instance: wit::Plugin, + pub store: LegacyStore, + pub metadata: PluginMetadata, +} + +impl WasmPlugin { + pub async fn on_load(&self, context: Arc) -> Result<(), String> { + let func = self.instance.func_on_load(); + let context_clone = context.clone(); + self.store + .call_guest(move |mut guest| { + Box::pin(async move { + let context_res = + guest.with(|mut store| store.data_mut().add_context(context_clone))?; + let (res,) = guest.call(func, (context_res,)).await?; + Ok(res) + }) + }) + .await + .map_err(|e| e.to_string())? + } + + pub async fn on_unload(&self, context: Arc) -> Result<(), String> { + let func = self.instance.func_on_unload(); + let context_clone = context.clone(); + self.store + .call_guest(move |mut guest| { + Box::pin(async move { + let context_res = + guest.with(|mut store| store.data_mut().add_context(context_clone))?; + let (res,) = guest.call(func, (context_res,)).await?; + Ok(res) + }) + }) + .await + .map_err(|e| e.to_string())? + } + + pub async fn handle_event( + &self, + handler_id: u32, + event: wit::vine::plugin::event::Event, + ) -> Result { + let func = self.instance.func_handle_event(); + self.store + .call_guest(move |mut guest| { + Box::pin(async move { + let (res,) = guest.call(func, (handler_id, event)).await?; + Ok(res) + }) + }) + .await + } + + pub async fn handle_command( + &self, + command_id: u32, + sender: wit::vine::plugin::types::CommandSender, + args: Vec, + ) -> Result, wasmtime::Error> { + let func = self.instance.func_handle_command(); + self.store + .call_guest(move |mut guest| { + Box::pin(async move { + let (res,) = guest.call(func, (command_id, sender, args)).await?; + Ok(res) + }) + }) + .await + } + + pub async fn handle_command_suggestion( + &self, + command_id: u32, + sender: wit::vine::plugin::types::CommandSender, + args: Vec, + ) -> Result, wasmtime::Error> { + let func = self.instance.func_handle_command_suggestion(); + self.store + .call_guest(move |mut guest| { + Box::pin(async move { + let (res,) = guest.call(func, (command_id, sender, args)).await?; + Ok(res) + }) + }) + .await + } +} + +impl Plugin for WasmPlugin { + fn on_load(&self, context: Arc) -> PluginFuture<'_, Result<(), String>> { + Box::pin(async move { Self::on_load(self, context).await }) + } + + fn on_unload(&self, context: Arc) -> PluginFuture<'_, Result<(), String>> { + Box::pin(async move { Self::on_unload(self, context).await }) + } +} + +pub struct PluginRuntime { + engine: Engine, + linker: Linker, + reentry: LegacySyncReentry, + spawner: Arc, +} + +impl PluginRuntime { + pub fn new() -> Result { + let mut config = wasmtime::Config::new(); + config.wasm_component_model(true); + config.wasm_component_model_async(true); + config.concurrency_support(true); + + let engine = Engine::new(&config).map_err(PluginInitError::EngineCreationFailed)?; + let linker = setup_linker(&engine).map_err(PluginInitError::LinkerSetupFailed)?; + let reentry = LegacySyncReentry::new(); + let spawner = Arc::new(TokioSpawner::new(tokio::runtime::Handle::current())); + + Ok(Self { + engine, + linker, + reentry, + spawner, + }) + } + + pub async fn init_plugin( + &self, + path: &Path, + proxy: Option, + ) -> Result<(Arc, PluginMetadata), PluginInitError> { + let wasm_bytes = + fs::read(path).map_err(|e| PluginInitError::FileReadFailed(path.to_path_buf(), e))?; + + let component = Component::new(&self.engine, &wasm_bytes) + .map_err(PluginInitError::ComponentCompilationFailed)?; + + let instance_pre = self + .linker + .instantiate_pre(&component) + .map_err(PluginInitError::InstantiationFailed)?; + + let plugin_pre = + wit::PluginPre::new(instance_pre).map_err(PluginInitError::InstantiationFailed)?; + + let filename = path + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + let data_folder = PathBuf::from("plugins").join("data").join(&filename); + let _ = fs::create_dir_all(&data_folder); + + let mut store = Store::new( + &self.engine, + PluginHostState::new(filename, data_folder, proxy), + ); + store.limiter(|state| &mut state.limits); + + let plugin = self + .reentry + .scope_bootstrap(plugin_pre.instantiate_async(&mut store)) + .await + .map_err(PluginInitError::InstantiationFailed)?; + + store + .run_concurrent(async |accessor| { + self.reentry + .scope_bootstrap(plugin.call_init_plugin(accessor)) + .await + }) + .await + .map_err(PluginInitError::InitPluginFailed)? + .map_err(PluginInitError::InitPluginFailed)?; + + let raw_meta = store + .run_concurrent(async |accessor| { + self.reentry + .scope_bootstrap(plugin.call_get_metadata(accessor)) + .await + }) + .await + .map_err(PluginInitError::GetMetadataFailed)? + .map_err(PluginInitError::GetMetadataFailed)?; + + let metadata = PluginMetadata { + name: raw_meta.name.clone(), + version: raw_meta.version, + authors: raw_meta.authors, + description: raw_meta.description, + dependencies: raw_meta.dependencies, + }; + + store.data_mut().plugin_name = raw_meta.name; + + let legacy_store = concurrent_store::start_legacy_store( + store, + self.reentry.clone(), + Arc::clone(&self.spawner) as Arc, + ) + .await + .map_err(PluginInitError::InstantiationFailed)?; + + let wasm_plugin = Arc::new(WasmPlugin { + instance: plugin, + store: legacy_store, + metadata: metadata.clone(), + }); + + let weak = Arc::downgrade(&wasm_plugin); + wasm_plugin + .store + .call(move |accessor| { + Box::pin(async move { + accessor.with(|mut store| { + store.data_mut().plugin = Some(weak); + }); + Ok(()) + }) + }) + .await + .map_err(PluginInitError::InstantiationFailed)?; + + Ok((wasm_plugin, metadata)) + } +} diff --git a/src/plugin/loader/wasm/wasm_host/state.rs b/src/plugin/loader/wasm/wasm_host/state.rs new file mode 100644 index 0000000..972e353 --- /dev/null +++ b/src/plugin/loader/wasm/wasm_host/state.rs @@ -0,0 +1,438 @@ +use std::path::PathBuf; +use std::sync::{Arc, Weak}; + +use wasmtime::component::{Resource, ResourceTable}; +use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; + +use crate::command::CommandDispatcher; +use crate::config::Config; +use crate::plugin::PluginManager; +use crate::plugin::api::{EventPriority, EventType, PluginContext}; +use crate::plugin::loader::wasm::wasm_host::WasmPlugin; +use crate::session::{PlayerAction, SessionManager}; + +use super::wit; + +#[derive(Clone)] +pub struct ProxyPluginContext { + pub config: Arc, + pub session_manager: Arc, + pub command_dispatcher: Arc, + pub plugin_manager: Arc, +} + +pub struct ContextResource { + pub context: Arc, +} + +pub struct PluginHostState { + pub wasi_ctx: WasiCtx, + pub resource_table: ResourceTable, + pub limits: wasmtime::StoreLimits, + pub proxy: Option, + pub plugin_name: String, + pub data_folder: PathBuf, + pub plugin: Option>, +} + +impl Default for PluginHostState { + fn default() -> Self { + Self::new(String::new(), PathBuf::new(), None) + } +} + +impl PluginHostState { + #[must_use] + pub fn new( + plugin_name: String, + data_folder: PathBuf, + proxy: Option, + ) -> Self { + Self { + wasi_ctx: WasiCtxBuilder::new() + .inherit_stdout() + .inherit_stderr() + .build(), + resource_table: ResourceTable::new(), + limits: wasmtime::StoreLimitsBuilder::new().build(), + proxy, + plugin_name, + data_folder, + plugin: None, + } + } + + pub fn add_context( + &mut self, + context: Arc, + ) -> wasmtime::Result> { + let res = self.resource_table.push(ContextResource { context })?; + Ok(Resource::new_own(res.rep())) + } + + pub fn get_context( + &self, + resource: &Resource, + ) -> wasmtime::Result<&ContextResource> { + Ok(self + .resource_table + .get(&Resource::new_borrow(resource.rep()))?) + } +} + +impl WasiView for PluginHostState { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.wasi_ctx, + table: &mut self.resource_table, + } + } +} + +impl wit::vine::plugin::types::Host for PluginHostState {} +impl wit::vine::plugin::metadata::Host for PluginHostState {} +impl wit::vine::plugin::event::Host for PluginHostState {} +impl wit::vine::plugin::context::Host for PluginHostState {} + +impl wit::vine::plugin::logging::Host for PluginHostState { + async fn log( + &mut self, + level: wit::vine::plugin::logging::Level, + message: String, + ) -> wasmtime::Result<()> { + match level { + wit::vine::plugin::logging::Level::Trace => { + tracing::trace!("[Plugin: {}] {}", self.plugin_name, message) + } + wit::vine::plugin::logging::Level::Debug => { + tracing::debug!("[Plugin: {}] {}", self.plugin_name, message) + } + wit::vine::plugin::logging::Level::Info => { + tracing::info!("[Plugin: {}] {}", self.plugin_name, message) + } + wit::vine::plugin::logging::Level::Warn => { + tracing::warn!("[Plugin: {}] {}", self.plugin_name, message) + } + wit::vine::plugin::logging::Level::Error => { + tracing::error!("[Plugin: {}] {}", self.plugin_name, message) + } + } + Ok(()) + } +} + +impl wit::vine::plugin::command::Host for PluginHostState { + async fn reply( + &mut self, + sender: wit::vine::plugin::types::CommandSender, + message: String, + ) -> wasmtime::Result<()> { + if let Some(proxy) = &self.proxy { + match sender.sender_type { + wit::vine::plugin::types::CommandSenderType::Console => { + tracing::info!("[Console] {}", message); + } + wit::vine::plugin::types::CommandSenderType::Player => { + if let Some(uuid_str) = sender.player_uuid + && let Ok(target_uuid) = uuid::Uuid::parse_str(&uuid_str) + && let Some(session) = proxy + .session_manager + .list_sessions() + .into_iter() + .find(|s| s.uuid == target_uuid) + { + let _ = session.action_tx.send(PlayerAction::Message( + pumpkin_util::text::TextComponent::text(message), + )); + } + } + } + } + Ok(()) + } +} + +impl wit::vine::plugin::proxy::Host for PluginHostState { + async fn get_player_count(&mut self) -> wasmtime::Result { + Ok(self + .proxy + .as_ref() + .map(|p| p.session_manager.count() as u32) + .unwrap_or(0)) + } + + async fn get_players(&mut self) -> wasmtime::Result> { + if let Some(proxy) = &self.proxy { + let list = proxy + .session_manager + .list_sessions() + .into_iter() + .map(|s| wit::vine::plugin::types::PlayerInfo { + uuid: s.uuid.to_string(), + username: s.username, + current_server: s.current_server, + client_ip: s.client_ip, + protocol_version: s.protocol_version, + }) + .collect(); + Ok(list) + } else { + Ok(Vec::new()) + } + } + + async fn get_player_by_name( + &mut self, + username: String, + ) -> wasmtime::Result> { + if let Some(proxy) = &self.proxy { + let session = proxy.session_manager.get_session(&username); + Ok(session.map(|s| wit::vine::plugin::types::PlayerInfo { + uuid: s.uuid.to_string(), + username: s.username, + current_server: s.current_server, + client_ip: s.client_ip, + protocol_version: s.protocol_version, + })) + } else { + Ok(None) + } + } + + async fn get_player_by_uuid( + &mut self, + uuid: String, + ) -> wasmtime::Result> { + if let Some(proxy) = &self.proxy + && let Ok(target_uuid) = uuid::Uuid::parse_str(&uuid) + { + let session = proxy + .session_manager + .list_sessions() + .into_iter() + .find(|s| s.uuid == target_uuid); + return Ok(session.map(|s| wit::vine::plugin::types::PlayerInfo { + uuid: s.uuid.to_string(), + username: s.username, + current_server: s.current_server, + client_ip: s.client_ip, + protocol_version: s.protocol_version, + })); + } + Ok(None) + } + + async fn get_servers(&mut self) -> wasmtime::Result> { + if let Some(proxy) = &self.proxy { + let sessions = proxy.session_manager.list_sessions(); + let mut list = Vec::new(); + for (name, srv) in &proxy.config.servers { + let player_count = sessions + .iter() + .filter(|s| s.current_server == *name) + .count() as u32; + list.push(wit::vine::plugin::types::ServerInfo { + name: name.clone(), + address: srv.address.clone(), + player_count, + }); + } + Ok(list) + } else { + Ok(Vec::new()) + } + } + + async fn broadcast(&mut self, message: String) -> wasmtime::Result<()> { + if let Some(proxy) = &self.proxy { + for session in proxy.session_manager.list_sessions() { + let _ = session.action_tx.send(PlayerAction::Message( + pumpkin_util::text::TextComponent::text(message.clone()), + )); + } + tracing::info!("[Broadcast] {}", message); + } + Ok(()) + } + + async fn send_message( + &mut self, + player_uuid: String, + message: String, + ) -> wasmtime::Result { + if let Some(proxy) = &self.proxy + && let Ok(target_uuid) = uuid::Uuid::parse_str(&player_uuid) + && let Some(session) = proxy + .session_manager + .list_sessions() + .into_iter() + .find(|s| s.uuid == target_uuid) + { + let _ = session.action_tx.send(PlayerAction::Message( + pumpkin_util::text::TextComponent::text(message), + )); + return Ok(true); + } + Ok(false) + } + + async fn disconnect_player( + &mut self, + player_uuid: String, + reason: String, + ) -> wasmtime::Result { + if let Some(proxy) = &self.proxy + && let Ok(target_uuid) = uuid::Uuid::parse_str(&player_uuid) + && let Some(session) = proxy + .session_manager + .list_sessions() + .into_iter() + .find(|s| s.uuid == target_uuid) + { + let _ = session.action_tx.send(PlayerAction::Disconnect(reason)); + return Ok(true); + } + Ok(false) + } + + async fn connect_player( + &mut self, + player_uuid: String, + server_name: String, + ) -> wasmtime::Result { + if let Some(proxy) = &self.proxy + && proxy.config.servers.contains_key(&server_name) + && let Ok(target_uuid) = uuid::Uuid::parse_str(&player_uuid) + && let Some(session) = proxy + .session_manager + .list_sessions() + .into_iter() + .find(|s| s.uuid == target_uuid) + { + let _ = session + .action_tx + .send(PlayerAction::Connect { server_name }); + return Ok(true); + } + Ok(false) + } + + async fn send_plugin_message( + &mut self, + player_uuid: String, + channel: String, + data: Vec, + ) -> wasmtime::Result { + if let Some(proxy) = &self.proxy + && let Ok(target_uuid) = uuid::Uuid::parse_str(&player_uuid) + && let Some(session) = proxy + .session_manager + .list_sessions() + .into_iter() + .find(|s| s.uuid == target_uuid) + { + let _ = session + .action_tx + .send(PlayerAction::PluginMessage { channel, data }); + return Ok(true); + } + Ok(false) + } +} + +impl wit::vine::plugin::context::HostContext for PluginHostState { + async fn register_event( + &mut self, + context: Resource, + handler_id: u32, + event_type: wit::vine::plugin::event::EventType, + priority: wit::vine::plugin::types::EventPriority, + ) -> wasmtime::Result<()> { + let plugin = self + .plugin + .as_ref() + .and_then(|p| p.upgrade()) + .ok_or_else(|| wasmtime::Error::msg("Plugin reference is no longer valid"))?; + + let api_event_type = match event_type { + wit::vine::plugin::event::EventType::ProxyPing => EventType::ProxyPing, + wit::vine::plugin::event::EventType::PlayerPreLogin => EventType::PlayerPreLogin, + wit::vine::plugin::event::EventType::PlayerLogin => EventType::PlayerLogin, + wit::vine::plugin::event::EventType::PlayerJoin => EventType::PlayerJoin, + wit::vine::plugin::event::EventType::ServerConnect => EventType::ServerConnect, + wit::vine::plugin::event::EventType::ServerConnected => EventType::ServerConnected, + wit::vine::plugin::event::EventType::PlayerDisconnect => EventType::PlayerDisconnect, + wit::vine::plugin::event::EventType::PlayerChat => EventType::PlayerChat, + wit::vine::plugin::event::EventType::PlayerCommand => EventType::PlayerCommand, + wit::vine::plugin::event::EventType::PluginMessage => EventType::PluginMessage, + }; + + let api_priority = match priority { + wit::vine::plugin::types::EventPriority::Lowest => EventPriority::Lowest, + wit::vine::plugin::types::EventPriority::Low => EventPriority::Low, + wit::vine::plugin::types::EventPriority::Normal => EventPriority::Normal, + wit::vine::plugin::types::EventPriority::High => EventPriority::High, + wit::vine::plugin::types::EventPriority::Highest => EventPriority::Highest, + wit::vine::plugin::types::EventPriority::Monitor => EventPriority::Monitor, + }; + + if let Some(proxy) = &self.proxy { + proxy + .plugin_manager + .register_event_handler(api_event_type, api_priority, handler_id, plugin) + .await; + } + + let ctx_res = self.get_context(&context)?; + let mut events = ctx_res.context.registered_events.lock().await; + events.push((handler_id, api_event_type, api_priority)); + + Ok(()) + } + + async fn register_command( + &mut self, + context: Resource, + command_id: u32, + name: String, + description: String, + aliases: Vec, + permission: Option, + ) -> wasmtime::Result<()> { + let plugin = self + .plugin + .as_ref() + .and_then(|p| p.upgrade()) + .ok_or_else(|| wasmtime::Error::msg("Plugin reference is no longer valid"))?; + + if let Some(proxy) = &self.proxy { + proxy + .plugin_manager + .register_command(command_id, name, description, aliases, permission, plugin) + .await; + } + + let ctx_res = self.get_context(&context)?; + let mut commands = ctx_res.context.registered_commands.lock().await; + commands.push(command_id); + + Ok(()) + } + + async fn get_data_folder( + &mut self, + _context: Resource, + ) -> wasmtime::Result { + Ok(self.data_folder.to_string_lossy().to_string()) + } + + async fn drop( + &mut self, + rep: Resource, + ) -> wasmtime::Result<()> { + let _ = self + .resource_table + .delete::(Resource::new_own(rep.rep())); + Ok(()) + } +} diff --git a/src/plugin/loader/wasm/wasm_host/wit/mod.rs b/src/plugin/loader/wasm/wasm_host/wit/mod.rs new file mode 100644 index 0000000..7fe7f09 --- /dev/null +++ b/src/plugin/loader/wasm/wasm_host/wit/mod.rs @@ -0,0 +1,12 @@ +use wasmtime::component::bindgen; + +bindgen!({ + path: "wit", + world: "plugin", + imports: { + default: async | trappable, + }, + exports: { + default: async | store | trappable, + }, +}); diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs new file mode 100644 index 0000000..ae37324 --- /dev/null +++ b/src/plugin/mod.rs @@ -0,0 +1,863 @@ +pub mod api; +pub mod loader; + +pub use api::*; +pub use loader::wasm::WasmPlugin; +use loader::wasm::WasmPluginLoader; +use loader::wasm::wasm_host::state::ProxyPluginContext; + +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::{OnceCell, RwLock}; +use tracing::{error, info, warn}; + +use crate::command::{CommandSender, ProxyCommandSource}; +use pumpkin_command::{ArgumentBuilder, CommandSource}; + +#[derive(Clone)] +pub struct RegisteredEventHandler { + pub handler_id: u32, + pub priority: EventPriority, + pub plugin: Arc, +} + +pub struct PluginManager { + plugins_dir: PathBuf, + loader: WasmPluginLoader, + loaded_plugins: RwLock>>, + event_handlers: RwLock>>, + proxy_ctx: OnceCell, +} + +impl PluginManager { + pub fn new( + plugins_dir: PathBuf, + ) -> Result, loader::wasm::wasm_host::PluginInitError> { + let loader = WasmPluginLoader::new()?; + Ok(Arc::new(Self { + plugins_dir, + loader, + loaded_plugins: RwLock::new(Vec::new()), + event_handlers: RwLock::new(HashMap::new()), + proxy_ctx: OnceCell::new(), + })) + } + + pub fn init_proxy_context( + self: &Arc, + config: Arc, + session_manager: Arc, + command_dispatcher: Arc, + ) { + let ctx = ProxyPluginContext { + config, + session_manager, + command_dispatcher, + plugin_manager: self.clone(), + }; + let _ = self.proxy_ctx.set(ctx); + } + + pub async fn register_event_handler( + &self, + event_type: EventType, + priority: EventPriority, + handler_id: u32, + plugin: Arc, + ) { + let mut handlers = self.event_handlers.write().await; + let list = handlers.entry(event_type).or_default(); + list.push(RegisteredEventHandler { + handler_id, + priority, + plugin, + }); + list.sort_by_key(|h| h.priority); + } + + pub async fn register_command( + &self, + command_id: u32, + name: String, + description: String, + aliases: Vec, + permission: Option, + plugin: Arc, + ) { + if let Some(proxy) = self.proxy_ctx.get() { + let names = std::iter::once(name.clone()) + .chain(aliases) + .collect::>(); + for cmd_name in names { + let plugin_cmd = plugin.clone(); + let perm_check = permission.clone(); + let cmd_builder = pumpkin_command::argument_builder::command(cmd_name, description.clone()) + .requires(move |src: &ProxyCommandSource| { + if let Some(ref p) = perm_check { + src.has_permission(p) + } else { + true + } + }) + .executes(PluginCommandExecutor { + command_id, + plugin: plugin_cmd.clone(), + }) + .then( + pumpkin_command::argument_builder::argument( + "args", + pumpkin_command::argument_types::core::string::StringArgumentType::GreedyPhrase, + ) + .suggests(PluginCommandSuggestionProvider { + command_id, + plugin: plugin_cmd.clone(), + }) + .executes(PluginCommandExecutor { + command_id, + plugin: plugin_cmd, + }), + ); + proxy.command_dispatcher.register(cmd_builder); + } + info!( + "Registered command '/{}' from plugin '{}'", + name, plugin.metadata.name + ); + } + } + + pub async fn load_plugins(&self) -> Result> { + if !self.plugins_dir.exists() { + let _ = fs::create_dir_all(&self.plugins_dir); + info!("Created plugins directory '{}'", self.plugins_dir.display()); + return Ok(0); + } + + let mut count = 0; + let start_time = std::time::Instant::now(); + let read_dir = match fs::read_dir(&self.plugins_dir) { + Ok(d) => d, + Err(e) => { + warn!("Failed to read plugins directory: {}", e); + return Ok(0); + } + }; + + let proxy = self.proxy_ctx.get().cloned(); + + for entry in read_dir.flatten() { + let path = entry.path(); + if path.is_file() && self.loader.can_load(&path) { + info!("Loading plugin from '{}'...", path.display()); + match self.loader.load(&path, proxy.clone()).await { + Ok((plugin, metadata)) => { + let data_folder = + PathBuf::from("plugins").join("data").join(&metadata.name); + let context = Arc::new(PluginContext::new(metadata.clone(), data_folder)); + match plugin.on_load(context).await { + Ok(()) => { + info!( + "Loaded plugin '{}' v{} by {}", + metadata.name, + metadata.version, + metadata.authors.join(", ") + ); + self.loaded_plugins.write().await.push(plugin); + count += 1; + } + Err(e) => { + error!("Plugin '{}' failed to on_load: {}", metadata.name, e); + } + } + } + Err(e) => { + error!("Failed to load plugin '{}': {}", path.display(), e); + } + } + } + } + + info!("Loaded {} plugin(s) in {:?}", count, start_time.elapsed()); + Ok(count) + } + + pub async fn unload_plugins(&self) { + let plugins = { + let mut lock = self.loaded_plugins.write().await; + std::mem::take(&mut *lock) + }; + + for plugin in plugins { + let data_folder = PathBuf::from("plugins") + .join("data") + .join(&plugin.metadata.name); + let context = Arc::new(PluginContext::new(plugin.metadata.clone(), data_folder)); + if let Err(e) = plugin.on_unload(context).await { + error!("Error unloading plugin '{}': {}", plugin.metadata.name, e); + } else { + info!("Unloaded plugin '{}'", plugin.metadata.name); + } + } + + self.event_handlers.write().await.clear(); + } + + // --- Event dispatchers --- + + pub async fn fire_proxy_ping(&self, event: &mut ProxyPingEvent) { + let handlers = { + let map = self.event_handlers.read().await; + map.get(&EventType::ProxyPing).cloned().unwrap_or_default() + }; + for handler in handlers { + let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::ProxyPing( + loader::wasm::wasm_host::wit::vine::plugin::event::ProxyPingEvent { + client_ip: event.client_ip.clone(), + protocol_version: event.protocol_version, + virtual_host: event.virtual_host.clone(), + motd: event.motd.clone(), + max_players: event.max_players, + online_players: event.online_players, + version_name: event.version_name.clone(), + }, + ); + match handler + .plugin + .handle_event(handler.handler_id, wit_evt) + .await + { + Ok(loader::wasm::wasm_host::wit::vine::plugin::event::Event::ProxyPing(res)) => { + event.motd = res.motd; + event.max_players = res.max_players; + event.online_players = res.online_players; + event.version_name = res.version_name; + } + Ok(_) => {} + Err(e) => { + error!( + "Error in plugin '{}' handling ProxyPing: {}", + handler.plugin.metadata.name, e + ); + } + } + } + } + + pub async fn fire_player_pre_login(&self, event: &mut PlayerPreLoginEvent) { + let handlers = { + let map = self.event_handlers.read().await; + map.get(&EventType::PlayerPreLogin) + .cloned() + .unwrap_or_default() + }; + for handler in handlers { + let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerPreLogin( + loader::wasm::wasm_host::wit::vine::plugin::event::PlayerPreLoginEvent { + client_ip: event.client_ip.clone(), + username: event.username.clone(), + protocol_version: event.protocol_version, + virtual_host: event.virtual_host.clone(), + cancelled: event.cancelled, + cancel_reason: event.cancel_reason.clone(), + }, + ); + match handler + .plugin + .handle_event(handler.handler_id, wit_evt) + .await + { + Ok(loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerPreLogin( + res, + )) => { + event.cancelled = res.cancelled; + event.cancel_reason = res.cancel_reason; + if event.cancelled { + break; + } + } + Ok(_) => {} + Err(e) => { + error!( + "Error in plugin '{}' handling PlayerPreLogin: {}", + handler.plugin.metadata.name, e + ); + } + } + } + } + + pub async fn fire_player_login(&self, event: &mut PlayerLoginEvent) { + let handlers = { + let map = self.event_handlers.read().await; + map.get(&EventType::PlayerLogin) + .cloned() + .unwrap_or_default() + }; + for handler in handlers { + let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerLogin( + loader::wasm::wasm_host::wit::vine::plugin::event::PlayerLoginEvent { + uuid: event.uuid.clone(), + username: event.username.clone(), + client_ip: event.client_ip.clone(), + cancelled: event.cancelled, + cancel_reason: event.cancel_reason.clone(), + }, + ); + match handler + .plugin + .handle_event(handler.handler_id, wit_evt) + .await + { + Ok(loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerLogin(res)) => { + event.cancelled = res.cancelled; + event.cancel_reason = res.cancel_reason; + if event.cancelled { + break; + } + } + Ok(_) => {} + Err(e) => { + error!( + "Error in plugin '{}' handling PlayerLogin: {}", + handler.plugin.metadata.name, e + ); + } + } + } + } + + pub async fn fire_player_join(&self, event: &PlayerJoinEvent) { + let handlers = { + let map = self.event_handlers.read().await; + map.get(&EventType::PlayerJoin).cloned().unwrap_or_default() + }; + for handler in handlers { + let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerJoin( + loader::wasm::wasm_host::wit::vine::plugin::event::PlayerJoinEvent { + uuid: event.uuid.clone(), + username: event.username.clone(), + client_ip: event.client_ip.clone(), + initial_server: event.initial_server.clone(), + }, + ); + let _ = handler + .plugin + .handle_event(handler.handler_id, wit_evt) + .await; + } + } + + pub async fn fire_server_connect(&self, event: &mut ServerConnectEvent) { + let handlers = { + let map = self.event_handlers.read().await; + map.get(&EventType::ServerConnect) + .cloned() + .unwrap_or_default() + }; + for handler in handlers { + let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::ServerConnect( + loader::wasm::wasm_host::wit::vine::plugin::event::ServerConnectEvent { + uuid: event.uuid.clone(), + username: event.username.clone(), + current_server: event.current_server.clone(), + target_server: event.target_server.clone(), + cancelled: event.cancelled, + cancel_reason: event.cancel_reason.clone(), + }, + ); + match handler + .plugin + .handle_event(handler.handler_id, wit_evt) + .await + { + Ok(loader::wasm::wasm_host::wit::vine::plugin::event::Event::ServerConnect( + res, + )) => { + event.target_server = res.target_server; + event.cancelled = res.cancelled; + event.cancel_reason = res.cancel_reason; + if event.cancelled { + break; + } + } + Ok(_) => {} + Err(e) => { + error!( + "Error in plugin '{}' handling ServerConnect: {}", + handler.plugin.metadata.name, e + ); + } + } + } + } + + pub async fn fire_server_connected(&self, event: &ServerConnectedEvent) { + let handlers = { + let map = self.event_handlers.read().await; + map.get(&EventType::ServerConnected) + .cloned() + .unwrap_or_default() + }; + for handler in handlers { + let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::ServerConnected( + loader::wasm::wasm_host::wit::vine::plugin::event::ServerConnectedEvent { + uuid: event.uuid.clone(), + username: event.username.clone(), + server_name: event.server_name.clone(), + }, + ); + let _ = handler + .plugin + .handle_event(handler.handler_id, wit_evt) + .await; + } + } + + pub async fn fire_player_disconnect(&self, event: &PlayerDisconnectEvent) { + let handlers = { + let map = self.event_handlers.read().await; + map.get(&EventType::PlayerDisconnect) + .cloned() + .unwrap_or_default() + }; + for handler in handlers { + let wit_evt = + loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerDisconnect( + loader::wasm::wasm_host::wit::vine::plugin::event::PlayerDisconnectEvent { + uuid: event.uuid.clone(), + username: event.username.clone(), + last_server: event.last_server.clone(), + }, + ); + let _ = handler + .plugin + .handle_event(handler.handler_id, wit_evt) + .await; + } + } + + pub async fn fire_player_chat(&self, event: &mut PlayerChatEvent) { + let handlers = { + let map = self.event_handlers.read().await; + map.get(&EventType::PlayerChat).cloned().unwrap_or_default() + }; + for handler in handlers { + let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerChat( + loader::wasm::wasm_host::wit::vine::plugin::event::PlayerChatEvent { + uuid: event.uuid.clone(), + username: event.username.clone(), + message: event.message.clone(), + cancelled: event.cancelled, + }, + ); + match handler + .plugin + .handle_event(handler.handler_id, wit_evt) + .await + { + Ok(loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerChat(res)) => { + event.message = res.message; + event.cancelled = res.cancelled; + if event.cancelled { + break; + } + } + Ok(_) => {} + Err(e) => { + error!( + "Error in plugin '{}' handling PlayerChat: {}", + handler.plugin.metadata.name, e + ); + } + } + } + } + + pub async fn fire_player_command(&self, event: &mut PlayerCommandEvent) { + let handlers = { + let map = self.event_handlers.read().await; + map.get(&EventType::PlayerCommand) + .cloned() + .unwrap_or_default() + }; + for handler in handlers { + let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerCommand( + loader::wasm::wasm_host::wit::vine::plugin::event::PlayerCommandEvent { + uuid: event.uuid.clone(), + username: event.username.clone(), + command: event.command.clone(), + cancelled: event.cancelled, + }, + ); + match handler + .plugin + .handle_event(handler.handler_id, wit_evt) + .await + { + Ok(loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerCommand( + res, + )) => { + event.command = res.command; + event.cancelled = res.cancelled; + if event.cancelled { + break; + } + } + Ok(_) => {} + Err(e) => { + error!( + "Error in plugin '{}' handling PlayerCommand: {}", + handler.plugin.metadata.name, e + ); + } + } + } + } + + pub async fn fire_plugin_message(&self, event: &mut PluginMessageEvent) { + let handlers = { + let map = self.event_handlers.read().await; + map.get(&EventType::PluginMessage) + .cloned() + .unwrap_or_default() + }; + for handler in handlers { + let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::PluginMessage( + loader::wasm::wasm_host::wit::vine::plugin::event::PluginMessageEvent { + channel: event.channel.clone(), + data: event.data.clone(), + player_uuid: event.player_uuid.clone(), + server_name: event.server_name.clone(), + cancelled: event.cancelled, + }, + ); + match handler + .plugin + .handle_event(handler.handler_id, wit_evt) + .await + { + Ok(loader::wasm::wasm_host::wit::vine::plugin::event::Event::PluginMessage( + res, + )) => { + event.data = res.data; + event.cancelled = res.cancelled; + if event.cancelled { + break; + } + } + Ok(_) => {} + Err(e) => { + error!( + "Error in plugin '{}' handling PluginMessage: {}", + handler.plugin.metadata.name, e + ); + } + } + } + } +} + +struct PluginCommandExecutor { + command_id: u32, + plugin: Arc, +} + +impl pumpkin_command::node::CommandExecutor for PluginCommandExecutor { + fn execute( + &self, + context: &pumpkin_command::context::command_context::CommandContext, + ) -> pumpkin_command::node::CommandExecutorResult { + let args: Vec = if let Ok(phrase) = + pumpkin_command::argument_types::core::string::StringArgumentType::get(context, "args") + { + phrase.split_whitespace().map(|s| s.to_string()).collect() + } else { + Vec::new() + }; + + let sender = match &context.source.sender { + CommandSender::Console => loader::wasm::wasm_host::wit::vine::plugin::types::CommandSender { + sender_type: + loader::wasm::wasm_host::wit::vine::plugin::types::CommandSenderType::Console, + name: "CONSOLE".to_string(), + player_uuid: None, + }, + CommandSender::Player { username, .. } => { + let uuid = context + .source + .session_manager + .get_session(username) + .map(|s| s.uuid.to_string()); + loader::wasm::wasm_host::wit::vine::plugin::types::CommandSender { + sender_type: + loader::wasm::wasm_host::wit::vine::plugin::types::CommandSenderType::Player, + name: username.clone(), + player_uuid: uuid, + } + } + }; + + let plugin = self.plugin.clone(); + let command_id = self.command_id; + let handle = tokio::runtime::Handle::current(); + let result = tokio::task::block_in_place(|| { + handle.block_on(async move { plugin.handle_command(command_id, sender, args).await }) + }); + + match result { + Ok(Ok(code)) => Ok(code), + Ok(Err(err_msg)) => { + context + .source + .send_message(pumpkin_util::text::TextComponent::text(format!( + "§cError: {err_msg}" + ))); + Ok(0) + } + Err(e) => { + context + .source + .send_message(pumpkin_util::text::TextComponent::text(format!( + "§cPlugin command error: {e}" + ))); + Ok(0) + } + } + } +} + +struct PluginCommandSuggestionProvider { + command_id: u32, + plugin: Arc, +} + +impl pumpkin_command::suggestion::provider::SuggestionProvider + for PluginCommandSuggestionProvider +{ + fn suggest( + &self, + context: &pumpkin_command::context::command_context::CommandContext, + builder: pumpkin_command::suggestion::suggestions::SuggestionsBuilder, + ) -> pumpkin_command::suggestion::provider::SuggestionProviderResult { + let args: Vec = if let Ok(phrase) = + pumpkin_command::argument_types::core::string::StringArgumentType::get(context, "args") + { + phrase.split_whitespace().map(|s| s.to_string()).collect() + } else { + Vec::new() + }; + + let sender = match &context.source.sender { + CommandSender::Console => loader::wasm::wasm_host::wit::vine::plugin::types::CommandSender { + sender_type: + loader::wasm::wasm_host::wit::vine::plugin::types::CommandSenderType::Console, + name: "CONSOLE".to_string(), + player_uuid: None, + }, + CommandSender::Player { username, .. } => { + let uuid = context + .source + .session_manager + .get_session(username) + .map(|s| s.uuid.to_string()); + loader::wasm::wasm_host::wit::vine::plugin::types::CommandSender { + sender_type: + loader::wasm::wasm_host::wit::vine::plugin::types::CommandSenderType::Player, + name: username.clone(), + player_uuid: uuid, + } + } + }; + + let plugin = self.plugin.clone(); + let command_id = self.command_id; + let handle = tokio::runtime::Handle::current(); + let suggestions_res = tokio::task::block_in_place(|| { + handle.block_on(async move { + plugin + .handle_command_suggestion(command_id, sender, args) + .await + }) + }); + + let mut builder = builder; + if let Ok(suggestions) = suggestions_res { + for s in suggestions { + builder = builder.suggest(s); + } + } + builder.build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::command::CommandDispatcher; + use crate::config::Config; + use crate::session::SessionManager; + use std::sync::Arc; + + #[tokio::test] + async fn test_plugin_manager_empty_dir() { + let temp_dir = tempfile::tempdir().expect("failed to create temp dir"); + let config = Arc::new(Config::default()); + let dispatcher = Arc::new(CommandDispatcher::new()); + let session_manager = Arc::new(SessionManager::new()); + let manager = + PluginManager::new(temp_dir.path().to_path_buf()).expect("failed to create manager"); + manager.init_proxy_context(config, session_manager, dispatcher); + + let count = manager.load_plugins().await.expect("load failed"); + assert_eq!(count, 0); + + manager.unload_plugins().await; + } + + #[tokio::test] + async fn test_plugin_manager_event_dispatch() { + let temp_dir = tempfile::tempdir().expect("failed to create temp dir"); + let config = Arc::new(Config::default()); + let dispatcher = Arc::new(CommandDispatcher::new()); + let session_manager = Arc::new(SessionManager::new()); + let manager = + PluginManager::new(temp_dir.path().to_path_buf()).expect("failed to create manager"); + manager.init_proxy_context(config, session_manager, dispatcher); + + // Proxy ping + let mut ping = ProxyPingEvent { + client_ip: "127.0.0.1".to_string(), + protocol_version: 765, + virtual_host: "localhost".to_string(), + motd: "A Minecraft Proxy".to_string(), + max_players: 100, + online_players: 0, + version_name: "Vine 1.21".to_string(), + }; + manager.fire_proxy_ping(&mut ping).await; + assert_eq!(ping.motd, "A Minecraft Proxy"); + assert_eq!(ping.max_players, 100); + + // Player pre-login + let mut pre_login = PlayerPreLoginEvent { + client_ip: "127.0.0.1".to_string(), + username: "Player1".to_string(), + protocol_version: 765, + virtual_host: "localhost".to_string(), + cancelled: false, + cancel_reason: None, + }; + manager.fire_player_pre_login(&mut pre_login).await; + assert!(!pre_login.cancelled); + + // Player login + let mut login = PlayerLoginEvent { + uuid: "00000000-0000-0000-0000-000000000001".to_string(), + username: "Player1".to_string(), + client_ip: "127.0.0.1".to_string(), + cancelled: false, + cancel_reason: None, + }; + manager.fire_player_login(&mut login).await; + assert!(!login.cancelled); + + // Server connect + let mut connect = ServerConnectEvent { + uuid: "00000000-0000-0000-0000-000000000001".to_string(), + username: "Player1".to_string(), + current_server: None, + target_server: "lobby".to_string(), + cancelled: false, + cancel_reason: None, + }; + manager.fire_server_connect(&mut connect).await; + assert_eq!(connect.target_server, "lobby"); + assert!(!connect.cancelled); + + // Server connected + let connected = ServerConnectedEvent { + uuid: "00000000-0000-0000-0000-000000000001".to_string(), + username: "Player1".to_string(), + server_name: "lobby".to_string(), + }; + manager.fire_server_connected(&connected).await; + + // Player join + let join = PlayerJoinEvent { + uuid: "00000000-0000-0000-0000-000000000001".to_string(), + username: "Player1".to_string(), + client_ip: "127.0.0.1".to_string(), + initial_server: "lobby".to_string(), + }; + manager.fire_player_join(&join).await; + + // Player chat + let mut chat = PlayerChatEvent { + uuid: "00000000-0000-0000-0000-000000000001".to_string(), + username: "Player1".to_string(), + message: "Hello proxy!".to_string(), + cancelled: false, + }; + manager.fire_player_chat(&mut chat).await; + assert_eq!(chat.message, "Hello proxy!"); + assert!(!chat.cancelled); + + // Player command + let mut cmd = PlayerCommandEvent { + uuid: "00000000-0000-0000-0000-000000000001".to_string(), + username: "Player1".to_string(), + command: "ping".to_string(), + cancelled: false, + }; + manager.fire_player_command(&mut cmd).await; + assert!(!cmd.cancelled); + + // Plugin message + let mut msg = PluginMessageEvent { + channel: "vine:test".to_string(), + data: vec![1, 2, 3], + player_uuid: Some("00000000-0000-0000-0000-000000000001".to_string()), + server_name: Some("lobby".to_string()), + cancelled: false, + }; + manager.fire_plugin_message(&mut msg).await; + assert!(!msg.cancelled); + + // Player disconnect + let disc = PlayerDisconnectEvent { + uuid: "00000000-0000-0000-0000-000000000001".to_string(), + username: "Player1".to_string(), + last_server: Some("lobby".to_string()), + }; + manager.fire_player_disconnect(&disc).await; + } + + #[test] + fn test_event_priority_ordering() { + assert!(EventPriority::Lowest < EventPriority::Low); + assert!(EventPriority::Low < EventPriority::Normal); + assert!(EventPriority::Normal < EventPriority::High); + assert!(EventPriority::High < EventPriority::Highest); + assert!(EventPriority::Highest < EventPriority::Monitor); + } + + #[test] + fn test_plugin_metadata_serde() { + let meta = PluginMetadata { + name: "test-plugin".to_string(), + version: "1.0.0".to_string(), + authors: vec!["Author1".to_string()], + description: Some("A test plugin".to_string()), + dependencies: vec!["other-plugin".to_string()], + }; + + let json = serde_json::to_string(&meta).expect("serialization failed"); + let decoded: PluginMetadata = serde_json::from_str(&json).expect("deserialization failed"); + assert_eq!(decoded.name, "test-plugin"); + assert_eq!(decoded.version, "1.0.0"); + assert_eq!(decoded.authors, vec!["Author1".to_string()]); + assert_eq!(decoded.dependencies, vec!["other-plugin".to_string()]); + } +} diff --git a/src/proxy.rs b/src/proxy.rs index 18d1ab2..7f94184 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -27,6 +27,7 @@ pub struct ProxyServer { online_players: Arc, session_manager: Arc, command_dispatcher: Arc, + plugin_manager: Arc, } impl ProxyServer { @@ -68,6 +69,14 @@ impl ProxyServer { let router = Arc::new(Router::new(config.routing.clone(), config.servers.clone())); + let plugins_dir = std::path::PathBuf::from(&config.plugins.plugin_dir); + let plugin_manager = crate::plugin::PluginManager::new(plugins_dir)?; + plugin_manager.init_proxy_context( + config.clone(), + session_manager.clone(), + command_dispatcher.clone(), + ); + Ok(Self { config, keystore, @@ -78,6 +87,7 @@ impl ProxyServer { online_players, session_manager, command_dispatcher, + plugin_manager, }) } @@ -221,6 +231,16 @@ impl ProxyServer { } }); + if self.config.plugins.enabled { + info!( + "Loading plugins from '{}'...", + self.config.plugins.plugin_dir + ); + if let Err(e) = self.plugin_manager.load_plugins().await { + error!("Failed to load plugins: {}", e); + } + } + loop { tokio::select! { accept_res = listener.accept() => { @@ -235,6 +255,7 @@ impl ProxyServer { let rate_limiter = self.rate_limiter.clone(); let session_manager = self.session_manager.clone(); let command_dispatcher = self.command_dispatcher.clone(); + let plugin_manager = self.plugin_manager.clone(); tokio::spawn(async move { if let Err(e) = network::configure_client_socket(&stream, &config.network) { @@ -249,8 +270,8 @@ impl ProxyServer { match network::parse_proxy_protocol(&mut stream, peer_addr).await { Ok(res) => { effective_addr = res.client_addr; - leftover_bytes = res.leftover_bytes; - debug!("[{}] PROXY protocol resolved client IP: {}", peer_addr, effective_addr); + leftover_bytes = res.leftover_bytes; + debug!("[{}] PROXY protocol resolved client IP: {}", peer_addr, effective_addr); } Err(err) => { warn!("[{}] Dropped connection: PROXY protocol error: {}", peer_addr, err); @@ -274,6 +295,7 @@ impl ProxyServer { online_players, session_manager, command_dispatcher, + plugin_manager, ); if let Err(err) = handler.handle(stream, leftover_bytes).await { @@ -296,6 +318,11 @@ impl ProxyServer { info!("Vine proxy has stopped accepting connections."); + if self.config.plugins.enabled { + info!("Unloading plugins..."); + self.plugin_manager.unload_plugins().await; + } + if let Some(handle) = telemetry_handle { let timeout_secs = self.config.server.shutdown_timeout_secs.max(1); let _ = tokio::time::timeout(Duration::from_secs(timeout_secs), handle).await; diff --git a/src/session.rs b/src/session.rs index 7aa06e7..2c22741 100644 --- a/src/session.rs +++ b/src/session.rs @@ -14,6 +14,10 @@ pub enum PlayerAction { Transfer { host: String, port: u16 }, /// Send a system chat message to the player Message(TextComponent), + /// Disconnect/kick the player with a reason + Disconnect(String), + /// Send a custom plugin message / payload to the player + PluginMessage { channel: String, data: Vec }, } /// Represents an active connected player session on the proxy @@ -23,6 +27,8 @@ pub struct PlayerSession { pub uuid: Uuid, pub current_server: String, pub action_tx: mpsc::UnboundedSender, + pub client_ip: String, + pub protocol_version: u32, } /// Thread-safe registry of connected player sessions @@ -111,6 +117,8 @@ mod tests { uuid: Uuid::new_v4(), current_server: "lobby".to_string(), action_tx: tx, + client_ip: "127.0.0.1:12345".to_string(), + protocol_version: 765, }; manager.register(session); @@ -135,6 +143,8 @@ mod tests { uuid: Uuid::new_v4(), current_server: "lobby".to_string(), action_tx: tx, + client_ip: "127.0.0.1:12346".to_string(), + protocol_version: 765, }; manager.register(session); assert_eq!(manager.get_session("Alex").unwrap().current_server, "lobby"); diff --git a/src/status.rs b/src/status.rs index a4ab0ee..25ccccd 100644 --- a/src/status.rs +++ b/src/status.rs @@ -122,7 +122,7 @@ impl StatusHandler { } /// Resolves MOTD description for the virtual host or falls back to global default - fn resolve_motd_text(&self, host: Option<&str>) -> String { + pub fn resolve_motd_text(&self, host: Option<&str>) -> String { if let Some(h) = host { let clean_host = h.split(':').next().unwrap_or(h).trim(); @@ -161,6 +161,58 @@ impl StatusHandler { CStatusResponse::new(json) } + /// Create custom CStatusResponse packet with overridden fields from plugins + pub fn create_custom_status_packet( + &self, + client_version: JavaMinecraftVersion, + motd: &str, + max_players: u32, + online_players: u32, + version_name: &str, + virtual_host: Option<&str>, + ) -> CStatusResponse { + let protocol = if client_version == JavaMinecraftVersion::Unknown { + self.motd.protocol_version + } else { + client_version.protocol_version() + }; + + let icon_uri = self.icon_manager.get_icon_for_host(virtual_host); + + let sample: Vec = if self.motd.show_sample_players { + self.motd + .sample_players + .iter() + .map(|name| PlayerSample { + name, + id: Uuid::nil().to_string(), + }) + .collect() + } else { + Vec::new() + }; + + let status = StatusJson { + version: StatusVersion { + name: version_name, + protocol, + }, + players: StatusPlayers { + max: max_players, + online: online_players as usize, + sample, + }, + description: StatusDescription { + text: motd.to_string(), + }, + enforce_secure_chat: false, + favicon: icon_uri.as_deref(), + }; + + let json = serde_json::to_string(&status).unwrap_or_else(|_| "{}".to_string()); + CStatusResponse::new(json) + } + /// Create CPingResponse packet pub fn create_pong_packet(payload: i64) -> CPingResponse { CPingResponse::new(payload) diff --git a/wit/command.wit b/wit/command.wit new file mode 100644 index 0000000..7797b0e --- /dev/null +++ b/wit/command.wit @@ -0,0 +1,6 @@ +interface command { + use types.{command-sender}; + + /// Reply to a command sender + reply: func(sender: command-sender, message: string); +} diff --git a/wit/context.wit b/wit/context.wit new file mode 100644 index 0000000..d45f9a5 --- /dev/null +++ b/wit/context.wit @@ -0,0 +1,15 @@ +interface context { + use types.{event-priority}; + use event.{event-type}; + + resource context { + /// Register an event listener for an event type with priority + register-event: func(handler-id: u32, event-type: event-type, priority: event-priority); + + /// Register a proxy command + register-command: func(command-id: u32, name: string, description: string, aliases: list, permission: option); + + /// Get path to the plugin's data folder (e.g. plugins/data/) + get-data-folder: func() -> string; + } +} diff --git a/wit/event.wit b/wit/event.wit new file mode 100644 index 0000000..d0fd0d2 --- /dev/null +++ b/wit/event.wit @@ -0,0 +1,106 @@ +interface event { + use types.{event-priority}; + + enum event-type { + proxy-ping, + player-pre-login, + player-login, + player-join, + server-connect, + server-connected, + player-disconnect, + player-chat, + player-command, + plugin-message, + } + + record proxy-ping-event { + client-ip: string, + protocol-version: u32, + virtual-host: string, + motd: string, + max-players: u32, + online-players: u32, + version-name: string, + } + + record player-pre-login-event { + client-ip: string, + username: string, + protocol-version: u32, + virtual-host: string, + cancelled: bool, + cancel-reason: option, + } + + record player-login-event { + uuid: string, + username: string, + client-ip: string, + cancelled: bool, + cancel-reason: option, + } + + record player-join-event { + uuid: string, + username: string, + client-ip: string, + initial-server: string, + } + + record server-connect-event { + uuid: string, + username: string, + current-server: option, + target-server: string, + cancelled: bool, + cancel-reason: option, + } + + record server-connected-event { + uuid: string, + username: string, + server-name: string, + } + + record player-disconnect-event { + uuid: string, + username: string, + last-server: option, + } + + record player-chat-event { + uuid: string, + username: string, + message: string, + cancelled: bool, + } + + record player-command-event { + uuid: string, + username: string, + command: string, + cancelled: bool, + } + + record plugin-message-event { + channel: string, + data: list, + player-uuid: option, + server-name: option, + cancelled: bool, + } + + variant event { + proxy-ping(proxy-ping-event), + player-pre-login(player-pre-login-event), + player-login(player-login-event), + player-join(player-join-event), + server-connect(server-connect-event), + server-connected(server-connected-event), + player-disconnect(player-disconnect-event), + player-chat(player-chat-event), + player-command(player-command-event), + plugin-message(plugin-message-event), + } +} diff --git a/wit/logging.wit b/wit/logging.wit new file mode 100644 index 0000000..6c7a075 --- /dev/null +++ b/wit/logging.wit @@ -0,0 +1,11 @@ +interface logging { + enum level { + trace, + debug, + info, + warn, + error, + } + + log: func(level: level, message: string); +} diff --git a/wit/metadata.wit b/wit/metadata.wit new file mode 100644 index 0000000..e1035df --- /dev/null +++ b/wit/metadata.wit @@ -0,0 +1,9 @@ +interface metadata { + record plugin-metadata { + name: string, + version: string, + authors: list, + description: option, + dependencies: list, + } +} diff --git a/wit/plugin.wit b/wit/plugin.wit new file mode 100644 index 0000000..baa2c91 --- /dev/null +++ b/wit/plugin.wit @@ -0,0 +1,34 @@ +package vine:plugin@0.1.0; + +world plugin { + use context.{context}; + use event.{event}; + use types.{command-sender}; + use metadata.{plugin-metadata}; + + import logging; + import proxy; + import context; + import command; + + /// Called once by the host to initialize the plugin instance. + export init-plugin: func(); + + /// Called when the plugin is loaded into the proxy. + export on-load: func(ctx: context) -> result<_, string>; + + /// Called when the plugin is being unloaded. + export on-unload: func(ctx: context) -> result<_, string>; + + /// Provides plugin metadata to the proxy host. + export get-metadata: func() -> plugin-metadata; + + /// Dispatches an event to the plugin handler. + export handle-event: func(handler-id: u32, evt: event) -> event; + + /// Dispatches a command execution to the plugin. + export handle-command: func(command-id: u32, sender: command-sender, args: list) -> result; + + /// Dispatches command suggestion / tab completion to the plugin. + export handle-command-suggestion: func(command-id: u32, sender: command-sender, args: list) -> list; +} diff --git a/wit/proxy.wit b/wit/proxy.wit new file mode 100644 index 0000000..978ead4 --- /dev/null +++ b/wit/proxy.wit @@ -0,0 +1,33 @@ +interface proxy { + use types.{player-info, server-info}; + + /// Returns the total number of connected players. + get-player-count: func() -> u32; + + /// Returns a list of all connected players. + get-players: func() -> list; + + /// Returns information for a specific player by username. + get-player-by-name: func(username: string) -> option; + + /// Returns information for a specific player by UUID string. + get-player-by-uuid: func(uuid: string) -> option; + + /// Returns a list of configured backend servers. + get-servers: func() -> list; + + /// Broadcasts a system chat message to all connected players and console. + broadcast: func(message: string); + + /// Sends a system chat message to a specific player by UUID. + send-message: func(player-uuid: string, message: string) -> bool; + + /// Disconnects/kicks a player by UUID with the given reason. + disconnect-player: func(player-uuid: string, reason: string) -> bool; + + /// Requests switching/connecting a player to a backend server. + connect-player: func(player-uuid: string, server-name: string) -> bool; + + /// Sends a plugin message / custom payload to a player. + send-plugin-message: func(player-uuid: string, channel: string, data: list) -> bool; +} diff --git a/wit/types.wit b/wit/types.wit new file mode 100644 index 0000000..23ce94c --- /dev/null +++ b/wit/types.wit @@ -0,0 +1,35 @@ +interface types { + record player-info { + uuid: string, + username: string, + current-server: string, + client-ip: string, + protocol-version: u32, + } + + record server-info { + name: string, + address: string, + player-count: u32, + } + + enum event-priority { + lowest, + low, + normal, + high, + highest, + monitor, + } + + enum command-sender-type { + console, + player, + } + + record command-sender { + sender-type: command-sender-type, + name: string, + player-uuid: option, + } +}