Compare commits
12
Commits
6058367d96
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c52146d8e | ||
|
|
b72279a7fd | ||
|
|
399aced29d | ||
|
|
a5a937694d | ||
|
|
0179da4bbf | ||
|
|
56f511d5f6 | ||
|
|
c4ff921a7c | ||
|
|
c9abf5df77 | ||
|
|
222567b735 | ||
|
|
b1a836fc2a | ||
|
|
b92358e9b6 | ||
|
|
1f874a5cff |
Generated
+144
-235
@@ -8,12 +8,6 @@ version = "0.2.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anyhow"
|
|
||||||
version = "1.0.102"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atoi"
|
name = "atoi"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -57,6 +51,61 @@ dependencies = [
|
|||||||
"fs_extra",
|
"fs_extra",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
|
||||||
|
dependencies = [
|
||||||
|
"axum-core",
|
||||||
|
"base64",
|
||||||
|
"bytes",
|
||||||
|
"form_urlencoded",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"itoa",
|
||||||
|
"matchit",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"serde_core",
|
||||||
|
"serde_json",
|
||||||
|
"serde_path_to_error",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sha1",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tokio-tungstenite",
|
||||||
|
"tower",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-core"
|
||||||
|
version = "0.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
@@ -135,17 +184,6 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "chacha20"
|
|
||||||
version = "0.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cpufeatures 0.3.0",
|
|
||||||
"rand_core 0.10.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cmake"
|
name = "cmake"
|
||||||
version = "0.1.57"
|
version = "0.1.57"
|
||||||
@@ -215,15 +253,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cpufeatures"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc"
|
name = "crc"
|
||||||
version = "3.4.0"
|
version = "3.4.0"
|
||||||
@@ -264,6 +293,12 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-encoding"
|
||||||
|
version = "2.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.7.10"
|
version = "0.7.10"
|
||||||
@@ -443,9 +478,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.32"
|
version = "0.3.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
@@ -471,27 +506,27 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.32"
|
version = "0.3.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.32"
|
version = "0.3.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.32"
|
version = "0.3.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.32"
|
version = "0.3.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
@@ -539,20 +574,6 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "getrandom"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"r-efi",
|
|
||||||
"rand_core 0.10.0",
|
|
||||||
"wasip2",
|
|
||||||
"wasip3",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
@@ -676,6 +697,12 @@ version = "1.10.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpdate"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
@@ -690,6 +717,7 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
|
"httpdate",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pin-utils",
|
"pin-utils",
|
||||||
@@ -820,12 +848,6 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "id-arena"
|
|
||||||
version = "2.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -855,8 +877,6 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.16.1",
|
"hashbrown 0.16.1",
|
||||||
"serde",
|
|
||||||
"serde_core",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -932,12 +952,6 @@ dependencies = [
|
|||||||
"spin",
|
"spin",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "leb128fmt"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.182"
|
version = "0.2.182"
|
||||||
@@ -1005,6 +1019,12 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchit"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md-5"
|
name = "md-5"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@@ -1258,16 +1278,6 @@ dependencies = [
|
|||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "prettyplease"
|
|
||||||
version = "0.2.37"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
@@ -1369,17 +1379,6 @@ dependencies = [
|
|||||||
"rand_core 0.9.5",
|
"rand_core 0.9.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand"
|
|
||||||
version = "0.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
|
|
||||||
dependencies = [
|
|
||||||
"chacha20",
|
|
||||||
"getrandom 0.4.1",
|
|
||||||
"rand_core 0.10.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_chacha"
|
name = "rand_chacha"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -1418,12 +1417,6 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_core"
|
|
||||||
version = "0.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
@@ -1470,6 +1463,7 @@ dependencies = [
|
|||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
@@ -1682,12 +1676,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "semver"
|
|
||||||
version = "1.0.27"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
@@ -1731,6 +1719,17 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_path_to_error"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -1747,6 +1746,8 @@ dependencies = [
|
|||||||
name = "server"
|
name = "server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"rand 0.9.2",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -1757,15 +1758,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "session-rs"
|
name = "session-rs"
|
||||||
version = "0.1.3"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2f82aa4448e402161e9c7c33c6ee53a19e073d3eeca6c871a8a14383f088e7e"
|
checksum = "905acd6716b3f59af947b671baa7c333cc6d7b297df56db5db77e9cf7580ae1a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"axum",
|
||||||
"rand 0.10.0",
|
"futures-util",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha1",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1776,7 +1776,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures 0.2.17",
|
"cpufeatures",
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1787,7 +1787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures 0.2.17",
|
"cpufeatures",
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1797,6 +1797,16 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||||
|
dependencies = [
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signature"
|
name = "signature"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
@@ -2203,6 +2213,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
@@ -2240,6 +2251,18 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-tungstenite"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"tokio",
|
||||||
|
"tungstenite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.18"
|
version = "0.7.18"
|
||||||
@@ -2266,6 +2289,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2336,6 +2360,22 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tungstenite"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"data-encoding",
|
||||||
|
"http",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"rand 0.9.2",
|
||||||
|
"sha1",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.19.0"
|
version = "1.19.0"
|
||||||
@@ -2369,12 +2409,6 @@ version = "0.1.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
|
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-xid"
|
|
||||||
version = "0.2.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -2445,15 +2479,6 @@ dependencies = [
|
|||||||
"wit-bindgen",
|
"wit-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasip3"
|
|
||||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
|
||||||
dependencies = [
|
|
||||||
"wit-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasite"
|
name = "wasite"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -2519,40 +2544,6 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-encoder"
|
|
||||||
version = "0.244.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
|
||||||
dependencies = [
|
|
||||||
"leb128fmt",
|
|
||||||
"wasmparser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-metadata"
|
|
||||||
version = "0.244.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"indexmap",
|
|
||||||
"wasm-encoder",
|
|
||||||
"wasmparser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasmparser"
|
|
||||||
version = "0.244.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"hashbrown 0.15.5",
|
|
||||||
"indexmap",
|
|
||||||
"semver",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.88"
|
version = "0.3.88"
|
||||||
@@ -2929,88 +2920,6 @@ name = "wit-bindgen"
|
|||||||
version = "0.51.0"
|
version = "0.51.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||||
dependencies = [
|
|
||||||
"wit-bindgen-rust-macro",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wit-bindgen-core"
|
|
||||||
version = "0.51.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"heck",
|
|
||||||
"wit-parser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wit-bindgen-rust"
|
|
||||||
version = "0.51.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"heck",
|
|
||||||
"indexmap",
|
|
||||||
"prettyplease",
|
|
||||||
"syn",
|
|
||||||
"wasm-metadata",
|
|
||||||
"wit-bindgen-core",
|
|
||||||
"wit-component",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wit-bindgen-rust-macro"
|
|
||||||
version = "0.51.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"prettyplease",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
"wit-bindgen-core",
|
|
||||||
"wit-bindgen-rust",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wit-component"
|
|
||||||
version = "0.244.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"bitflags",
|
|
||||||
"indexmap",
|
|
||||||
"log",
|
|
||||||
"serde",
|
|
||||||
"serde_derive",
|
|
||||||
"serde_json",
|
|
||||||
"wasm-encoder",
|
|
||||||
"wasm-metadata",
|
|
||||||
"wasmparser",
|
|
||||||
"wit-parser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wit-parser"
|
|
||||||
version = "0.244.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"id-arena",
|
|
||||||
"indexmap",
|
|
||||||
"log",
|
|
||||||
"semver",
|
|
||||||
"serde",
|
|
||||||
"serde_derive",
|
|
||||||
"serde_json",
|
|
||||||
"unicode-xid",
|
|
||||||
"wasmparser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "writeable"
|
name = "writeable"
|
||||||
|
|||||||
+5
-3
@@ -4,13 +4,15 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
reqwest = { version = "0.13.2", features = ["json"] }
|
axum = { version = "0.8.9", features = ["ws"] }
|
||||||
|
rand = "0.9"
|
||||||
|
reqwest = { version = "0.13.2", features = ["json", "query"] }
|
||||||
serde = { version = "1.0.228", features = ["serde_derive"] }
|
serde = { version = "1.0.228", features = ["serde_derive"] }
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
session-rs = "0.1.3"
|
session-rs = { version = "0.2.1", default-features = false, features = ["axum"] }
|
||||||
sqlx = { version = "0.8.6", features = [
|
sqlx = { version = "0.8.6", features = [
|
||||||
"sqlite",
|
"sqlite",
|
||||||
"runtime-tokio-native-tls",
|
"runtime-tokio-native-tls",
|
||||||
"macros",
|
"macros",
|
||||||
] }
|
] }
|
||||||
tokio = "1.49.0"
|
tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread", "net", "signal", "sync", "time"] }
|
||||||
|
|||||||
@@ -39,4 +39,7 @@ ENV BIND_ADDR=0.0.0.0:8080 \
|
|||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD ["server", "healthcheck"]
|
||||||
|
|
||||||
CMD ["server"]
|
CMD ["server"]
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ services:
|
|||||||
# Coolify's proxy; set the domain in Coolify as https://<domain>:8080.
|
# Coolify's proxy; set the domain in Coolify as https://<domain>:8080.
|
||||||
ports:
|
ports:
|
||||||
- "${HOST_PORT:-8080}:8080"
|
- "${HOST_PORT:-8080}:8080"
|
||||||
|
environment:
|
||||||
|
# Comma-separated client versions served at GET /versions.
|
||||||
|
SUPPORTED_VERSIONS: ${SUPPORTED_VERSIONS:-0.1.3-beta}
|
||||||
|
DEPRECATED_VERSIONS: ${DEPRECATED_VERSIONS:-0.1.2-beta}
|
||||||
volumes:
|
volumes:
|
||||||
- server-data:/data
|
- server-data:/data
|
||||||
|
|
||||||
|
|||||||
+2
-4
@@ -44,8 +44,7 @@ pub async fn buy(
|
|||||||
item_id: String,
|
item_id: String,
|
||||||
pool: Arc<SqlitePool>,
|
pool: Arc<SqlitePool>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
// Lock once
|
let uuid = crate::methods::auth::require(&uuid).await?;
|
||||||
let uuid = uuid.lock().await.clone();
|
|
||||||
|
|
||||||
let mut user = user::get(&uuid, &pool).await?;
|
let mut user = user::get(&uuid, &pool).await?;
|
||||||
|
|
||||||
@@ -88,8 +87,7 @@ pub async fn equip(
|
|||||||
item_id: String,
|
item_id: String,
|
||||||
pool: Arc<SqlitePool>,
|
pool: Arc<SqlitePool>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
// Lock once
|
let uuid = crate::methods::auth::require(&uuid).await?;
|
||||||
let uuid = uuid.lock().await.clone();
|
|
||||||
|
|
||||||
let mut user = user::get(&uuid, &pool).await?;
|
let mut user = user::get(&uuid, &pool).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
/// Most players a single `emote` or `send_player` request may target.
|
||||||
|
pub const MAX_TARGETS: usize = 256;
|
||||||
|
|
||||||
|
/// Validates and de-duplicates a request's target UUIDs.
|
||||||
|
pub fn targets(targets: Vec<String>) -> Result<HashSet<String>, String> {
|
||||||
|
if targets.len() > MAX_TARGETS {
|
||||||
|
return Err(format!("Too many targets (max {MAX_TARGETS})"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(targets.into_iter().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Token bucket: allows bursts of up to `burst` requests, refilled at one
|
||||||
|
/// token per `refill`.
|
||||||
|
pub struct RateLimiter {
|
||||||
|
state: Mutex<(f64, Instant)>,
|
||||||
|
burst: f64,
|
||||||
|
per_sec: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RateLimiter {
|
||||||
|
pub fn new(burst: u32, refill: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
state: Mutex::new((burst as f64, Instant::now())),
|
||||||
|
burst: burst as f64,
|
||||||
|
per_sec: 1.0 / refill.as_secs_f64(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check(&self) -> Result<(), String> {
|
||||||
|
let mut state = self.state.lock().await;
|
||||||
|
let (tokens, last) = &mut *state;
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
|
*tokens = (*tokens + now.duration_since(*last).as_secs_f64() * self.per_sec).min(self.burst);
|
||||||
|
*last = now;
|
||||||
|
|
||||||
|
if *tokens < 1.0 {
|
||||||
|
return Err("Rate limited, try again shortly".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
*tokens -= 1.0;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn allows_a_burst_then_refills() {
|
||||||
|
let limiter = RateLimiter::new(2, Duration::from_millis(50));
|
||||||
|
|
||||||
|
assert!(limiter.check().await.is_ok());
|
||||||
|
assert!(limiter.check().await.is_ok());
|
||||||
|
assert!(limiter.check().await.is_err());
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(60)).await;
|
||||||
|
assert!(limiter.check().await.is_ok());
|
||||||
|
assert!(limiter.check().await.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn caps_and_dedupes_targets() {
|
||||||
|
let many = (0..=MAX_TARGETS).map(|i| i.to_string()).collect();
|
||||||
|
assert!(targets(many).is_err());
|
||||||
|
|
||||||
|
let dupes = vec!["a".to_string(), "a".to_string(), "b".to_string()];
|
||||||
|
assert_eq!(targets(dupes).unwrap().len(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
+175
-60
@@ -1,35 +1,130 @@
|
|||||||
mod cosmetics;
|
mod cosmetics;
|
||||||
|
mod limits;
|
||||||
mod methods;
|
mod methods;
|
||||||
mod types;
|
mod types;
|
||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
use std::{collections::HashMap, pin::Pin, sync::Arc};
|
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use session_rs::server::SessionServer;
|
use axum::{
|
||||||
|
Json, Router,
|
||||||
|
extract::{State, WebSocketUpgrade},
|
||||||
|
http::StatusCode,
|
||||||
|
response::Response,
|
||||||
|
routing::get,
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
use session_rs::Session;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::types::SessionMap;
|
use crate::types::SessionMap;
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
/// Largest WebSocket message a client may send. Every RPC payload is tiny.
|
||||||
async fn main() -> session_rs::Result<()> {
|
const MAX_MESSAGE_SIZE: usize = 1 << 20;
|
||||||
let pool = Arc::new(user::init_db().await);
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct AppState {
|
||||||
|
pool: Arc<SqlitePool>,
|
||||||
|
sessions: SessionMap,
|
||||||
|
versions: Arc<VersionManifest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Client versions the server accepts. Clients on a `deprecated` version are
|
||||||
|
/// warned but still connect; clients on neither list refuse to connect.
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct VersionManifest {
|
||||||
|
supported: Vec<String>,
|
||||||
|
deprecated: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VersionManifest {
|
||||||
|
/// Reads comma-separated `SUPPORTED_VERSIONS` and `DEPRECATED_VERSIONS`.
|
||||||
|
fn from_env() -> Self {
|
||||||
|
let list = |key: &str, default: &str| -> Vec<String> {
|
||||||
|
std::env::var(key)
|
||||||
|
.unwrap_or_else(|_| default.to_string())
|
||||||
|
.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.map(String::from)
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
supported: list("SUPPORTED_VERSIONS", "0.1.3-beta"),
|
||||||
|
deprecated: list("DEPRECATED_VERSIONS", "0.1.2-beta"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
let bind_addr = std::env::var("BIND_ADDR").unwrap_or_else(|_| "127.0.0.1:8080".into());
|
let bind_addr = std::env::var("BIND_ADDR").unwrap_or_else(|_| "127.0.0.1:8080".into());
|
||||||
let server = SessionServer::bind(&bind_addr).await?;
|
|
||||||
|
// `server healthcheck` probes a running server; used by the Docker HEALTHCHECK.
|
||||||
|
if std::env::args().nth(1).as_deref() == Some("healthcheck") {
|
||||||
|
std::process::exit(healthcheck(&bind_addr).await);
|
||||||
|
}
|
||||||
|
|
||||||
|
let versions = VersionManifest::from_env();
|
||||||
|
println!("Client versions: {versions:?}");
|
||||||
|
|
||||||
|
let state = AppState {
|
||||||
|
pool: Arc::new(user::init_db().await),
|
||||||
|
sessions: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
versions: Arc::new(versions),
|
||||||
|
};
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/", get(ws))
|
||||||
|
.route("/health", get(health))
|
||||||
|
.route("/versions", get(versions_manifest))
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(&bind_addr).await?;
|
||||||
println!("Listening on {bind_addr}");
|
println!("Listening on {bind_addr}");
|
||||||
|
|
||||||
let sessions: SessionMap = Arc::new(Mutex::new(HashMap::new()));
|
axum::serve(listener, app)
|
||||||
|
.with_graceful_shutdown(shutdown_signal())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
server
|
async fn ws(upgrade: WebSocketUpgrade, State(state): State<AppState>) -> Response {
|
||||||
.session_loop({
|
upgrade
|
||||||
let pool = Arc::clone(&pool);
|
.max_message_size(MAX_MESSAGE_SIZE)
|
||||||
move |session, _| {
|
.max_frame_size(MAX_MESSAGE_SIZE)
|
||||||
let pool = Arc::clone(&pool);
|
.on_upgrade(move |socket| async move {
|
||||||
let sessions = Arc::clone(&sessions);
|
|
||||||
|
|
||||||
Box::pin(async move {
|
|
||||||
println!("Connected");
|
println!("Connected");
|
||||||
|
let session = Session::from_axum(socket);
|
||||||
|
|
||||||
|
register_handlers(&session, state).await;
|
||||||
|
|
||||||
|
session.start_ping(Duration::from_secs(30), Duration::from_secs(5));
|
||||||
|
session.start_receiver();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health(State(state): State<AppState>) -> (StatusCode, &'static str) {
|
||||||
|
match sqlx::query("SELECT 1").execute(state.pool.as_ref()).await {
|
||||||
|
Ok(_) => (StatusCode::OK, "ok"),
|
||||||
|
Err(_) => (StatusCode::SERVICE_UNAVAILABLE, "database unavailable"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn versions_manifest(State(state): State<AppState>) -> Json<Arc<VersionManifest>> {
|
||||||
|
Json(state.versions)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn register_handlers(session: &Session, state: AppState) {
|
||||||
|
let AppState { pool, sessions, .. } = state;
|
||||||
let uuid = Arc::new(Mutex::new(String::new()));
|
let uuid = Arc::new(Mutex::new(String::new()));
|
||||||
let name = Arc::new(Mutex::new(String::new()));
|
let name = Arc::new(Mutex::new(String::new()));
|
||||||
|
let pending: methods::auth::PendingChallenge = Arc::new(Mutex::new(None));
|
||||||
|
|
||||||
|
// Per-connection limits on the methods that fan out to other players.
|
||||||
|
let emote_limit = Arc::new(limits::RateLimiter::new(5, Duration::from_secs(1)));
|
||||||
|
let send_player_limit = Arc::new(limits::RateLimiter::new(10, Duration::from_secs(1)));
|
||||||
|
|
||||||
session
|
session
|
||||||
.on_close({
|
.on_close({
|
||||||
@@ -42,41 +137,44 @@ async fn main() -> session_rs::Result<()> {
|
|||||||
let sessions = Arc::clone(&sessions);
|
let sessions = Arc::clone(&sessions);
|
||||||
let session = session.clone();
|
let session = session.clone();
|
||||||
|
|
||||||
Box::pin(async move {
|
async move {
|
||||||
let uuid_lock = uuid.lock().await;
|
let uuid_lock = uuid.lock().await;
|
||||||
if !uuid_lock.is_empty() {
|
if !uuid_lock.is_empty() {
|
||||||
if let Some(sessions) =
|
if let Some(sessions) = sessions.lock().await.get_mut(uuid_lock.as_str()) {
|
||||||
sessions.lock().await.get_mut(uuid_lock.as_str())
|
|
||||||
{
|
|
||||||
sessions.remove(&session);
|
sessions.remove(&session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
session.start_ping(
|
session
|
||||||
tokio::time::Duration::from_secs(30),
|
.on_request::<methods::AuthChallenge, _>({
|
||||||
tokio::time::Duration::from_secs(5),
|
let uuid = Arc::clone(&uuid);
|
||||||
);
|
let pending = Arc::clone(&pending);
|
||||||
|
|
||||||
|
move |_, username| methods::auth::challenge(uuid.clone(), pending.clone(), username)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
session
|
session
|
||||||
.on_request::<methods::Auth, _>({
|
.on_request::<methods::AuthVerify, _>({
|
||||||
let pool = Arc::clone(&pool);
|
let pool = Arc::clone(&pool);
|
||||||
let uuid = Arc::clone(&uuid);
|
let uuid = Arc::clone(&uuid);
|
||||||
let sessions = Arc::clone(&sessions);
|
let sessions = Arc::clone(&sessions);
|
||||||
let name = Arc::clone(&name);
|
let name = Arc::clone(&name);
|
||||||
|
let pending = Arc::clone(&pending);
|
||||||
let session = session.clone();
|
let session = session.clone();
|
||||||
|
|
||||||
move |_, token| {
|
move |_, ()| {
|
||||||
methods::auth::authenticate(
|
methods::auth::verify(
|
||||||
sessions.clone(),
|
sessions.clone(),
|
||||||
session.clone(),
|
session.clone(),
|
||||||
name.clone(),
|
name.clone(),
|
||||||
uuid.clone(),
|
uuid.clone(),
|
||||||
token,
|
pending.clone(),
|
||||||
pool.clone(),
|
pool.clone(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -89,14 +187,11 @@ async fn main() -> session_rs::Result<()> {
|
|||||||
let pool = Arc::clone(&pool);
|
let pool = Arc::clone(&pool);
|
||||||
|
|
||||||
move |_, item_id| {
|
move |_, item_id| {
|
||||||
let pool = Arc::clone(&pool);
|
|
||||||
let item_id = item_id.clone();
|
|
||||||
|
|
||||||
cosmetics::buy(
|
cosmetics::buy(
|
||||||
cosmetics::CosmeticKind::Cloak,
|
cosmetics::CosmeticKind::Cloak,
|
||||||
uuid.clone(),
|
uuid.clone(),
|
||||||
item_id,
|
item_id,
|
||||||
pool,
|
Arc::clone(&pool),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -108,14 +203,11 @@ async fn main() -> session_rs::Result<()> {
|
|||||||
let pool = Arc::clone(&pool);
|
let pool = Arc::clone(&pool);
|
||||||
|
|
||||||
move |_, item_id| {
|
move |_, item_id| {
|
||||||
let pool = Arc::clone(&pool);
|
|
||||||
let item_id = item_id.clone();
|
|
||||||
|
|
||||||
cosmetics::buy(
|
cosmetics::buy(
|
||||||
cosmetics::CosmeticKind::Hat,
|
cosmetics::CosmeticKind::Hat,
|
||||||
uuid.clone(),
|
uuid.clone(),
|
||||||
item_id,
|
item_id,
|
||||||
pool,
|
Arc::clone(&pool),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -127,14 +219,11 @@ async fn main() -> session_rs::Result<()> {
|
|||||||
let pool = Arc::clone(&pool);
|
let pool = Arc::clone(&pool);
|
||||||
|
|
||||||
move |_, item_id| {
|
move |_, item_id| {
|
||||||
let pool = Arc::clone(&pool);
|
|
||||||
let item_id = item_id.clone();
|
|
||||||
|
|
||||||
cosmetics::equip(
|
cosmetics::equip(
|
||||||
cosmetics::CosmeticKind::Cloak,
|
cosmetics::CosmeticKind::Cloak,
|
||||||
uuid.clone(),
|
uuid.clone(),
|
||||||
item_id,
|
item_id,
|
||||||
pool,
|
Arc::clone(&pool),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -146,14 +235,11 @@ async fn main() -> session_rs::Result<()> {
|
|||||||
let pool = Arc::clone(&pool);
|
let pool = Arc::clone(&pool);
|
||||||
|
|
||||||
move |_, item_id| {
|
move |_, item_id| {
|
||||||
let pool = Arc::clone(&pool);
|
|
||||||
let item_id = item_id.clone();
|
|
||||||
|
|
||||||
cosmetics::equip(
|
cosmetics::equip(
|
||||||
cosmetics::CosmeticKind::Hat,
|
cosmetics::CosmeticKind::Hat,
|
||||||
uuid.clone(),
|
uuid.clone(),
|
||||||
item_id,
|
item_id,
|
||||||
pool,
|
Arc::clone(&pool),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -165,10 +251,12 @@ async fn main() -> session_rs::Result<()> {
|
|||||||
let uuid = Arc::clone(&uuid);
|
let uuid = Arc::clone(&uuid);
|
||||||
|
|
||||||
move |_, emote| {
|
move |_, emote| {
|
||||||
let sessions = Arc::clone(&sessions);
|
methods::emote::send_emote(
|
||||||
let uuid = Arc::clone(&uuid);
|
Arc::clone(&sessions),
|
||||||
|
Arc::clone(&uuid),
|
||||||
methods::emote::send_emote(sessions, uuid, emote)
|
Arc::clone(&emote_limit),
|
||||||
|
emote,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
@@ -178,13 +266,9 @@ async fn main() -> session_rs::Result<()> {
|
|||||||
let sessions = Arc::clone(&sessions);
|
let sessions = Arc::clone(&sessions);
|
||||||
let pool = Arc::clone(&pool);
|
let pool = Arc::clone(&pool);
|
||||||
|
|
||||||
move |_, uuid| {
|
let uuid = Arc::clone(&uuid);
|
||||||
methods::user::get_user(
|
|
||||||
sessions.clone(),
|
move |_, target| methods::user::get_user(sessions.clone(), uuid.clone(), target, pool.clone())
|
||||||
uuid.clone(),
|
|
||||||
pool.clone(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -200,16 +284,47 @@ async fn main() -> session_rs::Result<()> {
|
|||||||
sessions.clone(),
|
sessions.clone(),
|
||||||
name.clone(),
|
name.clone(),
|
||||||
uuid.clone(),
|
uuid.clone(),
|
||||||
targets.targets.clone(),
|
Arc::clone(&send_player_limit),
|
||||||
|
targets.targets,
|
||||||
pool.clone(),
|
pool.clone(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
Ok::<(), session_rs::Error>(())
|
/// Exit code 0 if `/health` on the local server answers 2xx.
|
||||||
}) as Pin<Box<dyn Future<Output = _> + Send>>
|
async fn healthcheck(bind_addr: &str) -> i32 {
|
||||||
|
let port = bind_addr.rsplit(':').next().unwrap_or("8080");
|
||||||
|
|
||||||
|
match reqwest::get(format!("http://127.0.0.1:{port}/health")).await {
|
||||||
|
Ok(res) if res.status().is_success() => 0,
|
||||||
|
_ => 1,
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.await
|
|
||||||
|
/// Resolves on Ctrl+C or SIGTERM (what Docker sends on stop).
|
||||||
|
async fn shutdown_signal() {
|
||||||
|
let ctrl_c = async {
|
||||||
|
let _ = tokio::signal::ctrl_c().await;
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
let terminate = async {
|
||||||
|
if let Ok(mut signal) =
|
||||||
|
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||||
|
{
|
||||||
|
signal.recv().await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
let terminate = std::future::pending::<()>();
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = ctrl_c => {}
|
||||||
|
_ = terminate => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Shutting down");
|
||||||
}
|
}
|
||||||
|
|||||||
+79
-22
@@ -2,63 +2,120 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use session_rs::session::Session;
|
use session_rs::session::Session;
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
types::{SessionMap, UUID},
|
types::{MinecraftAuthResponse, SessionMap, UUID},
|
||||||
user::User,
|
user::User,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn authenticate(
|
const HAS_JOINED_URL: &str = "https://sessionserver.mojang.com/session/minecraft/hasJoined";
|
||||||
|
|
||||||
|
/// A challenge issued to a connection that hasn't authenticated yet.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Challenge {
|
||||||
|
username: String,
|
||||||
|
server_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type PendingChallenge = Arc<Mutex<Option<Challenge>>>;
|
||||||
|
|
||||||
|
/// Returns the connection's authenticated UUID, or an error before `auth_verify`.
|
||||||
|
pub async fn require(uuid: &UUID) -> Result<String, String> {
|
||||||
|
let uuid = uuid.lock().await;
|
||||||
|
|
||||||
|
if uuid.is_empty() {
|
||||||
|
return Err("Not authenticated".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(uuid.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minecraft usernames: 1–16 characters of letters, digits and underscores.
|
||||||
|
fn is_valid_username(name: &str) -> bool {
|
||||||
|
(1..=16).contains(&name.len()) && name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Issues a random server id for the client to `join` with at Mojang.
|
||||||
|
pub async fn challenge(uuid: UUID, pending: PendingChallenge, username: String) -> Result<String, String> {
|
||||||
|
if !uuid.lock().await.is_empty() {
|
||||||
|
return Err("Already authenticated".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_valid_username(&username) {
|
||||||
|
return Err("Invalid username".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let server_id: String = rand::random::<[u8; 20]>()
|
||||||
|
.iter()
|
||||||
|
.map(|b| format!("{b:02x}"))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
*pending.lock().await = Some(Challenge {
|
||||||
|
username,
|
||||||
|
server_id: server_id.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(server_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Confirms with Mojang that the challenged player joined with our server id.
|
||||||
|
pub async fn verify(
|
||||||
sessions: SessionMap,
|
sessions: SessionMap,
|
||||||
session: Session,
|
session: Session,
|
||||||
name: UUID,
|
name: UUID,
|
||||||
uuid: UUID,
|
uuid: UUID,
|
||||||
session_token: String,
|
pending: PendingChallenge,
|
||||||
pool: Arc<SqlitePool>,
|
pool: Arc<SqlitePool>,
|
||||||
) -> Result<User, String> {
|
) -> Result<User, String> {
|
||||||
if !uuid.lock().await.is_empty() {
|
if !uuid.lock().await.is_empty() {
|
||||||
return Err(format!("Already authenticated"));
|
return Err("Already authenticated".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
// Single use: a failed verification needs a fresh challenge.
|
||||||
|
let Some(challenge) = pending.lock().await.take() else {
|
||||||
|
return Err("No pending challenge, call auth_challenge first".to_string());
|
||||||
|
};
|
||||||
|
|
||||||
let response = client
|
let response = reqwest::Client::new()
|
||||||
.get("https://api.minecraftservices.com/minecraft/profile")
|
.get(HAS_JOINED_URL)
|
||||||
.bearer_auth(&session_token)
|
.query(&[
|
||||||
|
("username", challenge.username.as_str()),
|
||||||
|
("serverId", challenge.server_id.as_str()),
|
||||||
|
])
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|_| "Failed to validate session".to_string())?;
|
.map_err(|_| "Failed to reach the Mojang session server".to_string())?;
|
||||||
|
|
||||||
|
// 204 No Content: the player didn't join with this server id.
|
||||||
|
if response.status() == reqwest::StatusCode::NO_CONTENT {
|
||||||
|
return Err("Session not verified by Mojang".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Authentication failed with code {}",
|
"Mojang session server returned {}",
|
||||||
response.status()
|
response.status()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let auth = response
|
let auth = response
|
||||||
.json::<crate::types::MinecraftAuthResponse>()
|
.json::<MinecraftAuthResponse>()
|
||||||
.await
|
.await
|
||||||
.map_err(|_| "Unable to parse auth response".to_string())
|
.map_err(|_| "Unable to parse Mojang response".to_string())?;
|
||||||
.and_then(|auth| {
|
|
||||||
let id = crate::types::format_uuid(&auth.id)?;
|
let id = crate::types::format_uuid(&auth.id)?;
|
||||||
Ok(crate::types::MinecraftAuthResponse {
|
|
||||||
name: auth.name,
|
|
||||||
id,
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
*uuid.lock().await = auth.id.clone();
|
*uuid.lock().await = id.clone();
|
||||||
*name.lock().await = auth.name.clone();
|
*name.lock().await = auth.name.clone();
|
||||||
|
|
||||||
sessions
|
sessions
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.entry(auth.id.clone())
|
.entry(id.clone())
|
||||||
.or_default()
|
.or_default()
|
||||||
.insert(session);
|
.insert(session);
|
||||||
|
|
||||||
println!("{:?}", auth);
|
println!("Authenticated {} ({id})", auth.name);
|
||||||
|
|
||||||
crate::user::get_put(&auth.id, &pool).await
|
crate::user::get_put(&id, &pool).await
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-3
@@ -1,16 +1,26 @@
|
|||||||
use crate::types::{SessionMap, UUID};
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
limits::{self, RateLimiter},
|
||||||
|
types::{SessionMap, UUID},
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn send_emote(
|
pub async fn send_emote(
|
||||||
sessions: SessionMap,
|
sessions: SessionMap,
|
||||||
uuid: UUID,
|
uuid: UUID,
|
||||||
|
limit: Arc<RateLimiter>,
|
||||||
emote: crate::types::EmoteRequest,
|
emote: crate::types::EmoteRequest,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
let from = crate::methods::auth::require(&uuid).await?;
|
||||||
|
let targets = limits::targets(emote.targets)?;
|
||||||
|
limit.check().await?;
|
||||||
|
|
||||||
let emote_event = crate::types::EventEmote {
|
let emote_event = crate::types::EventEmote {
|
||||||
from: uuid.lock().await.clone(),
|
from,
|
||||||
emote: emote.emote,
|
emote: emote.emote,
|
||||||
};
|
};
|
||||||
|
|
||||||
for target in emote.targets {
|
for target in targets {
|
||||||
if let Some(sessions) = sessions.lock().await.get_mut(&target) {
|
if let Some(sessions) = sessions.lock().await.get_mut(&target) {
|
||||||
let emote_event = emote_event.clone();
|
let emote_event = emote_event.clone();
|
||||||
let mut bad_sessions = Vec::new();
|
let mut bad_sessions = Vec::new();
|
||||||
|
|||||||
+17
-3
@@ -10,12 +10,26 @@ use crate::{
|
|||||||
user::User,
|
user::User,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Step 1 of authentication: the client sends its username and gets a random
|
||||||
|
/// server id to pass to Mojang's session server `join` endpoint.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Auth;
|
pub struct AuthChallenge;
|
||||||
|
|
||||||
impl Method for Auth {
|
impl Method for AuthChallenge {
|
||||||
const NAME: &'static str = "auth";
|
const NAME: &'static str = "auth_challenge";
|
||||||
type Request = String;
|
type Request = String;
|
||||||
|
type Response = String;
|
||||||
|
type Error = String;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Step 2 of authentication: after joining, the server confirms the player
|
||||||
|
/// with Mojang's `hasJoined` endpoint. The access token never reaches us.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AuthVerify;
|
||||||
|
|
||||||
|
impl Method for AuthVerify {
|
||||||
|
const NAME: &'static str = "auth_verify";
|
||||||
|
type Request = ();
|
||||||
type Response = User;
|
type Response = User;
|
||||||
type Error = String;
|
type Error = String;
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -3,6 +3,7 @@ use std::sync::Arc;
|
|||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
limits::{self, RateLimiter},
|
||||||
methods,
|
methods,
|
||||||
types::{PlayerStream, SessionMap, UUID},
|
types::{PlayerStream, SessionMap, UUID},
|
||||||
user::User,
|
user::User,
|
||||||
@@ -10,9 +11,12 @@ use crate::{
|
|||||||
|
|
||||||
pub async fn get_user(
|
pub async fn get_user(
|
||||||
sessions: SessionMap,
|
sessions: SessionMap,
|
||||||
|
caller: UUID,
|
||||||
uuid: String,
|
uuid: String,
|
||||||
pool: Arc<SqlitePool>,
|
pool: Arc<SqlitePool>,
|
||||||
) -> Result<Option<User>, String> {
|
) -> Result<Option<User>, String> {
|
||||||
|
crate::methods::auth::require(&caller).await?;
|
||||||
|
|
||||||
if !sessions.lock().await.contains_key(&uuid) {
|
if !sessions.lock().await.contains_key(&uuid) {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@@ -24,11 +28,15 @@ pub async fn send_user(
|
|||||||
sessions: SessionMap,
|
sessions: SessionMap,
|
||||||
name: UUID,
|
name: UUID,
|
||||||
uuid: UUID,
|
uuid: UUID,
|
||||||
|
limit: Arc<RateLimiter>,
|
||||||
targets: Vec<String>,
|
targets: Vec<String>,
|
||||||
pool: Arc<SqlitePool>,
|
pool: Arc<SqlitePool>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
let uuid = crate::methods::auth::require(&uuid).await?;
|
||||||
|
let targets = limits::targets(targets)?;
|
||||||
|
limit.check().await?;
|
||||||
|
|
||||||
println!("send player {targets:?}");
|
println!("send player {targets:?}");
|
||||||
let uuid = uuid.lock().await.to_string();
|
|
||||||
|
|
||||||
let user = PlayerStream {
|
let user = PlayerStream {
|
||||||
player: crate::user::get(&uuid, pool.as_ref()).await?,
|
player: crate::user::get(&uuid, pool.as_ref()).await?,
|
||||||
|
|||||||
Reference in New Issue
Block a user