diff --git a/Cargo.lock b/Cargo.lock index cd4d4e3b..4f30961c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,21 +19,22 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", "getrandom", "once_cell", "version_check", + "zerocopy", ] [[package]] name = "aho-corasick" -version = "1.0.5" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] @@ -108,18 +109,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.36", + "syn 2.0.39", ] [[package]] name = "async-trait" -version = "0.1.73" +version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.36", + "syn 2.0.39", ] [[package]] @@ -131,6 +132,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-write-file" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436" +dependencies = [ + "nix", + "rand", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -203,9 +214,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.4" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64ct" @@ -221,9 +232,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" dependencies = [ "serde", ] @@ -239,9 +250,9 @@ dependencies = [ [[package]] name = "blaze-ssl-async" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25662e81aaa3ed48b51b19c101c3e36b37285303c55b394624a018adfca6ecaa" +checksum = "98ee6687c94bbb4f40bc46ac4d617ec01d7128ef7b195b5a8f0ba40a1ba75b77" dependencies = [ "rsa 0.8.2", "tokio", @@ -265,9 +276,9 @@ checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" @@ -302,7 +313,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -311,17 +322,27 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] @@ -337,9 +358,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" @@ -400,7 +421,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.36", + "syn 2.0.39", ] [[package]] @@ -411,7 +432,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.36", + "syn 2.0.39", ] [[package]] @@ -513,7 +534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf0fab0b584e67341bbfedce7c8d59d9cebaa9088fa494338ed4f8be92130bd3" dependencies = [ "quote", - "syn 2.0.36", + "syn 2.0.39", "walkdir", ] @@ -534,23 +555,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] @@ -561,7 +571,7 @@ checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" dependencies = [ "cfg-if", "home", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -572,9 +582,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "fastrand" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "finl_unicode" @@ -590,9 +600,9 @@ checksum = "d52a7e408202050813e6f1d9addadcaafef3dca7530c7ddfb005d4081cce6779" [[package]] name = "flate2" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "libz-sys", @@ -601,13 +611,12 @@ dependencies = [ [[package]] name = "flume" -version = "0.10.14" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ "futures-core", "futures-sink", - "pin-project", "spin 0.9.8", ] @@ -619,22 +628,21 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" dependencies = [ "futures-channel", "futures-core", - "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -643,9 +651,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", "futures-sink", @@ -653,15 +661,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" dependencies = [ "futures-core", "futures-task", @@ -681,38 +689,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.36", + "syn 2.0.39", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-channel", "futures-core", @@ -738,9 +746,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", @@ -749,15 +757,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "h2" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes", "fnv", @@ -765,7 +773,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap", "slab", "tokio", "tokio-util", @@ -774,15 +782,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.0" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ "ahash", "allocator-api2", @@ -794,7 +796,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.0", + "hashbrown", ] [[package]] @@ -808,9 +810,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -842,14 +844,14 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "http" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -896,7 +898,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.9", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -905,9 +907,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http", @@ -919,16 +921,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] @@ -948,9 +950,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -958,22 +960,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - -[[package]] -name = "indexmap" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown", ] [[package]] @@ -990,14 +982,14 @@ checksum = "ce243b1bfa62ffc028f1cc3b6034ec63d649f3031bc8a4fbbb004e1ac17d1f68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.36", + "syn 2.0.39", ] [[package]] name = "ipnet" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itertools" @@ -1016,9 +1008,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" dependencies = [ "wasm-bindgen", ] @@ -1034,21 +1026,21 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.148" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libm" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libsqlite3-sys" -version = "0.26.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" dependencies = [ "cc", "pkg-config", @@ -1068,27 +1060,27 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.7" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "local-ip-address" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fefe707432eb6bd4704b3dacfc87aab269d56667ad05dcd6869534e8890e767" +checksum = "66357e687a569abca487dc399a9c9ac19beb3f13991ed49f00c144e02cbd42ab" dependencies = [ "libc", "neli", "thiserror", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -1150,24 +1142,25 @@ dependencies = [ [[package]] name = "matchit" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "md-5" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ + "cfg-if", "digest", ] [[package]] name = "memchr" -version = "2.6.3" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "mime" @@ -1192,13 +1185,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1226,6 +1219,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -1276,9 +1280,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", "libm", @@ -1311,9 +1315,9 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "ordered-float" -version = "3.9.1" +version = "3.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a54938017eacd63036332b4ae5c8a49fc8c0c1d6d629893057e4f13609edd06" +checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" dependencies = [ "num-traits", ] @@ -1339,7 +1343,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.36", + "syn 2.0.39", ] [[package]] @@ -1354,15 +1358,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", + "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -1402,9 +1406,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" @@ -1423,7 +1427,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.36", + "syn 2.0.39", ] [[package]] @@ -1458,7 +1462,7 @@ checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ "der 0.7.8", "pkcs8 0.10.2", - "spki 0.7.2", + "spki 0.7.3", ] [[package]] @@ -1478,7 +1482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der 0.7.8", - "spki 0.7.2", + "spki 0.7.3", ] [[package]] @@ -1489,19 +1493,18 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "pocket-relay" -version = "0.5.10" +version = "0.6.0-beta" dependencies = [ "argon2", "axum", "base64ct", - "bitflags 2.4.0", + "bitflags 2.4.1", "blaze-ssl-async", "bytes", "chrono", "email_address", "embeddy", "flate2", - "futures", "futures-util", "hyper", "indoc", @@ -1509,6 +1512,8 @@ dependencies = [ "log", "log-panics", "log4rs", + "parking_lot", + "rand", "reqwest", "ring", "sea-orm", @@ -1554,9 +1559,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.67" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] @@ -1602,32 +1607,23 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "regex" -version = "1.9.5" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.8", - "regex-syntax 0.7.5", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", ] [[package]] @@ -1641,13 +1637,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.8" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.5", + "regex-syntax 0.8.2", ] [[package]] @@ -1658,15 +1654,15 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.20" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ "base64", "bytes", @@ -1690,6 +1686,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", "tokio-rustls", "tower-service", @@ -1697,23 +1694,22 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.25.2", + "webpki-roots", "winreg", ] [[package]] name = "ring" -version = "0.16.20" +version = "0.17.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "684d5e6e18f669ccebf64a92236bb7db9a34f07be010e3627368182027180866" dependencies = [ "cc", + "getrandom", "libc", - "once_cell", - "spin 0.5.2", + "spin 0.9.8", "untrusted", - "web-sys", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -1738,22 +1734,20 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.2" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" dependencies = [ - "byteorder", "const-oid", "digest", "num-bigint-dig", "num-integer", - "num-iter", "num-traits", "pkcs1 0.7.5", "pkcs8 0.10.2", "rand_core", "signature", - "spki 0.7.2", + "spki 0.7.3", "subtle", "zeroize", ] @@ -1766,22 +1760,22 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.13" +version = "0.38.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" +checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.21.7" +version = "0.21.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" dependencies = [ "log", "ring", @@ -1791,18 +1785,18 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ "base64", ] [[package]] name = "rustls-webpki" -version = "0.101.5" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ "ring", "untrusted", @@ -1837,9 +1831,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ "ring", "untrusted", @@ -1855,14 +1849,14 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.36", + "syn 2.0.39", ] [[package]] name = "sea-orm" -version = "0.12.2" +version = "0.12.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61f6c7daef05dde3476d97001e11fca7a52b655aa3bf4fd610ab2da1176a2ed5" +checksum = "e8e2dd2f8a2d129c1632ec45dcfc15c44cc3d8b969adc8ac58c5f011ca7aecee" dependencies = [ "async-stream", "async-trait", @@ -1883,23 +1877,23 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "0.12.2" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd90e73d5f5b184bad525767da29fbfec132b4e62ebd6f60d2f2737ec6468f62" +checksum = "816183a751bf9c22087679b20b6142da0b5c6d8981835ebb7b99bf1bf924640a" dependencies = [ "heck", "proc-macro2", "quote", "sea-bae", - "syn 2.0.36", + "syn 2.0.39", "unicode-ident", ] [[package]] name = "sea-orm-migration" -version = "0.12.2" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21f673fcefb3a7e7b89a12b6c0e854ec0be14367635ac3435369c8ad7f11e09e" +checksum = "9d45937e5d4869a0dcf0222bb264df564c077cbe9b312265f3717401d023a633" dependencies = [ "async-trait", "futures", @@ -1911,9 +1905,9 @@ dependencies = [ [[package]] name = "sea-query" -version = "0.30.1" +version = "0.30.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c05a5bf6403834be253489bbe95fa9b1e5486bc843b61f60d26b5c9c1e244b" +checksum = "41558fa9bb5f4d73952dac0b9d9c2ce23966493fc9ee0008037b01d709838a68" dependencies = [ "chrono", "derivative", @@ -1935,14 +1929,14 @@ dependencies = [ [[package]] name = "sea-query-derive" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd78f2e0ee8e537e9195d1049b752e0433e2cac125426bccb7b5c3e508096117" +checksum = "25a82fcb49253abcb45cdcb2adf92956060ec0928635eb21b4f7a6d8f25ab0bc" dependencies = [ "heck", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.39", "thiserror", ] @@ -1971,29 +1965,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.188" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.36", + "syn 2.0.39", ] [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -2024,9 +2018,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -2035,9 +2029,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -2046,9 +2040,9 @@ dependencies = [ [[package]] name = "sharded-slab" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] @@ -2064,9 +2058,9 @@ dependencies = [ [[package]] name = "signature" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", "rand_core", @@ -2083,15 +2077,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", @@ -2099,12 +2093,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2134,9 +2128,9 @@ dependencies = [ [[package]] name = "spki" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der 0.7.8", @@ -2155,9 +2149,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e58421b6bc416714d5115a2ca953718f6c621a51b68e4f4922aea5a4391a721" +checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2168,9 +2162,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4cef4251aabbae751a3710927945901ee1d97ee96d757f6880ebb9a79bfd53" +checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" dependencies = [ "ahash", "atoi", @@ -2189,7 +2183,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.0.0", + "indexmap", "log", "memchr", "once_cell", @@ -2207,14 +2201,14 @@ dependencies = [ "tokio-stream", "tracing", "url", - "webpki-roots 0.24.0", + "webpki-roots", ] [[package]] name = "sqlx-macros" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "208e3165167afd7f3881b16c1ef3f2af69fa75980897aac8874a0696516d12c2" +checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5" dependencies = [ "proc-macro2", "quote", @@ -2225,10 +2219,11 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a4a8336d278c62231d87f24e8a7a74898156e34c1c18942857be2acb29c7dfc" +checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841" dependencies = [ + "atomic-write-file", "dotenvy", "either", "heck", @@ -2251,13 +2246,13 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482" +checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" dependencies = [ "atoi", "base64", - "bitflags 2.4.0", + "bitflags 2.4.1", "byteorder", "bytes", "chrono", @@ -2280,7 +2275,7 @@ dependencies = [ "once_cell", "percent-encoding", "rand", - "rsa 0.9.2", + "rsa 0.9.6", "serde", "sha1", "sha2", @@ -2294,13 +2289,13 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e" +checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" dependencies = [ "atoi", "base64", - "bitflags 2.4.0", + "bitflags 2.4.1", "byteorder", "chrono", "crc", @@ -2334,9 +2329,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4c21bf34c7cae5b283efb3ac1bcc7670df7561124dc2f8bdc0b59be40f79a2" +checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" dependencies = [ "atoi", "chrono", @@ -2353,6 +2348,7 @@ dependencies = [ "sqlx-core", "tracing", "url", + "urlencoding", ] [[package]] @@ -2403,9 +2399,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.36" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e02e55d62894af2a08aca894c6577281f76769ba47c94d5756bec8ac6e7373" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -2418,6 +2414,27 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tdf" version = "0.1.4" @@ -2437,50 +2454,49 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.36", + "syn 2.0.39", ] [[package]] name = "tempfile" -version = "3.8.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.3.5", + "redox_syscall", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "thiserror" -version = "1.0.48" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.48" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.36", + "syn 2.0.39", ] [[package]] name = "thread-id" -version = "4.2.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79474f573561cdc4871a0de34a51c92f7f5a56039113fbb5b9c9f96bdb756669" +checksum = "f0ec81c46e9eb50deaa257be2f148adf052d1fb7701cfd55ccfab2525280b70b" dependencies = [ "libc", - "redox_syscall 0.2.16", "winapi", ] @@ -2511,9 +2527,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.32.0" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" dependencies = [ "backtrace", "bytes", @@ -2523,20 +2539,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.4", + "socket2 0.5.5", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.36", + "syn 2.0.39", ] [[package]] @@ -2562,9 +2578,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -2604,11 +2620,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -2617,29 +2632,29 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.36", + "syn 2.0.39", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "once_cell", @@ -2697,21 +2712,27 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2751,9 +2772,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2761,24 +2782,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.36", + "syn 2.0.39", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" dependencies = [ "cfg-if", "js-sys", @@ -2788,9 +2809,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2798,28 +2819,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.36", + "syn 2.0.39", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" dependencies = [ "js-sys", "wasm-bindgen", @@ -2827,18 +2848,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.24.0" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" -dependencies = [ - "rustls-webpki", -] - -[[package]] -name = "webpki-roots" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" [[package]] name = "whoami" @@ -2864,9 +2876,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -2878,12 +2890,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -2892,7 +2904,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -2901,13 +2922,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -2916,42 +2952,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winreg" version = "0.50.0" @@ -2959,7 +3037,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2974,8 +3052,28 @@ dependencies = [ "spki 0.6.0", ] +[[package]] +name = "zerocopy" +version = "0.7.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f15f7ade05d2a4935e34a457b936c23dc70a05cc1d97133dc99e7a3fe0f0e" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbbad221e3f78500350ecbd7dfa4e63ef945c05f4c61cb7f4d3f84cd0bba649b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/Cargo.toml b/Cargo.toml index ca1d28f1..ed20f292 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pocket-relay" -version = "0.5.10" +version = "0.6.0-beta" description = "Pocket Relay Server" readme = "README.md" keywords = ["EA", "PocketRelay", "MassEffect"] @@ -32,7 +32,7 @@ argon2 = { version = "0.5", features = ["std"] } base64ct = { version = "1.5", features = ["alloc"] } flate2 = { version = "1", features = ["zlib"], default-features = false } -ring = "0.16" +ring = "0.17" # Library for obtaining the local IP address of the device local-ip-address = "0.5" @@ -46,16 +46,16 @@ email_address = "0.2.4" # Codec utils for encoding and decoding packets tokio-util = { version = "0.7", features = ["codec"] } -# Hyper for connection upgrades +# Hyper for connection upgrades (TODO: Update to 1.0 once reqwest supports it) hyper = "0.14.25" tower = "0.4" bitflags = { version = "2.3.1", features = ["serde"] } tdf = { version = "0.1" } bytes = "1.4.0" -futures = "0.3" indoc = "2" +parking_lot = "0.12.1" # SeaORM [dependencies.sea-orm] @@ -87,7 +87,7 @@ features = [ "sync", ] -# Axum web framework +# Axum web framework (TODO: Update to 0.7 once once reqwest supports hyper 1.0) [dependencies.axum] version = "0.6.1" default-features = false @@ -111,6 +111,10 @@ version = "0.4" default-features = false features = ["std", "serde"] +[dev-dependencies] +# Random numbers for seeding +rand = "0.8" + [profile.release] strip = true lto = true diff --git a/README.md b/README.md index ad9c52c0..86d534c1 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,29 @@ +# Pocket Relay + -# Pocket Relay *Mass Effect 3 Server Emulator / Private Server* - ![License](https://img.shields.io/github/license/PocketRelay/Server?style=for-the-badge) ![Build](https://img.shields.io/github/actions/workflow/status/PocketRelay/Server/rust.yml?style=for-the-badge) -[Discord Server (discord.gg/yvycWW8RgR)](https://discord.gg/yvycWW8RgR) -[Website (pocket-relay.pages.dev)](https://pocket-relay.pages.dev/) -Development is undergone on the [dev](https://github.com/PocketRelay/Server/tree/dev) branch so the master branch can be -considered semi stable with only non breaking changes being merged in-between releases +[Discord Server (discord.gg/yvycWW8RgR)](https://discord.gg/yvycWW8RgR) | [Website (pocket-relay.pages.dev)](https://pocket-relay.pages.dev/) + + +The master branch contains the latest changes and may not be stable for general use, if you would like to compile a stable version from source its recommened you use a specific tag rather than master **Pocket Relay** Is a custom implementation of the Mass Effect 3 multiplayer servers all bundled into a easy to use server with a Dashboard for managing accounts and inventories. -With **Pocket Relay** you can play Mass Effect 3 multiplayer offline by yourself, over LAN, or even over WAN as a public server +With **Pocket Relay**, you can play Mass Effect 3 multiplayer offline, over LAN, or even over WAN as a public server. -View the website for information https://pocket-relay.pages.dev/ +Visit the [website](https://pocket-relay.pages.dev/) for more information. -## 📌 EA / BioWare Notice +## 🌐 EA / BioWare Notice -The **Pocket Relay** software in all its forms are in no way or form supported, endorsed, or provided by BioWare or Electronic Arts. Mass Effect is a registered trademark of Bioware/EA International (Studio and Publishing), Ltd in the U.S. and/or other countries. All Mass Effect art, images, and lore are the sole property of Bioware/EA International (Studio and Publishing), Ltd and have been reproduced here in an effort to assist the Mass Effect player community. All other trademarks are the property of their respective owners. +The **Pocket Relay** software, in all its forms, is not supported, endorsed, or provided by BioWare or Electronic Arts. Mass Effect is a registered trademark of Bioware/EA International (Studio and Publishing), Ltd in the U.S. and/or other countries. All Mass Effect art, images, and lore are the sole property of Bioware/EA International (Studio and Publishing), Ltd and are reproduced here to assist the Mass Effect player community. All other trademarks are the property of their respective owners. ## 📖 Starting your own server @@ -35,8 +35,8 @@ the [Server Setup Guide](https://pocket-relay.pages.dev/guide/server/) Below is a table of the download links for the different platforms -| Platform | Download | -| -------- | ------------------------------------------------------------------------------------------------------- | +| Platform | Download | +| -------- | --------------------------------------------------------------------------------------------------- | | Windows | [Download](https://github.com/PocketRelay/Server/releases/latest/download/pocket-relay-windows.exe) | | Linux | [Download](https://github.com/PocketRelay/Server/releases/latest/download/pocket-relay-linux) | @@ -45,33 +45,24 @@ You can find individual releases on the [Releases](https://github.com/PocketRela ## 🔧 Configuration -In order to configure the server such as changing the ports you can see the -configuration documentation [Here (docs/CONFIG.md)](https://pocket-relay.pages.dev/guide/config/) - +To configure the server, such as changing ports, refer to the [Configuration Documentation](https://pocket-relay.pages.dev/guide/config/). ## ⚙️ Features -- **Origin Support** This server supports **Origin** / **EA Launcher** copies of the game through its fetching system. As long as the official servers are still available and you have internet access the server will connect to the official servers to authorize **Origin** accounts. *This behavior can be disabled using the `PR_ORIGIN_FETCH` environment variable* -- **Origin Fetching** Along with supporting **Origin** authentication your player data from the official servers can also be loaded for those logging into **Origin** accounts. *This behavior can be disabled using the `PR_ORIGIN_FETCH_DATA` environment variable* -- **Portable & Platform Independent** This server can be run on most hardware and software due to its low requirements and custom -implementations of lots of required portions allowing you to run it -on Windows, Linux, etc. *Note the server will store the player data and logging in a folder named `data` in the same folder as the exe* -- **Cracked Support** This server supports cracked Mass Effect 3 copies so you can play on the server using them. -- **Docker Support** This server includes a `Dockerfile` so that it can be run in a containerized environment. The server uses a small alpine linux container to run inside -- **Dashboard** The server includes a management dashboard - - This includes leaderboards displays - - Allowing players to edit their username, email, and password - - Deleting and managing accounts - - Viewing running games - - Inventory editing for admins (Weapons, Classes, Characters, etc) - - View server logs as super admin +- **Origin Support:** Connects to official servers to authorize **Origin**/**EA Launcher** accounts (configurable). +- **Origin Fetching:** Loads player data from official servers for **Origin** accounts (configurable). +- **Portable & Platform Independent:** Low hardware requirements, platform-independent (data stored in a 'data' folder). +- **Unofficial Support:** Allows playing with unofficially licensed Mass Effect 3 copies. +- **Docker Support:** Includes a `Dockerfile` for containerized deployment in a small Alpine Linux container. +- **Dashboard:** Management dashboard with leaderboards, account management, game monitoring, and more. + ## 🚀 Manual Building -Instructions for building the server can be found at https://pocket-relay.pages.dev/docs/server/manual-building +Build instructions can be found [here](https://pocket-relay.pages.dev/docs/server/manual-building). > **Note** -> Building the server can be quite a heavy load on your computer +> Building the server can be resource-intensive. ## 🧾 License diff --git a/examples/nginx-client-auth/.gitignore b/examples/nginx-client-auth/.gitignore new file mode 100644 index 00000000..9b07fb07 --- /dev/null +++ b/examples/nginx-client-auth/.gitignore @@ -0,0 +1,4 @@ +data +server.crt +server.key +client.crt \ No newline at end of file diff --git a/examples/nginx-client-auth/config.json b/examples/nginx-client-auth/config.json new file mode 100644 index 00000000..d6112d35 --- /dev/null +++ b/examples/nginx-client-auth/config.json @@ -0,0 +1,4 @@ +{ + "port": 80, + "reverse_proxy": true +} \ No newline at end of file diff --git a/examples/nginx-client-auth/docker-compose.yml b/examples/nginx-client-auth/docker-compose.yml new file mode 100644 index 00000000..debee1b0 --- /dev/null +++ b/examples/nginx-client-auth/docker-compose.yml @@ -0,0 +1,23 @@ +version: "3" +services: + server: + restart: unless-stopped + container_name: pocket-relay + image: jacobtread/pocket-relay:latest + volumes: + # Bind the server config to a local config.json file + - ./config.json:/app/config.json + # Binding the server data to a local data folder + - ./data:/app/data + nginx: + restart: unless-stopped + image: nginx + ports: + - "443:443/tcp" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./client.crt:/etc/nginx/client.crt:ro + - ./server.crt:/etc/nginx/server.crt:ro + - ./server.key:/etc/nginx/server.key:ro + depends_on: + - server \ No newline at end of file diff --git a/examples/nginx-client-auth/nginx.conf b/examples/nginx-client-auth/nginx.conf new file mode 100644 index 00000000..de47346c --- /dev/null +++ b/examples/nginx-client-auth/nginx.conf @@ -0,0 +1,28 @@ +events {} + +http { + server { + listen 443 ssl; + + server_name localhost; + + ssl_certificate /etc/nginx/server.crt; + ssl_certificate_key /etc/nginx/server.key; + + ssl_client_certificate /etc/nginx/client.crt; + ssl_verify_client on; + + + location / { + proxy_pass http://server:80; + + # Provide server with real IP address of clients + proxy_set_header X-Real-IP $remote_addr; + + # Upgrade websocket connections + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_http_version 1.1; + } + } +} diff --git a/src/config.rs b/src/config.rs index b6369e1d..0d9e2a38 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,11 @@ use log::LevelFilter; use serde::Deserialize; -use std::{env, fs::read_to_string, path::Path}; +use std::{ + env, + fs::read_to_string, + net::{IpAddr, Ipv4Addr}, + path::Path, +}; use crate::session::models::Port; @@ -60,6 +65,7 @@ pub fn load_config() -> Option { #[derive(Deserialize)] #[serde(default)] pub struct Config { + pub host: IpAddr, pub port: Port, pub qos: QosServerConfig, pub reverse_proxy: bool, @@ -73,6 +79,7 @@ pub struct Config { impl Default for Config { fn default() -> Self { Self { + host: IpAddr::V4(Ipv4Addr::UNSPECIFIED), port: 80, qos: QosServerConfig::default(), reverse_proxy: false, @@ -95,6 +102,13 @@ pub enum QosServerConfig { Local, /// Use a custom QoS server Custom { host: String, port: u16 }, + /// Disable the QoS server (Public IP *wont* be resolved) + Disabled, + /// Configuration to use when using hamachi + Hamachi { + /// The address of the host computer (Required for 127.0.0.1 resolution) + host: Ipv4Addr, + }, } #[derive(Deserialize)] diff --git a/src/database/entities/leaderboard_data.rs b/src/database/entities/leaderboard_data.rs new file mode 100644 index 00000000..317cd672 --- /dev/null +++ b/src/database/entities/leaderboard_data.rs @@ -0,0 +1,263 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3 + +use crate::database::DbResult; +use crate::utils::types::PlayerID; +use sea_orm::sea_query::OnConflict; +use sea_orm::ActiveValue::NotSet; +use sea_orm::{prelude::*, FromQueryResult, InsertResult, QueryOrder, QuerySelect}; +use sea_orm::{ActiveValue::Set, DatabaseConnection, EntityTrait}; +use serde::{Deserialize, Serialize}; +use std::future::Future; + +#[derive(Serialize, Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "leaderboard_data")] +pub struct Model { + /// Unique Identifier for the entry + #[sea_orm(primary_key)] + #[serde(skip)] + pub id: u32, + /// The type of leaderboard this data is for + #[serde(skip)] + pub ty: LeaderboardType, + /// ID of the player this data is for + pub player_id: PlayerID, + /// The value of this leaderboard data + pub value: u32, +} + +/// Type of leaderboard entity +#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, Deserialize, EnumIter, DeriveActiveEnum)] +#[sea_orm(rs_type = "u8", db_type = "TinyUnsigned")] +#[repr(u8)] +pub enum LeaderboardType { + /// Leaderboard based on the player N7 ratings + #[serde(rename = "n7")] + #[sea_orm(num_value = 0)] + N7Rating = 0, + /// Leaderboard based on the player challenge point number + #[serde(rename = "cp")] + #[sea_orm(num_value = 1)] + ChallengePoints = 1, +} + +impl From<&str> for LeaderboardType { + fn from(value: &str) -> Self { + if value.starts_with("N7Rating") { + Self::N7Rating + } else { + Self::ChallengePoints + } + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::players::Entity", + from = "Column::PlayerId", + to = "super::players::Column::Id" + )] + Player, +} + +// `Related` trait has to be implemented by hand +impl Related for Entity { + fn to() -> RelationDef { + Relation::Player.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(FromQueryResult, Serialize)] +pub struct LeaderboardDataAndRank { + /// Unique Identifier for the entry + #[serde(skip)] + pub id: u32, + /// ID of the player this data is for + pub player_id: PlayerID, + /// The name of the player this entry is for + pub player_name: String, + /// The value of this leaderboard data + pub value: u32, + /// The ranking of this entry (Position in the leaderboard) + pub rank: u32, +} + +impl Model { + /// Expression used to rank the leaderboard data + const RANK_EXPR: &'static str = "RANK() OVER (ORDER BY value DESC) rank"; + /// The name of the column used for the rank value + const RANK_COL: &'static str = "rank"; + /// The name of the column to store the loaded player name + const PLAYER_NAME_COL: &'static str = "player_name"; + + /// Counts the number of leaderboard data models for the + /// specific `ty` type of leaderboard + pub fn count( + db: &DatabaseConnection, + ty: LeaderboardType, + ) -> impl Future> + Send + '_ { + Entity::find() + // Filter by the type + .filter(Column::Ty.eq(ty)) + // Get the number of items + .count(db) + } + + /// Gets a collection of leaderboard data for the specific + /// `ty` type of leaderboard starting with the `start` rank + /// and including maximum of `count` entries + pub fn get_offset( + db: &DatabaseConnection, + ty: LeaderboardType, + start: u32, + count: u32, + ) -> impl Future>> + Send + '_ { + Entity::find() + // Add the ranking expression + .expr(Expr::cust(Self::RANK_EXPR)) + // Filter by the type + .filter(Column::Ty.eq(ty)) + // Order lowest to highest ranking + .order_by_asc(Expr::cust(Self::RANK_COL)) + // Offset to the starting position + .offset(start as u64) + // Only take the requested amouont + .limit(count as u64) + // Inner join on the players table + .join(sea_orm::JoinType::InnerJoin, Relation::Player.def()) + // Use the player name from the players table + .column_as(super::players::Column::DisplayName, Self::PLAYER_NAME_COL) + // Turn it into the new model + .into_model::() + // Collect all the matching entities + .all(db) + } + + /// Gets the leaderboard data for a specific player on a + /// specific leaderboard type + pub fn get_entry( + db: &DatabaseConnection, + ty: LeaderboardType, + player_id: PlayerID, + ) -> impl Future>> + Send + '_ { + Entity::find() + // Add the ranking expression + .expr(Expr::cust(Self::RANK_EXPR)) + // Filter by the type and the specific player ID + .filter(Column::Ty.eq(ty).and(Column::PlayerId.eq(player_id))) + // Order lowest to highest ranking + .order_by_asc(Expr::cust(Self::RANK_COL)) + // Inner join on the players table + .join(sea_orm::JoinType::InnerJoin, Relation::Player.def()) + // Use the player name from the players table + .column_as(super::players::Column::DisplayName, Self::PLAYER_NAME_COL) + // Turn it into the new model + .into_model::() + // Collect all the matching entities + .one(db) + } + + /// Gets a collection of leaderboard data for the specific + /// `ty` type of leaderboard including only the players + /// in the provided `player_ids` collection + pub fn get_filtered( + db: &DatabaseConnection, + ty: LeaderboardType, + player_ids: Vec, + ) -> impl Future>> + Send + '_ { + Entity::find() + // Add the ranking expression + .expr(Expr::cust(Self::RANK_EXPR)) + // Filter by the type and the requested player IDs + .filter(Column::Ty.eq(ty).and(Column::PlayerId.is_in(player_ids))) + // Order lowest to highest ranking + .order_by_asc(Expr::cust(Self::RANK_COL)) + // Inner join on the players table + .join(sea_orm::JoinType::InnerJoin, Relation::Player.def()) + // Use the player name from the players table + .column_as(super::players::Column::DisplayName, Self::PLAYER_NAME_COL) + // Turn it into the new model + .into_model::() + // Collect all the matching entities + .all(db) + } + + /// Gets a collection of leaderboard data for the specific + /// `ty` type of leaderboard including maximum of `count` entries + /// centering the results around the rank of the provided `player_id` + pub async fn get_centered( + db: &DatabaseConnection, + ty: LeaderboardType, + player_id: PlayerID, + count: u32, + ) -> DbResult>> { + // Find the entry we are centering on + let value = match Self::get_entry(db, ty, player_id).await? { + Some(value) => value, + // The specified player hasn't been ranked + None => return Ok(None), + }; + + // The number of ranks to start at before the centered rank + let before = (count / 2) + // Add 1 when the count is even + .saturating_add((count % 2 == 0) as u32); + + // Determine the starting rank saturating zero bounds + let start = value.rank.saturating_sub(before); + + let values = Self::get_offset(db, ty, start, count).await?; + Ok(Some(values)) + } + + /// Function providing the conflict handling for upserting + /// values into the leaderboard data + #[inline(always)] + fn conflict_handle() -> OnConflict { + // Update the value column if the player ID in that type already exists + OnConflict::columns([Column::PlayerId, Column::Ty]) + .update_column(Column::Value) + .to_owned() + } + + /// Sets the leaderboard value for the specified `player_id` on + /// a specific leaderboard `ty` type to the provided `value` + pub fn set( + db: &DatabaseConnection, + ty: LeaderboardType, + player_id: PlayerID, + value: u32, + ) -> impl Future>> + Send + '_ { + Entity::insert(ActiveModel { + id: NotSet, + ty: Set(ty), + player_id: Set(player_id), + value: Set(value), + }) + .on_conflict(Self::conflict_handle()) + .exec(db) + } + + /// Bulk updates the values for each player ID -> value pair on + /// the provided `ty` leaderboard + pub fn set_ty_bulk( + db: &DatabaseConnection, + ty: LeaderboardType, + data: impl Iterator, + ) -> impl Future>> + Send + '_ { + // Insert all the models + Entity::insert_many( + // Transform the key value pairs into insertable models + data.map(|(player_id, value)| ActiveModel { + id: NotSet, + ty: Set(ty), + player_id: Set(player_id), + value: Set(value), + }), + ) + .on_conflict(Self::conflict_handle()) + .exec(db) + } +} diff --git a/src/database/entities/mod.rs b/src/database/entities/mod.rs index 1c15938e..86e02b2d 100644 --- a/src/database/entities/mod.rs +++ b/src/database/entities/mod.rs @@ -1,8 +1,10 @@ pub mod galaxy_at_war; +pub mod leaderboard_data; pub mod player_data; pub mod players; pub type GalaxyAtWar = galaxy_at_war::Model; pub type Player = players::Model; pub type PlayerData = player_data::Model; +pub type LeaderboardData = leaderboard_data::Model; pub use players::PlayerRole; diff --git a/src/database/entities/player_data.rs b/src/database/entities/player_data.rs index 097f7aca..099fef5d 100644 --- a/src/database/entities/player_data.rs +++ b/src/database/entities/player_data.rs @@ -97,7 +97,7 @@ impl Model { }), ) .on_conflict( - // Update the valume column if a key already exists + // Update the value column if a key already exists OnConflict::columns([Column::PlayerId, Column::Key]) .update_column(Column::Value) .to_owned(), diff --git a/src/database/entities/players.rs b/src/database/entities/players.rs index 8a2a06b1..551b302e 100644 --- a/src/database/entities/players.rs +++ b/src/database/entities/players.rs @@ -30,7 +30,17 @@ pub struct Model { } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm(has_many = "super::leaderboard_data::Entity")] + LeaderboardData, +} + +// `Related` trait has to be implemented by hand +impl Related for Entity { + fn to() -> RelationDef { + Relation::LeaderboardData.def() + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/src/database/migration/m20231205_121139_leaderboard_data.rs b/src/database/migration/m20231205_121139_leaderboard_data.rs new file mode 100644 index 00000000..e38e4edc --- /dev/null +++ b/src/database/migration/m20231205_121139_leaderboard_data.rs @@ -0,0 +1,76 @@ +use sea_orm_migration::prelude::*; + +use super::m20221015_142649_players_table::Players; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(LeaderboardData::Table) + .if_not_exists() + .col( + ColumnDef::new(LeaderboardData::Id) + .unsigned() + .not_null() + .auto_increment() + .primary_key(), + ) + .col( + ColumnDef::new(LeaderboardData::PlayerId) + .unsigned() + .not_null(), + ) + .col(ColumnDef::new(LeaderboardData::Ty).unsigned().not_null()) + .col(ColumnDef::new(LeaderboardData::Value).unsigned().not_null()) + .foreign_key( + ForeignKey::create() + .from(LeaderboardData::Table, LeaderboardData::PlayerId) + .to(Players::Table, Players::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .unique() + .name("idx-pid-ty-key") + .table(LeaderboardData::Table) + .col(LeaderboardData::Ty) + .col(LeaderboardData::PlayerId) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(LeaderboardData::Table).to_owned()) + .await?; + + manager + .drop_index( + Index::drop() + .table(LeaderboardData::Table) + .name("idx-pid-ty-key") + .to_owned(), + ) + .await + } +} + +#[derive(Iden)] +enum LeaderboardData { + Table, + Id, + Ty, + PlayerId, + Value, +} diff --git a/src/database/migration/mod.rs b/src/database/migration/mod.rs index bc0939a6..f6f05e04 100644 --- a/src/database/migration/mod.rs +++ b/src/database/migration/mod.rs @@ -4,6 +4,7 @@ mod m20221015_142649_players_table; mod m20221015_153750_galaxy_at_war_table; mod m20221222_174733_player_data; mod m20230913_185124_player_data_unique; +mod m20231205_121139_leaderboard_data; pub struct Migrator; @@ -15,6 +16,7 @@ impl MigratorTrait for Migrator { Box::new(m20221015_153750_galaxy_at_war_table::Migration), Box::new(m20221222_174733_player_data::Migration), Box::new(m20230913_185124_player_data_unique::Migration), + Box::new(m20231205_121139_leaderboard_data::Migration), ] } } diff --git a/src/database/mod.rs b/src/database/mod.rs index ad14281c..ea0f1aea 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -9,6 +9,10 @@ use std::{ pub mod entities; mod migration; +/// Testing seeding logic +#[cfg(test)] +mod seed; + // Re-exports of database types pub use sea_orm::DatabaseConnection; pub use sea_orm::DbErr; @@ -25,11 +29,21 @@ pub type DbResult = Result; const DATABASE_PATH: &str = "data/app.db"; const DATABASE_PATH_URL: &str = "sqlite:data/app.db"; -/// Connects to the database returning a Database connection -/// which allows accessing the database without accessing sea_orm +/// Connects to the database and applies the admin changes if +/// required, returning the database connection pub async fn init(config: &RuntimeConfig) -> DatabaseConnection { info!("Connected to database.."); + let connection = connect_database().await; + + // Setup the super admin account + init_database_admin(&connection, config).await; + + connection +} + +/// Connects to the database +async fn connect_database() -> DatabaseConnection { let path = Path::new(&DATABASE_PATH); // Create path to database file if missing @@ -54,9 +68,6 @@ pub async fn init(config: &RuntimeConfig) -> DatabaseConnection { .await .expect("Unable to run database migrations"); - // Setup the super admin account - init_database_admin(&connection, config).await; - connection } diff --git a/src/database/seed/mod.rs b/src/database/seed/mod.rs new file mode 100644 index 00000000..0b42678c --- /dev/null +++ b/src/database/seed/mod.rs @@ -0,0 +1,311 @@ +use chrono::Local; +use rand::{distributions::Uniform, Rng}; +use sea_orm::{ + ActiveModelTrait, + ActiveValue::{NotSet, Set}, +}; +use tokio::{task::JoinSet, try_join}; + +use crate::{ + database::entities::{ + galaxy_at_war::ActiveModel as GawActiveModel, leaderboard_data::LeaderboardType, + players::ActiveModel as PlayerActiveModel, LeaderboardData, PlayerData, PlayerRole, + }, + utils::hashing::hash_password, +}; +use std::fmt::Write; + +use super::connect_database; + +/// The number of users to seed +const SEED_PLAYERS_COUNT: u32 = 10_000; + +/// Class names to seed +static CLASS_NAMES: &[&str] = &[ + "Adept", + "Soldier", + "Engineer", + "Sentinel", + "Infiltrator", + "Vanguard", +]; + +static CHARACTER_DATA: &[&str] = &[ + "20;4;AdeptHumanMale;MAdept;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;AdeptHumanFemale;FAdept;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;AdeptAsari;Asari;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;AdeptDrell;Drell;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;AdeptAsariCommando;Asari;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;AdeptHumanMaleCerberus;Human Male;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;AdeptN7;N7 Fury;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;AdeptVolus;Volus Adept;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;AdeptKrogan;Krogan Shaman;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;AdeptBatarian;Krogan Shaman;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;AdeptCollector;Awakened Collector;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SoldierHumanMale;MSoldier;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SoldierHumanFemale;FSoldier;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SoldierKrogan;Krogan;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SoldierTurian;Turian;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SoldierHumanMaleBF3;MSoldier;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SoldierBatarian;Batarian;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SoldierVorcha;Vorcha;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SoldierN7;N7 Destroyer;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;N7SoldierTurian;Turian Havoc;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SoldierGeth;Geth Trooper;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SoldierMQuarian;Geth Trooper;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SoldierGethDestroyer;Geth Juggernaut;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;EngineerHumanMale;MEngineer;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;EngineerHumanFemale;FEngineer;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;EngineerQuarian;FEngineer;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;EngineerSalarian;Salarian;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;EngineerGeth;Geth;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;EngineerQuarianMale;Quarian Male;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;EngineerN7;N7 Demolisher;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;EngineerVolus;Volus Engineer;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;EngineerTurian;Turian Saboteur;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;EngineerVorcha;Turian Saboteur;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;EngineerMerc;Talon Mercenary;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SentinelHumanMale;MSentinel;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SentinelHumanFemale;FSentinel;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SentinelTurian;Turian;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SentinelKrogan;Krogan;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SentinelBatarian;Batarian;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SentinelVorcha;Vorcha;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SentinelN7;N7 Paladin;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SentinelVolus;Volus Mercenary;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SentinelAsari;Volus Mercenary;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;SentinelKroganWarlord;Krogan Warlord;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;InfiltratorHumanMale;MInfiltrate;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;InfiltratorHumanFemale;FInfiltrate;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;InfiltratorSalarian;Salarian;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;InfiltratorQuarian;Quarian;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;InfiltratorGeth;Geth;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;InfiltratorQuarianMale;Quarian Male;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;InfiltratorN7;N7 Shadow;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;N7InfiltratorTurian;Turian Ghost;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;InfiltratorDrell;Drell Assassin;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;InfiltratorAsari;Drell Assassin;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;InfiltratorFembot;Krogan Warlord;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;InfiltratorHumanFemaleBF3;MSoldier;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;VanguardHumanMale;MVanguard;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;VanguardHumanFemale;FVanguard;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;VanguardDrell;Drell;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;VanguardAsari;Asari;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;VanguardKrogan;Krogan;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;VanguardHumanMaleCerberus;Human Male;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;VanguardN7;N7 Slayer;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;VanguardVolus;Volus Protector;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;VanguardBatarian;Volus Protector;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", + "20;4;VanguardTurianFemale;Cabal Vanguard;0;45;0;47;45;9;9;0;0;0;0;0;;;;;False;True", +]; + +/// Seeds the database with a collection of players and their associated +/// player data. Ensure the database is empty before seeding as to not +/// cause conflicts. +/// +/// Models are seeded 1 by 1 as memory usage could greatly increase for +/// larger seeding batches +#[tokio::test] +#[ignore] +pub async fn seed() { + let db = connect_database().await; + + // All accounts use the same default password + let default_password = hash_password("test").unwrap(); + + let current_time = Local::now().naive_local(); + + let mut rng = rand::thread_rng(); + + // Random sample used for role data + let role_sample = Uniform::new_inclusive(0, 3); + // Class level sample + let level_sample = Uniform::new_inclusive(0, 20); + // Random sample used for gaw groups + let gaw_sample = Uniform::new_inclusive(5000, 10099); + + const INVENTORY_LENGTH: usize = 671; + + let mut join_set: JoinSet<()> = JoinSet::new(); + + for i in 0..SEED_PLAYERS_COUNT { + println!("Seeding player {i}"); + + let email = format!("test{i}@test.com"); + let display_name = format!("Test {i}"); + let password = default_password.clone(); + + // Randomly decide if the user should be an admin + let role = match rng.sample(role_sample) { + 3 => PlayerRole::Admin, + _ => PlayerRole::Default, + }; + + // Create the new player account + let model = PlayerActiveModel { + id: NotSet, + email: Set(email), + display_name: Set(display_name), + password: Set(Some(password)), + role: Set(role), + } + .insert(&db) + .await + .unwrap(); + + // Set the player leaderboard data + try_join!( + LeaderboardData::set(&db, LeaderboardType::N7Rating, model.id, rng.gen()), + LeaderboardData::set(&db, LeaderboardType::ChallengePoints, model.id, rng.gen()) + ) + .unwrap(); + + // Create galaxy at war data for the player + GawActiveModel { + id: NotSet, + last_modified: Set(current_time), + player_id: Set(model.id), + group_a: Set(rng.sample(gaw_sample)), + group_b: Set(rng.sample(gaw_sample)), + group_c: Set(rng.sample(gaw_sample)), + group_d: Set(rng.sample(gaw_sample)), + group_e: Set(rng.sample(gaw_sample)), + } + .insert(&db) + .await + .unwrap(); + + let mut player_data: Vec<(String, String)> = Vec::new(); + + // See base player data + { + let mut inventory: String = String::with_capacity(INVENTORY_LENGTH * 2); + for _ in 0..INVENTORY_LENGTH { + // Generate a random value for the inventory item + let value: u8 = rng.gen(); + + // Store the value as a 2 char hex value + write!(&mut inventory, "{value:2x}").unwrap(); + } + + let credits: u32 = rng.gen(); + let credits_spent: u32 = rng.gen(); + let games_played: u32 = rng.gen(); + let seconds_played: u32 = rng.gen(); + + // Generate the player base data + let base_data = format!( + "20;4;{credits};-1;0;{credits_spent};0;{games_played};{seconds_played};0;{inventory}" + ); + + player_data.push(("Base".to_string(), base_data)); + } + + // Set the player class data for each class + for (index, class_name) in CLASS_NAMES.iter().enumerate() { + let level: u32 = rng.sample(level_sample); + let xp: f32 = rng.gen(); + + let key = format!("class{}", index + 1); + let value = format!("20;4;{class_name};{level};{xp:.4};0"); + player_data.push((key, value)); + } + + // Seed Completion data + { + let mut completion = String::from("22"); + + for _ in 0..746 { + let value: u8 = rng.gen(); + write!(&mut completion, ",{value}").unwrap(); + } + + player_data.push(("Completion".to_string(), completion)); + } + + // Seed cscompletion data + { + let mut completion = String::from("22"); + + for _ in 0..221 { + let value: u8 = rng.gen(); + write!(&mut completion, ",{value}").unwrap(); + } + + player_data.push(("cscompletion".to_string(), completion)); + } + + // Seed cstimestamps data + { + let mut value = String::new(); + + for _ in 0..250 { + let rand: u32 = rng.gen(); + write!(&mut value, "{rand},").unwrap(); + } + + // Pop trailing comma + value.pop(); + player_data.push(("cstimestamps".to_string(), value)); + } + + // Seed cstimestamps2 data + { + let mut value = String::new(); + + for _ in 0..250 { + let rand: u32 = rng.gen(); + write!(&mut value, "{rand},").unwrap(); + } + + // Pop trailing comma + value.pop(); + player_data.push(("cstimestamps2".to_string(), value)); + } + + // Seed cstimestamps3 data + { + let mut value = String::new(); + + for _ in 0..245 { + let rand: u32 = rng.gen(); + write!(&mut value, "{rand},").unwrap(); + } + + // Pop trailing comma + value.pop(); + player_data.push(("cstimestamps3".to_string(), value)); + } + + // Seed Progress data + { + let mut progress = String::from("22"); + + for _ in 0..745 { + let value: u32 = rng.gen(); + write!(&mut progress, ",{value}").unwrap(); + } + player_data.push(("Progress".to_string(), progress)); + } + + player_data.push(("csreward".to_string(), 0.to_string())); + player_data.push(("FaceCodes".to_string(), "20;".to_string())); + player_data.push(("NewItem".to_string(), "20;4;12 271".to_string())); + + // Seed character data + for (index, value) in CHARACTER_DATA.iter().enumerate() { + let key = format!("char{index}"); + player_data.push((key, value.to_string())); + } + + let db = db.clone(); + join_set.spawn(async move { + PlayerData::set_bulk(&db, model.id, player_data.into_iter()) + .await + .unwrap(); + }); + } + + // Wait for all the spawned tasks to finish + while join_set.join_next().await.is_some() {} +} diff --git a/src/main.rs b/src/main.rs index aeab5222..6fb96d0b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,14 @@ +#![warn(unused_crate_dependencies)] + use crate::{ config::{RuntimeConfig, VERSION}, - services::{ - game::manager::GameManager, leaderboard::Leaderboard, retriever::Retriever, - sessions::Sessions, - }, + services::{game::manager::GameManager, retriever::Retriever, sessions::Sessions}, utils::signing::SigningKey, }; use axum::{Extension, Server}; use config::load_config; use log::{debug, error, info, LevelFilter}; -use std::{ - net::{Ipv4Addr, SocketAddr}, - sync::Arc, -}; +use std::{net::SocketAddr, sync::Arc}; use tokio::{join, signal}; use utils::logging; @@ -37,7 +33,7 @@ async fn main() { logging::setup(config.logging); // Create the server socket address while the port is still available - let addr: SocketAddr = (Ipv4Addr::UNSPECIFIED, config.port).into(); + let addr: SocketAddr = SocketAddr::new(config.host, config.port); // Config data persisted to runtime let runtime_config = RuntimeConfig { @@ -60,7 +56,6 @@ async fn main() { ); let game_manager = Arc::new(GameManager::new()); - let leaderboard = Arc::new(Leaderboard::new()); let sessions = Arc::new(Sessions::new(signing_key)); let config = Arc::new(runtime_config); let retriever = Arc::new(retriever); @@ -72,7 +67,6 @@ async fn main() { router.add_extension(config.clone()); router.add_extension(retriever); router.add_extension(game_manager.clone()); - router.add_extension(leaderboard.clone()); router.add_extension(sessions.clone()); let router = router.build(); @@ -84,7 +78,6 @@ async fn main() { .layer(Extension(config)) .layer(Extension(router)) .layer(Extension(game_manager)) - .layer(Extension(leaderboard)) .layer(Extension(sessions)) .into_make_service_with_connect_info::(); diff --git a/src/routes/leaderboard.rs b/src/routes/leaderboard.rs index 02e36ef4..3edc8eb2 100644 --- a/src/routes/leaderboard.rs +++ b/src/routes/leaderboard.rs @@ -1,7 +1,8 @@ -use std::sync::Arc; - use crate::{ - services::leaderboard::{models::*, Leaderboard}, + database::entities::{ + leaderboard_data::{LeaderboardDataAndRank, LeaderboardType}, + LeaderboardData, + }, utils::types::PlayerID, }; use axum::{ @@ -10,7 +11,7 @@ use axum::{ response::{IntoResponse, Response}, Extension, Json, }; -use sea_orm::DatabaseConnection; +use sea_orm::{DatabaseConnection, DbErr}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -19,12 +20,13 @@ use thiserror::Error; /// searching for a specific player. #[derive(Debug, Error)] pub enum LeaderboardError { - /// The provided query range was out of bounds on the underlying query - #[error("Unacceptable query range")] - InvalidRange, /// The requested player was not found in the leaderboard #[error("Player not found")] PlayerNotFound, + + /// Database error occurred + #[error("Internal server error")] + Database(#[from] DbErr), } /// Structure of a query requesting a specific leaderboard contains @@ -34,7 +36,7 @@ pub enum LeaderboardError { pub struct LeaderboardQuery { /// The number of ranks to offset by #[serde(default)] - offset: usize, + offset: u32, /// The number of items to query for count has a maximum limit /// of 255 entries to prevent server strain from querying the /// entire list of leaderboard entries @@ -44,11 +46,11 @@ pub struct LeaderboardQuery { /// The different types of respones that can be created /// from a leaderboard request #[derive(Serialize)] -pub struct LeaderboardResponse<'a> { +pub struct LeaderboardResponse { /// The total number of players in the entire leaderboard total: usize, /// The entries retrieved at the provided offset - entries: &'a [LeaderboardEntry], + entries: Vec, /// Whether there is more entries past the provided offset more: bool, } @@ -63,32 +65,27 @@ pub struct LeaderboardResponse<'a> { pub async fn get_leaderboard( Path(ty): Path, Extension(db): Extension, - Extension(leaderboard): Extension>, Query(LeaderboardQuery { offset, count }): Query, -) -> Result { +) -> Result, LeaderboardError> { /// The default number of entries to return in a leaderboard response const DEFAULT_COUNT: u8 = 40; // The number of entries to return - let count: usize = count.unwrap_or(DEFAULT_COUNT) as usize; + let count: u32 = count.unwrap_or(DEFAULT_COUNT) as u32; // Calculate the start and ending indexes - let start: usize = offset * count; - - let group = leaderboard.query(ty, &db).await; + let start: u32 = offset * count; - let entries = group - .get_normal(start, count) - .ok_or(LeaderboardError::InvalidRange)?; + let values = LeaderboardData::get_offset(&db, ty, start, count).await?; + let total = LeaderboardData::count(&db, ty).await? as u32; - let more = group.has_more(start, count); + // There are more if the end < the total number of values + let more = (start + count) < (total + 1); - let response = Json(LeaderboardResponse { - total: group.values.len(), - entries, + Ok(Json(LeaderboardResponse { + total: total as usize, + entries: values, more, - }); - - Ok(response.into_response()) + })) } /// GET /api/leaderboard/:name/:player_id @@ -101,17 +98,13 @@ pub async fn get_leaderboard( pub async fn get_player_ranking( Path((ty, player_id)): Path<(LeaderboardType, PlayerID)>, Extension(db): Extension, - Extension(leaderboard): Extension>, -) -> Result { - let group = leaderboard.query(ty, &db).await; - - let entry = match group.get_entry(player_id) { +) -> Result, LeaderboardError> { + let entry = match LeaderboardData::get_entry(&db, ty, player_id).await? { Some(value) => value, None => return Err(LeaderboardError::PlayerNotFound), }; - let response = Json(entry); - Ok(response.into_response()) + Ok(Json(entry)) } /// IntoResponse implementation for LeaderboardError to allow it to be @@ -121,7 +114,7 @@ impl IntoResponse for LeaderboardError { fn into_response(self) -> Response { let status = match &self { Self::PlayerNotFound => StatusCode::NOT_FOUND, - Self::InvalidRange => StatusCode::BAD_REQUEST, + Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, }; (status, self.to_string()).into_response() } diff --git a/src/routes/server.rs b/src/routes/server.rs index 4e2f92f3..ee143556 100644 --- a/src/routes/server.rs +++ b/src/routes/server.rs @@ -102,7 +102,7 @@ pub async fn handle_upgrade( } }; - Session::start(upgraded, addr, router, sessions); + Session::start(upgraded, addr, router, sessions).await; } /// GET /api/server/log diff --git a/src/services/game/manager.rs b/src/services/game/manager.rs index 7b329045..612eea3f 100644 --- a/src/services/game/manager.rs +++ b/src/services/game/manager.rs @@ -5,6 +5,7 @@ use crate::{ AsyncMatchmakingStatus, GameSettings, GameSetupContext, MatchmakingResult, }, packet::Packet, + SessionLink, }, utils::{ components::game_manager, @@ -21,7 +22,10 @@ use std::{ }, time::{Duration, SystemTime}, }; -use tokio::{sync::RwLock, task::JoinSet}; +use tokio::{ + sync::{Mutex, RwLock}, + task::JoinSet, +}; /// Manager which controls all the active games on the server /// commanding them to do different actions and removing them @@ -32,7 +36,7 @@ pub struct GameManager { /// Stored value for the ID to give the next game next_id: AtomicU32, /// Matchmaking entry queue - queue: RwLock>, + queue: Mutex>, } /// Entry into the matchmaking queue @@ -49,7 +53,7 @@ const DEFAULT_FIT: u16 = 21600; impl GameManager { /// Max number of times to poll a game for shutdown before erroring - const MAX_RELEASE_ATTEMPTS: u8 = 5; + const MAX_RELEASE_ATTEMPTS: u8 = 20; /// Starts a new game manager service returning its link pub fn new() -> Self { @@ -114,13 +118,13 @@ impl GameManager { } pub async fn remove_queue(&self, player_id: PlayerID) { - let queue = &mut *self.queue.write().await; + let queue = &mut *self.queue.lock().await; queue.retain(|value| value.player.player.id != player_id); } pub async fn queue(&self, player: GamePlayer, rule_set: Arc) { let started = SystemTime::now(); - let queue = &mut *self.queue.write().await; + let queue = &mut *self.queue.lock().await; queue.push_back(MatchmakingEntry { player, rule_set, @@ -132,10 +136,9 @@ impl GameManager { &self, game_ref: GameRef, player: GamePlayer, + session: SessionLink, context: GameSetupContext, ) { - let player_link = player.link.clone(); - // Add the player to the game let game_id = { let game = &mut *game_ref.write().await; @@ -144,14 +147,20 @@ impl GameManager { }; // Update the player current game - player_link.set_game(game_id, game_ref).await; + session.set_game(game_id, Arc::downgrade(&game_ref)); } pub async fn add_from_matchmaking(&self, game_ref: GameRef, player: GamePlayer) { + let session = match player.link.upgrade() { + Some(value) => value, + // Session was dropped + None => return, + }; + let msid = player.player.id; // MUST be sent to players atleast once when matchmaking otherwise it may fail - player.link.push(Packet::notify( + player.notify_handle.notify(Packet::notify( game_manager::COMPONENT, game_manager::MATCHMAKING_ASYNC_STATUS, AsyncMatchmakingStatus { player_id: msid }, @@ -161,6 +170,7 @@ impl GameManager { self.add_to_game( game_ref, player, + session, GameSetupContext::Matchmaking { fit_score: DEFAULT_FIT, max_fit_score: DEFAULT_FIT, @@ -256,7 +266,7 @@ impl GameManager { } pub async fn process_queue(&self, link: GameRef, game_id: GameID) { - let queue = &mut *self.queue.write().await; + let queue = &mut *self.queue.lock().await; if queue.is_empty() { return; } diff --git a/src/services/game/mod.rs b/src/services/game/mod.rs index 8475deb8..606916dc 100644 --- a/src/services/game/mod.rs +++ b/src/services/game/mod.rs @@ -2,15 +2,19 @@ use self::{manager::GameManager, rules::RuleSet}; use crate::{ database::entities::Player, session::{ - models::game_manager::{ - AdminListChange, AdminListOperation, AttributesChange, GameSettings, GameSetupContext, - GameSetupResponse, GameState, GetGameDetails, HostMigrateFinished, HostMigrateStart, - JoinComplete, PlayerJoining, PlayerRemoved, PlayerState, PlayerStateChange, - RemoveReason, SettingChange, StateChange, + models::{ + game_manager::{ + AdminListChange, AdminListOperation, AttributesChange, GameSettings, + GameSetupContext, GameSetupResponse, GameState, GetGameDetails, + HostMigrateFinished, HostMigrateStart, JoinComplete, PlayerJoining, + PlayerNetConnectionStatus, PlayerRemoved, PlayerState, PlayerStateChange, + RemoveReason, SettingChange, SlotType, StateChange, UNSPECIFIED_TEAM_INDEX, + }, + util::LOCALE_NZ, }, packet::Packet, router::RawBlaze, - NetData, SessionLink, + NetData, SessionNotifyHandle, WeakSessionLink, }, utils::{ components::game_manager, @@ -19,7 +23,7 @@ use crate::{ }; use log::{debug, warn}; use serde::Serialize; -use std::sync::Arc; +use std::sync::{Arc, Weak}; use tdf::{ObjectId, TdfMap, TdfSerializer}; use tokio::sync::RwLock; @@ -27,6 +31,7 @@ pub mod manager; pub mod rules; pub type GameRef = Arc>; +pub type WeakGameRef = Weak>; /// Game service running within the server pub struct Game { @@ -69,8 +74,9 @@ pub type AttrMap = TdfMap; pub struct GamePlayer { /// Session player pub player: Arc, - /// Session address - pub link: SessionLink, + /// Weak reference to the assocated session + pub link: WeakSessionLink, + pub notify_handle: SessionNotifyHandle, /// Networking information for the player pub net: Arc, /// The mesh state of the player @@ -96,18 +102,42 @@ impl GamePlayer { /// `player` The session player /// `net` The player networking details /// `addr` The session address - pub fn new(player: Arc, net: Arc, link: SessionLink) -> Self { + pub fn new( + player: Arc, + net: Arc, + link: WeakSessionLink, + notify_handle: SessionNotifyHandle, + ) -> Self { Self { player, link, + notify_handle, net, state: PlayerState::ActiveConnecting, } } + pub fn try_clear_game(&self) { + if let Some(link) = self.link.upgrade() { + link.clear_game(); + } + } + + pub fn try_subscribe(&self, player_id: PlayerID, subscriber: SessionNotifyHandle) { + if let Some(link) = self.link.upgrade() { + link.add_subscriber(player_id, subscriber); + } + } + + pub fn try_unsubscribe(&self, player_id: PlayerID) { + if let Some(link) = self.link.upgrade() { + link.remove_subscriber(player_id); + } + } + #[inline] - pub fn push(&self, packet: Packet) { - self.link.push(packet) + pub fn notify(&self, packet: Packet) { + self.notify_handle.notify(packet) } /// Takes a snapshot of the current player state @@ -126,19 +156,33 @@ impl GamePlayer { pub fn encode(&self, game_id: GameID, slot: usize, w: &mut S) { w.group_body(|w| { + // Custom data w.tag_blob_empty(b"BLOB"); + // External ID w.tag_u8(b"EXID", 0); + // Game ID w.tag_owned(b"GID", game_id); - w.tag_u32(b"LOC", 0x64654445); + // Account locale + w.tag_u32(b"LOC", LOCALE_NZ); + // Player name w.tag_str(b"NAME", &self.player.display_name); + // Player ID w.tag_u32(b"PID", self.player.id); + // Playet network data w.tag_ref(b"PNET", &self.net.addr); + // Slot ID w.tag_owned(b"SID", slot); - w.tag_u8(b"SLOT", 0); + // Slot type + w.tag_alt(b"SLOT", SlotType::PublicParticipant); + // Player state w.tag_ref(b"STAT", &self.state); - w.tag_u16(b"TIDX", 0xffff); - w.tag_u8(b"TIME", 0); /* Unix timestamp in millseconds */ + // Team index + w.tag_u16(b"TIDX", UNSPECIFIED_TEAM_INDEX); + // Unix millisecond timestamp of the player joined the game in + w.tag_u8(b"TIME", 0); + // User group ID w.tag_alt(b"UGID", ObjectId::new_raw(0, 0, 0)); + // Player session ID w.tag_u32(b"UID", self.player.id); }); } @@ -160,97 +204,119 @@ pub enum GameJoinableState { impl Game { /// Constant for the maximum number of players allowed in /// a game at one time. Used to determine a games full state - const MAX_PLAYERS: usize = 4; + pub const MAX_PLAYERS: usize = 4; + + pub fn new( + id: GameID, + attributes: AttrMap, + settings: GameSettings, + game_manager: Arc, + ) -> Game { + Game { + id, + attributes, + settings, + state: Default::default(), + players: Default::default(), + game_manager, + } + } - pub async fn game_data(&self) -> RawBlaze { + pub fn game_data(&self) -> RawBlaze { let data = GetGameDetails { game: self }; data.into() } - pub fn add_player(&mut self, mut player: GamePlayer, context: GameSetupContext) { + pub fn add_player(&mut self, player: GamePlayer, context: GameSetupContext) { let slot = self.players.len(); - // Player is the host player (They are connected) - if slot == 0 { - player.state = PlayerState::ActiveConnected; - } + // Update other players with the client details + self.add_user_sub(&player); + + // Notify other players of the joining player + self.notify_all(Packet::notify( + game_manager::COMPONENT, + game_manager::PLAYER_JOINING, + PlayerJoining { + slot, + player: &player, + game_id: self.id, + }, + )); self.players.push(player); - // Obtain the player that was just added + // Get the player that was just added let player = self .players .last() - .expect("Player was added but is missing from players"); - - // Player isn't the host player - if slot != 0 { - // Notify other players of the joined player - self.push_all(&Packet::notify( - game_manager::COMPONENT, - game_manager::PLAYER_JOINING, - PlayerJoining { - slot, - player, - game_id: self.id, - }, - )); - - // Update other players with the client details - self.add_user_sub(player.player.id, player.link.clone()); - } + .expect("Expected inserted player was missing"); - // Notify the joiner of the game details - self.notify_game_setup(player, context); + // Send the player the game setup details + player.notify(Packet::notify( + game_manager::COMPONENT, + game_manager::GAME_SETUP, + GameSetupResponse { + game: self, + context, + }, + )); } - pub fn update_mesh(&mut self, id: PlayerID, target: PlayerID, state: PlayerState) { - if let PlayerState::ActiveConnecting = state { - // Ensure the target player is in the game - if !self.players.iter().any(|value| value.player.id == target) { - return; - } + pub fn add_admin_player(&mut self, target_id: PlayerID) { + // Add the player to the admin list + self.modify_admin_list(target_id, AdminListOperation::Add); + } - // Find the index of the session player - let session = self.players.iter_mut().find(|value| value.player.id == id); + pub fn is_host_player(&self, player_id: PlayerID) -> bool { + self.players + .first() + .is_some_and(|host| host.player.id == player_id) + } - let session = match session { - Some(value) => value, - None => return, - }; + pub fn update_mesh(&mut self, target_id: PlayerID, status: PlayerNetConnectionStatus) { + // We only care about a connected state + if !matches!(status, PlayerNetConnectionStatus::Connected) { + return; + } - // Update the session state - session.state = PlayerState::ActiveConnected; + // Obtain the target player + let target_slot = match self + .players + .iter_mut() + .find(|slot| slot.player.id == target_id) + { + Some(value) => value, + None => { + debug!( + "Unable to find player to update mesh state for (PID: {} GID: {})", + target_id, self.id + ); + return; + } + }; - let player_id = session.player.id; - let state_change = PlayerStateChange { + // Mark the player as connected and update the state for all users + target_slot.state = PlayerState::ActiveConnected; + self.notify_all(Packet::notify( + game_manager::COMPONENT, + game_manager::GAME_PLAYER_STATE_CHANGE, + PlayerStateChange { gid: self.id, - pid: player_id, - state: session.state, - }; - - // TODO: Move into a "connection complete" function - - // Notify players of the player state change - self.push_all(&Packet::notify( - game_manager::COMPONENT, - game_manager::GAME_PLAYER_STATE_CHANGE, - state_change, - )); - - // Notify players of the completed connection - self.push_all(&Packet::notify( - game_manager::COMPONENT, - game_manager::PLAYER_JOIN_COMPLETED, - JoinComplete { - game_id: self.id, - player_id, - }, - )); - - // Add the player to the admin list - self.modify_admin_list(player_id, AdminListOperation::Add); - } + pid: target_id, + state: PlayerState::ActiveConnected, + }, + )); + + // Notify all players that the player has completely joined + self.notify_all(Packet::notify( + game_manager::COMPONENT, + game_manager::PLAYER_JOIN_COMPLETED, + JoinComplete { + game_id: self.id, + player_id: target_id, + }, + )); } pub fn remove_player(&mut self, id: u32, reason: RemoveReason) { @@ -272,14 +338,11 @@ impl Game { let player = self.players.remove(index); // Clear current game of this player - let clear_link = player.link.clone(); - tokio::spawn(async move { - let _ = clear_link.clear_game().await; - }); + player.try_clear_game(); // Update the other players self.notify_player_removed(&player, reason); - self.rem_user_sub(player.player.id, player.link.clone()); + self.rem_user_sub(&player); self.modify_admin_list(player.player.id, AdminListOperation::Remove); debug!( @@ -287,6 +350,8 @@ impl Game { player.player.id, self.id ); + drop(player); + // If the player was in the host slot attempt migration if index == 0 { self.try_migrate_host(); @@ -298,22 +363,6 @@ impl Game { } } - pub fn new( - id: GameID, - attributes: AttrMap, - settings: GameSettings, - game_manager: Arc, - ) -> Game { - Game { - id, - attributes, - settings, - state: Default::default(), - players: Default::default(), - game_manager, - } - } - /// Called by the game manager service once this game has been stopped and /// removed from the game list fn stopped(self) { @@ -377,10 +426,10 @@ impl Game { /// it to be placed into each sessions write buffers. /// /// `packet` The packet to write - fn push_all(&self, packet: &Packet) { + fn notify_all(&self, packet: Packet) { self.players .iter() - .for_each(|value| value.push(packet.clone())); + .for_each(|value| value.notify(packet.clone())); } pub fn set_state(&mut self, state: GameState) { @@ -388,7 +437,7 @@ impl Game { debug!("Updated game state (Value: {:?})", &state); - self.push_all(&Packet::notify( + self.notify_all(Packet::notify( game_manager::COMPONENT, game_manager::GAME_STATE_CHANGE, StateChange { id: self.id, state }, @@ -400,7 +449,7 @@ impl Game { debug!("Updated game setting (Value: {:?})", &settings); - self.push_all(&Packet::notify( + self.notify_all(Packet::notify( game_manager::COMPONENT, game_manager::GAME_SETTINGS_CHANGE, SettingChange { @@ -424,72 +473,39 @@ impl Game { debug!("Updated game attributes"); - self.push_all(&packet); + self.notify_all(packet); } /// Creates a subscription between all the users and the the target player - fn add_user_sub(&self, target_id: PlayerID, target_link: SessionLink) { + fn add_user_sub(&self, target: &GamePlayer) { debug!("Adding user subscriptions"); - // Subscribe all the clients to eachother + // Subscribe all the clients to each other self.players .iter() - .filter(|other| other.player.id.ne(&target_id)) + .filter(|other| other.player.id != target.player.id) .for_each(|other| { - let other_id = other.player.id; - let other_link = other.link.clone(); - let target_link = target_link.clone(); - - tokio::spawn(async move { - target_link - .add_subscriber(other_id, other_link.clone()) - .await; - other_link - .add_subscriber(target_id, target_link.clone()) - .await; - }); + target.try_subscribe(other.player.id, other.notify_handle.clone()); + other.try_subscribe(target.player.id, target.notify_handle.clone()); }); } /// Notifies the provided player and all other players /// in the game that they should remove eachother from /// their player data list - fn rem_user_sub(&self, target_id: PlayerID, target_link: SessionLink) { + fn rem_user_sub(&self, target: &GamePlayer) { debug!("Removing user subscriptions"); // Unsubscribe all the clients from eachother self.players .iter() - .filter(|other| other.player.id.ne(&target_id)) + .filter(|other| other.player.id != target.player.id) .for_each(|other| { - let other_id = other.player.id; - let other_link = other.link.clone(); - let target_link = target_link.clone(); - - tokio::spawn(async move { - target_link.remove_subscriber(other_id).await; - other_link.remove_subscriber(target_id).await; - }); + target.try_unsubscribe(other.player.id); + other.try_unsubscribe(target.player.id); }); } - /// Notifies the provided player that the game has been setup and - /// is ready for them to attempt to join. - /// - /// `session` The session to notify - /// `slot` The slot the player is joining into - fn notify_game_setup(&self, player: &GamePlayer, context: GameSetupContext) { - let packet = Packet::notify( - game_manager::COMPONENT, - game_manager::GAME_SETUP, - GameSetupResponse { - game: self, - context, - }, - ); - player.push(packet); - } - /// Modifies the psudo admin list this list doesn't actually exist in /// our implementation but we still need to tell the clients these /// changes. @@ -502,7 +518,7 @@ impl Game { None => return, }; - self.push_all(&Packet::notify( + self.notify_all(Packet::notify( game_manager::COMPONENT, game_manager::ADMIN_LIST_CHANGE, AdminListChange { @@ -530,8 +546,8 @@ impl Game { reason, }, ); - self.push_all(&packet); - player.push(packet); + self.notify_all(packet.clone()); + player.notify(packet); } /// Attempts to migrate the host of this game if there are still players @@ -549,7 +565,7 @@ impl Game { // Start host migration self.set_state(GameState::Migrating); - self.push_all(&Packet::notify( + self.notify_all(Packet::notify( game_manager::COMPONENT, game_manager::HOST_MIGRATION_START, HostMigrateStart { @@ -562,7 +578,7 @@ impl Game { // Finished host migration self.set_state(GameState::InGame); - self.push_all(&Packet::notify( + self.notify_all(Packet::notify( game_manager::COMPONENT, game_manager::HOST_MIGRATION_FINISHED, HostMigrateFinished { game_id: self.id }, diff --git a/src/services/game/rules.rs b/src/services/game/rules.rs index 25faae47..4482e8e1 100644 --- a/src/services/game/rules.rs +++ b/src/services/game/rules.rs @@ -14,23 +14,23 @@ pub struct RuleSet { impl RuleSet { /// Attribute determining the game privacy for public /// match checking - const PRIVACY_ATTR: &str = "ME3privacy"; + const PRIVACY_ATTR: &'static str = "ME3privacy"; /// Map attribute and rule keys - const MAP_ATTR: &str = "ME3map"; - const MAP_RULE: &str = "ME3_gameMapMatchRule"; + const MAP_ATTR: &'static str = "ME3map"; + const MAP_RULE: &'static str = "ME3_gameMapMatchRule"; /// Enemy attribute and rule keys - const ENEMY_ATTR: &str = "ME3gameEnemyType"; - const ENEMY_RULE: &str = "ME3_gameEnemyTypeRule"; + const ENEMY_ATTR: &'static str = "ME3gameEnemyType"; + const ENEMY_RULE: &'static str = "ME3_gameEnemyTypeRule"; /// Difficulty attribute and rule keys - const DIFFICULTY_ATTR: &str = "ME3gameDifficulty"; - const DIFFICULTY_RULE: &str = "ME3_gameDifficultyRule"; + const DIFFICULTY_ATTR: &'static str = "ME3gameDifficulty"; + const DIFFICULTY_RULE: &'static str = "ME3_gameDifficultyRule"; /// Value for rules that have been abstained from matching /// when a rule is abstained it is ignored - const ABSTAIN: &str = "abstain"; + const ABSTAIN: &'static str = "abstain"; /// Creates a new rule set from the provided list /// of rule key values diff --git a/src/services/leaderboard/mod.rs b/src/services/leaderboard/mod.rs deleted file mode 100644 index 44ee43a1..00000000 --- a/src/services/leaderboard/mod.rs +++ /dev/null @@ -1,235 +0,0 @@ -use self::models::*; -use crate::{ - database::{ - entities::players, - entities::{Player, PlayerData}, - DatabaseConnection, DbResult, - }, - utils::{ - parsing::{KitNameDeployed, PlayerClass}, - types::PlayerID, - }, -}; -use futures_util::future::BoxFuture; -use log::{debug, error}; -use sea_orm::{EntityTrait, PaginatorTrait, QueryOrder}; -use std::{collections::HashMap, sync::Arc, time::Instant}; -use tokio::{sync::RwLock, task::JoinSet}; - -pub mod models; - -pub struct Leaderboard { - /// Map between the group types and the actual leaderboard group content - groups: RwLock>, -} - -/// Extra state wrapper around a leaderboard group which -/// holds the state of whether the group is being actively -/// recomputed -struct GroupState { - /// Whether the group is being computed - computing: bool, - /// The underlying group - group: Arc, -} - -impl Leaderboard { - /// Starts a new leaderboard service - pub fn new() -> Leaderboard { - Leaderboard { - groups: Default::default(), - } - } - - pub async fn query( - &self, - ty: LeaderboardType, - db: &DatabaseConnection, - ) -> Arc { - { - let groups = &mut *self.groups.write().await; - // If the group already exists and is not expired we can respond with it - if let Some(group) = groups.get_mut(&ty) { - let inner = &group.group; - - // Response with current values if the group isn't expired or is computing - if group.computing || !inner.is_expired() { - // Value is not expired respond immediately - return inner.clone(); - } - - // Mark the group as currently being computed - group.computing = true; - } else { - // Create dummy empty group to hand out while computing - let dummy = GroupState { - computing: true, - group: Arc::new(LeaderboardGroup::dummy()), - }; - groups.insert(ty, dummy); - } - } - - // Compute new leaderboard values - let values = Self::compute(&ty, db).await; - let group = Arc::new(LeaderboardGroup::new(values)); - - // Store the updated group - { - let groups = &mut *self.groups.write().await; - groups.insert( - ty, - GroupState { - computing: false, - group: group.clone(), - }, - ); - } - - group - } - - /// Computes the ranking values for the provided `ty` this consists of - /// streaming the values from the database in chunks of 20, processing the - /// chunks converting them into entries then sorting the entries based - /// on their value. - /// - /// `ty` The leaderboard type - async fn compute(ty: &LeaderboardType, db: &DatabaseConnection) -> Box<[LeaderboardEntry]> { - let start_time = Instant::now(); - - // The amount of players to process in each database request - const BATCH_COUNT: u64 = 20; - - let mut values: Vec = Vec::new(); - - let mut join_set = JoinSet::new(); - - let mut paginator = players::Entity::find() - .order_by_asc(players::Column::Id) - .paginate(db, BATCH_COUNT); - - // Function pointer to the computing function for the desired type - let fun: fn(DatabaseConnection, Player) -> Lf = match ty { - LeaderboardType::N7Rating => compute_n7_player, - LeaderboardType::ChallengePoints => compute_cp_player, - }; - - loop { - let players = match paginator.fetch_and_next().await { - Ok(None) => break, - Ok(Some(value)) => value, - Err(err) => { - error!("Unable to load players for leaderboard: {:?}", err); - break; - } - }; - - // Add the futures for all the players - for player in players { - join_set.spawn(fun(db.clone(), player)); - } - - // Await computed results - while let Some(value) = join_set.join_next().await { - if let Ok(Ok(value)) = value { - values.push(value) - } - } - } - - // Sort the values based on their value - values.sort_by(|a, b| b.value.cmp(&a.value)); - - // Apply the new rank order to the rank values - let mut rank = 1; - for value in &mut values { - value.rank = rank; - rank += 1; - } - - debug!("Computed leaderboard took: {:.2?}", start_time.elapsed()); - - values.into_boxed_slice() - } -} - -type Lf = BoxFuture<'static, DbResult>; - -/// Computes a ranking for the provided player based on the N7 ranking -/// of that player. -/// -/// `db` The database connection -/// `player` The player to rank -fn compute_n7_player(db: DatabaseConnection, player: Player) -> Lf { - Box::pin(async move { - let mut total_promotions: u32 = 0; - let mut total_level: u32 = 0; - - let data: Vec = PlayerData::all(&db, player.id).await?; - - let mut classes: Vec = Vec::new(); - let mut characters: Vec = Vec::new(); - - for datum in &data { - if datum.key.starts_with("class") { - if let Some(value) = PlayerClass::parse(&datum.value) { - classes.push(value); - } - } else if datum.key.starts_with("char") { - if let Some(value) = KitNameDeployed::parse(&datum.value) { - characters.push(value); - } - } - } - - for class in classes { - // Classes are active if atleast one character from the class is deployed - let is_active = characters - .iter() - .any(|char| char.kit_name.contains(class.name) && char.deployed); - if is_active { - total_level += class.level as u32; - } - total_promotions += class.promotions; - } - - // 30 -> 20 from leveling class + 10 bonus for promoting - let rating: u32 = total_promotions * 30 + total_level; - Ok(LeaderboardEntry { - player_id: player.id, - player_name: player.display_name.into_boxed_str(), - // Rank is not computed yet at this stage - rank: 0, - value: rating, - }) - }) -} - -/// Computes a ranking for the provided player based on the number of -/// challenge points the player has -/// -/// `db` The database connection -/// `player` The player to rank -fn compute_cp_player(db: DatabaseConnection, player: Player) -> Lf { - Box::pin(async move { - let value = get_challenge_points(&db, player.id).await.unwrap_or(0); - Ok(LeaderboardEntry { - player_id: player.id, - player_name: player.display_name.into_boxed_str(), - // Rank is not computed yet at this stage - rank: 0, - value, - }) - }) -} - -async fn get_challenge_points(db: &DatabaseConnection, player_id: PlayerID) -> Option { - let list = PlayerData::get(db, player_id, "Completion") - .await - .ok()?? - .value; - let part = list.split(',').nth(1)?; - let value: u32 = part.parse().ok()?; - Some(value) -} diff --git a/src/services/leaderboard/models.rs b/src/services/leaderboard/models.rs deleted file mode 100644 index 07c8cd94..00000000 --- a/src/services/leaderboard/models.rs +++ /dev/null @@ -1,142 +0,0 @@ -use crate::utils::types::PlayerID; -use serde::{Deserialize, Serialize}; -use std::time::{Duration, SystemTime}; - -/// Structure for an entry in a leaderboard group -#[derive(Serialize)] -pub struct LeaderboardEntry { - /// The ID of the player this entry is for - pub player_id: PlayerID, - /// The name of the player this entry is for - pub player_name: Box, - /// The ranking of this entry (Position in the leaderboard) - pub rank: usize, - /// The value this ranking is based on - pub value: u32, -} - -/// Structure for a group of leaderboard entities ranked based -/// on a certain value the expires indicates when the value will -/// no longer be considered valid -pub struct LeaderboardGroup { - /// The values stored in this entity group - pub values: Box<[LeaderboardEntry]>, - /// The time at which this entity group will become expired - pub expires: SystemTime, -} - -impl LeaderboardGroup { - /// Leaderboard contents are cached for 1 hour - const LIFETIME: Duration = Duration::from_secs(60 * 60); - - /// Creates a new leaderboard group which has an expiry time set - /// to the LIFETIME and uses the provided values - pub fn new(values: Box<[LeaderboardEntry]>) -> Self { - let expires = SystemTime::now() + Self::LIFETIME; - Self { expires, values } - } - - /// Creates a dummy leaderboard group which has no values and - /// is already considered to be expired. Used to hand out - /// a value while computed to prevent mulitple computes happening - pub fn dummy() -> Self { - Self { - expires: SystemTime::UNIX_EPOCH, - values: Box::new([]), - } - } - - /// Checks whether this group is expired - pub fn is_expired(&self) -> bool { - let now = SystemTime::now(); - now.ge(&self.expires) - } - - /// Checks whether there are more items after the provided offset and size - pub fn has_more(&self, start: usize, count: usize) -> bool { - let length = self.values.len(); - start + count < length - } - - /// Gets a normal collection of leaderboard entries at the start offset of the - /// provided count. Will return the slice of entires as well as whether there are - /// more entries after the desired offset - /// - /// `start` The start offset index - /// `count` The number of leaderboard entries - pub fn get_normal(&self, start: usize, count: usize) -> Option<&[LeaderboardEntry]> { - let end_index = (start + count).min(self.values.len()); - self.values.get(start..end_index) - } - - /// Gets a leaderboard entry for the provided player ID if one is present - /// - /// `player_id` The ID of the player to find the entry for - pub fn get_entry(&self, player_id: PlayerID) -> Option<&LeaderboardEntry> { - self.values - .iter() - .find(|value| value.player_id == player_id) - } - - pub fn get_filtered(&self, players: &[PlayerID]) -> Vec<&LeaderboardEntry> { - self.values - .iter() - .filter(move |entry| players.contains(&entry.player_id)) - .collect() - } - - /// Gets a collection of leaderboard entries centered on the provided player with - /// half `count` items before and after if possible. - /// - /// `player_id` The ID of the player to center on - /// `count` The total number of players to center on - pub fn get_centered(&self, player_id: PlayerID, count: usize) -> Option<&[LeaderboardEntry]> { - if count == 0 { - return None; - } - - // The number of items before the center index - let before = if count % 2 == 0 { - (count / 2).saturating_add(1) - } else { - count / 2 - }; - - // The number of items after the center index - let after = count / 2; - - // The index of the centered player - let player_index = self - .values - .iter() - .position(|value| value.player_id == player_id)?; - - // The index of the first item - let start_index = player_index.saturating_sub(before).min(player_index); - // The index of the last item - let end_index = player_index.saturating_add(after).min(self.values.len()); - - self.values.get(start_index..end_index) - } -} - -/// Type of leaderboard entity -#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, Deserialize)] -pub enum LeaderboardType { - /// Leaderboard based on the player N7 ratings - #[serde(rename = "n7")] - N7Rating, - /// Leaderboard based on the player challenge point number - #[serde(rename = "cp")] - ChallengePoints, -} - -impl From<&str> for LeaderboardType { - fn from(value: &str) -> Self { - if value.starts_with("N7Rating") { - Self::N7Rating - } else { - Self::ChallengePoints - } - } -} diff --git a/src/services/mod.rs b/src/services/mod.rs index 3b905893..b497ce6b 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,4 +1,3 @@ pub mod game; -pub mod leaderboard; pub mod retriever; pub mod sessions; diff --git a/src/services/retriever/mod.rs b/src/services/retriever/mod.rs index 33e5c672..8b0bdb83 100644 --- a/src/services/retriever/mod.rs +++ b/src/services/retriever/mod.rs @@ -105,7 +105,7 @@ impl OfficialInstance { /// standardSecure_v3 /// 0 /// - const REDIRECTOR_HOST: &str = "gosredirector.ea.com"; + const REDIRECTOR_HOST: &'static str = "gosredirector.ea.com"; /// The port for the redirector server. const REDIRECT_PORT: Port = 42127; @@ -303,7 +303,7 @@ impl OfficialSession { let stream = BlazeStream::connect((host, port)).await?; Ok(Self { id: 0, - stream: Framed::new(stream, PacketCodec), + stream: Framed::new(stream, PacketCodec::default()), }) } diff --git a/src/services/retriever/models.rs b/src/services/retriever/models.rs index a2bb7388..ee1182f3 100644 --- a/src/services/retriever/models.rs +++ b/src/services/retriever/models.rs @@ -1,3 +1,4 @@ +use crate::session::models::util::LOCALE_NZ; use tdf::{TdfDeserializeOwned, TdfSerialize, TdfType}; /// Packet encoding for Redirector GetServerInstance packets @@ -17,7 +18,7 @@ impl TdfSerialize for InstanceRequest { w.tag_str(b"DSDK", "8.14.7.1"); w.tag_str(b"ENV", "prod"); w.tag_union_unset(b"FPID"); - w.tag_u32(b"LOC", 0x656e4e5a); + w.tag_u32(b"LOC", LOCALE_NZ); w.tag_str(b"NAME", "masseffect-3-pc"); w.tag_str(b"PLAT", "Windows"); w.tag_str(b"PROF", "standardSecure_v3"); diff --git a/src/services/sessions/mod.rs b/src/services/sessions/mod.rs index d679ef3f..6f60d7e7 100644 --- a/src/services/sessions/mod.rs +++ b/src/services/sessions/mod.rs @@ -1,18 +1,25 @@ //! Service for storing links to all the currenly active //! authenticated sessions on the server +use crate::session::{SessionLink, WeakSessionLink}; use crate::utils::hashing::IntHashMap; +use crate::utils::signing::SigningKey; use crate::utils::types::PlayerID; -use crate::{session::SessionLink, utils::signing::SigningKey}; use base64ct::{Base64UrlUnpadded, Encoding}; +use parking_lot::Mutex; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use tokio::sync::RwLock; + +type SessionMap = IntHashMap; /// Service for storing links to authenticated sessions and /// functionality for authenticating sessions pub struct Sessions { - /// Map of the authenticated players to their session links - sessions: RwLock>, + /// Lookup mapping between player IDs and their session links + /// + /// This uses a blocking mutex as there is little to no overhead + /// since all operations are just map read and writes which don't + /// warrant the need for the async variant + sessions: Mutex, /// HMAC key used for computing signatures key: SigningKey, @@ -98,19 +105,29 @@ impl Sessions { Ok(id) } - pub async fn remove_session(&self, player_id: PlayerID) { - let sessions = &mut *self.sessions.write().await; + pub fn remove_session(&self, player_id: PlayerID) { + let sessions = &mut *self.sessions.lock(); sessions.remove(&player_id); } - pub async fn add_session(&self, player_id: PlayerID, link: SessionLink) { - let sessions = &mut *self.sessions.write().await; + pub fn add_session(&self, player_id: PlayerID, link: WeakSessionLink) { + let sessions = &mut *self.sessions.lock(); sessions.insert(player_id, link); } - pub async fn lookup_session(&self, player_id: PlayerID) -> Option { - let sessions = &*self.sessions.read().await; - sessions.get(&player_id).cloned() + pub fn lookup_session(&self, player_id: PlayerID) -> Option { + let sessions = &mut *self.sessions.lock(); + let session = sessions.get(&player_id)?; + let session = match session.upgrade() { + Some(value) => value, + // Session has stopped remove it from the map + None => { + sessions.remove(&player_id); + return None; + } + }; + + Some(session) } } diff --git a/src/session/mod.rs b/src/session/mod.rs index 728e89e2..a9cd13b1 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -17,35 +17,33 @@ use self::{ use crate::{ database::entities::Player, services::{ - game::{Game, GameRef}, + game::{GameRef, WeakGameRef}, sessions::Sessions, }, session::models::{NetworkAddress, QosNetworkData}, utils::{ components::{component_key, user_sessions, DEBUG_IGNORED_PACKETS}, - types::{GameID, PlayerID, SessionID}, + lock::{QueueLock, QueueLockGuard, TicketAquireFuture}, + types::{GameID, PlayerID}, }, }; -use futures_util::{ - stream::{SplitSink, SplitStream}, - SinkExt, StreamExt, -}; +use futures_util::{future::BoxFuture, Sink, Stream}; use hyper::upgrade::Upgraded; use log::{debug, log_enabled, warn}; +use parking_lot::Mutex; use serde::Serialize; use std::{ fmt::Debug, net::Ipv4Addr, + pin::Pin, sync::{ atomic::{AtomicU32, Ordering}, Arc, }, - time::Duration, -}; -use tokio::{ - sync::{mpsc, RwLock}, - task::JoinSet, + task::{ready, Context, Poll}, }; +use std::{future::Future, sync::Weak}; +use tokio::sync::mpsc; use tokio_util::codec::Framed; pub mod models; @@ -54,26 +52,46 @@ pub mod router; pub mod routes; pub type SessionLink = Arc; +pub type WeakSessionLink = Weak; pub struct Session { - id: SessionID, + id: u32, addr: Ipv4Addr, - writer: mpsc::UnboundedSender, - data: RwLock>, - router: Arc, + busy_lock: QueueLock, + tx: mpsc::UnboundedSender, + data: Mutex>, sessions: Arc, } +#[derive(Clone)] +pub struct SessionNotifyHandle { + busy_lock: QueueLock, + tx: mpsc::UnboundedSender, +} + +impl SessionNotifyHandle { + /// Pushes a new notification packet, this will aquire a queue position + /// waiting until the current response is handled before sending + pub fn notify(&self, packet: Packet) { + let tx = self.tx.clone(); + let busy_lock = self.busy_lock.aquire(); + tokio::spawn(async move { + let _guard = busy_lock.await; + let _ = tx.send(packet); + }); + } +} + pub struct SessionExtData { player: Arc, net: Arc, game: Option, - subscribers: Vec<(PlayerID, SessionLink)>, + subscribers: Vec<(PlayerID, SessionNotifyHandle)>, } struct SessionGameData { game_id: GameID, - game_ref: Arc>, + game_ref: WeakGameRef, } impl SessionExtData { @@ -93,48 +111,48 @@ impl SessionExtData { } } - fn add_subscriber(&mut self, player_id: PlayerID, subscriber: SessionLink) { - // Create the details packets - let added_notify = Packet::notify( + fn add_subscriber(&mut self, player_id: PlayerID, subscriber: SessionNotifyHandle) { + // Notify the addition of this user data to the subscriber + subscriber.notify(Packet::notify( user_sessions::COMPONENT, user_sessions::USER_ADDED, NotifyUserAdded { session_data: self.ext(), user: UserIdentification::from_player(&self.player), }, - ); + )); - // Create update notifying the user of the subscription - let update_notify = Packet::notify( + // Notify the user that they are now subscribed to this user + subscriber.notify(Packet::notify( user_sessions::COMPONENT, user_sessions::USER_UPDATED, NotifyUserUpdated { flags: UserDataFlags::SUBSCRIBED | UserDataFlags::ONLINE, player_id: self.player.id, }, - ); + )); - self.subscribers.push((player_id, subscriber.clone())); - subscriber.push(added_notify); - subscriber.push(update_notify); + // Add the subscriber + self.subscribers.push((player_id, subscriber)); } fn remove_subscriber(&mut self, player_id: PlayerID) { - let index = match self.subscribers.iter().position(|(id, _)| player_id.eq(id)) { - Some(value) => value, - None => return, - }; - - let (_, subscriber) = self.subscribers.swap_remove(index); - - // Create the details packets - let removed_notify = Packet::notify( - user_sessions::COMPONENT, - user_sessions::USER_REMOVED, - NotifyUserRemoved { player_id }, - ); - - subscriber.push(removed_notify) + let subscriber = self + .subscribers + .iter() + // Find the subscriber to remove + .position(|(id, _sub)| player_id.eq(id)) + // Remove the subscriber + .map(|index| self.subscribers.swap_remove(index)); + + if let Some((_, subscriber)) = subscriber { + // Notify the subscriber they've removed the user subcription + subscriber.notify(Packet::notify( + user_sessions::COMPONENT, + user_sessions::USER_REMOVED, + NotifyUserRemoved { player_id }, + )) + } } /// Publishes changes of the session data to all the @@ -149,9 +167,9 @@ impl SessionExtData { }, ); - for (_, subscriber) in &self.subscribers { - subscriber.push(packet.clone()); - } + self.subscribers + .iter() + .for_each(|(_, sub)| sub.notify(packet.clone())); } } @@ -160,15 +178,22 @@ pub struct NetData { pub addr: NetworkAddress, pub qos: QosNetworkData, pub hardware_flags: HardwareFlags, + pub ping_site_latency: Vec, } impl NetData { // Re-creates the current net data using the provided address and QOS data - pub fn with_basic(&self, addr: NetworkAddress, qos: QosNetworkData) -> Self { + pub fn with_basic( + &self, + addr: NetworkAddress, + qos: QosNetworkData, + ping_site_latency: Vec, + ) -> Self { Self { addr, qos, hardware_flags: self.hardware_flags, + ping_site_latency, } } @@ -178,6 +203,7 @@ impl NetData { addr: self.addr.clone(), qos: self.qos, hardware_flags: flags, + ping_site_latency: self.ping_site_latency.clone(), } } } @@ -185,77 +211,57 @@ impl NetData { static SESSION_IDS: AtomicU32 = AtomicU32::new(1); impl Session { - /// Max number of times to poll a session for shutdown before erroring - const MAX_RELEASE_ATTEMPTS: u8 = 5; - - pub fn start(io: Upgraded, addr: Ipv4Addr, router: Arc, sessions: Arc) { + pub async fn start( + io: Upgraded, + addr: Ipv4Addr, + router: Arc, + sessions: Arc, + ) { // Obtain a session ID let id = SESSION_IDS.fetch_add(1, Ordering::AcqRel); - let framed = Framed::new(io, PacketCodec); - let (write, read) = framed.split(); let (tx, rx) = mpsc::unbounded_channel(); let session = Arc::new(Self { id, - writer: tx, + busy_lock: QueueLock::new(), + tx, data: Default::default(), addr, - router, sessions, }); - let reader = SessionReader { - link: session.clone(), - inner: read, - }; - - let writer = SessionWriter { - link: session.clone(), + SessionFuture { + io: Framed::new(io, PacketCodec::default()), + router: &router, rx, - inner: write, - }; + session: session.clone(), + read_state: ReadState::Recv, + write_state: WriteState::Recv, + stop: false, + } + .await; - tokio::spawn(reader.process()); - tokio::spawn(writer.process()); + session.stop(); } - /// Handles routing a packet - async fn handle_packet(self: Arc, packet: Packet) { - let route_link = self.clone(); - let this = &*self; - - this.debug_log_packet("Receive", &packet).await; - let response = match this.router.handle(route_link, packet) { - // Await the handler response future - Ok(fut) => fut.await, - - // Handle no handler for packet - Err(packet) => { - debug!("Missing packet handler"); - Packet::response_empty(&packet) - } - }; - // Push the response to the client - this.push(response); + pub fn notify_handle(&self) -> SessionNotifyHandle { + SessionNotifyHandle { + busy_lock: self.busy_lock.clone(), + tx: self.tx.clone(), + } } - /// Internal session stopped function called by the reader when - /// the connection is terminated, cleans up any references and - /// asserts only 1 strong reference exists - async fn stop(self: Arc) { - // Tell the write half to close and wait until its closed - _ = self.writer.send(WriteMessage::Close); - self.writer.closed().await; - + /// Called when the session is considered stopped (Reader/Writer future has completed) + /// in order to clean up any remaining references to the session before dropping + fn stop(self: Arc) { // Clear authentication - self.clear_player().await; - - let mut attempt: u8 = 1; + self.clear_player(); - let mut arc = self; - let session = loop { - if attempt > Self::MAX_RELEASE_ATTEMPTS { + // Session should now be the sole owner + let session = match Arc::try_unwrap(self) { + Ok(value) => value, + Err(arc) => { let references = Arc::strong_count(&arc); warn!( "Failed to stop session {} there are still {} references to it", @@ -263,51 +269,29 @@ impl Session { ); return; } - match Arc::try_unwrap(arc) { - Ok(value) => break value, - Err(value) => { - let wait = 5 * attempt as u64; - let references = Arc::strong_count(&value); - debug!( - "Session {} still has {} references to it, waiting {}s", - value.id, references, wait - ); - tokio::time::sleep(Duration::from_secs(wait)).await; - arc = value; - attempt += 1; - continue; - } - } }; debug!("Session stopped (SID: {})", session.id); } - pub async fn add_subscriber(&self, player_id: PlayerID, subscriber: SessionLink) { - let data = &mut *self.data.write().await; - let data = match data { - Some(value) => value, - // TODO: Handle this as an error for unauthenticated - None => return, - }; - data.add_subscriber(player_id, subscriber); + pub fn add_subscriber(&self, player_id: PlayerID, subscriber: SessionNotifyHandle) { + let data = &mut *self.data.lock(); + if let Some(data) = data { + data.add_subscriber(player_id, subscriber); + } } - - pub async fn remove_subscriber(&self, player_id: PlayerID) { - let data = &mut *self.data.write().await; - let data = match data { - Some(value) => value, - // TODO: Handle this as an error for unauthenticated - None => return, - }; - data.remove_subscriber(player_id); + pub fn remove_subscriber(&self, player_id: PlayerID) { + let data = &mut *self.data.lock(); + if let Some(data) = data { + data.remove_subscriber(player_id) + } } - pub async fn set_player(&self, player: Player) -> Arc { + pub fn set_player(&self, player: Player) -> Arc { // Clear the current authentication - self.clear_player().await; + self.clear_player(); - let data = &mut *self.data.write().await; + let data = &mut *self.data.lock(); let data = data.insert(SessionExtData::new(player)); data.player.clone() @@ -317,30 +301,47 @@ impl Session { /// the player was in a game /// /// Called by the game itself when the player has been removed - pub async fn clear_game(&self) -> Option<(PlayerID, GameRef)> { - // Check that theres authentication - let data = &mut *self.data.write().await; - let data = data.as_mut()?; - let game = data.game.take(); - data.publish_update(); + pub fn clear_game(&self) -> Option<(PlayerID, WeakGameRef)> { + let mut game: Option = None; + let mut player_id: Option = None; + + self.update_data(|data| { + game = data.game.take(); + player_id = Some(data.player.id); + }); + let game = game?; + let player_id = player_id?; - Some((data.player.id, game.game_ref)) + Some((player_id, game.game_ref)) } /// Called to remove the player from its current game - pub async fn remove_from_game(&self) { - if let Some((player_id, game_ref)) = self.clear_game().await { + pub fn remove_from_game(&self) { + let (player_id, game_ref) = match self.clear_game() { + Some(value) => value, + // Player isn't in a game + None => return, + }; + + let game_ref = match game_ref.upgrade() { + Some(value) => value, + // Game doesn't exist anymore + None => return, + }; + + // Spawn an async task to handle removing the player + tokio::spawn(async move { let game = &mut *game_ref.write().await; game.remove_player(player_id, RemoveReason::PlayerLeft); - } + }); } - pub async fn clear_player(&self) { - self.remove_from_game().await; + pub fn clear_player(&self) { + self.remove_from_game(); // Check that theres authentication - let data = &mut *self.data.write().await; + let data = &mut *self.data.lock(); let data = match data { Some(value) => value, None => return, @@ -350,18 +351,24 @@ impl Session { data.subscribers.clear(); // Remove the session from the sessions service - self.sessions.remove_session(data.player.id).await; + self.sessions.remove_session(data.player.id); } - pub async fn get_game(&self) -> Option<(GameID, GameRef)> { - let data = &*self.data.read().await; + pub fn get_game(&self) -> Option<(GameID, GameRef)> { + let data = &*self.data.lock(); data.as_ref() .and_then(|value| value.game.as_ref()) - .map(|value| (value.game_id, value.game_ref.clone())) + // Try upgrading the reference to get an actual game + .and_then(|value| { + value + .game_ref + .upgrade() + .map(|game_ref| (value.game_id, game_ref)) + }) } - pub async fn get_lookup(&self) -> Option { - let data = &*self.data.read().await; + pub fn get_lookup(&self) -> Option { + let data = &*self.data.lock(); data.as_ref().map(|data| LookupResponse { player: data.player.clone(), extended_data: data.ext(), @@ -369,57 +376,49 @@ impl Session { } #[inline] - async fn update_data(&self, update: F) + fn update_data(&self, update: F) where F: FnOnce(&mut SessionExtData), { - let data = &mut *self.data.write().await; + let data = &mut *self.data.lock(); if let Some(data) = data { update(data); data.publish_update(); } } - pub async fn set_game(&self, game_id: GameID, game_ref: GameRef) { + pub fn set_game(&self, game_id: GameID, game_ref: WeakGameRef) { + // Remove the player from the game if they are already present in one + self.remove_from_game(); + // Set the current game self.update_data(|data| { - // Remove the player from the game if they are already present in one - if let Some(game) = data.game.take() { - let player_id = data.player.id; - tokio::spawn(async move { - let game = &mut *game.game_ref.write().await; - game.remove_player(player_id, RemoveReason::PlayerLeft); - }); - } - data.game = Some(SessionGameData { game_id, game_ref }); - }) - .await; + }); } #[inline] - pub async fn set_hardware_flags(&self, value: HardwareFlags) { + pub fn set_hardware_flags(&self, value: HardwareFlags) { self.update_data(|data| { data.net = Arc::new(data.net.with_hardware_flags(value)); - }) - .await; + }); } - #[inline] - pub async fn set_network_info(&self, address: NetworkAddress, qos: QosNetworkData) { - self.update_data(|data| { - data.net = Arc::new(data.net.with_basic(address, qos)); - }) - .await; + pub fn network_info(&self) -> Option> { + let data = &mut *self.data.lock(); + data.as_ref().map(|value| value.net.clone()) } - /// Pushes a new packet to the back of the packet buffer - /// and sends a flush notification - /// - /// `packet` The packet to push to the buffer - pub fn push(&self, packet: Packet) { - _ = self.writer.send(WriteMessage::Write(packet)) - // TODO: Handle failing to send contents to writer + #[inline] + pub fn set_network_info( + &self, + address: NetworkAddress, + qos: QosNetworkData, + ping_site_latency: Vec, + ) { + self.update_data(|data| { + data.net = Arc::new(data.net.with_basic(address, qos, ping_site_latency)); + }); } /// Logs the contents of the provided packet to the debug output along with @@ -428,23 +427,26 @@ impl Session { /// `action` The name of the action this packet is undergoing. /// (e.g. Writing or Reading) /// `packet` The packet that is being logged - async fn debug_log_packet(&self, action: &'static str, packet: &Packet) { + fn debug_log_packet(&self, action: &'static str, packet: &Packet) { // Skip if debug logging is disabled if !log_enabled!(log::Level::Debug) { return; } let key = component_key(packet.frame.component, packet.frame.command); - let ignored = DEBUG_IGNORED_PACKETS.contains(&key); - if ignored { + + // Don't log the packet if its debug ignored + if DEBUG_IGNORED_PACKETS.contains(&key) { return; } - let data = &*self.data.read().await; + // Get the authenticated player to include in the debug message + let auth = self.data.lock().as_ref().map(|data| data.player.clone()); + let debug_data = DebugSessionData { action, id: self.id, - data, + auth, }; let debug_packet = PacketDebug { packet }; @@ -452,71 +454,188 @@ impl Session { } } -struct DebugSessionData<'a> { - id: SessionID, - data: &'a Option, +struct DebugSessionData { + id: u32, + auth: Option>, action: &'static str, } -impl Debug for DebugSessionData<'_> { +impl Debug for DebugSessionData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!(f, "Session ({}): {}", self.id, self.action)?; - if let Some(data) = self.data.as_ref() { - writeln!( - f, - "Auth ({}): (Name: {})", - data.player.id, &data.player.display_name, - )?; + if let Some(auth) = &self.auth { + writeln!(f, "Auth ({}): (Name: {})", auth.id, &auth.display_name,)?; } Ok(()) } } -// Writer for writing packets -struct SessionWriter { - inner: SplitSink, Packet>, - rx: mpsc::UnboundedReceiver, - link: SessionLink, +/// Future for processing a session +struct SessionFuture<'a> { + /// The IO for reading and writing + io: Framed, + /// Receiver for packets to write + rx: mpsc::UnboundedReceiver, + /// The session this link is for + session: SessionLink, + /// The router to use + router: &'a BlazeRouter, + /// The reading state + read_state: ReadState<'a>, + /// The writing state + write_state: WriteState, + /// Whether the future has been stopped + stop: bool, } -pub enum WriteMessage { - Write(Packet), - Close, +/// Session future writing state +enum WriteState { + /// Waiting for a packet to write + Recv, + /// Waiting for the framed to become read + Write { packet: Option }, + /// Flushing the framed + Flush, +} + +/// Session future reading state +enum ReadState<'a> { + /// Waiting for a packet + Recv, + /// Aquiring a lock guard + Aquire { + /// Future for the locking guard + ticket: TicketAquireFuture, + /// The packet that was read + packet: Option, + }, + /// Future for a handler is being polled + Handle { + /// Locking guard + guard: QueueLockGuard, + /// Handle future + future: BoxFuture<'a, Packet>, + }, } -impl SessionWriter { - pub async fn process(mut self) { - while let Some(msg) = self.rx.recv().await { - let packet = match msg { - WriteMessage::Write(packet) => packet, - WriteMessage::Close => break, - }; - - self.link.debug_log_packet("Send", &packet).await; - if self.inner.send(packet).await.is_err() { - break; +impl SessionFuture<'_> { + /// Polls the write state, the poll ready state returns whether + /// the future should continue + fn poll_write_state(&mut self, cx: &mut Context<'_>) -> Poll<()> { + match &mut self.write_state { + WriteState::Recv => { + // Try receive a packet from the write channel + let result = ready!(Pin::new(&mut self.rx).poll_recv(cx)); + + if let Some(packet) = result { + self.write_state = WriteState::Write { + packet: Some(packet), + }; + } else { + // All writers have closed, session must be closed (Future end) + self.stop = true; + } + } + WriteState::Write { packet } => { + // Wait until the inner is ready + if ready!(Pin::new(&mut self.io).poll_ready(cx)).is_ok() { + let packet = packet + .take() + .expect("Unexpected write state without packet"); + + self.session.debug_log_packet("Send", &packet); + + // Write the packet to the buffer + Pin::new(&mut self.io) + .start_send(packet) + // Packet encoder impl shouldn't produce errors + .expect("Packet encoder errored"); + + self.write_state = WriteState::Flush; + } else { + // Failed to ready, session must be closed + self.stop = true; + } + } + WriteState::Flush => { + // Wait until the flush is complete + if ready!(Pin::new(&mut self.io).poll_flush(cx)).is_ok() { + self.write_state = WriteState::Recv; + } else { + // Failed to flush, session must be closed + self.stop = true + } + } + } + + Poll::Ready(()) + } + + /// Polls the read state, the poll ready state returns whether + /// the future should continue + fn poll_read_state(&mut self, cx: &mut Context<'_>) -> Poll<()> { + match &mut self.read_state { + ReadState::Recv => { + // Try receive a packet from the write channel + let result = ready!(Pin::new(&mut self.io).poll_next(cx)); + + if let Some(Ok(packet)) = result { + let ticket = self.session.busy_lock.aquire(); + self.read_state = ReadState::Aquire { + ticket, + packet: Some(packet), + } + } else { + // Reader has closed or reading encountered an error (Either way stop reading) + self.stop = true; + } + } + ReadState::Aquire { ticket, packet } => { + let guard = ready!(Pin::new(ticket).poll(cx)); + let packet = packet + .take() + .expect("Unexpected aquire state without packet"); + + self.session.debug_log_packet("Receive", &packet); + + let future = self.router.handle(self.session.clone(), packet); + + // Move onto a handling state + self.read_state = ReadState::Handle { guard, future }; + } + ReadState::Handle { + guard: _gaurd, + future, + } => { + // Poll the handler until completion + let response = ready!(Pin::new(future).poll(cx)); + + // Send the response to the writer + _ = self.session.tx.send(response); + + // Reset back to the reading state + self.read_state = ReadState::Recv; } } + Poll::Ready(()) } } -struct SessionReader { - inner: SplitStream>, - link: SessionLink, -} +impl Future for SessionFuture<'_> { + type Output = (); -impl SessionReader { - pub async fn process(mut self) { - let mut tasks = JoinSet::new(); + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); - while let Some(Ok(packet)) = self.inner.next().await { - let link = self.link.clone(); - tasks.spawn(link.handle_packet(packet)); - } + while this.poll_write_state(cx).is_ready() {} + while this.poll_read_state(cx).is_ready() {} - tasks.shutdown().await; - self.link.stop().await; + if this.stop { + Poll::Ready(()) + } else { + Poll::Pending + } } } diff --git a/src/session/models/errors.rs b/src/session/models/errors.rs index ef9080d5..8d534974 100644 --- a/src/session/models/errors.rs +++ b/src/session/models/errors.rs @@ -12,7 +12,7 @@ pub type ServerResult = Result; #[test] fn decode_error() { - let value: i32 = 96258; + let value: i32 = 1966084; let bytes = value.to_le_bytes(); let mut out = [0u8; 2]; out.copy_from_slice(&bytes[2..]); diff --git a/src/session/models/game_manager.rs b/src/session/models/game_manager.rs index 7d1f13bc..742a0470 100644 --- a/src/session/models/game_manager.rs +++ b/src/session/models/game_manager.rs @@ -1,13 +1,16 @@ use bitflags::bitflags; use serde::Serialize; -use tdf::{Blob, GroupSlice, TdfDeserialize, TdfDeserializeOwned, TdfSerialize, TdfType, TdfTyped}; +use tdf::{ + types::tagged_union::TAGGED_UNSET_KEY, Blob, GroupSlice, TdfDeserialize, TdfDeserializeOwned, + TdfSerialize, TdfType, TdfTyped, +}; use crate::{ services::game::{rules::RuleSet, AttrMap, Game, GamePlayer}, - utils::types::{GameID, PlayerID, SessionID}, + utils::types::{GameID, PlayerID}, }; -use super::NetworkAddress; +use super::{util::PING_SITE_ALIAS, NetworkAddress}; #[derive(Debug, Clone)] #[repr(u16)] @@ -15,6 +18,7 @@ use super::NetworkAddress; pub enum GameManagerError { InvalidGameId = 0x2, GameFull = 0x4, + PermissionDenied = 0x1e, PlayerNotFound = 0x65, AlreadyGameMember = 0x67, RemovePlayerFailed = 0x68, @@ -91,16 +95,32 @@ pub struct UpdateMeshRequest { #[tdf(tag = "GID")] pub game_id: GameID, #[tdf(tag = "TARG")] - pub targets: Vec, + pub targets: Vec, } #[derive(TdfDeserialize, TdfTyped)] #[tdf(group)] -pub struct MeshTarget { +pub struct PlayerConnectionStatus { #[tdf(tag = "PID")] pub player_id: PlayerID, #[tdf(tag = "STAT")] - pub state: PlayerState, + pub status: PlayerNetConnectionStatus, +} + +#[derive(TdfDeserialize, TdfTyped)] +#[repr(u8)] +pub enum PlayerNetConnectionStatus { + Disconnected = 0x0, + EstablishingConnection = 0x1, + Connected = 0x2, +} + +#[derive(TdfDeserialize)] +pub struct AddAdminPlayerRequest { + #[tdf(tag = "GID")] + pub game_id: GameID, + #[tdf(tag = "PID")] + pub player_id: PlayerID, } /// Structure of the request for starting matchmaking. Contains @@ -144,7 +164,7 @@ impl TdfDeserializeOwned for MatchmakingRequest { pub struct MatchmakingResponse { /// The current session ID #[tdf(tag = "MSID")] - pub id: SessionID, + pub id: PlayerID, } #[derive(TdfDeserialize)] @@ -556,18 +576,32 @@ impl TdfSerialize for PlayerJoining<'_> { )] #[repr(u8)] pub enum GameState { + /// Data structure just created NewState = 0x0, + /// Closed to joins/matchmaking #[tdf(default)] #[default] Initializing = 0x1, - Virtual = 0x2, + /// Game will need topology host assigned when player joins. + InactiveVirtual = 0x2, + /// Game created via matchmaking is waiting for connections to be established and validated. + ConnectionVerification = 0x3, + /// Pre game state, obey joinMode flags PreGame = 0x82, + /// Game available, obey joinMode flag InGame = 0x83, + /// After game is done,closed to joins/matchmaking PostGame = 0x4, + /// Game migration state, closed to joins/matchmaking Migrating = 0x5, + /// Game destruction state, closed to joins/matchmaking Destructing = 0x6, + /// Game resetable state, closed to joins/matchmaking, but available to be reset Resetable = 0x7, - ReplaySetup = 0x8, + /// Unresponsive, closed to joins/matchmaking + Unresponsive = 0x9, + /// Initialized state, intended for the use of game group + GameGroupInitialized = 0x10, } bitflags! { @@ -605,7 +639,26 @@ impl From for GameSettings { } } -const VSTR: &str = "ME3-295976325-179181965240128"; +const GAME_PROTOCOL_VERSION: &str = "ME3-295976325-179181965240128"; + +/// UNSPECIFIED_TEAM_INDEX will assign the player to whichever team has room. +pub const UNSPECIFIED_TEAM_INDEX: u16 = 0xffff; + +/// Game version hashing +/// +/// Credits to Aim4kill https://github.com/PocketRelay/Server/issues/59 +fn compute_version_hash(version: &str) -> u64 { + const OFFSET: u64 = 2166136261; + const PRIME: u64 = 16777619; + + version + .as_bytes() + .iter() + .copied() + .fold(OFFSET, |hash, byte| { + (hash.wrapping_mul(PRIME)) ^ (byte as u64) + }) +} #[derive(TdfSerialize, TdfTyped)] pub enum GameSetupContext { @@ -655,6 +708,54 @@ pub enum DatalessContext { // HostInjectionSetupContext = 0x4, } +#[allow(unused)] +#[derive(Debug, Copy, Clone, TdfSerialize, TdfTyped)] +#[repr(u8)] +pub enum PresenceMode { + // No presence management. E.g. For games that should never be advertised in shell UX and cannot be used for 1st party invites. + None = 0x0, + // Full presence as defined by the platform. + Standard = 0x1, + // Private presence as defined by the platform. For private games which are closed to uninvited/outside users. + Private = 0x2, +} + +#[allow(unused)] +#[derive(Debug, Copy, Clone, TdfSerialize, TdfTyped)] +#[repr(u8)] +pub enum VoipTopology { + /// VOIP is disabled (for a game) + Disabled = 0x0, + // /// VOIP uses a star topology; typically some form of 3rd party server dedicated to mixing/broadcasting voip streams. + // DedicatedServer = 0x1 + /// VOIP uses a full mesh topology; each player makes peer connections to the other players/members for voip traffic. + PeerToPeer = 0x2, +} + +#[allow(unused)] +#[derive(Debug, Copy, Clone, TdfSerialize, TdfTyped)] +#[repr(u8)] +pub enum GameNetworkTopology { + /// client server peer hosted network topology + PeerHosted = 0x0, + /// client server dedicated server topology + Dedicated = 0x1, + /// Peer to peer full mesh network topology + FullMesh = 0x82, + /// Networking is disabled?? + Disabled = 0xFF, +} + +#[allow(unused)] +#[derive(Debug, Copy, Clone, TdfSerialize, TdfTyped)] +#[repr(u8)] +pub enum SlotType { + // Public participant slot, usable by any participant + PublicParticipant = 0x0, + // Private participant slot, reserved for invited participant + PrivateParticipant = 0x1, +} + pub struct GameSetupResponse<'a> { pub game: &'a Game, pub context: GameSetupContext, @@ -666,32 +767,66 @@ impl TdfSerialize for GameSetupResponse<'_> { let host = game.players.first().expect("Missing game host for setup"); w.group(b"GAME", |w| { + // Admin player list w.tag_list_iter_owned(b"ADMN", game.players.iter().map(|player| player.player.id)); + // Game attributes w.tag_ref(b"ATTR", &game.attributes); - w.tag_list_slice::(b"CAP", &[4, 0]); + // Slot Capacities + w.tag_list_slice::( + b"CAP", + &[ + Game::MAX_PLAYERS, /* Public slots */ + 0, /* Private slots */ + ], + ); + // Game ID w.tag_u32(b"GID", game.id); + // Game Name w.tag_str(b"GNAM", &host.player.display_name); - w.tag_u64(b"GPVH", 0x5a4f2b378b715c6); + // Game Protocol Version Hash + w.tag_u64(b"GPVH", compute_version_hash(GAME_PROTOCOL_VERSION)); + // Game settings w.tag_owned(b"GSET", game.settings.bits()); + // Game Reporting ID w.tag_u64(b"GSID", 0x4000000a76b645); + // Game state w.tag_ref(b"GSTA", &game.state); - + // Game Type used for game reporting as passed up in the request. w.tag_str_empty(b"GTYP"); + { + // Topology host network list (The heat bug is present so this encoded as a group even though its a union) w.tag_list_start(b"HNET", TdfType::Group, 1); - w.write_byte(2); + if let NetworkAddress::AddressPair(pair) = &host.net.addr { + w.write_byte(2 /* Address pair type */); TdfSerialize::serialize(pair, w) + } else { + // Uh oh.. host networking is missing...? + w.write_byte(TAGGED_UNSET_KEY); + w.write_byte(0); } } + // Host session ID w.tag_u32(b"HSES", host.player.id); w.tag_zero(b"IGNO"); - w.tag_u8(b"MCAP", 4); + + // Max player capacity + w.tag_usize(b"MCAP", Game::MAX_PLAYERS); + + // Host network qos data w.tag_ref(b"NQOS", &host.net.qos); - w.tag_zero(b"NRES"); - w.tag_zero(b"NTOP"); + + // Flag to indicate that this game is not resetable. This applies only to the CLIENT_SERVER_DEDICATED topology. The game will be prevented from ever going into the RESETABlE state. + w.tag_bool(b"NRES", false); + + // Game network topology + w.tag_alt(b"NTOP", GameNetworkTopology::PeerHosted); + + // Persisted Game id for the game, used only when game setting's enablePersistedGameIds is true. w.tag_str_empty(b"PGID"); + // Persisted Game id secret for the game, used only when game setting's enablePersistedGameIds is true. w.tag_blob_empty(b"PGSR"); // Platform host info @@ -700,24 +835,33 @@ impl TdfSerialize for GameSetupResponse<'_> { w.tag_zero(b"HSLT"); }); - w.tag_u8(b"PRES", 0x1); - w.tag_str_empty(b"PSAS"); + // Presence mode + w.tag_alt(b"PRES", PresenceMode::Standard); + // Ping site alias + w.tag_str(b"PSAS", PING_SITE_ALIAS); // Queue capacity w.tag_zero(b"QCAP"); - // Shared game randomness seed? + // Shared game randomness seed? (a 32 bit number shared between clients) + // TODO: Randomly generate this when creating a game? w.tag_u32(b"SEED", 0x4cbc8585); - // tEAM capacity + // Team capacity w.tag_zero(b"TCAP"); - // Topology host info + // The topology host for the game (everyone connects to this person). w.group(b"THST", |w| { + // Player ID w.tag_u32(b"HPID", host.player.id); + // Slot ID w.tag_zero(b"HSLT"); }); w.tag_str(b"UUID", "286a2373-3e6e-46b9-8294-3ef05e479503"); - w.tag_u8(b"VOIP", 0x2); - w.tag_str(b"VSTR", VSTR); + // VOIP type + w.tag_alt(b"VOIP", VoipTopology::PeerToPeer); + + // Game Protocol Version + w.tag_str(b"VSTR", GAME_PROTOCOL_VERSION); + w.tag_blob_empty(b"XNNC"); w.tag_blob_empty(b"XSES"); }); @@ -743,35 +887,91 @@ impl TdfSerialize for GetGameDetails<'_> { w.tag_list_start(b"GDAT", TdfType::Group, 1); w.group_body(|w| { + // Admin player list w.tag_list_iter_owned(b"ADMN", game.players.iter().map(|player| player.player.id)); + // Game attributes w.tag_ref(b"ATTR", &game.attributes); - w.tag_list_slice(b"CAP", &[4u8, 0u8]); + // Slot Capacities + w.tag_list_slice::( + b"CAP", + &[ + Game::MAX_PLAYERS, /* Public slots */ + 0, /* Private slots */ + ], + ); + + // Game ID w.tag_u32(b"GID", game.id); + // Game name w.tag_str(b"GNAM", &host.player.display_name); + // Game setting w.tag_u16(b"GSET", game.settings.bits()); + // Game state w.tag_ref(b"GSTA", &game.state); { + // Topology host network list (The heat bug is present so this encoded as a group even though its a union) w.tag_list_start(b"HNET", TdfType::Group, 1); - w.write_byte(2); + if let NetworkAddress::AddressPair(pair) = &host.net.addr { + w.write_byte(2 /* Address pair type */); TdfSerialize::serialize(pair, w) + } else { + // Uh oh.. host networking is missing...? + w.write_byte(TAGGED_UNSET_KEY); + w.write_byte(0); } } + // Host player ID w.tag_u32(b"HOST", host.player.id); - w.tag_zero(b"NTOP"); - w.tag_list_slice(b"PCNT", &[1u8, 0u8]); + // Game network topology + w.tag_alt(b"NTOP", GameNetworkTopology::PeerHosted); + + // Player counts by slot + w.tag_list_slice::( + b"PCNT", + &[ + game.players.len(), /* Public count */ + 0, /* Private count */ + ], + ); + + // Presence mode + w.tag_alt(b"PRES", PresenceMode::Standard); - w.tag_u8(b"PRES", 0x2); - w.tag_str(b"PSAS", "ea-sjc"); - w.tag_str_empty(b"PSID"); + // Ping site alias + w.tag_str(b"PSAS", PING_SITE_ALIAS); + + // Persisted Game id for the game, used only when game setting's enablePersistedGameIds is true. + w.tag_str_empty(b"PGID"); + + // Max queue capacity. w.tag_zero(b"QCAP"); + // Current number of player in the queue. w.tag_zero(b"QCNT"); + // External session ID w.tag_zero(b"SID"); + // Team capacity w.tag_zero(b"TCAP"); - w.tag_u8(b"VOIP", 0x2); - w.tag_str(b"VSTR", VSTR); + // VOIP type + w.tag_alt(b"VOIP", VoipTopology::PeerToPeer); + // Game Protocol Version + w.tag_str(b"VSTR", GAME_PROTOCOL_VERSION); }); } } + +#[cfg(test)] +mod test { + use super::compute_version_hash; + + /// Ensure the version hashing algorithm produces the correct result + #[test] + fn test_compute_version_hash() { + let input: &str = "ME3-295976325-179181965240128"; + let expected: u64 = 0x5a4f2b378b715c6; + let output: u64 = compute_version_hash(input); + assert_eq!(output, expected); + } +} diff --git a/src/session/models/mod.rs b/src/session/models/mod.rs index 6420e40c..3d4fd50f 100644 --- a/src/session/models/mod.rs +++ b/src/session/models/mod.rs @@ -123,13 +123,13 @@ pub enum InstanceNet { #[derive(Debug, Copy, Clone, Default, Serialize, TdfSerialize, TdfDeserialize, TdfTyped)] #[tdf(group)] pub struct QosNetworkData { - /// Downstream bits per second + /// The client's downstream network bandwidth (in bits per second). #[tdf(tag = "DBPS")] pub dbps: u32, - /// Natt type + /// The client's network address translation type (aka firewall/router type). #[tdf(tag = "NATT")] pub natt: NatType, - /// Upstream bits per second + /// The client's upstream network bandwidth (in bits per second). #[tdf(tag = "UBPS")] pub ubps: u32, } @@ -138,11 +138,16 @@ pub struct QosNetworkData { #[derive(Debug, Default, Copy, Clone, Serialize, TdfDeserialize, TdfSerialize, TdfTyped)] #[repr(u8)] pub enum NatType { + /// Players behind an open NAT can usually connect to any other player and are ideal game hosts. #[default] Open = 0x0, + /// Players behind a moderate NAT can usually connect to other open or moderate players. Moderate = 0x1, - Sequential = 0x2, + /// Players behind a strict (but sequential) NAT can usually only connect to open players and are poor game hosts. + StrictSequential = 0x2, + /// Players behind a strict (unsequential) NAT can usually only connect to open players and are the worst game hosts. Strict = 0x3, + /// unknown NAT type; possibly timed out trying to detect NAT. #[tdf(default)] Unknown = 0x4, } @@ -168,7 +173,7 @@ pub enum NetworkAddress { pub type Port = u16; /// Pair of socket addresses -#[derive(Debug, Clone, TdfDeserialize, TdfSerialize, TdfTyped, Serialize)] +#[derive(Debug, Default, Clone, TdfDeserialize, TdfSerialize, TdfTyped, Serialize)] #[tdf(group)] pub struct IpPairAddress { #[tdf(tag = "EXIP")] @@ -186,3 +191,12 @@ pub struct PairAddress { #[tdf(tag = "PORT")] pub port: u16, } + +impl Default for PairAddress { + fn default() -> Self { + Self { + addr: Ipv4Addr::UNSPECIFIED, + port: 0, + } + } +} diff --git a/src/session/models/stats.rs b/src/session/models/stats.rs index 671409aa..fd373895 100644 --- a/src/session/models/stats.rs +++ b/src/session/models/stats.rs @@ -1,9 +1,53 @@ -use tdf::{TdfDeserialize, TdfSerialize, TdfType, TdfTyped}; - use crate::{ - services::leaderboard::models::{LeaderboardEntry, LeaderboardType}, + database::entities::leaderboard_data::{LeaderboardDataAndRank, LeaderboardType}, utils::{components::user_sessions::PLAYER_TYPE, types::PlayerID}, }; +use tdf::{TdfDeserialize, TdfMap, TdfSerialize, TdfType, TdfTyped, VarIntList}; + +#[derive(TdfDeserialize)] +pub struct SubmitGameReportRequest { + #[tdf(tag = "RPRT")] + pub report: GameReport, +} + +#[derive(TdfDeserialize, TdfTyped)] +#[tdf(group)] +pub struct GameReport { + // Must be read since it uses the same duplicate tag + #[tdf(tag = "GAME")] + pub game_ids: VarIntList, + + #[tdf(tag = "GAME")] + pub game: GameReportGame, +} + +#[derive(TdfDeserialize, TdfTyped)] +#[tdf(group)] +pub struct GameReportGame { + /// The details for each specific player + #[tdf(tag = "PLYR")] + pub players: TdfMap, +} + +#[derive(TdfDeserialize, TdfTyped)] +#[tdf(group)] +pub struct GameReportPlayerData { + /// Locale string encoded as int + #[tdf(tag = "CTRY")] + pub country: u32, + /// Number of challenge points the player has + #[tdf(tag = "NCHP")] + pub challenge_points: u32, + /// N7 Rating value for the player + #[tdf(tag = "NRAT")] + pub n7_rating: u32, +} + +#[test] +fn test() { + let bytes = 17477u32.to_be_bytes(); + println!("{}", String::from_utf8_lossy(&bytes)); +} /// Structure for the request to retrieve the entity count /// of a leaderboard @@ -51,23 +95,22 @@ pub struct CenteredLeaderboardRequest { pub center: PlayerID, /// The entity count #[tdf(tag = "COUN")] - pub count: usize, + pub count: u32, /// The leaderboard name #[tdf(tag = "NAME", into = &str)] pub name: LeaderboardType, } -pub enum LeaderboardResponse<'a> { - Owned(Vec<&'a LeaderboardEntry>), - Borrowed(&'a [LeaderboardEntry]), +pub struct LeaderboardResponse { + pub values: Vec, } -impl TdfSerialize for LeaderboardEntry { +impl TdfSerialize for LeaderboardDataAndRank { fn serialize(&self, w: &mut S) { w.group_body(|w| { w.tag_str(b"ENAM", &self.player_name); w.tag_u32(b"ENID", self.player_id); - w.tag_usize(b"RANK", self.rank); + w.tag_u32(b"RANK", self.rank); let value_str = self.value.to_string(); w.tag_str(b"RSTA", &value_str); @@ -81,20 +124,13 @@ impl TdfSerialize for LeaderboardEntry { } } -impl TdfTyped for LeaderboardEntry { +impl TdfTyped for LeaderboardDataAndRank { const TYPE: TdfType = TdfType::Group; } -impl TdfSerialize for LeaderboardResponse<'_> { +impl TdfSerialize for LeaderboardResponse { fn serialize(&self, w: &mut S) { - match self { - LeaderboardResponse::Owned(value) => { - w.tag_list_slice_ref(b"LDLS", value); - } - LeaderboardResponse::Borrowed(value) => { - w.tag_list_slice(b"LDLS", value); - } - } + w.tag_list_slice(b"LDLS", &self.values); } } @@ -122,13 +158,13 @@ impl TdfSerialize for LeaderboardResponse<'_> { pub struct LeaderboardRequest { /// The entity count #[tdf(tag = "COUN")] - pub count: usize, + pub count: u32, /// The leaderboard name #[tdf(tag = "NAME", into = &str)] pub name: LeaderboardType, /// The rank offset to start at #[tdf(tag = "STRT")] - pub start: usize, + pub start: u32, } /// Structure for a request to get a leaderboard only diff --git a/src/session/models/user_sessions.rs b/src/session/models/user_sessions.rs index e33b0f49..b297650d 100644 --- a/src/session/models/user_sessions.rs +++ b/src/session/models/user_sessions.rs @@ -7,7 +7,7 @@ use crate::{ }; use bitflags::bitflags; use serde::Serialize; -use tdf::{ObjectId, TdfDeserialize, TdfSerialize, TdfTyped}; +use tdf::{ObjectId, TdfDeserialize, TdfMap, TdfSerialize, TdfTyped}; use super::{util::PING_SITE_ALIAS, NetworkAddress, QosNetworkData}; @@ -32,6 +32,9 @@ pub struct UpdateNetworkRequest { /// The client address net groups #[tdf(tag = "ADDR")] pub address: NetworkAddress, + /// Latency to the different ping sites + #[tdf(tag = "NLMP")] + pub ping_site_latency: TdfMap, /// The client Quality of Service data #[tdf(tag = "NQOS")] pub qos: QosNetworkData, @@ -86,6 +89,10 @@ pub struct UserSessionExtendedData { impl TdfSerialize for UserSessionExtendedData { fn serialize(&self, w: &mut S) { + const DMAP_LEADERBOARD_N7_RATING: u32 = 0x70001; + // TODO: Maybe actually load this value + const DMAP_LEADERBOARD_N7_RATING_VALUE: u32 = 100; + w.group_body(|w| { // Network address w.tag_ref(b"ADDR", &self.net.addr); @@ -95,12 +102,18 @@ impl TdfSerialize for UserSessionExtendedData { w.tag_str_empty(b"CTY"); // Client data w.tag_var_int_list_empty(b"CVAR"); - // Data map - w.tag_map_tuples(b"DMAP", &[(0x70001, 0x409a)]); + // Data map (Custom player data integer keyed) + w.tag_map_tuples( + b"DMAP", + &[ + // The players n7 rating + (DMAP_LEADERBOARD_N7_RATING, DMAP_LEADERBOARD_N7_RATING_VALUE), + ], + ); // Hardware flags w.tag_owned(b"HWFG", self.net.hardware_flags.bits()); // Ping server latency list - w.tag_list_slice(b"PSLM", &[0xfff0fff]); + w.tag_list_slice(b"PSLM", &self.net.ping_site_latency); // Quality of service data w.tag_ref(b"QDAT", &self.net.qos); // User info attributes diff --git a/src/session/models/util.rs b/src/session/models/util.rs index 24d0c87d..096952ad 100644 --- a/src/session/models/util.rs +++ b/src/session/models/util.rs @@ -3,8 +3,9 @@ use crate::{ config::{QosServerConfig, RuntimeConfig}, utils::types::PlayerID, }; -use std::{borrow::Cow, sync::Arc}; -use tdf::{TdfDeserialize, TdfMap, TdfSerialize, TdfType}; +use bitflags::bitflags; +use std::{borrow::Cow, net::Ipv4Addr, sync::Arc}; +use tdf::{TdfDeserialize, TdfMap, TdfSerialize, TdfType, TdfTyped}; #[derive(Debug, Clone)] #[repr(u16)] @@ -36,6 +37,9 @@ pub const TELEMETRY_PORT: Port = 42129; // The constant port for the local http server pub const LOCAL_HTTP_PORT: Port = 42131; +// English locale NZ +pub const LOCALE_NZ: u32 = u32::from_be_bytes(*b"enNZ"); + /// Structure for encoding the telemetry server details pub struct TelemetryServer; @@ -48,7 +52,7 @@ impl TdfSerialize for TelemetryServer { w.tag_str(b"DISA", TELEMTRY_DISA); w.tag_str(b"FILT", "-UION/****"); // Encoded locale actually BE encoded string bytes (enNZ) - w.tag_u32(b"LOC", 1701727834); + w.tag_u32(b"LOC", LOCALE_NZ); w.tag_str(b"NOOK", "US,CA,MX"); // Last known telemetry port: 9988 w.tag_owned(b"PORT", TELEMETRY_PORT); @@ -82,8 +86,8 @@ impl TdfSerialize for TickerServer { } } -/// Server SRC version -pub const SRC_VERSION: &str = "303107"; +/// Origin auth source? +pub const AUTH_SOURCE: &str = "303107"; pub const BLAZE_VERSION: &str = "Blaze 3.15.08.0 (CL# 1629389)"; pub const PING_PERIOD: &str = "15s"; @@ -98,9 +102,9 @@ pub struct PreAuthResponse { impl TdfSerialize for PreAuthResponse { fn serialize(&self, w: &mut S) { w.tag_zero(b"ANON"); - w.tag_str(b"ASRC", SRC_VERSION); - // This list appears to contain the IDs of the components that the game - // uses throughout its lifecycle + // Authentication source + w.tag_str(b"ASRC", AUTH_SOURCE); + // List of components configured on the server. w.tag_list_slice( b"CIDS", &[ @@ -110,13 +114,14 @@ impl TdfSerialize for PreAuthResponse { ); w.tag_str_empty(b"CNGN"); - // Double nested map containing configuration options for - // ping intervals and VOIP headset update rates + // Client configuration provided by the server w.group(b"CONF", |w| { w.tag_map_tuples( b"CONF", &[ + // Client to server ping period ("pingPeriod", PING_PERIOD), + // VOIP headset update rate ("voipHeadsetUpdateRate", "1000"), // XLSP (Xbox Live Server Platform) ("xlspConnectionIdleTimeout", "300"), @@ -124,58 +129,79 @@ impl TdfSerialize for PreAuthResponse { ); }); + // Service name. w.tag_str(b"INST", "masseffect-3-pc"); - w.tag_zero(b"MINR"); + // Underage support + w.tag_bool(b"MINR", false); + // Persona namespace w.tag_str(b"NASP", "cem_ea_id"); + // Title-specific identifier for legal documents retrieval w.tag_str_empty(b"PILD"); + // Server platform. w.tag_str(b"PLAT", "pc"); + w.tag_str_empty(b"PTAG"); // Quality Of Service Server details w.group(b"QOSS", |w| { let qos = &self.config.qos; + let mut disabled = false; + let (http_host, http_port) = match qos { QosServerConfig::Official => ("gossjcprod-qos01.ea.com", 17502), QosServerConfig::Local => ("127.0.0.1", LOCAL_HTTP_PORT), QosServerConfig::Custom { host, port } => (host.as_str(), *port), + QosServerConfig::Disabled | QosServerConfig::Hamachi { .. } => { + disabled = true; + ("0", 0) + } }; // let http_host = "127.0.0.1"; // let http_port = 17499; - // Bioware Primary Server + // (qtyp=2) w.group(b"BWPS", |w| { w.tag_str(b"PSA", http_host); w.tag_u16(b"PSP", http_port); w.tag_str(b"SNA", "prod-sjc"); }); - w.tag_u8(b"LNP", 10); + // Number of probes to send to BWPS + w.tag_u8(b"LNP", 1); // List of other Quality Of Service servers? Values present in this // list are later included in a ping list { - w.tag_map_start(b"LTPS", TdfType::String, TdfType::Group, 1); - - // Key for the server - PING_SITE_ALIAS.serialize(w); - - w.group_body(|w| { - // Same as the Bioware primary server - w.tag_str(b"PSA", http_host); - w.tag_u16(b"PSP", http_port); - w.tag_str(b"SNA", "prod-sjc"); - }); + w.tag_map_start( + b"LTPS", + TdfType::String, + TdfType::Group, + if disabled { 0 } else { 1 }, + ); + + if !disabled { + // Key for the server + PING_SITE_ALIAS.serialize(w); + + // (qtyp=1) + w.group_body(|w| { + // Same as the Bioware primary server + w.tag_str(b"PSA", http_host); + w.tag_u16(b"PSP", http_port); + w.tag_str(b"SNA", "prod-sjc"); + }); + } } // Possibly server version ID (1161889797) w.tag_u32(b"SVID", 0x45410805); }); - // Server src version - w.tag_str(b"RSRC", SRC_VERSION); - // Server blaze version + // Registration source + w.tag_str(b"RSRC", AUTH_SOURCE); + // Server version. w.tag_str(b"SVER", BLAZE_VERSION) } } @@ -196,8 +222,9 @@ impl TdfSerialize for PostAuthResponse { w.group(b"PSS", |w| { w.tag_str(b"ADRS", "playersyncservice.ea.com"); w.tag_blob_empty(b"CSIG"); - w.tag_str(b"PJID", SRC_VERSION); + w.tag_str(b"PJID", AUTH_SOURCE); w.tag_u16(b"PORT", 443); + // Purchases (1) | FriendsList (2) | Achievements (4) | Consumables (8) = 0xF w.tag_u8(b"RPRT", 0xF); w.tag_u8(b"TIID", 0); }); @@ -266,3 +293,66 @@ pub struct SettingsResponse { #[tdf(tag = "SMAP")] pub settings: TdfMap, } + +bitflags! { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct UpnpFlags: u16 { + /// NAT type promoted from Moderate to Open due to UPnP success result. + const NAT_PROMOTED = 0x1; + /// WAN IP address does not match IP address seen by Blaze server. + const DOUBLE_NAT = 0x2; + /// External port derived by QoS was overridden by UPnP external port. + const PORT_OVERRIDE = 0x4; + } +} + +impl From for UpnpFlags { + fn from(value: u16) -> Self { + Self::from_bits_retain(value) + } +} + +#[derive(Default, Debug, Clone, Copy, TdfDeserialize, TdfTyped)] +#[repr(u8)] +pub enum UpnpStatus { + /// Upnp status unknown. + #[default] + #[tdf(default)] + Unknown = 0, + /// Upnp found, but not fully working. + Found = 1, + /// Upnp is enabled (found and port mapping added). + Enabled = 2, +} + +/// Contains UPnP data such as status flags, device info, etc. +#[derive(TdfDeserialize)] +pub struct SetClientMetricsRequest { + /// pnp Blaze status flags. + #[tdf(tag = "UBFL", into = u16)] + pub blaze_flags: UpnpFlags, + + /// Upnp device info. + #[tdf(tag = "UDEV")] + pub device_info: String, + + /// Upnp status flags. + #[tdf(tag = "UFLG")] + pub flags: u16, + + /// Upnp last result code. + #[tdf(tag = "ULRC")] + pub last_result_code: i32, + + /// Upnp metrics report NAT type. + #[tdf(tag = "UNAT")] + pub nat_type: u16, + + /// Upnp status. + #[tdf(tag = "USTA")] + pub status: UpnpStatus, + + /// WAN IP address + #[tdf(tag = "UWAN", into = u32)] + pub wan: Ipv4Addr, +} diff --git a/src/session/packet.rs b/src/session/packet.rs index a7f8b8bf..5f88c89e 100644 --- a/src/session/packet.rs +++ b/src/session/packet.rs @@ -62,6 +62,17 @@ pub struct FireFrame { pub seq: u16, } +/// Represents a frame thats been partially decoded but is +/// still waiting on more data +pub struct PartialFrame { + /// The length of the frame bytes + length: usize, + // Whether the jump length still needs to be read + need_jumbo: bool, + // The initial frame heading + frame: FireFrame, +} + impl FireFrame { const MIN_HEADER_SIZE: usize = 12; const JUMBO_SIZE: usize = std::mem::size_of::(); @@ -120,7 +131,9 @@ impl FireFrame { pub fn write(&self, dst: &mut BytesMut, length: usize) { let mut options = self.options; - if length > 0xFFFF { + + // If the length cannot be represented by a u16 then the frame is a jumbo frame + if length > u16::MAX as usize { options |= PacketOptions::JUMBO_FRAME; } @@ -138,39 +151,26 @@ impl FireFrame { } } - pub fn read(src: &mut BytesMut) -> Option<(FireFrame, usize)> { - if src.len() < Self::MIN_HEADER_SIZE { - return None; - } - - let mut length = src.get_u16() as usize; + /// Reads the initial header portion of the frame returning both the + /// frame itself and the length of the frames contents + pub fn read(src: &mut BytesMut) -> FireFrame { let component = src.get_u16(); let command = src.get_u16(); let error = src.get_u16(); let ty = src.get_u8() >> 4; + let ty = FrameType::from(ty); let options = src.get_u8() >> 4; let options = PacketOptions::from_bits_retain(options); let seq = src.get_u16(); - if options.contains(PacketOptions::JUMBO_FRAME) { - // We need another two bytes for the extended length - if src.len() < Self::JUMBO_SIZE { - return None; - } - let ext_length = (src.get_u16() as usize) << 16; - length |= ext_length; - } - - let ty = FrameType::from(ty); - let header = FireFrame { + FireFrame { component, command, error, ty, options, seq, - }; - Some((header, length)) + } } } @@ -285,44 +285,76 @@ impl Packet { let mut r = TdfDeserializer::new(&self.contents); V::deserialize(&mut r) } - - pub fn read(src: &mut BytesMut) -> Option { - let (frame, length) = FireFrame::read(src)?; - - if src.len() < length { - return None; - } - - let contents = src.split_to(length); - Some(Self { - frame, - contents: contents.freeze(), - }) - } - - pub fn write(&self, dst: &mut BytesMut) { - let contents = &self.contents; - self.frame.write(dst, contents.len()); - dst.extend_from_slice(contents); - } } /// Tokio codec for encoding and decoding packets -pub struct PacketCodec; +#[derive(Default)] +pub struct PacketCodec { + /// The current partially decoded frame + partial: Option, +} impl Decoder for PacketCodec { type Error = io::Error; type Item = Packet; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - let mut read_src = src.clone(); - let result = Packet::read(&mut read_src); + let partial = match self.partial.as_mut() { + // We are already processing a partial frame + Some(value) => value, + // We need to start processing a frame + None => { + // Don't attempt reading unless we have atleast the required header length + if src.len() < FireFrame::MIN_HEADER_SIZE { + return Ok(None); + } + + // Read the length bytes + let length = src.get_u16() as usize; + // Read the inital frame + let frame = FireFrame::read(src); + // Whether the length needs the jumbo frame to be loaded + let need_jumbo = frame.options.contains(PacketOptions::JUMBO_FRAME); + + self.partial.insert(PartialFrame { + length, + need_jumbo, + frame, + }) + } + }; + + if partial.need_jumbo { + // We need another two bytes for the extended length + if src.len() < FireFrame::JUMBO_SIZE { + return Ok(None); + } - if result.is_some() { - *src = read_src; + let ext_length = (src.get_u16() as usize) << 16; + + // Extend the frame length with the new value + partial.length |= ext_length; + + // We no longer need the jumbo frame bytes + partial.need_jumbo = false; + } + + // We don't have enough bytes for the content yet + if src.len() < partial.length { + return Ok(None); } - Ok(result) + let partial = self + .partial + .take() + .expect("Current frame partial was missing"); + + let contents = src.split_to(partial.length); + + Ok(Some(Packet { + contents: contents.freeze(), + frame: partial.frame, + })) } } @@ -330,7 +362,10 @@ impl Encoder for PacketCodec { type Error = io::Error; fn encode(&mut self, item: Packet, dst: &mut BytesMut) -> Result<(), Self::Error> { - item.write(dst); + let contents = &item.contents; + item.frame.write(dst, contents.len()); + dst.extend_from_slice(contents); + Ok(()) } } diff --git a/src/session/router.rs b/src/session/router.rs index bbfeac1a..4f526cd7 100644 --- a/src/session/router.rs +++ b/src/session/router.rs @@ -1,11 +1,7 @@ //! Router implementation for routing packet components to different functions //! and automatically decoding the packet contents to the function type -use super::{ - models::errors::BlazeError, - packet::{FireFrame, Packet}, - SessionLink, -}; +use super::{models::errors::BlazeError, packet::Packet, SessionLink}; use crate::{ database::entities::Player, services::game::GamePlayer, @@ -17,7 +13,7 @@ use crate::{ }; use bytes::Bytes; use futures_util::future::BoxFuture; -use log::error; +use log::{debug, error}; use std::{ any::{Any, TypeId}, convert::Infallible, @@ -26,7 +22,7 @@ use std::{ marker::PhantomData, sync::Arc, }; -use tdf::{serialize_vec, TdfDeserialize, TdfDeserializer, TdfSerialize}; +use tdf::{serialize_vec, TdfDeserialize, TdfSerialize}; pub trait Handler: Send + Sync + 'static { fn handle(&self, req: PacketRequest) -> BoxFuture<'_, Packet>; @@ -64,12 +60,17 @@ where pub struct PacketRequest { pub state: SessionLink, pub packet: Packet, - pub extensions: Arc, + pub extensions: Extensions, } -impl PacketRequest { - pub fn extension(&self) -> Option<&T> { - self.extensions +#[derive(Clone)] +pub struct Extensions { + inner: Arc, +} + +impl Extensions { + pub fn get(&self) -> Option<&T> { + self.inner .get(&TypeId::of::()) .and_then(|boxed| (&**boxed as &(dyn Any + 'static)).downcast_ref()) } @@ -120,7 +121,9 @@ impl BlazeRouterBuilder { pub fn build(self) -> Arc { Arc::new(BlazeRouter { routes: self.routes, - extensions: Arc::new(self.extensions), + extensions: Extensions { + inner: Arc::new(self.extensions), + }, }) } } @@ -128,28 +131,29 @@ impl BlazeRouterBuilder { pub struct BlazeRouter { /// Map for looking up a route based on the component key routes: RouteMap, - extensions: Arc, + pub extensions: Extensions, } impl BlazeRouter { - pub fn handle( - &self, - state: SessionLink, - packet: Packet, - ) -> Result, Packet> { - let route = match self + pub fn handle(&self, state: SessionLink, packet: Packet) -> BoxFuture<'_, Packet> { + match self .routes .get(&component_key(packet.frame.component, packet.frame.command)) { - Some(value) => value, - None => return Err(packet), - }; - - Ok(route.handle(PacketRequest { - state, - packet, - extensions: self.extensions.clone(), - })) + Some(route) => route.handle(PacketRequest { + state, + packet, + extensions: self.extensions.clone(), + }), + // Respond with a default empty packet + None => { + debug!( + "Missing packet handler for {:#06x}->{:#06x}", + packet.frame.component, packet.frame.command + ); + Box::pin(ready(Packet::response_empty(&packet))) + } + } } } @@ -167,16 +171,6 @@ pub trait FromPacketRequest: Sized { /// serialization [IntoPacketResponse] for TDF contents pub struct Blaze(pub V); -/// Wrapper for providing deserialization [FromPacketRequest] and -/// serialization [IntoPacketResponse] for TDF contents -/// -/// Stores the packet header so that it can be used for generating -/// responses -pub struct BlazeWithHeader { - pub req: V, - pub frame: FireFrame, -} - /// [Blaze] tdf type for contents that have already been /// serialized ahead of time pub struct RawBlaze(Bytes); @@ -200,7 +194,8 @@ where Self: 'a, { Box::pin(ready( - req.extension() + req.extensions + .get() .ok_or_else(|| { error!( "Attempted to extract missing extension {}", @@ -224,12 +219,13 @@ impl FromPacketRequest for GamePlayer { Self: 'a, { Box::pin(async move { - let data = &*req.state.data.read().await; + let data = &*req.state.data.lock(); let data = data.as_ref().ok_or(GlobalError::AuthenticationRequired)?; Ok(GamePlayer::new( data.player.clone(), data.net.clone(), - req.state.clone(), + Arc::downgrade(&req.state), + req.state.notify_handle(), )) }) } @@ -245,7 +241,7 @@ impl FromPacketRequest for SessionAuth { Self: 'a, { Box::pin(async move { - let data = &*req.state.data.read().await; + let data = &*req.state.data.lock(); let data = data.as_ref().ok_or(GlobalError::AuthenticationRequired)?; let player = data.player.clone(); Ok(SessionAuth(player)) @@ -288,46 +284,6 @@ where } } -impl BlazeWithHeader { - pub fn response(&self, res: E) -> Packet - where - E: TdfSerialize, - { - Packet { - frame: self.frame.response(), - contents: Bytes::from(serialize_vec(&res)), - } - } -} - -impl FromPacketRequest for BlazeWithHeader -where - for<'a> V: TdfDeserialize<'a> + Send + 'a, -{ - type Rejection = BlazeError; - - fn from_packet_request<'a>( - req: &'a mut PacketRequest, - ) -> BoxFuture<'a, Result> - where - Self: 'a, - { - let mut r = TdfDeserializer::new(&req.packet.contents); - - Box::pin(ready( - V::deserialize(&mut r) - .map(|value| BlazeWithHeader { - req: value, - frame: req.packet.frame.clone(), - }) - .map_err(|err| { - error!("Error while decoding packet: {:?}", err); - GlobalError::System.into() - }), - )) - } -} - impl FromPacketRequest for SessionLink { type Rejection = Infallible; diff --git a/src/session/routes/auth.rs b/src/session/routes/auth.rs index 32b703aa..e265711f 100644 --- a/src/session/routes/auth.rs +++ b/src/session/routes/auth.rs @@ -49,8 +49,8 @@ pub async fn handle_login( // Update the session stored player - let player = session.set_player(player).await; - sessions.add_session(player.id, session).await; + let player = session.set_player(player); + sessions.add_session(player.id, Arc::downgrade(&session)); let session_token: String = sessions.create_token(player.id); @@ -78,8 +78,8 @@ pub async fn handle_silent_login( .ok_or(AuthenticationError::InvalidToken)?; // Update the session stored player - let player = session.set_player(player).await; - sessions.add_session(player.id, session).await; + let player = session.set_player(player); + sessions.add_session(player.id, Arc::downgrade(&session)); Ok(Blaze(AuthResponse { player, @@ -108,8 +108,8 @@ pub async fn handle_origin_login( })?; // Update the session stored player - let player = session.set_player(player).await; - sessions.add_session(player.id, session).await; + let player = session.set_player(player); + sessions.add_session(player.id, Arc::downgrade(&session)); let session_token: String = sessions.create_token(player.id); @@ -133,8 +133,8 @@ pub async fn handle_logout( SessionAuth(player): SessionAuth, Extension(sessions): Extension>, ) { - session.clear_player().await; - sessions.remove_session(player.id).await; + session.clear_player(); + sessions.remove_session(player.id); } // Skip formatting these entitlement creations @@ -309,8 +309,8 @@ pub async fn handle_create_account( let player: Player = Player::create(&db, email, display_name, Some(hashed_password), &config).await?; - let player = session.set_player(player).await; - sessions.add_session(player.id, session).await; + let player = session.set_player(player); + sessions.add_session(player.id, Arc::downgrade(&session)); let session_token = sessions.create_token(player.id); diff --git a/src/session/routes/game_manager.rs b/src/session/routes/game_manager.rs index 868b0028..34a2f4a5 100644 --- a/src/session/routes/game_manager.rs +++ b/src/session/routes/game_manager.rs @@ -17,20 +17,19 @@ use std::sync::Arc; pub async fn handle_join_game( player: GamePlayer, + session: SessionLink, Blaze(JoinGameRequest { user }): Blaze, Extension(sessions): Extension>, Extension(game_manager): Extension>, ) -> ServerResult> { // Lookup the session join target - let session = sessions + let other_session = sessions .lookup_session(user.id) - .await .ok_or(GameManagerError::JoinPlayerFailed)?; // Find the game ID for the target session - let (game_id, game_ref) = session + let (game_id, game_ref) = other_session .get_game() - .await .ok_or(GameManagerError::InvalidGameId)?; // Check the game is joinable @@ -39,27 +38,28 @@ pub async fn handle_join_game( game.joinable_state(None) }; - // Join the game - if let GameJoinableState::Joinable = join_state { - debug!("Joining game from invite (GID: {})", game_id); - - game_manager - .add_to_game( - game_ref, - player, - GameSetupContext::Dataless { - context: DatalessContext::JoinGameSetup, - }, - ) - .await; - - Ok(Blaze(JoinGameResponse { - game_id, - state: JoinGameState::JoinedGame, - })) - } else { - Err(GameManagerError::GameFull.into()) + if !matches!(join_state, GameJoinableState::Joinable) { + return Err(GameManagerError::GameFull.into()); } + + // Join the game + debug!("Joining game from invite (GID: {})", game_id); + + game_manager + .add_to_game( + game_ref, + player, + session, + GameSetupContext::Dataless { + context: DatalessContext::JoinGameSetup, + }, + ) + .await; + + Ok(Blaze(JoinGameResponse { + game_id, + state: JoinGameState::JoinedGame, + })) } pub async fn handle_get_game_data( @@ -73,7 +73,7 @@ pub async fn handle_get_game_data( .ok_or(GameManagerError::InvalidGameId)?; let game = &*game.read().await; - let body = game.game_data().await; + let body = game.game_data(); Ok(body) } @@ -131,6 +131,7 @@ pub async fn handle_get_game_data( /// ``` pub async fn handle_create_game( player: GamePlayer, + session: SessionLink, Extension(game_manager): Extension>, Blaze(CreateGameRequest { attributes, @@ -140,20 +141,24 @@ pub async fn handle_create_game( let (link, game_id) = game_manager.create_game(attributes, setting).await; // Notify matchmaking of the new game - tokio::spawn(async move { - game_manager - .add_to_game( - link.clone(), - player, - GameSetupContext::Dataless { - context: DatalessContext::CreateGameSetup, - }, - ) - .await; - - // Update matchmaking with the new game - game_manager.process_queue(link, game_id).await; - }); + let mut player = player; + + // Player is the host player (They are connected by default) + player.state = PlayerState::ActiveConnected; + + game_manager + .add_to_game( + link.clone(), + player, + session, + GameSetupContext::Dataless { + context: DatalessContext::CreateGameSetup, + }, + ) + .await; + + // Update matchmaking with the new game + game_manager.process_queue(link, game_id).await; Ok(Blaze(CreateGameResponse { game_id })) } @@ -196,15 +201,13 @@ pub async fn handle_set_attributes( } // Update matchmaking for the changed game - tokio::spawn(async move { - let join_state = { - let game = &*link.read().await; - game.joinable_state(None) - }; - if let GameJoinableState::Joinable = join_state { - game_manager.process_queue(link, game_id).await; - } - }); + let join_state = { + let game = &*link.read().await; + game.joinable_state(None) + }; + if let GameJoinableState::Joinable = join_state { + game_manager.process_queue(link, game_id).await; + } Ok(()) } @@ -293,6 +296,8 @@ pub async fn handle_remove_player( /// Handles updating mesh connections /// +/// Only sent by the host player (I think) +/// /// ``` /// Route: GameManager(UpdateMeshConnection) /// ID: 147 @@ -315,9 +320,8 @@ pub async fn handle_update_mesh_connection( mut targets, }): Blaze, ) -> ServerResult<()> { - let target = match targets.pop() { - Some(value) => value, - None => return Ok(()), + let Some(target) = targets.pop() else { + return Ok(()); }; let link = game_manager @@ -326,7 +330,31 @@ pub async fn handle_update_mesh_connection( .ok_or(GameManagerError::InvalidGameId)?; let game = &mut *link.write().await; - game.update_mesh(player.id, target.player_id, target.state); + + // Ensure the host is the one making the change + if game.is_host_player(player.id) { + game.update_mesh(target.player_id, target.status); + } + + Ok(()) +} + +pub async fn handle_add_admin_player( + SessionAuth(player): SessionAuth, + Extension(game_manager): Extension>, + Blaze(AddAdminPlayerRequest { game_id, player_id }): Blaze, +) -> ServerResult<()> { + let link = game_manager + .get_game(game_id) + .await + .ok_or(GameManagerError::InvalidGameId)?; + + let game = &mut *link.write().await; + + // Ensure the host is the one making the change + if game.is_host_player(player.id) { + game.add_admin_player(player_id); + } Ok(()) } @@ -457,19 +485,18 @@ pub async fn handle_start_matchmaking( Extension(game_manager): Extension>, Blaze(MatchmakingRequest { rules }): Blaze, ) -> ServerResult> { - let session_id = player.player.id; + let player_id = player.player.id; info!("Player {} started matchmaking", player.player.display_name); - tokio::spawn(async move { - let rule_set = Arc::new(rules); - // If adding failed attempt to queue instead - if let Err(player) = game_manager.try_add(player, &rule_set).await { - game_manager.queue(player, rule_set).await; - } - }); + let rule_set = Arc::new(rules); + + // If adding failed attempt to queue instead + if let Err(player) = game_manager.try_add(player, &rule_set).await { + game_manager.queue(player, rule_set).await; + } - Ok(Blaze(MatchmakingResponse { id: session_id })) + Ok(Blaze(MatchmakingResponse { id: player_id })) } /// Handles cancelling matchmaking for the current session removing @@ -487,6 +514,6 @@ pub async fn handle_cancel_matchmaking( SessionAuth(player): SessionAuth, Extension(game_manager): Extension>, ) { - session.remove_from_game().await; + session.remove_from_game(); game_manager.remove_queue(player.id).await; } diff --git a/src/session/routes/messaging.rs b/src/session/routes/messaging.rs index 0297d4f3..172c5257 100644 --- a/src/session/routes/messaging.rs +++ b/src/session/routes/messaging.rs @@ -51,6 +51,6 @@ pub async fn handle_fetch_messages( }, ); - session.push(notify); + session.notify_handle().notify(notify); Blaze(FetchMessageResponse { count: 1 }) } diff --git a/src/session/routes/mod.rs b/src/session/routes/mod.rs index e93295eb..dbdabe0f 100644 --- a/src/session/routes/mod.rs +++ b/src/session/routes/mod.rs @@ -51,6 +51,7 @@ pub fn router() -> BlazeRouterBuilder { builder.route(g::COMPONENT, g::SET_GAME_ATTRIBUTES, handle_set_attributes); builder.route(g::COMPONENT, g::REMOVE_PLAYER, handle_remove_player); builder.route(g::COMPONENT, g::UPDATE_MESH_CONNECTION,handle_update_mesh_connection); + builder.route(g::COMPONENT, g::ADD_ADMIN_PLAYER,handle_add_admin_player); builder.route(g::COMPONENT, g::START_MATCHMAKING,handle_start_matchmaking); builder.route(g::COMPONENT, g::CANCEL_MATCHMAKING,handle_cancel_matchmaking); builder.route(g::COMPONENT, g::GET_GAME_DATA_FROM_ID, handle_get_game_data); @@ -84,6 +85,7 @@ pub fn router() -> BlazeRouterBuilder { builder.route(u::COMPONENT, u::GET_TELEMETRY_SERVER, handle_get_telemetry_server); builder.route(u::COMPONENT, u::GET_TICKER_SERVER, handle_get_ticker_server); builder.route(u::COMPONENT, u::USER_SETTINGS_LOAD_ALL, handle_load_settings); + builder.route(u::COMPONENT, u::SET_CLIENT_METRICS, handle_set_client_metrics); } // Messaging diff --git a/src/session/routes/other.rs b/src/session/routes/other.rs index 474b9385..7e6e9a31 100644 --- a/src/session/routes/other.rs +++ b/src/session/routes/other.rs @@ -1,9 +1,20 @@ +use log::error; +use sea_orm::DatabaseConnection; +use tokio::try_join; + use crate::{ - session::{models::other::*, packet::Packet, router::Blaze, SessionLink}, + database::entities::{leaderboard_data::LeaderboardType, LeaderboardData}, + session::{ + models::{other::*, stats::SubmitGameReportRequest}, + packet::Packet, + router::{Blaze, Extension, SessionAuth}, + SessionLink, + }, utils::components::game_reporting, }; -/// Handles submission of offline game reports from clients. +/// Handles submission of offline game reports from clients. This contains +/// the new leaderboard information for the player /// /// ``` /// Route: GameReporting(SubmitOfflineGameReport) @@ -12,14 +23,14 @@ use crate::{ /// "FNSH": 0, /// "PRVT": VarList [], /// "RPVT": { -/// "GAME": VarList [1], +/// "GAME": VarList [1 /* Game ID */], /// "GAME": { /// "GAME": {}, /// "PLYR": Map { -/// 1: { -/// "CTRY": 16725, -/// "NCHP": 0, -/// "NRAT": 1 +/// 1 /* The player */: { +/// "CTRY": 16725, /* Player country */ +/// "NCHP": 0, /* Challenge points */ +/// "NRAT": 1 /* N7 Rating */ /// } /// } /// } @@ -28,8 +39,32 @@ use crate::{ /// "GTYP": "massEffectReport" /// } /// ``` -pub async fn handle_submit_offline(session: SessionLink) { - session.push(Packet::notify( +pub async fn handle_submit_offline( + session: SessionLink, + SessionAuth(_): SessionAuth, + Extension(db): Extension, + Blaze(SubmitGameReportRequest { report }): Blaze, +) { + let game = report.game; + let players = game.players; + + let n7_data = players + .iter() + .map(|(player_id, player_data)| (*player_id, player_data.n7_rating)); + let cp_data = players + .iter() + .map(|(player_id, player_data)| (*player_id, player_data.challenge_points)); + + if let Err(err) = try_join!( + LeaderboardData::set_ty_bulk(&db, LeaderboardType::N7Rating, n7_data), + LeaderboardData::set_ty_bulk(&db, LeaderboardType::ChallengePoints, cp_data), + ) { + // TODO: Handle failed to update leaderboards + error!("Failed to update leaderboards: {}", err); + return; + } + + session.notify_handle().notify(Packet::notify( game_reporting::COMPONENT, game_reporting::GAME_REPORT_SUBMITTED, GameReportResponse, diff --git a/src/session/routes/stats.rs b/src/session/routes/stats.rs index 19c63686..400e56b7 100644 --- a/src/session/routes/stats.rs +++ b/src/session/routes/stats.rs @@ -1,49 +1,43 @@ use crate::{ - services::leaderboard::Leaderboard, + database::entities::LeaderboardData, session::{ models::stats::*, - packet::Packet, - router::{Blaze, BlazeWithHeader, Extension}, + router::{Blaze, Extension}, }, }; use sea_orm::DatabaseConnection; -use std::sync::Arc; pub async fn handle_normal_leaderboard( - Extension(leaderboard): Extension>, Extension(db): Extension, - req: BlazeWithHeader, -) -> Packet { - let query = &req.req; - let group = leaderboard.query(query.name, &db).await; - let slice = group - .get_normal(query.start, query.count) + Blaze(query): Blaze, +) -> Blaze { + let values = LeaderboardData::get_offset(&db, query.name, query.start, query.count) + .await .unwrap_or_default(); - req.response(LeaderboardResponse::Borrowed(slice)) + Blaze(LeaderboardResponse { values }) } pub async fn handle_centered_leaderboard( - Extension(leaderboard): Extension>, Extension(db): Extension, - req: BlazeWithHeader, -) -> Packet { - let query = &req.req; - let group = leaderboard.query(query.name, &db).await; - let slice = group - .get_centered(query.center, query.count) + Blaze(query): Blaze, +) -> Blaze { + let values = LeaderboardData::get_centered(&db, query.name, query.center, query.count) + .await + .unwrap_or_default() .unwrap_or_default(); - req.response(LeaderboardResponse::Borrowed(slice)) + + Blaze(LeaderboardResponse { values }) } pub async fn handle_filtered_leaderboard( - Extension(leaderboard): Extension>, Extension(db): Extension, - req: BlazeWithHeader, -) -> Packet { - let query = &req.req; - let group = leaderboard.query(query.name, &db).await; - let response = group.get_filtered(&query.ids); - req.response(LeaderboardResponse::Owned(response)) + Blaze(query): Blaze, +) -> Blaze { + let values = LeaderboardData::get_filtered(&db, query.name, query.ids) + .await + .unwrap_or_default(); + + Blaze(LeaderboardResponse { values }) } /// Handles returning the number of leaderboard objects present. @@ -62,13 +56,16 @@ pub async fn handle_filtered_leaderboard( /// } /// ``` pub async fn handle_leaderboard_entity_count( - Extension(leaderboard): Extension>, Extension(db): Extension, Blaze(req): Blaze, ) -> Blaze { - let group = leaderboard.query(req.name, &db).await; - let count = group.values.len(); - Blaze(EntityCountResponse { count }) + let total = LeaderboardData::count(&db, req.name) + .await + .unwrap_or_default(); + + Blaze(EntityCountResponse { + count: total as usize, + }) } fn get_locale_name(code: &str) -> &str { diff --git a/src/session/routes/user_sessions.rs b/src/session/routes/user_sessions.rs index 7f8d1195..ff0a60e1 100644 --- a/src/session/routes/user_sessions.rs +++ b/src/session/routes/user_sessions.rs @@ -1,4 +1,5 @@ use crate::{ + config::{QosServerConfig, RuntimeConfig}, database::entities::Player, services::sessions::{Sessions, VerifyError}, session::{ @@ -38,13 +39,11 @@ pub async fn handle_lookup_user( let session = sessions .lookup_session(req.player_id) - .await .ok_or(UserSessionsError::UserNotFound)?; // Get the lookup response from the session let response = session .get_lookup() - .await .ok_or(UserSessionsError::UserNotFound)?; Ok(Blaze(response)) @@ -78,8 +77,8 @@ pub async fn handle_resume_session( .await? .ok_or(AuthenticationError::InvalidToken)?; - let player = session.set_player(player).await; - sessions.add_session(player.id, session).await; + let player = session.set_player(player); + sessions.add_session(player.id, Arc::downgrade(&session)); Ok(Blaze(AuthResponse { player, @@ -119,23 +118,47 @@ pub async fn handle_resume_session( /// ``` pub async fn handle_update_network( session: SessionLink, - Blaze(UpdateNetworkRequest { mut address, qos }): Blaze, + Extension(config): Extension>, + Blaze(UpdateNetworkRequest { + mut address, + qos, + ping_site_latency, + }): Blaze, ) { - // TODO: This won't be required after QoS servers are correctly functioning - if let NetworkAddress::AddressPair(pair) = &mut address { - let ext = &mut pair.external; + match &config.qos { + QosServerConfig::Disabled => {} + // Hamachi should override local addresses + QosServerConfig::Hamachi { host } => { + // TODO: This won't be required after QoS servers are correctly functioning + if let NetworkAddress::AddressPair(pair) = &mut address { + let int = &mut pair.internal; - // If address is missing - if ext.addr.is_unspecified() { - // Replace address with new address and port with same as local port - ext.addr = session.addr; - ext.port = pair.internal.port; + if session.addr.is_loopback() { + int.addr = *host; + } else { + int.addr = session.addr; + } + } + } + + _ => { + // TODO: This won't be required after QoS servers are correctly functioning + if let NetworkAddress::AddressPair(pair) = &mut address { + let ext = &mut pair.external; + + // If address is missing + if ext.addr.is_unspecified() { + // Replace address with new address and port with same as local port + ext.addr = session.addr; + ext.port = pair.internal.port; + } + } } } - tokio::spawn(async move { - session.set_network_info(address, qos).await; - }); + let ping_site_latency: Vec = ping_site_latency.values().copied().collect(); + + session.set_network_info(address, qos, ping_site_latency); } /// Handles updating the stored hardware flag with the client provided hardware flag @@ -151,7 +174,5 @@ pub async fn handle_update_hardware_flag( session: SessionLink, Blaze(UpdateHardwareFlagsRequest { hardware_flags }): Blaze, ) { - tokio::spawn(async move { - session.set_hardware_flags(hardware_flags).await; - }); + session.set_hardware_flags(hardware_flags); } diff --git a/src/session/routes/util.rs b/src/session/routes/util.rs index 638855d7..e61a7d1d 100644 --- a/src/session/routes/util.rs +++ b/src/session/routes/util.rs @@ -5,6 +5,7 @@ use crate::{ models::{ errors::{BlazeError, GlobalError, ServerResult}, util::*, + IpPairAddress, NetworkAddress, }, router::{Blaze, Extension, SessionAuth}, SessionLink, @@ -13,7 +14,7 @@ use crate::{ use base64ct::{Base64, Encoding}; use embeddy::Embedded; use flate2::{write::ZlibEncoder, Compression}; -use log::error; +use log::{debug, error}; use sea_orm::DatabaseConnection; use std::{ cmp::Ordering, @@ -99,7 +100,7 @@ pub async fn handle_post_auth( SessionAuth(player): SessionAuth, ) -> ServerResult> { // Subscribe to the session with itself - session.add_subscriber(player.id, session.clone()).await; + session.add_subscriber(player.id, session.notify_handle()); Ok(Blaze(PostAuthResponse { telemetry: TelemetryServer, @@ -524,3 +525,64 @@ pub async fn handle_load_settings( Ok(Blaze(SettingsResponse { settings })) } + +/// Handles client updating networking through Upnp changes +/// +/// ``` +/// Request (27): Util->SetClientMetrics (0x0009->0x0016) +/// Content: { +/// "UBFL": 2, +/// "UDEV": "DEVICE NAME", +/// "UFLG": 31, +/// "ULRC": 0, +/// "UNAT": 4, +/// "USTA": 2, +/// "UWAN": 0 /* WAN IP ADDRESS FROM UPNP */, +/// } +/// ``` +pub async fn handle_set_client_metrics( + session: SessionLink, + Blaze(SetClientMetricsRequest { + blaze_flags, + device_info, + flags, + nat_type, + status, + wan, + .. + }): Blaze, +) { + debug!( + "Handling UPNP (Device: {}, BlazeFlags: {:?} Flags: {:?}, NAT: {:?}, WAN: {}, STATUS: {:?})", + device_info, blaze_flags, flags, nat_type, wan, status + ); + + // Don't do anything if Upnp failed + if !matches!(status, UpnpStatus::Enabled) { + return; + } + + // Set external address using Upnp specified + if !wan.is_unspecified() && !blaze_flags.contains(UpnpFlags::DOUBLE_NAT) { + debug!("Using client Upnp WAN address override: {}", wan); + + let network_info = session.network_info().unwrap_or_default(); + let ping_site_latency = network_info.ping_site_latency.clone(); + let qos = network_info.qos; + let mut pair_addr = match &network_info.addr { + NetworkAddress::AddressPair(pair) => pair.clone(), + // Fallback handle behavior for unset or default address + _ => IpPairAddress::default(), + }; + + // Update WAN address with Upnp address + pair_addr.external.addr = wan; + + // Update network info with new details + session.set_network_info( + NetworkAddress::AddressPair(pair_addr), + qos, + ping_site_latency, + ); + } +} diff --git a/src/utils/lock.rs b/src/utils/lock.rs new file mode 100644 index 00000000..56cd0cf3 --- /dev/null +++ b/src/utils/lock.rs @@ -0,0 +1,125 @@ +use log::warn; +use std::future::Future; +use std::pin::Pin; +use std::sync::{atomic::AtomicUsize, Arc}; +use std::task::{ready, Context, Poll}; +use tokio::sync::{OwnedSemaphorePermit, Semaphore}; +use tokio_util::sync::PollSemaphore; + +/// Lock with strict ordering for permits, maintains strict +/// FIFI ordering +#[derive(Clone)] +pub struct QueueLock { + inner: Arc, +} + +impl QueueLock { + pub fn new() -> QueueLock { + let inner = QueueLockInner { + semaphore: Arc::new(Semaphore::new(1)), + next_ticket: AtomicUsize::new(1), + current_ticket: AtomicUsize::new(1), + }; + + QueueLock { + inner: Arc::new(inner), + } + } + + /// Aquire a ticket for the queue, returns a future + /// which completes when its the tickets turn to access + pub fn aquire(&self) -> TicketAquireFuture { + let ticket = self + .inner + .next_ticket + .fetch_add(1, std::sync::atomic::Ordering::AcqRel); + let poll = PollSemaphore::new(self.inner.semaphore.clone()); + + TicketAquireFuture { + inner: self.inner.clone(), + poll, + ticket, + } + } +} + +struct QueueLockInner { + /// Underlying async acquisition primitive + semaphore: Arc, + /// The next ticket to provide access to + next_ticket: AtomicUsize, + /// The current ticket allowed access + current_ticket: AtomicUsize, +} + +/// Future while waiting to aquire its lock +/// +/// TODO: If these futures are dropped early then +/// the lock wont be able to unlock, figure out how +/// to fix this..? +pub struct TicketAquireFuture { + /// The queue lock being waited on + inner: Arc, + /// Pollable semaphore + poll: PollSemaphore, + /// The ticket for this queue position + ticket: usize, +} + +impl Drop for TicketAquireFuture { + fn drop(&mut self) { + let current = self + .inner + .current_ticket + .load(std::sync::atomic::Ordering::SeqCst); + + // Ensure we are the ticket that is allowed + if current != self.ticket { + warn!("Early dropped ticket aquire {}", self.ticket); + } + } +} + +/// Guard which releases the queue lock when dropped +pub struct QueueLockGuard { + /// Acquisition permit + _permit: OwnedSemaphorePermit, + inner: Arc, +} + +impl Future for TicketAquireFuture { + type Output = QueueLockGuard; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + let permit = ready!(this.poll.poll_acquire(cx)).expect("Queue task semaphore was closed"); + + let current = this + .inner + .current_ticket + .load(std::sync::atomic::Ordering::SeqCst); + + // Ensure we are the ticket that is allowed + if current == this.ticket { + Poll::Ready(QueueLockGuard { + _permit: permit, + inner: this.inner.clone(), + }) + } else { + // Make sure this future is polled again when possible + // TODO: Is this okay to do?? (Tokio defers their version but thats internal crate access) + cx.waker().wake_by_ref(); + + Poll::Pending + } + } +} + +impl Drop for QueueLockGuard { + fn drop(&mut self) { + // Set the current ticket to the next ticket + self.inner + .current_ticket + .fetch_add(1, std::sync::atomic::Ordering::AcqRel); + } +} diff --git a/src/utils/logging.rs b/src/utils/logging.rs index 8b5d44a7..b055ae8d 100644 --- a/src/utils/logging.rs +++ b/src/utils/logging.rs @@ -1,3 +1,4 @@ +use futures_util::TryFutureExt; use log::{info, LevelFilter}; use log4rs::{ append::{console::ConsoleAppender, file::FileAppender}, @@ -100,17 +101,22 @@ pub async fn public_address() -> Option { // Try all addresses using the first valid value for address in addresses { - let response = match reqwest::get(address).await { + let addr = match reqwest::get(address) + // Read the response as text + .and_then(reqwest::Response::text) + .await + { Ok(value) => value, Err(_) => continue, }; - let ip = match response.text().await { - Ok(value) => value.trim().replace('\n', ""), - Err(_) => continue, - }; + let addr = addr + // Trim whitespace and new lines + .trim_matches(|c: char| c == '\n' || c.is_whitespace()) + // Attempt to parse as an IPv4 address + .parse::(); - if let Ok(parsed) = ip.parse() { + if let Ok(parsed) = addr { return Some(parsed); } } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index aabd5855..ef0987e3 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,5 +1,6 @@ pub mod components; pub mod hashing; +pub mod lock; pub mod logging; pub mod parsing; pub mod signing; diff --git a/src/utils/parsing.rs b/src/utils/parsing.rs index f9df6836..71ceb3e6 100644 --- a/src/utils/parsing.rs +++ b/src/utils/parsing.rs @@ -31,17 +31,6 @@ impl<'a> MEParser<'a> { next.parse::().ok() } - pub fn next_bool(&mut self) -> Option { - let next = self.next()?; - if next == "True" { - Some(true) - } else if next == "False" { - Some(false) - } else { - None - } - } - pub fn skip(&mut self, n: usize) -> Option<()> { for _ in 0..n { self.next()?; @@ -84,27 +73,6 @@ impl PlayerClass<'_> { } } -/// Structure for holding the parsed kit_name and deployed state -/// for a player character as the result of parsing -pub struct KitNameDeployed<'a> { - pub kit_name: &'a str, - pub deployed: bool, -} - -impl KitNameDeployed<'_> { - pub fn parse(value: &str) -> Option> { - let mut parser = MEParser::new(value)?; - let kit_name = parser.next()?; - - // Skip the 17 other items - parser.skip(17)?; - - let deployed: bool = parser.next_bool()?; - - Some(KitNameDeployed { kit_name, deployed }) - } -} - // Unused full format declaration for the player character data // // /// Structure for a player character model stored in the database diff --git a/src/utils/types.rs b/src/utils/types.rs index 96551ff8..1736cd5e 100644 --- a/src/utils/types.rs +++ b/src/utils/types.rs @@ -1,4 +1,3 @@ /// Types for differentiating between fields pub type PlayerID = u32; -pub type SessionID = u32; pub type GameID = u32;