From 4ffe8a434fe36789197ad5d93a3fc742db3f1e9e Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 20 Aug 2026 17:05:37 +0200 Subject: [PATCH 1/2] [Updt] Mtp 0.3.0 --- Cargo.lock | 514 +----------------------- Cargo.toml | 7 +- README.md | 13 + mtp-type-maps | 2 +- src/config.rs | 96 ++++- src/db/iota_repo.rs | 10 +- src/db/user_repo.rs | 49 +-- src/error.rs | 25 ++ src/identity.rs | 314 +++++++++++++++ src/main.rs | 69 ++-- src/models/mod.rs | 3 +- src/server/api.rs | 53 +-- src/server/middleware.rs | 27 +- src/server/mod.rs | 2 +- src/server/server.rs | 16 - src/server/short_link.rs | 2 +- src/server/web.rs | 47 +++ src/sql/connection_status.rs | 11 +- src/sql/user_online_tracker.rs | 123 ++---- src/state.rs | 60 ++- src/transport/connection.rs | 154 ++++++- src/transport/handlers/account.rs | 161 +++++--- src/transport/handlers/links.rs | 4 +- src/transport/handlers/notifications.rs | 139 ++++--- src/transport/handlers/presence.rs | 239 ++--------- src/transport/handlers/register.rs | 25 +- src/transport/handlers/states.rs | 23 +- src/transport/handlers/user_data.rs | 134 +++--- src/transport/omikron_connection.rs | 344 ++++++++++++++-- src/transport/omikron_manager.rs | 21 +- src/transport/relay_router.rs | 21 +- src/util/file_util.rs | 282 +------------ src/util/logger.rs | 21 +- 33 files changed, 1480 insertions(+), 1531 deletions(-) create mode 100644 src/identity.rs delete mode 100644 src/server/server.rs create mode 100644 src/server/web.rs diff --git a/Cargo.lock b/Cargo.lock index e71cd5f..2a4b6e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "aead" version = "0.5.2" @@ -18,17 +12,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "aes" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" -dependencies = [ - "cipher 0.5.2", - "cpubits", - "cpufeatures 0.3.0", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -212,7 +195,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", - "zeroize", ] [[package]] @@ -233,15 +215,6 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" -[[package]] -name = "bzip2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" -dependencies = [ - "libbz2-rs-sys", -] - [[package]] name = "cc" version = "1.4.3" @@ -273,7 +246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", - "cipher 0.4.4", + "cipher", "cpufeatures 0.2.17", ] @@ -296,7 +269,7 @@ checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", "chacha20 0.9.1", - "cipher 0.4.4", + "cipher", "poly1305", "zeroize", ] @@ -308,20 +281,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common 0.1.7", - "inout 0.1.4", + "inout", "zeroize", ] -[[package]] -name = "cipher" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" -dependencies = [ - "crypto-common 0.2.2", - "inout 0.2.2", -] - [[package]] name = "cmake" version = "0.1.58" @@ -359,22 +322,6 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" -[[package]] -name = "constant_time_eq" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" - -[[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" version = "0.10.1" @@ -391,12 +338,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "cpubits" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" - [[package]] name = "cpufeatures" version = "0.2.17" @@ -430,15 +371,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - [[package]] name = "crossbeam-queue" version = "0.3.13" @@ -547,12 +479,6 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" -[[package]] -name = "deflate64" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" - [[package]] name = "der" version = "0.7.10" @@ -617,7 +543,6 @@ dependencies = [ "const-oid 0.10.2", "crypto-common 0.2.2", "ctutils", - "zeroize", ] [[package]] @@ -683,15 +608,6 @@ dependencies = [ "serde", ] -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -765,16 +681,6 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "miniz_oxide", - "zlib-rs", -] - [[package]] name = "flume" version = "0.11.1" @@ -1199,22 +1105,6 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", ] [[package]] @@ -1223,23 +1113,12 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64 0.22.1", "bytes", - "futures-channel", - "futures-util", "http", "http-body", "hyper", - "ipnet", - "libc", - "percent-encoding", "pin-project-lite", - "socket2", - "system-configuration", "tokio", - "tower-service", - "tracing", - "windows-registry", ] [[package]] @@ -1365,21 +1244,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "inout" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "ipnet" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" - [[package]] name = "itoa" version = "1.0.18" @@ -1484,12 +1348,6 @@ dependencies = [ "spin", ] -[[package]] -name = "libbz2-rs-sys" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" - [[package]] name = "libc" version = "0.2.189" @@ -1551,15 +1409,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lzma-rust2" -version = "0.16.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca93e534d1142d1d0dcca6d25fe302508a5dfb40b302802904577725ea0b695b" -dependencies = [ - "sha2 0.11.0", -] - [[package]] name = "md-5" version = "0.10.6" @@ -1576,28 +1425,12 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - [[package]] name = "mio" version = "1.2.2" @@ -1665,7 +1498,7 @@ dependencies = [ [[package]] name = "mtp" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "mtp-client", "mtp-codec", @@ -1681,7 +1514,7 @@ dependencies = [ [[package]] name = "mtp-client" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "mtp-codec", "mtp-common", @@ -1694,7 +1527,7 @@ dependencies = [ [[package]] name = "mtp-codec" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "base64 0.23.1", "byteorder", @@ -1708,7 +1541,7 @@ dependencies = [ [[package]] name = "mtp-common" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "quinn", "rustls", @@ -1719,7 +1552,7 @@ dependencies = [ [[package]] name = "mtp-crypto" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "argon2", "base64 0.22.1", @@ -1742,7 +1575,7 @@ dependencies = [ [[package]] name = "mtp-files" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "mtp-crypto", "rand 0.10.2", @@ -1753,7 +1586,7 @@ dependencies = [ [[package]] name = "mtp-host" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "mtp-codec", "mtp-common", @@ -1769,7 +1602,7 @@ dependencies = [ [[package]] name = "mtp-transport" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "async-trait", "mtp-codec", @@ -1789,7 +1622,7 @@ dependencies = [ [[package]] name = "mtp-type-map" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "serde", "serde_yaml", @@ -1798,7 +1631,7 @@ dependencies = [ [[package]] name = "mtp-webserver" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "async-trait", "bytes", @@ -1922,17 +1755,15 @@ dependencies = [ "mtp", "once_cell", "rand 0.10.2", - "reqwest", "rustls", "serde", "serde_json", "sqlx", - "strum", - "strum_macros", "thiserror 2.0.20", "tokio", + "tokio-util", "uuid", - "zip", + "zeroize", ] [[package]] @@ -1993,16 +1824,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "pbkdf2" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" -dependencies = [ - "digest 0.11.3", - "hmac 0.13.0", -] - [[package]] name = "pem" version = "3.0.6" @@ -2118,12 +1939,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppmd-rust" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2306,44 +2121,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "reqwest" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" -dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures-core", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "ring" version = "0.17.14" @@ -2446,7 +2223,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ - "core-foundation 0.10.1", + "core-foundation", "core-foundation-sys", "jni", "log", @@ -2522,7 +2299,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", - "core-foundation 0.10.1", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -2623,17 +2400,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "sha1" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", -] - [[package]] name = "sha2" version = "0.10.9" @@ -2713,12 +2479,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "simd-adler32" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" - [[package]] name = "simd_cesu8" version = "1.2.0" @@ -2918,7 +2678,7 @@ dependencies = [ "rand 0.8.7", "rsa", "serde", - "sha1 0.10.7", + "sha1", "sha2 0.10.9", "smallvec", "sqlx-core", @@ -3006,24 +2766,6 @@ dependencies = [ "unicode-properties", ] -[[package]] -name = "strum" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" - -[[package]] -name = "strum_macros" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "subtle" version = "2.6.1" @@ -3052,15 +2794,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - [[package]] name = "synstructure" version = "0.13.2" @@ -3072,27 +2805,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -3140,7 +2852,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", - "js-sys", "num-conv", "powerfmt", "serde_core", @@ -3247,56 +2958,12 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", "libc", "pin-project-lite", "tokio", ] -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - [[package]] name = "tracing" version = "0.1.44" @@ -3329,18 +2996,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typed-path" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" - [[package]] name = "typenum" version = "1.20.1" @@ -3453,15 +3108,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -3487,16 +3133,6 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "wasm-bindgen-macro" version = "0.2.127" @@ -3529,16 +3165,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "web-sys" -version = "0.3.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "web-time" version = "1.1.0" @@ -3605,35 +3231,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -3982,81 +3579,8 @@ dependencies = [ "syn 3.0.3", ] -[[package]] -name = "zip" -version = "8.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" -dependencies = [ - "aes", - "bzip2", - "constant_time_eq", - "crc32fast", - "deflate64", - "flate2", - "getrandom 0.4.3", - "hmac 0.13.0", - "indexmap", - "lzma-rust2", - "memchr", - "pbkdf2", - "ppmd-rust", - "sha1 0.11.0", - "time", - "typed-path", - "zeroize", - "zopfli", - "zstd", -] - -[[package]] -name = "zlib-rs" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" - [[package]] name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - -[[package]] -name = "zopfli" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/Cargo.toml b/Cargo.toml index 28e732a..395ad06 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ edition = "2024" mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [ "crypto", "files", - "raw", "web-server", ] } @@ -19,7 +18,6 @@ dotenv = "0.15.0" http = "1" once_cell = "1.21.4" rand = "0.10.2" -reqwest = "0.13.4" rustls = { version = "0.23.42", default-features = false, features = [ "std", "tls12", @@ -27,11 +25,10 @@ rustls = { version = "0.23.42", default-features = false, features = [ "prefer-post-quantum", ] } sqlx = { version = "0.8.6", features = ["mysql", "runtime-tokio", "migrate"] } -strum = "0.28.0" -strum_macros = "0.28.0" tokio = { version = "*", features = ["full"] } +tokio-util = { version = "0.7.19", features = ["rt"] } uuid = { version = "1.24.0", features = ["v4", "v7"] } -zip = "8.6.0" thiserror = "2.0.19" serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" +zeroize = "1.9" diff --git a/README.md b/README.md index a1d4baf..2c05741 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,15 @@ # Omega The Omega is Tensamin's central Server. It maintains the centralized user Registry & manages Omikron useage. + +Omega requires the non-empty `OMEGA_IDENTITY_SECRET` environment variable at startup. On first start it creates the protected private identity at `./omega.mk` and the matching public bundle at `./omega.mpkb`. Existing identity files are loaded fail-closed, so a malformed keyring or mismatched public bundle stops startup. A missing public bundle is rebuilt from a valid private keyring without generating a new identity. + +## MTP routing contract + +Omega preserves the MTP distinction between an absent routing field and a +present zero value on the wire. For Omega request and response operations, +both forms are rejected: + +- request IDs are required and nonzero for correlation; +- authenticated sender and receiver IDs are required and nonzero; +- fields that are optional for a particular notification remain `Option` and + are not converted to zero. diff --git a/mtp-type-maps b/mtp-type-maps index 486541b..a297dcc 160000 --- a/mtp-type-maps +++ b/mtp-type-maps @@ -1 +1 @@ -Subproject commit 486541b9483356ff49ff3ec7016f87d3ecbeaa0e +Subproject commit a297dcce60bc6e84696c6a16f5fd510beb2ca643 diff --git a/src/config.rs b/src/config.rs index 66f5382..d3c7b29 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,5 @@ use std::{env, time::Duration}; +use thiserror::Error; #[derive(Clone, Debug)] pub struct RateLimitConfig { @@ -9,6 +10,26 @@ pub struct RateLimitConfig { pub transport_connections_per_ip: usize, } +#[derive(Clone, Debug)] +pub struct DispatchConfig { + pub omikron_handler_concurrency: usize, + pub global_handler_concurrency: usize, +} + +#[derive(Clone, Debug, Default)] +pub struct OmegaConfig { + pub rate_limits: RateLimitConfig, + pub dispatch: DispatchConfig, +} + +#[derive(Debug, Error)] +pub enum ConfigError { + #[error("invalid configuration value for {name}: {value}")] + InvalidValue { name: String, value: String }, + #[error("configuration value for {name} is not valid Unicode")] + InvalidUnicode { name: String }, +} + pub const fn cors_origin() -> &'static str { "*" } @@ -26,40 +47,75 @@ impl Default for RateLimitConfig { } impl RateLimitConfig { - pub fn from_env() -> Self { + pub fn from_env() -> Result { let defaults = Self::default(); - Self { - window: env_duration("RATE_LIMIT_WINDOW_SECONDS", defaults.window), - general_requests: env_usize("RATE_LIMIT_GENERAL_REQUESTS", defaults.general_requests), + Ok(Self { + window: env_duration("RATE_LIMIT_WINDOW_SECONDS", defaults.window)?, + general_requests: env_usize("RATE_LIMIT_GENERAL_REQUESTS", defaults.general_requests)?, registration_requests: env_usize( "RATE_LIMIT_REGISTRATION_REQUESTS", defaults.registration_requests, - ), + )?, transport_connections: env_usize( "RATE_LIMIT_TRANSPORT_CONNECTIONS", defaults.transport_connections, - ), + )?, transport_connections_per_ip: env_usize( "RATE_LIMIT_TRANSPORT_CONNECTIONS_PER_IP", defaults.transport_connections_per_ip, - ), + )?, + }) + } + + pub fn from_env_or_default() -> Self { + Self::from_env().unwrap_or_default() + } +} + +impl Default for DispatchConfig { + fn default() -> Self { + Self { + omikron_handler_concurrency: 32, + global_handler_concurrency: 256, } } } -fn env_usize(name: &str, fallback: usize) -> usize { - env::var(name) - .ok() - .and_then(|value| value.parse::().ok()) - .filter(|value| *value > 0) - .unwrap_or(fallback) +impl OmegaConfig { + pub fn from_env() -> Result { + let rate_limits = RateLimitConfig::from_env()?; + let defaults = DispatchConfig::default(); + Ok(Self { + rate_limits, + dispatch: DispatchConfig { + omikron_handler_concurrency: env_usize( + "OMEGA_OMIKRON_HANDLER_CONCURRENCY", + defaults.omikron_handler_concurrency, + )?, + global_handler_concurrency: env_usize( + "OMEGA_GLOBAL_HANDLER_CONCURRENCY", + defaults.global_handler_concurrency, + )?, + }, + }) + } } -fn env_duration(name: &str, fallback: Duration) -> Duration { - env::var(name) - .ok() - .and_then(|value| value.parse::().ok()) - .filter(|value| *value > 0) - .map(Duration::from_secs) - .unwrap_or(fallback) +fn env_usize(name: &str, fallback: usize) -> Result { + match env::var(name) { + Ok(value) => value + .parse::() + .ok() + .filter(|value| *value > 0) + .ok_or_else(|| ConfigError::InvalidValue { + name: name.into(), + value, + }), + Err(env::VarError::NotPresent) => Ok(fallback), + Err(env::VarError::NotUnicode(_)) => Err(ConfigError::InvalidUnicode { name: name.into() }), + } +} + +fn env_duration(name: &str, fallback: Duration) -> Result { + env_usize(name, fallback.as_secs() as usize).map(|value| Duration::from_secs(value as u64)) } diff --git a/src/db/iota_repo.rs b/src/db/iota_repo.rs index 3eedce8..fb50432 100644 --- a/src/db/iota_repo.rs +++ b/src/db/iota_repo.rs @@ -48,20 +48,12 @@ pub async fn register_complete_iota(id: IotaId, public_key: PublicKeyBundle) -> } sqlx::query("INSERT INTO iotas (id, public_key) VALUES (?, ?)") .bind(id.0) - .bind(public_key.as_bytes()) + .bind(public_key.try_as_bytes()?) .execute(&pool().await?) .await?; Ok(()) } -pub async fn change_iota_key(id: IotaId, key: PublicKeyBundle) -> Result<()> { - sqlx::query("UPDATE iotas SET public_key = ? WHERE id = ?") - .bind(key.as_bytes()) - .bind(id.0) - .execute(&pool().await?) - .await?; - Ok(()) -} pub async fn delete_iota(id: IotaId) -> Result<()> { sqlx::query("DELETE FROM iotas WHERE id = ?") .bind(id.0) diff --git a/src/db/user_repo.rs b/src/db/user_repo.rs index 79b9034..0d10d4c 100644 --- a/src/db/user_repo.rs +++ b/src/db/user_repo.rs @@ -20,7 +20,7 @@ pub async fn get_register_id() -> Result { .unwrap_or(0); let ts = timestamp as i64; - if ts >= 1 && ts <= MAX_PROTOCOL_ID { + if (1..=MAX_PROTOCOL_ID).contains(&ts) { return Ok(UserId::from(ts)); } @@ -335,15 +335,6 @@ pub async fn change_status(id: UserId, value: String) -> Result<()> { .await } -pub async fn change_presence_preference(id: UserId, value: String) -> Result<()> { - update( - id, - "UPDATE users SET presence_preference = ? WHERE id = ?", - value.into_bytes(), - ) - .await -} - pub async fn change_iota_id(id: UserId, value: Option) -> Result<()> { sqlx::query("UPDATE users SET iota_id = ? WHERE id = ?") .bind(value.map(|id| id.0)) @@ -360,14 +351,6 @@ pub async fn change_token(id: UserId, value: String) -> Result<()> { ) .await } -pub async fn delete_user(id: UserId) -> Result<()> { - sqlx::query("DELETE FROM users WHERE id = ?") - .bind(id.0) - .execute(&pool().await?) - .await?; - Ok(()) -} - /// Delete the central identity while retaining a durable instruction for the /// last hosting Iota. The pending row is intentionally independent of users: /// it must outlive the account row. @@ -422,8 +405,9 @@ pub async fn acknowledge_pending_erasure(user_id: UserId, iota_id: IotaId) -> Re } pub async fn change_keys(id: UserId, public_key: PublicKeyBundle) -> Result<()> { + let public_key = public_key.try_as_bytes()?; sqlx::query("UPDATE users SET public_key = ? WHERE id = ?") - .bind(public_key.as_bytes()) + .bind(public_key) .bind(id.0) .execute(&pool().await?) .await?; @@ -446,6 +430,7 @@ pub async fn register_complete_user( if !valid_username(&username) { return Err(OmegaError::Validation("invalid username".into())); } + let public_key_bytes = public_key.try_as_bytes()?; let mut transaction = pool().await?.begin().await?; let lease = sqlx::query( @@ -472,7 +457,7 @@ pub async fn register_complete_user( ) .bind(id.0) .bind(username.as_bytes()) - .bind(public_key.as_bytes()) + .bind(&public_key_bytes) .bind(iota_id.0) .bind(token.as_bytes()) .execute(&mut *transaction) @@ -485,20 +470,20 @@ pub async fn register_complete_user( .bind(id.0) .fetch_optional(&mut *transaction) .await?; - match existing - .map(User::try_from) - .transpose() - .map_err(OmegaError::from)? - { - Some(existing) - if existing.iota_id == Some(iota_id) + let existing_matches = match existing { + Some(existing) => { + let existing = User::try_from(existing).map_err(OmegaError::from)?; + existing.iota_id == Some(iota_id) && existing.username == username - && existing.public_key.as_bytes() == public_key.as_bytes() - && existing.token == token => - { - Ok(()) + && existing.public_key.try_as_bytes()? == public_key_bytes + && existing.token == token } - _ => Err(insert_error.into()), + None => false, + }; + if existing_matches { + Ok(()) + } else { + Err(insert_error.into()) } } }; diff --git a/src/error.rs b/src/error.rs index 9911d25..446afec 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,11 +1,34 @@ +use std::path::PathBuf; use thiserror::Error; +#[derive(Debug, Error)] +pub enum IdentityError { + #[error("identity storage error at {path}: {source}")] + Storage { + path: PathBuf, + #[source] + source: mtp::files::FileError, + }, + #[error("identity I/O error at {path}: {source}")] + Io { + path: PathBuf, + #[source] + source: std::io::Error, + }, + #[error("public key bundle at {path} does not match the private keyring")] + PublicBundleMismatch { path: PathBuf }, +} + #[derive(Debug, Error)] pub enum OmegaError { #[error("database pool is not initialized")] DatabaseNotInitialized, #[error("database error: {0}")] Database(sqlx::Error), + #[error("cryptographic error: {0}")] + Crypto(#[from] mtp::crypto::CryptoError), + #[error("identity error: {0}")] + Identity(#[from] IdentityError), #[error("invalid input: {0}")] Validation(String), #[error("resource not found")] @@ -45,6 +68,8 @@ impl OmegaError { Self::NotFound => http::StatusCode::NOT_FOUND, Self::DatabaseNotInitialized | Self::Database(_) + | Self::Crypto(_) + | Self::Identity(_) | Self::Transport(_) | Self::NotConnected | Self::NotAuthenticated diff --git a/src/identity.rs b/src/identity.rs new file mode 100644 index 0000000..a7a831d --- /dev/null +++ b/src/identity.rs @@ -0,0 +1,314 @@ +use crate::error::{IdentityError, Result}; +use mtp::crypto::{Keyring, PublicKeyBundle}; +use mtp::files::{ + FileError, load_keyring, load_public_key_bundle, save_keyring, save_public_key_bundle, +}; +use std::{ + fs, + path::{Path, PathBuf}, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, +}; + +pub const KEYRING_PATH: &str = "./omega.mk"; +pub const PUBLIC_KEY_PATH: &str = "./omega.mpkb"; + +pub struct OmegaIdentity { + keyring: Arc, +} + +static PUBLIC_BUNDLE_TEMP_COUNTER: AtomicU64 = AtomicU64::new(0); + +impl OmegaIdentity { + pub fn load_or_create(passphrase: &[u8]) -> Result { + Self::load_or_create_at( + Path::new(KEYRING_PATH), + Path::new(PUBLIC_KEY_PATH), + passphrase, + ) + } + + pub(crate) fn load_or_create_at( + keyring_path: impl AsRef, + public_key_path: impl AsRef, + passphrase: &[u8], + ) -> Result { + let keyring_path = keyring_path.as_ref(); + let public_key_path = public_key_path.as_ref(); + let keyring = match load_keyring(keyring_path, passphrase) { + Ok(keyring) => keyring, + Err(FileError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => { + return Self::create_at(keyring_path, public_key_path, passphrase); + } + Err(error) => { + return Err(IdentityError::Storage { + path: keyring_path.to_path_buf(), + source: error, + } + .into()); + } + }; + + let identity = Self { + keyring: Arc::new(keyring), + }; + match load_public_key_bundle(public_key_path) { + Ok(persisted_bundle) => { + identity.verify_public_bundle(public_key_path, &persisted_bundle)? + } + Err(FileError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => { + Self::persist_public_bundle(&identity.public_key_bundle(), public_key_path)?; + } + Err(error) => { + return Err(IdentityError::Storage { + path: public_key_path.to_path_buf(), + source: error, + } + .into()); + } + } + Ok(identity) + } + + fn create_at(keyring_path: &Path, public_key_path: &Path, passphrase: &[u8]) -> Result { + let keyring = Keyring::generate(); + save_keyring(&keyring, keyring_path, passphrase).map_err(|error| { + IdentityError::Storage { + path: keyring_path.to_path_buf(), + source: error, + } + })?; + Self::persist_public_bundle(&keyring.public_key_bundle(), public_key_path)?; + Ok(Self { + keyring: Arc::new(keyring), + }) + } + + fn persist_public_bundle(bundle: &PublicKeyBundle, path: &Path) -> Result<()> { + let temporary_path = temporary_path(path)?; + let result = save_public_key_bundle(bundle, &temporary_path).map_err(|source| { + IdentityError::Storage { + path: path.to_path_buf(), + source, + } + }); + if let Err(error) = result { + let _ = fs::remove_file(&temporary_path); + return Err(error.into()); + } + if let Err(source) = fs::File::open(&temporary_path).and_then(|file| file.sync_all()) { + let _ = fs::remove_file(&temporary_path); + return Err(IdentityError::Io { + path: path.to_path_buf(), + source, + } + .into()); + } + if let Err(source) = fs::rename(&temporary_path, path) { + let _ = fs::remove_file(&temporary_path); + return Err(IdentityError::Io { + path: path.to_path_buf(), + source, + } + .into()); + } + Ok(()) + } + + fn verify_public_bundle(&self, path: &Path, persisted_bundle: &PublicKeyBundle) -> Result<()> { + let expected = self.keyring.public_key_bundle().try_as_bytes()?; + let actual = persisted_bundle.try_as_bytes()?; + if expected != actual { + return Err(IdentityError::PublicBundleMismatch { + path: path.to_path_buf(), + } + .into()); + } + Ok(()) + } + + pub fn keyring(&self) -> &Keyring { + &self.keyring + } + + pub fn public_key_bundle(&self) -> PublicKeyBundle { + self.keyring().public_key_bundle() + } + + pub fn clone_keyring(&self) -> Result { + let bytes = self.keyring.try_to_bytes()?; + Ok(Keyring::from_bytes(&bytes)?) + } + + #[cfg(test)] + pub(crate) fn from_keyring(keyring: Keyring) -> Self { + Self { + keyring: Arc::new(keyring), + } + } +} + +fn temporary_path(path: &Path) -> Result { + let parent = path.parent().unwrap_or_else(|| Path::new(".")); + let file_name = path.file_name().ok_or_else(|| IdentityError::Io { + path: path.to_path_buf(), + source: std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "identity path has no file name", + ), + })?; + let mut temporary_name = file_name.to_os_string(); + temporary_name.push(format!( + ".tmp-{}-{}", + std::process::id(), + PUBLIC_BUNDLE_TEMP_COUNTER.fetch_add(1, Ordering::Relaxed) + )); + Ok(parent.join(temporary_name)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::sync::atomic::{AtomicU64, Ordering}; + + fn test_directory() -> std::path::PathBuf { + static COUNTER: AtomicU64 = AtomicU64::new(0); + let id = COUNTER.fetch_add(1, Ordering::Relaxed); + let path = + std::env::temp_dir().join(format!("omega-identity-test-{}-{id}", std::process::id())); + fs::create_dir_all(&path).expect("create test directory"); + path + } + + #[test] + fn generated_identity_survives_restart_with_separate_file_formats() { + let directory = test_directory(); + let keyring_path = directory.join("omega.mk"); + let public_key_path = directory.join("omega.mpkb"); + let passphrase = b"test-passphrase"; + + let first = OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path, passphrase) + .expect("create identity"); + let first_bundle = first.public_key_bundle().try_as_bytes().expect("bundle"); + + let keyring_bytes = fs::read(&keyring_path).expect("read keyring"); + let bundle_bytes = fs::read(&public_key_path).expect("read bundle"); + assert_eq!(&keyring_bytes[..4], b"MTMK"); + assert_eq!(&bundle_bytes[..4], b"MPKB"); + + let restarted = + OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path, passphrase) + .expect("reload identity"); + assert_eq!( + restarted + .public_key_bundle() + .try_as_bytes() + .expect("bundle"), + first_bundle + ); + assert_eq!( + load_public_key_bundle(&public_key_path) + .expect("load public bundle") + .try_as_bytes() + .expect("bundle"), + first_bundle + ); + + fs::remove_dir_all(directory).expect("remove test directory"); + } + + #[test] + fn invalid_existing_keyring_does_not_create_a_new_identity() { + let directory = test_directory(); + let keyring_path = directory.join("omega.mk"); + let public_key_path = directory.join("omega.mpkb"); + fs::write(&keyring_path, b"not-a-keyring").expect("write invalid keyring"); + + let result = + OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path, b"test-passphrase"); + assert!(result.is_err()); + assert!(!public_key_path.exists()); + + fs::remove_dir_all(directory).expect("remove test directory"); + } + + #[test] + fn missing_public_bundle_is_repaired_without_changing_keyring() { + let directory = test_directory(); + let keyring_path = directory.join("omega.mk"); + let public_key_path = directory.join("omega.mpkb"); + let passphrase = b"test-passphrase"; + + OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path, passphrase) + .expect("create identity"); + let original_keyring = fs::read(&keyring_path).expect("read keyring"); + fs::remove_file(&public_key_path).expect("remove bundle"); + + let repaired = + OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path, passphrase) + .expect("repair bundle"); + + assert_eq!( + fs::read(&keyring_path).expect("read keyring"), + original_keyring + ); + assert_eq!( + repaired.public_key_bundle().try_as_bytes().expect("bundle"), + load_public_key_bundle(&public_key_path) + .expect("load bundle") + .try_as_bytes() + .expect("bundle") + ); + + fs::remove_dir_all(directory).expect("remove test directory"); + } + + #[test] + fn mismatched_public_bundle_stops_startup() { + let directory = test_directory(); + let keyring_path = directory.join("omega.mk"); + let public_key_path = directory.join("omega.mpkb"); + let passphrase = b"test-passphrase"; + + OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path, passphrase) + .expect("create identity"); + let other_keyring = Keyring::generate(); + save_public_key_bundle(&other_keyring.public_key_bundle(), &public_key_path) + .expect("save mismatched bundle"); + + assert!(matches!( + OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path, passphrase), + Err(crate::OmegaError::Identity( + IdentityError::PublicBundleMismatch { .. } + )) + )); + + fs::remove_dir_all(directory).expect("remove test directory"); + } + + #[test] + fn wrong_passphrase_does_not_replace_existing_keyring() { + let directory = test_directory(); + let keyring_path = directory.join("omega.mk"); + let public_key_path = directory.join("omega.mpkb"); + let passphrase = b"test-passphrase"; + + OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path, passphrase) + .expect("create identity"); + let original_keyring = fs::read(&keyring_path).expect("read keyring"); + + assert!( + OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path, b"wrong-passphrase") + .is_err() + ); + assert_eq!( + fs::read(&keyring_path).expect("read keyring"), + original_keyring + ); + + fs::remove_dir_all(directory).expect("remove test directory"); + } +} diff --git a/src/main.rs b/src/main.rs index 16b850e..e04238c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod api; mod config; mod db; pub mod error; +mod identity; mod models; mod server; mod sql; @@ -17,45 +18,18 @@ use crate::transport::omikron_connection; use crate::util::file_util::get_directory; use crate::util::logger::PrintType; use crate::util::logger::startup; +use crate::{config::OmegaConfig, server::middleware}; use dotenv::from_path; -use mtp::crypto::Keyring; -use mtp::files::{load_keyring_raw, save_keyring_raw, save_public_key_bundle}; -use once_cell::sync::Lazy; use rustls::crypto::aws_lc_rs::default_provider; use std::env; use std::path::Path; use std::time::Duration; use tokio::time::interval; - -const KEYRING_PATH: &str = "./omega.mk"; - -static KEYRING: Lazy = Lazy::new(|| { - load_keyring_raw(KEYRING_PATH).unwrap_or_else(|_| { - let kr = Keyring::generate(); - if let Err(error) = save_keyring_raw(&kr, KEYRING_PATH) { - eprintln!("Failed to save generated keyring: {error}"); - } - if let Err(error) = save_public_key_bundle(&kr.public_key_bundle(), KEYRING_PATH) { - eprintln!("Failed to save generated public key bundle: {error}"); - } - eprintln!("Generated new keyring at {}", KEYRING_PATH); - kr - }) -}); - -pub fn get_keyring() -> &'static Keyring { - &KEYRING -} -pub fn load_keyring() -> Keyring { - Keyring::from_bytes(&KEYRING.to_bytes()).unwrap_or_else(|error| { - eprintln!("Failed to clone keyring: {error}"); - Keyring::generate() - }) -} +use zeroize::Zeroizing; #[tokio::main] async fn main() { - if let Err(_) = default_provider().install_default() { + if default_provider().install_default().is_err() { println!("Error loading Provider"); return; } @@ -64,6 +38,39 @@ async fn main() { log_in!("Incoming messages"); log_out!("Outgoing messages"); + let identity_secret = match env::var("OMEGA_IDENTITY_SECRET") { + Ok(secret) if !secret.is_empty() => secret, + Ok(_) => { + log!("[FATAL] OMEGA_IDENTITY_SECRET must not be empty"); + return; + } + Err(error) => { + log!("[FATAL] Unable to load OMEGA_IDENTITY_SECRET: {}", error); + return; + } + }; + let identity_secret = Zeroizing::new(identity_secret); + let config = match OmegaConfig::from_env() { + Ok(config) => config, + Err(error) => { + log!("[FATAL] Omega configuration is invalid: {}", error); + return; + } + }; + if middleware::initialize_config(config.rate_limits.clone()).is_err() { + log!("[FATAL] Omega rate-limit configuration was initialized more than once"); + return; + } + let identity = match identity::OmegaIdentity::load_or_create(identity_secret.as_bytes()) { + Ok(identity) => identity, + Err(error) => { + log!("[FATAL] Omega identity initialization failed: {}", error); + return; + } + }; + drop(identity_secret); + let state = OmegaState::new(identity, config); + log!("Started"); log!(" .env"); if let Err(e) = initialize().await { @@ -96,7 +103,7 @@ async fn main() { .unwrap_or(443); tokio::select! { - result = omikron_connection::start(port, OmegaState::new()) => { + result = omikron_connection::start(port, state) => { if let Err(e) = result { log_err!(0, PrintType::General, "Server error: {:?}", e); } diff --git a/src/models/mod.rs b/src/models/mod.rs index 291729d..da21449 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -11,7 +11,8 @@ fn serialize_public_key( where S: serde::Serializer, { - serializer.serialize_str(&key.to_base64()) + let encoded = key.try_to_base64().map_err(serde::ser::Error::custom)?; + serializer.serialize_str(&encoded) } pub use ids::{IotaId, OmikronId, UserId}; diff --git a/src/server/api.rs b/src/server/api.rs index 23debe4..41e5d22 100644 --- a/src/server/api.rs +++ b/src/server/api.rs @@ -8,7 +8,7 @@ use crate::db::{ user_repo::{get_by_user_id, get_by_username}, }; use crate::error::{OmegaError, Result}; -use crate::load_keyring; +use crate::identity::OmegaIdentity; use crate::models::UserId; use crate::server::{ middleware, @@ -24,6 +24,7 @@ use bytes::Bytes; use http::{Method, StatusCode}; use mtp::webserver::{HttpRequest, HttpResponse, RouteParams}; use std::collections::BTreeMap; +use std::sync::Arc; fn error_body(error: &OmegaError) -> String { json(&StatusResponse { @@ -35,11 +36,11 @@ fn error_body(error: &OmegaError) -> String { }) } -fn user_response(user: crate::models::User) -> UserResponse { - UserResponse { +fn user_response(user: crate::models::User) -> Result { + Ok(UserResponse { status: "success", username: user.username, - public_key: user.public_key.to_base64(), + public_key: user.public_key.try_to_base64()?, user_id: user.id.0, iota_id: user.iota_id.map(|id| id.0), sub_level: user.sub_level, @@ -50,10 +51,10 @@ fn user_response(user: crate::models::User) -> UserResponse { avatar: user .avatar .map(|value| base64::engine::general_purpose::STANDARD.encode(value)), - } + }) } -async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> { +async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusCode, String)> { match path_parts { ["api", "get", "omikron"] => { let connection = get_random_omikron() @@ -69,7 +70,7 @@ async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> { json(&OmikronResponse { status: "success", id, - public_key: omikron.public_key.to_base64(), + public_key: omikron.public_key.try_to_base64()?, ip_address: omikron.ip_address, port: omikron.port, }), @@ -105,7 +106,7 @@ async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> { json(&OmikronResponse { status: "success", id: omikron.id.0, - public_key: omikron.public_key.to_base64(), + public_key: omikron.public_key.try_to_base64()?, ip_address: omikron.ip_address, port: omikron.port, }), @@ -120,12 +121,7 @@ async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> { .map(|(omikron_id, iotas)| { let iotas = iotas .into_iter() - .map(|(iota_id, users)| { - ( - iota_id.to_string(), - users.into_iter().map(i64::from).collect(), - ) - }) + .map(|(iota_id, users)| (iota_id.to_string(), users.into_iter().collect())) .collect(); (omikron_id.to_string(), iotas) }) @@ -146,7 +142,7 @@ async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> { json(&IotaResponse { status: "success", iota_id: iota.id.0, - public_key: iota.public_key.to_base64(), + public_key: iota.public_key.try_to_base64()?, }), )) } @@ -158,7 +154,7 @@ async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> { json(&UsernameResponse { status: "success", username: user.username, - public_key: user.public_key.to_base64(), + public_key: user.public_key.try_to_base64()?, user_id: user.id.0, iota_id: user.iota_id.map(|id| id.0), sub_level: user.sub_level, @@ -168,7 +164,7 @@ async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> { } ["api", "get", "public_key"] => { let public_key = base64::engine::general_purpose::STANDARD - .encode(load_keyring().public_key_bundle().as_bytes()); + .encode(identity.public_key_bundle().try_as_bytes()?); Ok(( StatusCode::OK, json(&PublicKeyResponse { @@ -180,7 +176,7 @@ async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> { ["api", "get", "user", id] => { let id = parse_positive_id(id)?; let user = get_by_user_id(UserId::from(id)).await?; - Ok((StatusCode::OK, json(&user_response(user)))) + Ok((StatusCode::OK, json(&user_response(user)?))) } _ => Ok(( StatusCode::INTERNAL_SERVER_ERROR, @@ -189,13 +185,17 @@ async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> { } } -pub async fn handle(request: HttpRequest, response: HttpResponse) -> HttpResponse { +pub async fn handle( + request: HttpRequest, + response: HttpResponse, + identity: Arc, +) -> HttpResponse { let method = request.method; let path = request.uri.path().to_string(); if method != Method::OPTIONS && !middleware::allow(request.remote_addr.ip(), &path) { return response .status(StatusCode::TOO_MANY_REQUESTS) - .header("access-control-allow-origin", &crate::config::cors_origin()) + .header("access-control-allow-origin", crate::config::cors_origin()) .body(json(&StatusResponse { status: "error_rate_limited", })); @@ -203,7 +203,7 @@ pub async fn handle(request: HttpRequest, response: HttpResponse) -> HttpRespons if method == Method::OPTIONS { return response .status(StatusCode::OK) - .header("access-control-allow-origin", &crate::config::cors_origin()) + .header("access-control-allow-origin", crate::config::cors_origin()) .header("access-control-allow-methods", "GET, POST, OPTIONS") .header("access-control-allow-headers", "*"); } @@ -213,7 +213,7 @@ pub async fn handle(request: HttpRequest, response: HttpResponse) -> HttpRespons return match std::fs::read(file_path) { Ok(bytes) => response .status(StatusCode::OK) - .header("access-control-allow-origin", &crate::config::cors_origin()) + .header("access-control-allow-origin", crate::config::cors_origin()) .header("content-type", "application/zip") .header( "content-disposition", @@ -222,7 +222,7 @@ pub async fn handle(request: HttpRequest, response: HttpResponse) -> HttpRespons .body(Bytes::from(bytes)), Err(_) => response .status(StatusCode::NOT_FOUND) - .header("access-control-allow-origin", &crate::config::cors_origin()) + .header("access-control-allow-origin", crate::config::cors_origin()) .body(json(&StatusResponse { status: "error_not_found", })), @@ -237,12 +237,12 @@ pub async fn handle(request: HttpRequest, response: HttpResponse) -> HttpRespons .status(StatusCode::TEMPORARY_REDIRECT) .header("location", &location); } - let (status, body) = route(&path_parts) + let (status, body) = route(&path_parts, &identity) .await .unwrap_or_else(|error| (error.status_code(), error_body(&error))); response .status(status) - .header("access-control-allow-origin", &crate::config::cors_origin()) + .header("access-control-allow-origin", crate::config::cors_origin()) .header("access-control-allow-headers", "*") .header("access-control-allow-methods", "GET, POST, OPTIONS") .body(body) @@ -252,6 +252,7 @@ pub async fn handle_pattern( request: HttpRequest, response: HttpResponse, _params: RouteParams, + identity: Arc, ) -> HttpResponse { - handle(request, response).await + handle(request, response, identity).await } diff --git a/src/server/middleware.rs b/src/server/middleware.rs index 5f373ec..b279298 100644 --- a/src/server/middleware.rs +++ b/src/server/middleware.rs @@ -1,14 +1,25 @@ use dashmap::DashMap; use once_cell::sync::Lazy; use std::net::IpAddr; -use std::{collections::VecDeque, time::Instant}; +use std::{collections::VecDeque, sync::OnceLock, time::Instant}; use tokio::time::interval; static REQUESTS: Lazy>> = Lazy::new(DashMap::new); -static CONFIG: Lazy = - Lazy::new(crate::config::RateLimitConfig::from_env); +static CONFIG: OnceLock = OnceLock::new(); const MAX_TRACKED_CLIENT_BUCKETS: usize = 100_000; +pub(crate) fn initialize_config( + config: crate::config::RateLimitConfig, +) -> Result<(), crate::config::RateLimitConfig> { + CONFIG.set(config) +} + +fn config() -> &'static crate::config::RateLimitConfig { + static FALLBACK: Lazy = + Lazy::new(crate::config::RateLimitConfig::from_env_or_default); + CONFIG.get().unwrap_or(&FALLBACK) +} + pub fn allow(remote_addr: IpAddr, path: &str) -> bool { let key = if path.contains("register") { "registration" @@ -23,15 +34,15 @@ pub fn allow(remote_addr: IpAddr, path: &str) -> bool { } } let limit = if key == "registration" { - CONFIG.registration_requests + config().registration_requests } else { - CONFIG.general_requests + config().general_requests }; let now = Instant::now(); let mut entries = REQUESTS.entry(map_key).or_default(); while entries .front() - .is_some_and(|time| now.duration_since(*time) >= CONFIG.window) + .is_some_and(|time| now.duration_since(*time) >= config().window) { entries.pop_front(); } @@ -44,7 +55,7 @@ pub fn allow(remote_addr: IpAddr, path: &str) -> bool { pub fn spawn_cleanup_task() -> tokio::task::JoinHandle<()> { tokio::spawn(async { - let mut ticker = interval(CONFIG.window); + let mut ticker = interval(config().window); loop { ticker.tick().await; cleanup_expired(); @@ -57,7 +68,7 @@ fn cleanup_expired() { REQUESTS.retain(|_, entries| { while entries .front() - .is_some_and(|time| now.duration_since(*time) >= CONFIG.window) + .is_some_and(|time| now.duration_since(*time) >= config().window) { entries.pop_front(); } diff --git a/src/server/mod.rs b/src/server/mod.rs index 2d01c5b..64df9e9 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,6 +1,6 @@ pub mod api; pub mod index; pub mod middleware; -pub mod server; pub mod short_link; pub mod validation; +pub mod web; diff --git a/src/server/server.rs b/src/server/server.rs deleted file mode 100644 index 9ac4f06..0000000 --- a/src/server/server.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::server::{api, index::index_handler}; -use mtp::webserver::WebServerConfig; - -pub fn build_web_config() -> Result { - WebServerConfig::new() - .route("/api/download/iota_frontend", api::handle)? - .route("/api/get/omikron", api::handle)? - .route("/api/get/connections", api::handle)? - .route("/api/get/public_key", api::handle)? - .route_pattern("/api/get/omikron/{id}", api::handle_pattern)? - .route_pattern("/api/get/iota/{id}", api::handle_pattern)? - .route_pattern("/api/get/id/{username}", api::handle_pattern)? - .route_pattern("/api/get/user/{id}", api::handle_pattern)? - .route_pattern("/direct/{short}", api::handle_pattern)? - .fallback(|_request, response| async move { index_handler(response) }) -} diff --git a/src/server/short_link.rs b/src/server/short_link.rs index dd24ebf..6089957 100644 --- a/src/server/short_link.rs +++ b/src/server/short_link.rs @@ -34,7 +34,7 @@ pub async fn get_short_link(short: &str) -> Result { short }; let frag = short.replace(key, ""); - let normalized = normalize_short(&key); + let normalized = normalize_short(key); let target = short_link_repo::get(&normalized) .await diff --git a/src/server/web.rs b/src/server/web.rs new file mode 100644 index 0000000..5975888 --- /dev/null +++ b/src/server/web.rs @@ -0,0 +1,47 @@ +use crate::identity::OmegaIdentity; +use crate::server::{api, index::index_handler}; +use mtp::webserver::{HttpRequest, HttpResponse, RouteParams, WebServerConfig}; +use std::{future::Future, pin::Pin, sync::Arc}; + +type RouteFuture = Pin + Send>>; + +fn api_handler( + identity: Arc, +) -> impl Fn(HttpRequest, HttpResponse) -> RouteFuture + Send + Sync + 'static { + move |request, response| Box::pin(api::handle(request, response, identity.clone())) +} + +fn api_pattern_handler( + identity: Arc, +) -> impl Fn(HttpRequest, HttpResponse, RouteParams) -> RouteFuture + Send + Sync + 'static { + move |request, response, params| { + Box::pin(api::handle_pattern( + request, + response, + params, + identity.clone(), + )) + } +} + +pub fn build_web_config( + identity: Arc, +) -> Result { + WebServerConfig::new() + .route("/api/download/iota_frontend", api_handler(identity.clone()))? + .route("/api/get/omikron", api_handler(identity.clone()))? + .route("/api/get/connections", api_handler(identity.clone()))? + .route("/api/get/public_key", api_handler(identity.clone()))? + .route_pattern( + "/api/get/omikron/{id}", + api_pattern_handler(identity.clone()), + )? + .route_pattern("/api/get/iota/{id}", api_pattern_handler(identity.clone()))? + .route_pattern( + "/api/get/id/{username}", + api_pattern_handler(identity.clone()), + )? + .route_pattern("/api/get/user/{id}", api_pattern_handler(identity.clone()))? + .route_pattern("/direct/{short}", api_pattern_handler(identity))? + .fallback(|_request, response| async move { index_handler(response) }) +} diff --git a/src/sql/connection_status.rs b/src/sql/connection_status.rs index 104e316..00be3c9 100644 --- a/src/sql/connection_status.rs +++ b/src/sql/connection_status.rs @@ -12,11 +12,7 @@ pub enum UserStatus { iota_online, iota_borked, } -#[allow(unused)] impl UserStatus { - pub fn to_string(&self) -> String { - format!("{:?}", self) - } pub fn from_client_preference(s: &str) -> Option { match s { "user_online" => Some(Self::user_online), @@ -38,11 +34,18 @@ impl UserStatus { /// Parse a value received from a client or persisted as an account /// preference. Derived connectivity and diagnostic states are never valid /// preferences. + #[allow(dead_code)] pub fn from_str(s: &str) -> Option { Self::from_client_preference(s) } } +impl std::fmt::Display for UserStatus { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "{self:?}") + } +} + #[cfg(test)] mod tests { use super::UserStatus; diff --git a/src/sql/user_online_tracker.rs b/src/sql/user_online_tracker.rs index be3e87c..cac8ab6 100644 --- a/src/sql/user_online_tracker.rs +++ b/src/sql/user_online_tracker.rs @@ -275,10 +275,6 @@ impl PresenceTracker { Ok(()) } - pub fn check_index_consistency(&self) -> Result<(), String> { - Self::check_indices(&self.routes.read().unwrap()) - } - pub fn set_preference(&self, user_id: i64, status: UserStatus) { self.preferences.write().unwrap().insert(user_id, status); } @@ -335,14 +331,6 @@ impl PresenceTracker { }) } - pub fn track_iota(&self, iota_id: i64, omikron_id: i64) { - self.connect_iota(iota_id, omikron_id); - } - - pub fn untrack_iota(&self, iota_id: i64, omikron_id: i64) -> bool { - self.disconnect_iota(iota_id, omikron_id) - } - pub fn track_session(&self, user_id: i64, session_id: i64, omikron_id: i64, iota_id: i64) { let mut routes = self.routes.write().unwrap(); Self::insert_session_locked( @@ -373,15 +361,6 @@ impl PresenceTracker { .is_some_and(|route| route.omikron_id == omikron_id) } - pub fn session_route(&self, user_id: i64, session_id: i64) -> Option { - self.routes - .read() - .unwrap() - .sessions - .get(&(user_id, session_id)) - .map(|route| route.clone()) - } - pub fn resolve_public_state(&self, user_id: i64, iota_id: i64) -> UserStatus { if !self.has_iota_route(iota_id) { return UserStatus::iota_offline; @@ -496,12 +475,6 @@ impl PresenceTracker { Self::debug_assert_indices(&routes); } - pub fn remove_subscription(&self, user_id: i64, session_id: i64) { - let mut routes = self.routes.write().unwrap(); - Self::remove_subscription_locked(&mut routes, (user_id, session_id)); - Self::debug_assert_indices(&routes); - } - pub fn subscribers(&self, target_user_id: i64) -> Vec { let mut subscribers = self .routes @@ -542,15 +515,6 @@ impl PresenceTracker { sessions } - pub fn replace_routes_owned_by( - &self, - omikron_id: i64, - iota_ids: &[i64], - sessions: &[(i64, i64, i64)], - ) { - self.replace_omikron_snapshot(omikron_id, iota_ids, sessions); - } - pub fn replace_omikron_snapshot( &self, omikron_id: i64, @@ -621,10 +585,6 @@ impl PresenceTracker { ) } - pub fn track_iota_connection(&self, iota_id: i64, omikron_id: i64, primary: bool) { - self.connect_iota_with_priority(iota_id, omikron_id, primary); - } - pub fn untrack_iota_connection(&self, iota_id: i64, omikron_id: i64) -> bool { let connections_empty = self.disconnect_iota(iota_id, omikron_id); self.remove_sessions_for_iota(iota_id, omikron_id); @@ -671,29 +631,6 @@ impl PresenceTracker { user_routes.into_iter().map(|(_, route)| route).collect() } - pub fn sessions_for_user(&self, user_id: i64) -> Vec<(i64, ClientSessionRoute)> { - let routes = self.routes.read().unwrap(); - let mut sessions = routes - .indices - .sessions_by_user - .get(&user_id) - .into_iter() - .flat_map(|session_ids| session_ids.iter()) - .filter_map(|session_id| { - routes - .sessions - .get(&(user_id, *session_id)) - .map(|route| (*session_id, route.clone())) - }) - .collect::>(); - sessions.sort_by_key(|(session_id, route)| (*session_id, route.omikron_id)); - sessions - } - - pub fn route_for_session(&self, user_id: i64, session_id: i64) -> Option { - self.session_route(user_id, session_id) - } - pub fn user_route(&self, user_id: i64) -> Option { self.routes_for_user(user_id).into_iter().next() } @@ -745,10 +682,6 @@ impl PresenceTracker { } result } - - pub fn remove_omikron_and_offline_users(&self, omikron_id: i64) -> RemovedOmikronState { - self.remove_omikron(omikron_id) - } } #[cfg(test)] @@ -756,6 +689,10 @@ mod tests { use super::{ClientSessionRoute, PresenceTracker}; use crate::sql::connection_status::UserStatus; + fn check_index_consistency(tracker: &PresenceTracker) -> Result<(), String> { + PresenceTracker::check_indices(&tracker.routes.read().unwrap()) + } + #[test] fn effective_state_is_derived_from_routes_and_preference() { let tracker = PresenceTracker::default(); @@ -765,7 +702,7 @@ mod tests { UserStatus::iota_offline ); - tracker.track_iota(11, 42); + tracker.connect_iota(11, 42); assert_eq!( tracker.resolve_public_state(7, 11), UserStatus::user_offline @@ -783,7 +720,7 @@ mod tests { #[test] fn session_must_match_assigned_iota_and_live_route() { let tracker = PresenceTracker::default(); - tracker.track_iota(11, 42); + tracker.connect_iota(11, 42); tracker.track_session(7, 3, 42, 12); assert!(!tracker.has_active_session_for_iota(7, 11)); assert_eq!( @@ -807,7 +744,7 @@ mod tests { #[test] fn resolver_covers_all_public_preference_states() { let tracker = PresenceTracker::default(); - tracker.track_iota(11, 42); + tracker.connect_iota(11, 42); tracker.track_session(7, 3, 42, 11); for (preference, expected) in [ @@ -826,14 +763,20 @@ mod tests { fn session_routes_replace_by_session_without_replacing_preferences() { let tracker = PresenceTracker::default(); tracker.set_preference(7, UserStatus::user_dnd); - tracker.track_iota(11, 42); - tracker.track_iota(12, 43); + tracker.connect_iota(11, 42); + tracker.connect_iota(12, 43); tracker.track_session(7, 3, 42, 11); tracker.track_session(7, 4, 43, 12); assert_eq!(tracker.preference(7), UserStatus::user_dnd); assert_eq!( - tracker.session_route(7, 3), + tracker + .routes + .read() + .unwrap() + .sessions + .get(&(7, 3)) + .cloned(), Some(ClientSessionRoute { omikron_id: 42, iota_id: 11, @@ -865,21 +808,21 @@ mod tests { #[test] fn losing_one_iota_route_keeps_iota_online() { let tracker = PresenceTracker::default(); - tracker.track_iota(11, 42); - tracker.track_iota(11, 43); + tracker.connect_iota(11, 42); + tracker.connect_iota(11, 43); assert!(tracker.has_iota_route(11)); - assert!(!tracker.untrack_iota(11, 42)); + assert!(!tracker.disconnect_iota(11, 42)); assert!(tracker.has_iota_route(11)); - assert!(tracker.untrack_iota(11, 43)); + assert!(tracker.disconnect_iota(11, 43)); assert!(!tracker.has_iota_route(11)); } #[test] fn iota_connection_routes_track_primary_and_replacement() { let tracker = PresenceTracker::default(); - tracker.track_iota_connection(11, 42, true); - tracker.track_iota_connection(11, 43, false); + tracker.connect_iota(11, 42); + tracker.connect_iota(11, 43); assert_eq!(tracker.iota_connections(11), Some(vec![42, 43])); assert_eq!(tracker.primary_iota_route(11), Some(42)); @@ -961,8 +904,8 @@ mod tests { #[test] fn active_session_can_use_one_of_several_live_iota_routes() { let tracker = PresenceTracker::default(); - tracker.track_iota(11, 42); - tracker.track_iota(11, 43); + tracker.connect_iota(11, 42); + tracker.connect_iota(11, 43); tracker.track_session(7, 3, 43, 11); assert!(tracker.has_active_session_for_iota(7, 11)); @@ -973,13 +916,13 @@ mod tests { #[test] fn route_snapshot_replaces_only_one_omikron() { let tracker = PresenceTracker::default(); - tracker.track_iota(11, 42); - tracker.track_iota(12, 43); + tracker.connect_iota(11, 42); + tracker.connect_iota(12, 43); tracker.track_session(7, 3, 42, 11); tracker.track_session(8, 4, 43, 12); tracker.replace_subscription(7, 3, 42, vec![20]); - tracker.replace_routes_owned_by(42, &[12], &[(9, 5, 12)]); + tracker.replace_omikron_snapshot(42, &[12], &[(9, 5, 12)]); assert!(!tracker.owns_session(7, 3, 42)); assert!(tracker.owns_session(8, 4, 43)); @@ -992,8 +935,8 @@ mod tests { fn omikron_cleanup_preserves_other_routes_and_preferences() { let tracker = PresenceTracker::default(); tracker.set_preference(7, UserStatus::user_dnd); - tracker.track_iota(11, 42); - tracker.track_iota(11, 43); + tracker.connect_iota(11, 42); + tracker.connect_iota(11, 43); tracker.track_session(7, 3, 42, 11); tracker.track_session(7, 4, 43, 11); tracker.replace_subscription(7, 3, 42, vec![20]); @@ -1036,14 +979,14 @@ mod tests { .cloned(), Some([20, 21].into_iter().collect()) ); - assert!(tracker.check_index_consistency().is_ok()); + assert!(check_index_consistency(&tracker).is_ok()); tracker.replace_subscription(7, 3, 42, vec![22]); assert!(tracker.subscribers(20).is_empty()); assert!(tracker.subscribers(21).is_empty()); assert_eq!(tracker.subscribers(22).len(), 1); tracker.remove_session(7, 3, 42); - assert!(tracker.check_index_consistency().is_ok()); + assert!(check_index_consistency(&tracker).is_ok()); assert!( tracker .routes @@ -1076,7 +1019,7 @@ mod tests { assert_eq!(removed.iota_ids, vec![11]); assert_eq!(removed.session_user_ids, vec![7]); assert_eq!(removed.subscriber_sessions, vec![(7, 3)]); - assert!(tracker.check_index_consistency().is_ok()); + assert!(check_index_consistency(&tracker).is_ok()); } #[test] @@ -1093,6 +1036,6 @@ mod tests { .unwrap() .clear(); - assert!(tracker.check_index_consistency().is_err()); + assert!(check_index_consistency(&tracker).is_err()); } } diff --git a/src/state.rs b/src/state.rs index 02c899b..215f1b3 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,7 +1,10 @@ -use crate::sql::user_online_tracker::PresenceTracker; +use crate::{ + config::OmegaConfig, identity::OmegaIdentity, sql::user_online_tracker::PresenceTracker, +}; use dashmap::DashMap; use std::sync::Arc; use std::time::{Duration, Instant}; +use tokio::sync::Semaphore; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum AccountChallengeOperation { @@ -11,28 +14,50 @@ pub enum AccountChallengeOperation { #[derive(Clone, Debug)] pub struct AccountChallenge { - pub operation: AccountChallengeOperation, - pub user_id: i64, - pub requester_iota_id: i64, pub nonce: u64, pub created_at: Instant, } pub struct OmegaState { pub presence: Arc, + pub identity: Arc, + pub(crate) config: Arc, + pub(crate) global_handler_limit: Arc, + pub(crate) omikron_handler_concurrency: usize, challenges: DashMap<(AccountChallengeOperation, i64, i64), AccountChallenge>, } -impl Default for OmegaState { - fn default() -> Self { +impl OmegaState { + fn with_handler_limits(identity: OmegaIdentity, config: OmegaConfig) -> Self { + let omikron_handler_concurrency = config.dispatch.omikron_handler_concurrency; + let global_handler_concurrency = config.dispatch.global_handler_concurrency; Self { presence: Arc::new(PresenceTracker::default()), + identity: Arc::new(identity), + config: Arc::new(config), + global_handler_limit: Arc::new(Semaphore::new(global_handler_concurrency)), + omikron_handler_concurrency, challenges: DashMap::new(), } } -} -impl OmegaState { + pub fn new(identity: OmegaIdentity, config: OmegaConfig) -> Arc { + Arc::new(Self::with_handler_limits(identity, config)) + } + + #[cfg(test)] + fn test_state() -> Arc { + use mtp::crypto::Keyring; + + let mut config = OmegaConfig::default(); + config.dispatch.omikron_handler_concurrency = 4; + config.dispatch.global_handler_concurrency = 8; + Arc::new(Self::with_handler_limits( + OmegaIdentity::from_keyring(Keyring::generate()), + config, + )) + } + pub fn issue_challenge( &self, operation: AccountChallengeOperation, @@ -43,9 +68,6 @@ impl OmegaState { self.challenges.insert( (operation, user_id, requester_iota_id), AccountChallenge { - operation, - user_id, - requester_iota_id, nonce, created_at: Instant::now(), }, @@ -68,12 +90,6 @@ impl OmegaState { } } -impl OmegaState { - pub fn new() -> Arc { - Arc::new(Self::default()) - } -} - #[cfg(test)] mod tests { use super::OmegaState; @@ -81,10 +97,10 @@ mod tests { #[test] fn state_instances_have_independent_presence_trackers() { - let first = OmegaState::new(); - let second = OmegaState::new(); + let first = OmegaState::test_state(); + let second = OmegaState::test_state(); - first.presence.track_iota_connection(11, 42, true); + first.presence.connect_iota(11, 42); assert!(first.presence.has_iota_route(11)); assert!(!second.presence.has_iota_route(11)); @@ -94,10 +110,10 @@ mod tests { #[test] fn two_session_private_and_public_presence_flow_is_authoritative() { - let state = OmegaState::new(); + let state = OmegaState::test_state(); state.presence.set_preference(7, UserStatus::user_online); state.presence.set_preference(8, UserStatus::user_online); - state.presence.track_iota_connection(11, 42, true); + state.presence.connect_iota(11, 42); state.presence.track_session(7, 100, 42, 11); state.presence.track_session(7, 101, 42, 11); state.presence.replace_subscription(7, 100, 42, vec![8]); diff --git a/src/transport/connection.rs b/src/transport/connection.rs index 6cada3e..66d9eb3 100644 --- a/src/transport/connection.rs +++ b/src/transport/connection.rs @@ -2,24 +2,94 @@ pub(crate) use super::omikron_connection::{OmikronConnection, OmikronResult}; use mtp::codec::{CommunicationValue, DataValue}; -pub(crate) trait MtpValueCompat { - fn get_id(&self) -> u32; - fn get_sender(&self) -> u64; - fn get_receiver(&self) -> u64; +pub(crate) trait RequiredMtpFields { + /// Require a nonzero request correlation ID. `None` and `Some(0)` are + /// distinct MTP wire states, but both are invalid for Omega requests. + fn require_id(&self) -> OmikronResult; + /// Require a nonzero authenticated peer identity. + fn require_sender(&self) -> OmikronResult; + /// Require a nonzero application routing target. + fn require_receiver(&self) -> OmikronResult; + fn require_sender_i64(&self) -> OmikronResult; + fn require_receiver_i64(&self) -> OmikronResult; } -impl MtpValueCompat for CommunicationValue { - fn get_id(&self) -> u32 { - self.id().unwrap_or_default() +impl RequiredMtpFields for CommunicationValue { + fn require_id(&self) -> OmikronResult { + self.id().filter(|id| *id != 0).ok_or_else(|| { + crate::OmegaError::Validation("MTP message is missing request id".into()) + }) } - fn get_sender(&self) -> u64 { - self.sender().unwrap_or_default() + fn require_sender(&self) -> OmikronResult { + self.sender() + .filter(|sender| *sender != 0) + .ok_or_else(|| crate::OmegaError::Validation("MTP message is missing sender".into())) } - fn get_receiver(&self) -> u64 { - self.receiver().unwrap_or_default() + fn require_receiver(&self) -> OmikronResult { + self.receiver() + .filter(|receiver| *receiver != 0) + .ok_or_else(|| crate::OmegaError::Validation("MTP message is missing receiver".into())) } + + fn require_sender_i64(&self) -> OmikronResult { + positive_i64(self.require_sender()?, "sender") + } + + fn require_receiver_i64(&self) -> OmikronResult { + positive_i64(self.require_receiver()?, "receiver") + } +} + +pub(crate) fn positive_i64(value: impl TryInto, field: &str) -> OmikronResult { + let value = value + .try_into() + .map_err(|_| crate::OmegaError::Validation(format!("invalid {field}")))?; + let value = i64::try_from(value) + .map_err(|_| crate::OmegaError::Validation(format!("invalid {field}")))?; + if value <= 0 { + return Err(crate::OmegaError::Validation(format!("invalid {field}"))); + } + Ok(value) +} + +pub(crate) fn validate_dispatch_fields(value: &CommunicationValue) -> OmikronResult<()> { + let Some(message_type) = value.get_comm_type_enum() else { + return Err(crate::OmegaError::Validation( + "MTP message has an unknown communication type".into(), + )); + }; + + if !matches!( + message_type, + mtp::codec::CommunicationType::ClientChanged + | mtp::codec::CommunicationType::PushNotification + ) { + value.require_id()?; + } + + if matches!( + message_type, + mtp::codec::CommunicationType::GetUserData + | mtp::codec::CommunicationType::ChangeUserData + | mtp::codec::CommunicationType::ChangeIotaData + | mtp::codec::CommunicationType::DeleteUser + | mtp::codec::CommunicationType::AttachUserBegin + | mtp::codec::CommunicationType::AttachUserComplete + | mtp::codec::CommunicationType::DeleteUserCredentialBegin + | mtp::codec::CommunicationType::DeleteUserCredentialComplete + | mtp::codec::CommunicationType::EraseHostedUserDataAck + | mtp::codec::CommunicationType::ReleaseUserFromIota + | mtp::codec::CommunicationType::DeleteIota + | mtp::codec::CommunicationType::GetNotifications + | mtp::codec::CommunicationType::ReadNotification + | mtp::codec::CommunicationType::StateSubscribe + ) { + value.require_sender()?; + } + + Ok(()) } pub(crate) trait OptionalDataValueCompat { @@ -46,3 +116,65 @@ impl OptionalDataValueCompat for Option<&DataValue> { self.and_then(|value| value.as_bytes()) } } + +#[cfg(test)] +mod tests { + use super::{RequiredMtpFields, validate_dispatch_fields}; + use mtp::codec::{CommunicationType, CommunicationValue}; + + #[test] + fn missing_request_id_is_rejected_without_a_zero_fallback() { + let value = CommunicationValue::new(CommunicationType::GetUserData).without_id(); + assert!(value.require_id().is_err()); + assert!(validate_dispatch_fields(&value).is_err()); + } + + #[test] + fn zero_routing_values_are_rejected_by_omega_contract() { + let value = CommunicationValue::new(CommunicationType::GetUserData) + .with_id(0) + .with_sender(0) + .with_receiver(0); + + assert!(value.require_id().is_err()); + assert!(value.require_sender().is_err()); + assert!(value.require_receiver().is_err()); + assert!(validate_dispatch_fields(&value).is_err()); + } + + #[test] + fn security_sensitive_messages_require_sender() { + for message_type in [ + CommunicationType::GetUserData, + CommunicationType::ChangeUserData, + CommunicationType::ChangeIotaData, + CommunicationType::DeleteUser, + CommunicationType::AttachUserBegin, + CommunicationType::AttachUserComplete, + CommunicationType::DeleteUserCredentialBegin, + CommunicationType::DeleteUserCredentialComplete, + CommunicationType::EraseHostedUserDataAck, + CommunicationType::ReleaseUserFromIota, + CommunicationType::DeleteIota, + CommunicationType::GetNotifications, + CommunicationType::ReadNotification, + CommunicationType::StateSubscribe, + ] { + let value = CommunicationValue::new(message_type).with_id(1); + assert!(value.require_sender().is_err()); + assert!(validate_dispatch_fields(&value).is_err()); + } + } + + #[test] + fn optional_client_changed_id_remains_optional() { + let value = CommunicationValue::new(CommunicationType::ClientChanged).without_id(); + assert!(validate_dispatch_fields(&value).is_ok()); + } + + #[test] + fn push_notification_uses_its_logical_sender_field() { + let value = CommunicationValue::new(CommunicationType::PushNotification); + assert!(validate_dispatch_fields(&value).is_ok()); + } +} diff --git a/src/transport/handlers/account.rs b/src/transport/handlers/account.rs index 0374f78..ae7215f 100644 --- a/src/transport/handlers/account.rs +++ b/src/transport/handlers/account.rs @@ -1,5 +1,5 @@ use super::super::connection::{ - MtpValueCompat, OmikronConnection, OmikronResult, OptionalDataValueCompat, + OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields, }; use crate::{ db::{iota_repo, user_repo}, @@ -22,24 +22,30 @@ async fn delete( Err(error) => CommunicationValue::new(CommunicationType::ErrorInternal) .add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())), }; - connection.send(&response.with_id(value.get_id())).await + connection + .send(&response.with_id(value.require_id()?)) + .await } pub async fn user( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { - let user_id = UserId::from(value.get_sender() as i64); + value.require_id()?; + value.require_sender_i64()?; + let user_id = UserId::from(value.require_sender_i64()?); complete_delete(connection, value, user_id).await } pub async fn iota( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { + value.require_id()?; + value.require_sender_i64()?; delete( connection, value.clone(), - iota_repo::delete_iota(IotaId::from(value.get_sender() as i64)), + iota_repo::delete_iota(IotaId::from(value.require_sender_i64()?)), ) .await } @@ -48,6 +54,8 @@ pub async fn release_from_iota( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { + value.require_id()?; + value.require_sender_i64()?; let Some(user_id) = value .get_data(DataType::UserId) .as_signed_number() @@ -55,18 +63,21 @@ pub async fn release_from_iota( .filter(|id| *id > 0) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId) .await; }; - let requester = IotaId::from(value.get_sender() as i64); + let requester = IotaId::from(value.require_sender_i64()?); let Ok(user) = user_repo::get_by_user_id(UserId::from(user_id)).await else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) + .send_error_response(value.require_id()?, CommunicationType::ErrorNotFound) .await; }; if user.iota_id != Some(requester) { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated) + .send_error_response( + value.require_id()?, + CommunicationType::ErrorNotAuthenticated, + ) .await; } let previous_iota = user.iota_id; @@ -76,14 +87,17 @@ pub async fn release_from_iota( crate::transport::omikron_manager::publish_iota_user_snapshot(iota.0).await; } connection - .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) + .send( + &CommunicationValue::new(CommunicationType::Success) + .with_id(value.require_id()?), + ) .await } Err(error) => { connection .send( &CommunicationValue::new(CommunicationType::ErrorInternal) - .with_id(value.get_id()) + .with_id(value.require_id()?) .add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())), ) .await @@ -104,6 +118,8 @@ pub async fn attach_begin( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { + value.require_id()?; + value.require_sender_i64()?; let Some(user_id) = value .get_data(DataType::UserId) .as_signed_number() @@ -111,7 +127,7 @@ pub async fn attach_begin( .filter(|v| *v > 0) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId) .await; }; if user_repo::get_by_user_id(UserId::from(user_id)) @@ -119,10 +135,10 @@ pub async fn attach_begin( .is_err() { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) + .send_error_response(value.require_id()?, CommunicationType::ErrorNotFound) .await; } - let requester = value.get_sender() as i64; + let requester = value.require_sender_i64()?; let nonce = connection .state() @@ -130,7 +146,7 @@ pub async fn attach_begin( connection .send( &CommunicationValue::new(CommunicationType::AttachUserChallenge) - .with_id(value.get_id()) + .with_id(value.require_id()?) .add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into())) .add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into())), ) @@ -141,6 +157,8 @@ pub async fn attach_complete( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { + value.require_id()?; + value.require_sender_i64()?; let Some(user_id) = value .get_data(DataType::UserId) .as_signed_number() @@ -148,24 +166,30 @@ pub async fn attach_complete( .filter(|v| *v > 0) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId) .await; }; - let requester = value.get_sender() as i64; + let requester = value.require_sender_i64()?; let Some(nonce) = value .get_data(DataType::ServerNonce) .as_signed_number() .and_then(|v| u64::try_from(v).ok()) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge) + .send_error_response( + value.require_id()?, + CommunicationType::ErrorInvalidChallenge, + ) .await; }; let signature = value.get_data(DataType::Signature).as_bytes(); let pq_signature = value.get_data(DataType::PqSignature).as_bytes(); let (Some(signature), Some(pq_signature)) = (signature, pq_signature) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge) + .send_error_response( + value.require_id()?, + CommunicationType::ErrorInvalidChallenge, + ) .await; }; if !connection.state().consume_challenge( @@ -175,12 +199,15 @@ pub async fn attach_complete( nonce, ) { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge) + .send_error_response( + value.require_id()?, + CommunicationType::ErrorInvalidChallenge, + ) .await; } let Ok(user) = user_repo::get_by_user_id(UserId::from(user_id)).await else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) + .send_error_response(value.require_id()?, CommunicationType::ErrorNotFound) .await; }; let payload = lifecycle_payload(b"tensamin:user-attach:v1\0", user_id, requester, nonce); @@ -188,7 +215,10 @@ pub async fn attach_complete( || verify_ml_dsa(&user.public_key.sig_pq_public_key, &payload, &pq_signature).is_err() { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated) + .send_error_response( + value.require_id()?, + CommunicationType::ErrorNotAuthenticated, + ) .await; } let previous_iota = user.iota_id; @@ -199,12 +229,15 @@ pub async fn attach_complete( } crate::transport::omikron_manager::publish_iota_user_snapshot(requester).await; connection - .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) + .send( + &CommunicationValue::new(CommunicationType::Success) + .with_id(value.require_id()?), + ) .await } Err(_) => { connection - .send_error_response(value.get_id(), CommunicationType::ErrorInternal) + .send_error_response(value.require_id()?, CommunicationType::ErrorInternal) .await } } @@ -225,7 +258,7 @@ async fn complete_delete( connection .send( &CommunicationValue::new(CommunicationType::Success) - .with_id(value.get_id()) + .with_id(value.require_id()?) .add_typed_default( DataType::CleanupPending, DataValue::Bool(cleanup_pending), @@ -235,14 +268,14 @@ async fn complete_delete( } Err(crate::error::OmegaError::NotFound) => { connection - .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) + .send_error_response(value.require_id()?, CommunicationType::ErrorNotFound) .await } Err(error) => { connection .send( &CommunicationValue::new(CommunicationType::ErrorInternal) - .with_id(value.get_id()) + .with_id(value.require_id()?) .add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())), ) .await @@ -254,6 +287,8 @@ pub async fn delete_credential_begin( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { + value.require_id()?; + value.require_sender_i64()?; let Some(user_id) = value .get_data(DataType::UserId) .as_signed_number() @@ -261,7 +296,7 @@ pub async fn delete_credential_begin( .filter(|v| *v > 0) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId) .await; }; if user_repo::get_by_user_id(UserId::from(user_id)) @@ -269,10 +304,10 @@ pub async fn delete_credential_begin( .is_err() { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) + .send_error_response(value.require_id()?, CommunicationType::ErrorNotFound) .await; } - let requester = value.get_sender() as i64; + let requester = value.require_sender_i64()?; let nonce = connection .state() @@ -280,7 +315,7 @@ pub async fn delete_credential_begin( connection .send( &CommunicationValue::new(CommunicationType::DeleteUserCredentialChallenge) - .with_id(value.get_id()) + .with_id(value.require_id()?) .add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into())) .add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into())), ) @@ -291,6 +326,8 @@ pub async fn delete_credential_complete( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { + value.require_id()?; + value.require_sender_i64()?; let Some(user_id) = value .get_data(DataType::UserId) .as_signed_number() @@ -298,17 +335,20 @@ pub async fn delete_credential_complete( .filter(|v| *v > 0) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId) .await; }; - let requester = value.get_sender() as i64; + let requester = value.require_sender_i64()?; let Some(nonce) = value .get_data(DataType::ServerNonce) .as_signed_number() .and_then(|v| u64::try_from(v).ok()) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge) + .send_error_response( + value.require_id()?, + CommunicationType::ErrorInvalidChallenge, + ) .await; }; let (Some(signature), Some(pq_signature)) = ( @@ -316,7 +356,10 @@ pub async fn delete_credential_complete( value.get_data(DataType::PqSignature).as_bytes(), ) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge) + .send_error_response( + value.require_id()?, + CommunicationType::ErrorInvalidChallenge, + ) .await; }; if !connection.state().consume_challenge( @@ -326,12 +369,15 @@ pub async fn delete_credential_complete( nonce, ) { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge) + .send_error_response( + value.require_id()?, + CommunicationType::ErrorInvalidChallenge, + ) .await; } let Ok(user) = user_repo::get_by_user_id(UserId::from(user_id)).await else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) + .send_error_response(value.require_id()?, CommunicationType::ErrorNotFound) .await; }; let payload = lifecycle_payload(b"tensamin:user-delete:v1\0", user_id, requester, nonce); @@ -339,7 +385,10 @@ pub async fn delete_credential_complete( || verify_ml_dsa(&user.public_key.sig_pq_public_key, &payload, &pq_signature).is_err() { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated) + .send_error_response( + value.require_id()?, + CommunicationType::ErrorNotAuthenticated, + ) .await; } complete_delete(connection, value, user.id).await @@ -349,6 +398,8 @@ pub async fn erase_hosted_user_data_ack( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { + value.require_id()?; + value.require_sender_i64()?; let Some(user_id) = value .get_data(DataType::UserId) .as_signed_number() @@ -356,45 +407,31 @@ pub async fn erase_hosted_user_data_ack( .filter(|v| *v > 0) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId) .await; }; - let iota_id = IotaId::from(value.get_sender() as i64); + let iota_id = IotaId::from(value.require_sender_i64()?); match user_repo::acknowledge_pending_erasure(UserId::from(user_id), iota_id).await { Ok(true) => { connection - .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) + .send( + &CommunicationValue::new(CommunicationType::Success) + .with_id(value.require_id()?), + ) .await } Ok(false) => { connection - .send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated) + .send_error_response( + value.require_id()?, + CommunicationType::ErrorNotAuthenticated, + ) .await } Err(_) => { connection - .send_error_response(value.get_id(), CommunicationType::ErrorInternal) + .send_error_response(value.require_id()?, CommunicationType::ErrorInternal) .await } } } - -/// New lifecycle operation names are intentionally fail-closed until their -/// proof and durable-erasure handlers are enabled. This explicit dispatch -/// prevents either a bare Iota request or the legacy DeleteUser path from -/// acquiring account-deletion authority during a staged rollout. -pub async fn lifecycle_unavailable( - connection: Arc, - value: CommunicationValue, -) -> OmikronResult<()> { - connection - .send( - &CommunicationValue::new(CommunicationType::ErrorNotAuthenticated) - .with_id(value.get_id()) - .add_typed_default( - DataType::ErrorType, - DataValue::Str("user lifecycle proof handler is not enabled".into()), - ), - ) - .await -} diff --git a/src/transport/handlers/links.rs b/src/transport/handlers/links.rs index ad45c05..10df9e4 100644 --- a/src/transport/handlers/links.rs +++ b/src/transport/handlers/links.rs @@ -1,5 +1,5 @@ use super::super::connection::{ - MtpValueCompat, OmikronConnection, OmikronResult, OptionalDataValueCompat, + OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields, }; use crate::server::short_link::add_short_link; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; @@ -17,7 +17,7 @@ pub async fn shorten( .await .map_err(|_| crate::error::OmegaError::Transport("short link error".to_string()))?; let response = CommunicationValue::new(CommunicationType::ShortenLink) - .with_id(value.get_id()) + .with_id(value.require_id()?) .add_typed_default(DataType::Link, DataValue::Str(short)); connection.send(&response).await } diff --git a/src/transport/handlers/notifications.rs b/src/transport/handlers/notifications.rs index 9405bf0..59883c5 100644 --- a/src/transport/handlers/notifications.rs +++ b/src/transport/handlers/notifications.rs @@ -1,5 +1,5 @@ use super::super::connection::{ - MtpValueCompat, OmikronConnection, OmikronResult, OptionalDataValueCompat, + OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields, }; use crate::{db::notification_repo, log, models::UserId}; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; @@ -10,35 +10,36 @@ pub async fn get( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { - let notifications = - match notification_repo::get_notifications(UserId::from(value.get_sender() as i64)).await { - Ok(items) => items - .into_iter() - .map(|item| { - let tm = TypeMap::latest(); - let Some(sender) = DataType::SenderId.try_to_id(&tm) else { - return DataValue::Container(Vec::new()); - }; - let Some(amount) = DataType::Amount.try_to_id(&tm) else { - return DataValue::Container(Vec::new()); - }; - DataValue::Container(vec![ - (sender, DataValue::SignedNumber(item.sender_id.0.into())), - (amount, DataValue::SignedNumber(item.amount.into())), - ]) - }) - .collect(), - Err(error) => { - log!( - crate::util::logger::PrintType::General, - "SQL get_notifications error: {}", - error - ); - Vec::new() - } - }; + let request_id = value.require_id()?; + let sender = value.require_sender_i64()?; + let notifications = match notification_repo::get_notifications(UserId::from(sender)).await { + Ok(items) => items + .into_iter() + .map(|item| { + let tm = TypeMap::latest(); + let Some(sender) = DataType::SenderId.try_to_id(&tm) else { + return DataValue::Container(Vec::new()); + }; + let Some(amount) = DataType::Amount.try_to_id(&tm) else { + return DataValue::Container(Vec::new()); + }; + DataValue::Container(vec![ + (sender, DataValue::SignedNumber(item.sender_id.0.into())), + (amount, DataValue::SignedNumber(item.amount.into())), + ]) + }) + .collect(), + Err(error) => { + log!( + crate::util::logger::PrintType::General, + "SQL get_notifications error: {}", + error + ); + Vec::new() + } + }; let response = CommunicationValue::new(CommunicationType::GetNotifications) - .with_id(value.get_id()) + .with_id(request_id) .add_typed_default(DataType::Notifications, DataValue::Array(notifications)); connection.send(&response).await } @@ -47,19 +48,17 @@ pub async fn read( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { - let receiver = match value.get_sender() { - sender if sender > 0 => sender as i64, - _ => match value.get_data(DataType::ReceiverId).as_number() { - Some(id) => id as i64, - None => return Ok(()), - }, - }; + let request_id = value.require_id()?; + let receiver = value.require_sender_i64()?; let Some(other) = value .get_data(DataType::SenderId) .as_number() - .map(|id| id as i64) + .and_then(|id| i64::try_from(id).ok()) + .filter(|id| *id > 0) else { - return Ok(()); + return connection + .send_error_response(request_id, CommunicationType::ErrorInvalidData) + .await; }; if let Err(error) = notification_repo::read_notification(UserId::from(receiver), UserId::from(other)).await @@ -71,7 +70,7 @@ pub async fn read( ); } else { let response = - CommunicationValue::new(CommunicationType::ReadNotification).with_id(value.get_id()); + CommunicationValue::new(CommunicationType::ReadNotification).with_id(request_id); let _ = connection.send(&response).await; let sync = CommunicationValue::new(CommunicationType::ReadNotification) .with_receiver(receiver as u64) @@ -85,18 +84,54 @@ pub async fn push( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { - let receiver = match value.get_receiver() { - receiver if receiver > 0 => receiver as i64, - _ => match value.get_data(DataType::ReceiverId).as_number() { - Some(id) => id as i64, - None => return Ok(()), - }, + let request_id = value.id().filter(|id| *id != 0); + let receiver = value.require_receiver_i64().ok().or_else(|| { + value + .get_data(DataType::ReceiverId) + .as_number() + .and_then(|id| i64::try_from(id).ok()) + .filter(|id| *id > 0) + }); + let Some(receiver) = receiver else { + if let Some(request_id) = request_id { + return connection + .send_error_response(request_id, CommunicationType::ErrorInvalidData) + .await; + } + return Ok(()); }; - let sender = value + let Some(sender) = value .get_data(DataType::SenderId) .as_number() - .map(|id| id as i64) - .unwrap_or(value.get_sender() as i64); + .and_then(|sender| i64::try_from(sender).ok()) + .filter(|sender| *sender > 0) + else { + if let Some(request_id) = request_id { + return connection + .send_error_response(request_id, CommunicationType::ErrorInvalidData) + .await; + } + return Ok(()); + }; + let source_omikron = connection + .clone() + .get_omikron_id() + .await + .ok_or(crate::OmegaError::NotConnected)?; + if !connection + .state() + .presence + .routes_for_user(sender) + .iter() + .any(|route| route.omikron_id == source_omikron) + { + if let Some(request_id) = request_id { + return connection + .send_error_response(request_id, CommunicationType::ErrorNotAuthenticated) + .await; + } + return Ok(()); + } if let Err(error) = notification_repo::add_notification(UserId::from(receiver), UserId::from(sender)).await { @@ -106,9 +141,11 @@ pub async fn push( error ); } else { - let response = - CommunicationValue::new(CommunicationType::PushNotification).with_id(value.get_id()); - let _ = connection.send(&response).await; + if let Some(request_id) = request_id { + let response = + CommunicationValue::new(CommunicationType::PushNotification).with_id(request_id); + let _ = connection.send(&response).await; + } let push = CommunicationValue::new(CommunicationType::PushNotification) .with_receiver(receiver as u64) .add_typed_default(DataType::SenderId, DataValue::SignedNumber(sender.into())); diff --git a/src/transport/handlers/presence.rs b/src/transport/handlers/presence.rs index 5c631bf..a958a92 100644 --- a/src/transport/handlers/presence.rs +++ b/src/transport/handlers/presence.rs @@ -1,5 +1,5 @@ use super::super::connection::{ - MtpValueCompat, OmikronConnection, OmikronResult, OptionalDataValueCompat, + OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields, }; use crate::{ db::user_repo, log_in, models::IotaId, sql::connection_status::UserStatus, state::OmegaState, @@ -11,7 +11,10 @@ use std::{ }; fn parse_subscription(value: &CommunicationValue) -> Result<(i64, i64, Vec), &'static str> { - let user_id = i64::try_from(value.get_sender()) + let Some(sender) = value.sender() else { + return Err("user_id"); + }; + let user_id = i64::try_from(sender) .ok() .filter(|id| *id > 0) .ok_or("user_id")?; @@ -51,13 +54,11 @@ fn states_for_users(state: &OmegaState, users: &[crate::models::User]) -> HashMa users .iter() .map(|user| { - ( - user.id.0, - state.presence.resolve_public_state( - user.id.0, - user.iota_id.map(|id| id.0).unwrap_or_default(), - ), - ) + let status = user + .iota_id + .map(|iota_id| state.presence.resolve_public_state(user.id.0, iota_id.0)) + .unwrap_or(UserStatus::user_offline); + (user.id.0, status) }) .collect() } @@ -70,9 +71,10 @@ fn changed_states( let mut changes = users .iter() .filter_map(|user| { - let after = state - .presence - .resolve_public_state(user.id.0, user.iota_id.map(|id| id.0).unwrap_or_default()); + let after = user + .iota_id + .map(|iota_id| state.presence.resolve_public_state(user.id.0, iota_id.0)) + .unwrap_or(UserStatus::user_offline); (before.get(&user.id.0) != Some(&after)).then_some((user.id.0, after)) }) .collect::>(); @@ -96,21 +98,6 @@ fn state_notification( .add_typed_default(DataType::UserState, DataValue::Str(user_state.to_string())) } -fn private_state_notification( - user_id: i64, - session_id: i64, - user_state: &UserStatus, -) -> CommunicationValue { - CommunicationValue::new(CommunicationType::ClientChanged) - .with_receiver(user_id as u64) - .add_typed_default( - DataType::SessionId, - DataValue::SignedNumber(session_id.into()), - ) - .add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into())) - .add_typed_default(DataType::UserState, DataValue::Str(user_state.to_string())) -} - async fn publish_state_changes(state: &OmegaState, changes: &[(i64, UserStatus)]) { let mut grouped = BTreeMap::>::new(); for (user_id, user_state) in changes { @@ -143,28 +130,6 @@ async fn publish_changed_states( publish_state_changes(state, &changed_states(state, before, users)).await; } -async fn publish_private_state(state: &OmegaState, user_id: i64, user_state: &UserStatus) { - let mut grouped = BTreeMap::>::new(); - for (session_id, route) in state.presence.sessions_for_user(user_id) { - grouped - .entry(route.omikron_id) - .or_default() - .push(private_state_notification(user_id, session_id, user_state)); - } - for (omikron_id, notifications) in grouped { - if let Err(error) = - crate::transport::omikron_manager::send_state_batch(omikron_id, notifications).await - { - log_in!( - crate::util::logger::PrintType::General, - "Failed to deliver private presence state batch to Omikron {}: {}", - omikron_id, - error - ); - } - } -} - pub async fn state_subscribe( state: Arc, connection: Arc, @@ -175,13 +140,13 @@ pub async fn state_subscribe( Ok(subscription) => subscription, Err("user_id") => { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNoUserId) + .send_error_response(value.require_id()?, CommunicationType::ErrorNoUserId) .await; } Err(detail) => { return connection .send_error_response_with_detail( - value.get_id(), + value.require_id()?, CommunicationType::ErrorInvalidData, detail, ) @@ -190,14 +155,14 @@ pub async fn state_subscribe( }; if !state.presence.owns_session(user_id, session_id, omikron_id) { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNoIota) + .send_error_response(value.require_id()?, CommunicationType::ErrorNoIota) .await; } state .presence .replace_subscription(user_id, session_id, omikron_id, user_ids); connection - .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) + .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.require_id()?)) .await } @@ -242,7 +207,7 @@ pub async fn user_connected( .filter(|id| *id > 0) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) .await; }; let Some(session_id) = value @@ -252,7 +217,7 @@ pub async fn user_connected( .filter(|id| *id > 0) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) .await; }; let Some(iota_id) = value @@ -262,25 +227,25 @@ pub async fn user_connected( .filter(|id| *id > 0) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) .await; }; let user = match user_repo::get_by_user_id(user_id.into()).await { Ok(user) => user, Err(crate::error::OmegaError::NotFound) => { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) + .send_error_response(value.require_id()?, CommunicationType::ErrorNotFound) .await; } - Err(error) => return Err(error.into()), + Err(error) => return Err(error), }; let preferences = match user_repo::get_presence_preferences(&[user_id]).await { Ok(preferences) => preferences, - Err(error) => return Err(error.into()), + Err(error) => return Err(error), }; if user.iota_id.map(|id| id.0) != Some(iota_id) || !state.presence.has_iota_route(iota_id) { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNoIota) + .send_error_response(value.require_id()?, CommunicationType::ErrorNoIota) .await; } apply_preferences(&state, preferences); @@ -291,7 +256,7 @@ pub async fn user_connected( .track_session(user_id, session_id, omikron_id, iota_id); publish_changed_states(&state, &before, &users).await; connection - .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) + .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.require_id()?)) .await } @@ -309,7 +274,7 @@ pub async fn user_disconnected( .filter(|id| *id > 0) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) .await; }; let Some(session_id) = value @@ -319,7 +284,7 @@ pub async fn user_disconnected( .filter(|id| *id > 0) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) .await; }; if let Ok(user) = user_repo::get_by_user_id(user_id.into()).await { @@ -337,137 +302,7 @@ pub async fn user_disconnected( .remove_session(user_id, session_id, omikron_id); } connection - .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) - .await -} - -pub async fn set_user_state( - state: Arc, - connection: Arc, - value: CommunicationValue, - omikron_id: i64, -) -> OmikronResult<()> { - let Some(user_id) = i64::try_from(value.get_sender()).ok().filter(|id| *id > 0) else { - return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNoUserId) - .await; - }; - if let Some(requested_user) = value.get_data(DataType::UserId) { - let Some(requested_user_id) = requested_user - .as_number() - .and_then(|id| i64::try_from(id).ok()) - else { - return connection - .send_error_response_with_detail( - value.get_id(), - CommunicationType::ErrorInvalidData, - "user_id", - ) - .await; - }; - if requested_user_id != user_id { - return connection - .send_error_response_with_detail( - value.get_id(), - CommunicationType::ErrorInvalidData, - "user_id", - ) - .await; - } - } - let Some(iota_id) = value - .get_data(DataType::IotaId) - .as_number() - .and_then(|id| i64::try_from(id).ok()) - else { - return connection - .send_error_response_with_detail( - value.get_id(), - CommunicationType::ErrorInvalidData, - "iota_id", - ) - .await; - }; - let Some(requested_state) = value - .get_data(DataType::UserState) - .as_str() - .and_then(UserStatus::from_client_preference) - else { - return connection - .send_error_response_with_detail( - value.get_id(), - CommunicationType::ErrorInvalidData, - "user_state", - ) - .await; - }; - if !state.presence.has_iota_route(iota_id) { - return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNoIota) - .await; - } - let Some(session_id) = value - .get_data(DataType::SessionId) - .as_number() - .and_then(|id| i64::try_from(id).ok()) - .filter(|id| *id > 0) - else { - return connection - .send_error_response_with_detail( - value.get_id(), - CommunicationType::ErrorInvalidData, - "session_id", - ) - .await; - }; - let Some(route) = state.presence.session_route(user_id, session_id) else { - return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNoIota) - .await; - }; - if route.omikron_id != omikron_id || route.iota_id != iota_id { - return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) - .await; - } - if !state.presence.has_active_session_for_iota(user_id, iota_id) { - return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNoIota) - .await; - } - let previous_preference = state.presence.preference(user_id); - let previous_state = state.presence.resolve_public_state(user_id, iota_id); - if let Err(error) = - user_repo::change_presence_preference(user_id.into(), requested_state.to_string()).await - { - log_in!( - crate::util::logger::PrintType::General, - "Failed to persist presence preference: {}", - error - ); - return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInternal) - .await; - } - state - .presence - .set_preference(user_id, requested_state.clone()); - let new_state = state.presence.resolve_public_state(user_id, iota_id); - if requested_state != previous_preference { - publish_private_state(&state, user_id, &requested_state).await; - } - if requested_state != previous_preference && new_state != previous_state { - publish_state_changes(&state, &[(user_id, new_state)]).await; - } - connection - .send( - &CommunicationValue::new(CommunicationType::Success) - .with_id(value.get_id()) - .add_typed_default( - DataType::UserState, - DataValue::Str(requested_state.to_string()), - ), - ) + .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.require_id()?)) .await } @@ -481,10 +316,11 @@ pub async fn iota_connected( let Some(iota_id) = value .get_data(DataType::IotaId) .as_number() - .map(|id| id as i64) + .and_then(|id| i64::try_from(id).ok()) + .filter(|id| *id > 0) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) .await; }; let users = user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await?; @@ -503,7 +339,7 @@ pub async fn iota_connected( crate::transport::omikron_manager::deliver_pending_erasures(iota_id).await; publish_changed_states(&state, &before, &users).await; connection - .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) + .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.require_id()?)) .await } @@ -517,10 +353,11 @@ pub async fn iota_disconnected( let Some(iota_id) = value .get_data(DataType::IotaId) .as_number() - .map(|id| id as i64) + .and_then(|id| i64::try_from(id).ok()) + .filter(|id| *id > 0) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) .await; }; let users = user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await?; @@ -530,7 +367,7 @@ pub async fn iota_disconnected( state.presence.untrack_iota_connection(iota_id, omikron_id); publish_changed_states(&state, &before, &users).await; connection - .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) + .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.require_id()?)) .await } @@ -540,7 +377,7 @@ pub async fn sync_status( value: CommunicationValue, omikron_id: i64, ) -> OmikronResult<()> { - let request_id = value.get_id(); + let request_id = value.require_id()?; let Some(DataValue::Array(iota_values)) = value.get_data(DataType::IotaIds) else { return connection .send_error_response(request_id, CommunicationType::ErrorInvalidData) diff --git a/src/transport/handlers/register.rs b/src/transport/handlers/register.rs index 8799893..2c2501a 100644 --- a/src/transport/handlers/register.rs +++ b/src/transport/handlers/register.rs @@ -1,5 +1,5 @@ use super::super::connection::{ - MtpValueCompat, OmikronConnection, OmikronResult, OptionalDataValueCompat, + OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields, }; use crate::{ db::{iota_repo, user_repo}, @@ -22,13 +22,13 @@ pub async fn get_register( .filter(|id| user_repo::valid_protocol_id(*id)); let Some(iota_id) = iota_id else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) .await; }; let (register_id, registration_token) = - user_repo::allocate_registration(IotaId::from(iota_id), value.get_id()).await?; + user_repo::allocate_registration(IotaId::from(iota_id), value.require_id()?).await?; let response = CommunicationValue::new(CommunicationType::GetRegister) - .with_id(value.get_id()) + .with_id(value.require_id()?) .add_typed_default( DataType::UserId, DataValue::SignedNumber(register_id.0.into()), @@ -47,7 +47,7 @@ pub async fn complete_iota( .and_then(|key| PublicKeyBundle::from_base64(key).ok()); let Some(public_key) = public_key else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) .await; }; match iota_repo::create_new_iota(public_key).await { @@ -55,7 +55,7 @@ pub async fn complete_iota( connection .send( &CommunicationValue::new(CommunicationType::CompleteRegisterIota) - .with_id(value.get_id()) + .with_id(value.require_id()?) .add_typed_default(DataType::IotaId, DataValue::SignedNumber(id.0.into())), ) .await @@ -64,7 +64,7 @@ pub async fn complete_iota( connection .send( &CommunicationValue::new(CommunicationType::ErrorInternal) - .with_id(value.get_id()) + .with_id(value.require_id()?) .add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())), ) .await @@ -108,7 +108,7 @@ pub async fn complete_user( }) else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) .await; }; // Omikron supplies the authenticated Iota ID in the payload. The lease @@ -120,7 +120,7 @@ pub async fn complete_user( .filter(|id| user_repo::valid_protocol_id(*id)); let Some(iota_id) = iota_id else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) .await; }; @@ -136,14 +136,17 @@ pub async fn complete_user( { Ok(()) => { connection - .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) + .send( + &CommunicationValue::new(CommunicationType::Success) + .with_id(value.require_id()?), + ) .await } Err(error) => { connection .send( &CommunicationValue::new(CommunicationType::ErrorInternal) - .with_id(value.get_id()) + .with_id(value.require_id()?) .add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())), ) .await diff --git a/src/transport/handlers/states.rs b/src/transport/handlers/states.rs index e1fc12d..0b46254 100644 --- a/src/transport/handlers/states.rs +++ b/src/transport/handlers/states.rs @@ -1,5 +1,5 @@ use super::super::connection::{ - MtpValueCompat, OmikronConnection, OmikronResult, OptionalDataValueCompat, + OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields, }; use crate::db::user_repo; use mtp::{ @@ -34,7 +34,7 @@ pub async fn get( let Some(DataValue::Array(ids)) = value.get_data(DataType::UserIds) else { return send_error( connection, - value.get_id(), + value.require_id()?, CommunicationType::ErrorInvalidData, None, ) @@ -47,7 +47,7 @@ pub async fn get( if session_id.is_none() && !legacy_peer { return send_error( connection, - value.get_id(), + value.require_id()?, CommunicationType::ErrorInvalidData, None, ) @@ -60,7 +60,7 @@ pub async fn get( let DataValue::SignedNumber(id) = id else { return send_error( connection, - value.get_id(), + value.require_id()?, CommunicationType::ErrorInvalidData, session_id, ) @@ -69,7 +69,7 @@ pub async fn get( let Ok(user_id) = i64::try_from(*id) else { return send_error( connection, - value.get_id(), + value.require_id()?, CommunicationType::ErrorInvalidData, session_id, ) @@ -78,7 +78,7 @@ pub async fn get( if user_id <= 0 { return send_error( connection, - value.get_id(), + value.require_id()?, CommunicationType::ErrorInvalidData, session_id, ) @@ -95,7 +95,7 @@ pub async fn get( Err(_) => { return send_error( connection, - value.get_id(), + value.require_id()?, CommunicationType::ErrorInternal, session_id, ) @@ -110,9 +110,10 @@ pub async fn get( missing_user_ids.push(user_id); continue; }; - let status = state - .presence - .resolve_public_state(user_id, user.iota_id.map(|id| id.0).unwrap_or_default()) + let status = user + .iota_id + .map(|iota_id| state.presence.resolve_public_state(user_id, iota_id.0)) + .unwrap_or(crate::sql::connection_status::UserStatus::user_offline) .to_string(); let mut map = Vec::new(); if let Some(kind) = DataType::UserId.try_to_id(&tm) { @@ -124,7 +125,7 @@ pub async fn get( states.push(DataValue::Container(map)); } let response = CommunicationValue::new(CommunicationType::GetStates) - .with_id(value.get_id()) + .with_id(value.require_id()?) .add_typed_default(DataType::UserStates, DataValue::Array(states)); let response = if let Some(session_id) = session_id { response.add_typed_default(DataType::SessionId, DataValue::SignedNumber(session_id)) diff --git a/src/transport/handlers/user_data.rs b/src/transport/handlers/user_data.rs index 1c248fa..e6b676a 100644 --- a/src/transport/handlers/user_data.rs +++ b/src/transport/handlers/user_data.rs @@ -1,5 +1,5 @@ use super::super::connection::{ - MtpValueCompat, OmikronConnection, OmikronResult, OptionalDataValueCompat, + OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields, }; use crate::{ db::{iota_repo, user_repo}, @@ -29,11 +29,16 @@ pub async fn get_user( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { + let request_id = value.require_id()?; + let sender = value.require_sender_i64()?; let state = connection.state(); - let user = if let Some(id) = value.get_data(DataType::UserId).as_number() { - user_repo::get_by_user_id(UserId::from(id as i64)) - .await - .ok() + let user = if let Some(id) = value + .get_data(DataType::UserId) + .as_number() + .and_then(|id| i64::try_from(id).ok()) + .filter(|id| *id > 0) + { + user_repo::get_by_user_id(UserId::from(id)).await.ok() } else if let Some(name) = value.get_data(DataType::Username).as_str() { user_repo::get_by_username(name).await.ok() } else { @@ -41,7 +46,7 @@ pub async fn get_user( }; let Some(user) = user else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) + .send_error_response(request_id, CommunicationType::ErrorNotFound) .await; }; let id = user.id.0; @@ -52,11 +57,11 @@ pub async fn get_user( .filter(|name| !name.is_empty()) .unwrap_or_else(|| username.clone()); let mut response = CommunicationValue::new(CommunicationType::GetUserData) - .with_id(value.get_id()) + .with_id(request_id) .add_typed_default(DataType::Username, DataValue::Str(username)) .add_typed_default( DataType::PublicKey, - DataValue::Str(user.public_key.to_base64()), + DataValue::Str(user.public_key.try_to_base64()?), ) .add_typed_default(DataType::UserId, DataValue::SignedNumber(id.into())) .add_typed_default(DataType::Display, DataValue::Str(display)) @@ -79,7 +84,7 @@ pub async fn get_user( response.add_typed_default(DataType::Avatar, DataValue::Str(STANDARD.encode(avatar))); } let route = state.presence.user_route(id); - let private_request = value.get_sender() as i64 == id; + let private_request = sender == id; let resolved_status = if private_request { if !state .presence @@ -125,13 +130,21 @@ pub async fn get_iota( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { - let found = if let Some(id) = value.get_data(DataType::IotaId).as_number() { - iota_repo::get_iota_by_id(IotaId::from(id as i64)) + let request_id = value.require_id()?; + let found = if let Some(id) = value + .get_data(DataType::IotaId) + .as_number() + .and_then(|id| i64::try_from(id).ok()) + .filter(|id| *id > 0) + { + iota_repo::get_iota_by_id(IotaId::from(id)) .await .ok() .map(|iota| (iota.id.0, iota.public_key, None, None)) } else if let Some(id) = value.get_data(DataType::UserId).as_number() { - if let Ok(user) = user_repo::get_by_user_id(UserId::from(id as i64)).await { + if let Some(id) = i64::try_from(id).ok().filter(|id| *id > 0) + && let Ok(user) = user_repo::get_by_user_id(UserId::from(id)).await + { match user.iota_id { Some(iota_id) => iota_repo::get_iota_by_id(iota_id) .await @@ -163,12 +176,12 @@ pub async fn get_iota( }; let Some((id, key, user_id, username)) = found else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) + .send_error_response(request_id, CommunicationType::ErrorNotFound) .await; }; let mut response = CommunicationValue::new(CommunicationType::GetIotaData) - .with_id(value.get_id()) - .add_typed_default(DataType::PublicKey, DataValue::Str(key.to_base64())) + .with_id(request_id) + .add_typed_default(DataType::PublicKey, DataValue::Str(key.try_to_base64()?)) .add_typed_default(DataType::IotaId, DataValue::SignedNumber(id.into())) .add_typed_default(DataType::OmikronConnections, connections(&connection, id)); if let Some(user_id) = user_id { @@ -185,7 +198,8 @@ async fn update_user( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { - let id = UserId::from(value.get_sender() as i64); + let request_id = value.require_id()?; + let id = UserId::from(value.require_sender_i64()?); let mut error = None; if let Some(name) = value.get_data(DataType::Username).as_str() { error = user_repo::change_username(id, name.to_owned()) @@ -193,56 +207,55 @@ async fn update_user( .err() .map(|e| e.to_string()); } - if error.is_none() { - if let Some(name) = value.get_data(DataType::Display).as_str() { - error = user_repo::change_display_name(id, name.to_owned()) - .await - .err() - .map(|e| e.to_string()); - } + if error.is_none() + && let Some(name) = value.get_data(DataType::Display).as_str() + { + error = user_repo::change_display_name(id, name.to_owned()) + .await + .err() + .map(|e| e.to_string()); } - if error.is_none() { - if let Some(avatar) = value.get_data(DataType::Avatar).as_str() { - error = user_repo::change_avatar(id, avatar.to_owned()) - .await - .err() - .map(|e| e.to_string()); - } + if error.is_none() + && let Some(avatar) = value.get_data(DataType::Avatar).as_str() + { + error = user_repo::change_avatar(id, avatar.to_owned()) + .await + .err() + .map(|e| e.to_string()); } - if error.is_none() { - if let Some(about) = value.get_data(DataType::About).as_str() { - error = user_repo::change_about(id, about.to_owned()) - .await - .err() - .map(|e| e.to_string()); - } + if error.is_none() + && let Some(about) = value.get_data(DataType::About).as_str() + { + error = user_repo::change_about(id, about.to_owned()) + .await + .err() + .map(|e| e.to_string()); } - if error.is_none() { - if let Some(status) = value.get_data(DataType::Status).as_str() { - error = user_repo::change_status(id, status.to_owned()) - .await - .err() - .map(|e| e.to_string()); - } + if error.is_none() + && let Some(status) = value.get_data(DataType::Status).as_str() + { + error = user_repo::change_status(id, status.to_owned()) + .await + .err() + .map(|e| e.to_string()); } - if error.is_none() { - if let Some(key) = value + if error.is_none() + && let Some(key) = value .get_data(DataType::PublicKey) .as_str() .and_then(|key| PublicKeyBundle::from_base64(key).ok()) - { - error = user_repo::change_keys(id, key) - .await - .err() - .map(|e| e.to_string()); - } + { + error = user_repo::change_keys(id, key) + .await + .err() + .map(|e| e.to_string()); } let response = match error { None => CommunicationValue::new(CommunicationType::Success), Some(error) => CommunicationValue::new(CommunicationType::ErrorInternal) .add_typed_default(DataType::ErrorType, DataValue::Str(error)), }; - connection.send(&response.with_id(value.get_id())).await + connection.send(&response.with_id(request_id)).await } pub async fn change_user( @@ -256,34 +269,35 @@ pub async fn change_iota( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { + let request_id = value.require_id()?; let reset_data = value.get_data(DataType::ResetToken); let Some(reset) = reset_data.as_str() else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) .await; }; let new_token_data = value.get_data(DataType::NewToken); let Some(new_token) = new_token_data.as_str() else { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) + .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) .await; }; - let user_id = UserId::from(value.get_sender() as i64); + let user_id = UserId::from(value.require_sender_i64()?); let user = match user_repo::get_by_user_id(user_id).await { Ok(user) => user, Err(_) => { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) + .send_error_response(request_id, CommunicationType::ErrorNotFound) .await; } }; if user.token != reset { return connection - .send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge) + .send_error_response(request_id, CommunicationType::ErrorInvalidChallenge) .await; } let result = - match user_repo::change_iota_id(user_id, Some(IotaId::from(value.get_sender() as i64))) + match user_repo::change_iota_id(user_id, Some(IotaId::from(value.require_sender_i64()?))) .await { Ok(()) => user_repo::change_token(user_id, new_token.to_owned()).await, @@ -294,5 +308,5 @@ pub async fn change_iota( Err(error) => CommunicationValue::new(CommunicationType::ErrorInternal) .add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())), }; - connection.send(&response.with_id(value.get_id())).await + connection.send(&response.with_id(request_id)).await } diff --git a/src/transport/omikron_connection.rs b/src/transport/omikron_connection.rs index 82b9ece..83a9df7 100644 --- a/src/transport/omikron_connection.rs +++ b/src/transport/omikron_connection.rs @@ -1,12 +1,13 @@ use super::capabilities::{OmegaCapabilities, PeerCapabilities}; use crate::models::OmikronId; use crate::{ - load_keyring, log, log_cv_in, log_cv_out, log_err, log_in, server, + log, log_cv_in, log_cv_out, log_err, log_in, server, state::OmegaState, + transport::connection::{RequiredMtpFields, validate_dispatch_fields}, transport::omikron_manager, util::{file_util::load_file_vec, logger::PrintType}, }; -use dashmap::DashMap; +use dashmap::{DashMap, mapref::entry::Entry}; use mtp::{ codec::{CommunicationType, CommunicationValue}, crypto::PublicKeyBundle, @@ -16,19 +17,21 @@ use mtp::{ use std::{ net::{IpAddr, Ipv4Addr}, sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, + Arc, Mutex as StdMutex, + atomic::{AtomicU32, AtomicUsize, Ordering}, }, time::{Duration, Instant}, }; use tokio::{ - sync::{Mutex, mpsc}, + sync::{Mutex, Semaphore, mpsc, oneshot}, time::interval, }; +use tokio_util::{sync::CancellationToken, task::TaskTracker}; const CLEANUP_INTERVAL: Duration = Duration::from_secs(30); const MAX_WAITING_AGE: Duration = Duration::from_secs(60); static ACTIVE_CONNECTIONS: AtomicUsize = AtomicUsize::new(0); +static NEXT_CORRELATION_ID: AtomicU32 = AtomicU32::new(1); static ACTIVE_CONNECTIONS_BY_IP: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(DashMap::new); @@ -47,16 +50,27 @@ impl Drop for ConnectionLimitGuard { pub type OmikronResult = crate::error::Result; pub struct WaitingTask { - pub task: Box, CommunicationValue) -> bool + Send + Sync>, + pub sender: oneshot::Sender, pub inserted_at: Instant, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum DispatchClass { + Concurrent, + Ordered, +} + pub struct OmikronConnection { id: u64, state: Arc, sender: Mutex>, waiting_tasks: DashMap, cleanup_handle: std::sync::Mutex>>, + handler_tasks: TaskTracker, + handler_cancel: CancellationToken, + handler_limit: Arc, + ordered_sender: mpsc::Sender, + ordered_receiver: StdMutex>>, peer_capabilities: PeerCapabilities, } impl Drop for OmikronConnection { @@ -64,6 +78,9 @@ impl Drop for OmikronConnection { if let Some(handle) = self.cleanup_handle.lock().unwrap().take() { handle.abort(); } + self.handler_cancel.cancel(); + self.handler_tasks.close(); + self.waiting_tasks.clear(); } } @@ -75,17 +92,24 @@ impl OmikronConnection { state: Arc, authenticated: bool, ) -> Option> { - if !authenticated { + if !authenticated || id == 0 || i64::try_from(id).is_err() { return None; } let peer_capabilities = PeerCapabilities::from_identification_description(description).ok()?; + let handler_concurrency = state.omikron_handler_concurrency; + let (ordered_sender, ordered_receiver) = mpsc::channel(handler_concurrency); Some(Arc::new(Self { id, state, sender: Mutex::new(Some(sender)), waiting_tasks: DashMap::new(), cleanup_handle: std::sync::Mutex::new(None), + handler_tasks: TaskTracker::new(), + handler_cancel: CancellationToken::new(), + handler_limit: Arc::new(Semaphore::new(handler_concurrency)), + ordered_sender, + ordered_receiver: StdMutex::new(Some(ordered_receiver)), peer_capabilities, })) } @@ -94,6 +118,115 @@ impl OmikronConnection { &self.peer_capabilities } + fn track_handler(&self, task: F) + where + F: std::future::Future + Send + 'static, + { + let handle = self.handler_tasks.spawn(task); + let id = self.id as i64; + tokio::spawn(async move { + if let Err(error) = handle.await { + log_err!( + id, + PrintType::Omega, + "Omikron handler task failed: {}", + error + ); + } + }); + } + + fn start_ordered_worker(self: &Arc) { + let Some(mut receiver) = self.ordered_receiver.lock().unwrap().take() else { + return; + }; + let connection = self.clone(); + let cancellation = self.handler_cancel.clone(); + self.track_handler(async move { + loop { + let value = tokio::select! { + _ = cancellation.cancelled() => break, + value = receiver.recv() => match value { + Some(value) => value, + None => break, + }, + }; + let global_permit = tokio::select! { + _ = cancellation.cancelled() => break, + permit = connection.state.global_handler_limit.clone().acquire_owned() => { + match permit { + Ok(permit) => permit, + Err(_) => break, + } + } + }; + let permit = tokio::select! { + _ = cancellation.cancelled() => break, + permit = connection.handler_limit.clone().acquire_owned() => { + match permit { + Ok(permit) => permit, + Err(_) => break, + } + } + }; + let result = tokio::select! { + _ = cancellation.cancelled() => break, + result = connection.clone().dispatch(value) => result, + }; + drop(permit); + drop(global_permit); + if let Err(error) = result { + log_err!( + connection.id as i64, + PrintType::Omega, + "Error processing ordered Omikron message: {}", + error + ); + } + } + }); + } + + async fn stop_handlers(&self) { + self.handler_cancel.cancel(); + self.handler_tasks.close(); + let _ = tokio::time::timeout(Duration::from_secs(1), self.handler_tasks.wait()).await; + self.waiting_tasks.clear(); + } + + fn correlation_response(value: &CommunicationValue) -> bool { + matches!( + value.get_comm_type_enum(), + Some(CommunicationType::Success) + | Some(CommunicationType::ErrorProtocol) + | Some(CommunicationType::ErrorAnonymous) + | Some(CommunicationType::ErrorInternal) + | Some(CommunicationType::ErrorInvalidData) + | Some(CommunicationType::ErrorInvalidUserId) + | Some(CommunicationType::ErrorInvalidOmikronId) + | Some(CommunicationType::ErrorNotFound) + | Some(CommunicationType::ErrorNotAuthenticated) + | Some(CommunicationType::ErrorNoIota) + | Some(CommunicationType::ErrorInvalidChallenge) + | Some(CommunicationType::ErrorInvalidSecret) + | Some(CommunicationType::ErrorInvalidPrivateKey) + | Some(CommunicationType::ErrorInvalidPublicKey) + | Some(CommunicationType::ErrorNoUserId) + | Some(CommunicationType::ErrorNoCallId) + | Some(CommunicationType::ErrorInvalidCallId) + ) + } + + fn next_correlation_id(&self) -> Option { + for _ in 0..u32::MAX { + let id = NEXT_CORRELATION_ID.fetch_add(1, Ordering::Relaxed); + if id != 0 && !self.waiting_tasks.contains_key(&id) { + return Some(id); + } + } + None + } + pub async fn handle(self: Arc, receiver: &mut WebMtpReceiver) { log_in!( self.id as i64, @@ -117,12 +250,16 @@ impl OmikronConnection { self.clone().cleanup().await; return; } - let cleanup_conn = self.clone(); + self.start_ordered_worker(); + let cleanup_conn = Arc::downgrade(&self); *self.cleanup_handle.lock().unwrap() = Some(tokio::spawn(async move { let mut ticker = interval(CLEANUP_INTERVAL); loop { ticker.tick().await; - cleanup_conn + let Some(connection) = cleanup_conn.upgrade() else { + break; + }; + connection .waiting_tasks .retain(|_, task| task.inserted_at.elapsed() < MAX_WAITING_AGE); } @@ -164,16 +301,81 @@ impl OmikronConnection { async fn process_message(self: Arc, value: CommunicationValue) -> OmikronResult<()> { log_cv_in!(PrintType::Omikron, &value); - if value.is_type(CommunicationType::Relay) { - return self.dispatch(value).await; - } - if let Some(message_id) = value.id() + if Self::correlation_response(&value) + && let Some(message_id) = value.id() && let Some((_, task)) = self.waiting_tasks.remove(&message_id) { - let _ = (task.task)(self.clone(), value); + let _ = task.sender.send(value); return Ok(()); } - self.dispatch(value).await + + if !value.is_type(CommunicationType::Relay) { + validate_dispatch_fields(&value)?; + } + + let dispatch_class = Self::dispatch_class(&value); + if dispatch_class == DispatchClass::Ordered { + tokio::select! { + _ = self.handler_cancel.cancelled() => { + return Err(crate::error::OmegaError::NotConnected); + } + result = self.ordered_sender.send(value) => { + result.map_err(|_| crate::error::OmegaError::NotConnected)?; + } + } + return Ok(()); + } + + let global_permit = tokio::select! { + _ = self.handler_cancel.cancelled() => { + return Err(crate::error::OmegaError::NotConnected); + } + permit = self.state.global_handler_limit.clone().acquire_owned() => { + permit.map_err(|_| crate::error::OmegaError::NotConnected)? + } + }; + let permit = tokio::select! { + _ = self.handler_cancel.cancelled() => { + drop(global_permit); + return Err(crate::error::OmegaError::NotConnected); + } + permit = self.handler_limit.clone().acquire_owned() => { + permit.map_err(|_| crate::error::OmegaError::NotConnected)? + } + }; + let connection = self.clone(); + let cancellation = self.handler_cancel.clone(); + self.track_handler(async move { + tokio::select! { + _ = cancellation.cancelled() => {} + result = connection.clone().dispatch(value) => { + if let Err(error) = result { + log_err!( + connection.id as i64, + PrintType::Omega, + "Error processing Omikron message: {}", + error + ); + } + } + } + drop(permit); + drop(global_permit); + }); + Ok(()) + } + + fn dispatch_class(value: &CommunicationValue) -> DispatchClass { + match value.get_comm_type_enum() { + Some(CommunicationType::UserConnected) + | Some(CommunicationType::UserDisconnected) + | Some(CommunicationType::IotaConnected) + | Some(CommunicationType::IotaDisconnected) + | Some(CommunicationType::SyncClientIotaStatus) + | Some(CommunicationType::StateSubscribe) + | Some(CommunicationType::ClientChanged) => DispatchClass::Ordered, + _ => DispatchClass::Concurrent, + } } async fn dispatch(self: Arc, value: CommunicationValue) -> OmikronResult<()> { @@ -341,33 +543,39 @@ impl OmikronConnection { value: &CommunicationValue, timeout: Duration, ) -> OmikronResult { - let (tx, mut rx) = mpsc::channel(1); - let message_id = value.id().unwrap_or_default(); - let task_tx = tx.clone(); - self.waiting_tasks.insert( - message_id, - WaitingTask { - task: Box::new(move |_, response| { - let task_tx = task_tx.clone(); - tokio::spawn(async move { - let _ = task_tx.send(response).await; - }); - true - }), - inserted_at: Instant::now(), - }, - ); + value.require_id()?; + let (tx, rx) = oneshot::channel(); + let message_id = self.next_correlation_id().ok_or_else(|| { + crate::OmegaError::Transport("no relay correlation id available".into()) + })?; + match self.waiting_tasks.entry(message_id) { + Entry::Vacant(entry) => { + entry.insert(WaitingTask { + sender: tx, + inserted_at: Instant::now(), + }); + } + Entry::Occupied(_) => { + return Err(crate::OmegaError::Transport( + "relay correlation id collision".into(), + )); + } + } + let outbound = value.clone().with_id(message_id); - if let Err(error) = self.clone().send(value).await { + if let Err(error) = self.clone().send(&outbound).await { self.waiting_tasks.remove(&message_id); return Err(error); } - match tokio::time::timeout(timeout, rx.recv()).await { - Ok(Some(response)) => Ok(response), - Ok(None) => Err(crate::error::OmegaError::Transport( - "Relay response channel closed".into(), - )), + match tokio::time::timeout(timeout, rx).await { + Ok(Ok(response)) => Ok(response), + Ok(Err(_)) => { + self.waiting_tasks.remove(&message_id); + Err(crate::error::OmegaError::Transport( + "Relay response channel closed".into(), + )) + } Err(_) => { self.waiting_tasks.remove(&message_id); Err(crate::error::OmegaError::Transport( @@ -411,6 +619,7 @@ impl OmikronConnection { } } async fn cleanup(self: Arc) { + self.stop_handlers().await; if self.id != 0 { log_in!(self.id as i64, PrintType::Omega, "Omikron disconnected"); if omikron_manager::remove_omikron(self.id as i64, &self).await { @@ -441,7 +650,8 @@ pub async fn get_by_omikron_id( description: Option, ) -> Option { PeerCapabilities::from_identification_description(description.as_deref()).ok()?; - crate::db::omikron_repo::get_omikron_by_id(OmikronId::from(omikron_id as i64)) + let omikron_id = i64::try_from(omikron_id).ok().filter(|id| *id > 0)?; + crate::db::omikron_repo::get_omikron_by_id(OmikronId::from(omikron_id)) .await .ok() .map(|omikron| omikron.public_key) @@ -453,7 +663,7 @@ pub async fn complete_register(_: PublicKeyBundle, _: Option) -> u64 { pub async fn start(port: u16, state: Arc) -> Result<(), Box> { let cert_pem = load_file_vec("certs", "cert.pem")?; let key_pem = load_file_vec("certs", "key.pem")?; - let web_config = server::server::build_web_config()? + let web_config = server::web::build_web_config(state.identity.clone())? .serve_tcp_https(true) .max_tcp_connections(256); let ip = IpAddr::from(Ipv4Addr::new(0, 0, 0, 0)); @@ -478,7 +688,7 @@ pub async fn start(port: u16, state: Arc) -> Result<(), Box) -> Result<(), Box) -> Result<(), Box config.transport_connections || peer_active.is_some_and(|count| count > config.transport_connections_per_ip) { - drop(ConnectionLimitGuard(peer_ip)); + drop(connection_limit_guard); log_err!( 0, PrintType::Omega, @@ -526,6 +737,7 @@ pub async fn start(port: u16, state: Arc) -> Result<(), Box) -> Result<(), Box>> = - Lazy::new(|| DashMap::new()); + Lazy::new(DashMap::new); pub async fn add_omikron(conn: Arc) { let id = match conn.clone().get_omikron_id().await { @@ -64,12 +64,11 @@ pub async fn get_all_connections() .map_err(|_| ())?; for user in users { for route in state.presence.routes_for_user(user.id.0) { - if let Some(iotas) = result.get_mut(&route.omikron_id) { - if let Some(iota_id) = user.iota_id - && let Some(users) = iotas.get_mut(&iota_id.0) - { - users.push(user.id.0); - } + if let Some(iotas) = result.get_mut(&route.omikron_id) + && let Some(iota_id) = user.iota_id + && let Some(users) = iotas.get_mut(&iota_id.0) + { + users.push(user.id.0); } } } @@ -97,10 +96,10 @@ pub async fn send_state_batch( pub async fn get_random_omikron() -> Result, ()> { let keys: Vec<_> = OMIKRON_CONNECTIONS.iter().map(|e| *e.key()).collect(); - if let Some(key) = keys.into_iter().choose(&mut rand::rng()) { - if let Some(connection) = get_connected_omikron(key) { - return Ok(connection); - } + if let Some(key) = keys.into_iter().choose(&mut rand::rng()) + && let Some(connection) = get_connected_omikron(key) + { + return Ok(connection); } Err(()) diff --git a/src/transport/relay_router.rs b/src/transport/relay_router.rs index 3d76705..0cbd1f2 100644 --- a/src/transport/relay_router.rs +++ b/src/transport/relay_router.rs @@ -21,14 +21,6 @@ pub enum RouteTarget { } impl RouteTarget { - pub fn wire_id(self) -> Option { - let (kind, id) = match self { - Self::User(id) => (USER_TARGET_KIND, id), - Self::Iota(id) => (IOTA_TARGET_KIND, id), - }; - (id > 0 && id <= TARGET_ID_MASK).then_some(kind | id) - } - pub fn from_wire_id(value: u64) -> Option { let id = value & TARGET_ID_MASK; if id == 0 || value & !(TARGET_KIND_MASK | TARGET_ID_MASK) != 0 { @@ -40,12 +32,6 @@ impl RouteTarget { _ => None, } } - - pub const fn id(self) -> u64 { - match self { - Self::User(id) | Self::Iota(id) => id, - } - } } #[derive(Debug, Error)] @@ -183,10 +169,11 @@ mod tests { use mtp::codec::DataValue; fn wire(target: RouteTarget) -> u64 { - let Some(value) = target.wire_id() else { - panic!("valid route target was rejected"); + let (kind, id) = match target { + RouteTarget::User(id) => (USER_TARGET_KIND, id), + RouteTarget::Iota(id) => (IOTA_TARGET_KIND, id), }; - value + kind | id } fn relay_frame() -> CommunicationValue { diff --git a/src/util/file_util.rs b/src/util/file_util.rs index 8630489..05b9018 100644 --- a/src/util/file_util.rs +++ b/src/util/file_util.rs @@ -1,293 +1,15 @@ use once_cell::sync::Lazy; -use std::fs::{self, File}; -use std::io::{self, BufReader, Read}; -use std::path::{Path, PathBuf}; -use tokio::io::AsyncWriteExt; -use uuid::Uuid; -use zip::ZipArchive; - -use crate::log; +use std::path::PathBuf; static WORKING_DIR: Lazy = Lazy::new(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))); -pub fn delete_file(path: &str, name: &str) -> bool { - let dir = Path::new(&get_directory()).join(path); - let file = dir.join(name); - if !file.exists() { - return false; - } - fs::remove_file(file).is_ok() -} - -#[allow(dead_code)] -pub fn delete_directory(path: &str) -> bool { - let dir = Path::new(&get_directory()).join(path); - delete_dir_recursive(&dir) -} - -#[allow(dead_code)] -fn delete_dir_recursive(directory: &Path) -> bool { - if !directory.exists() { - return false; - } - if let Err(e) = fs::remove_dir_all(directory) { - log!( - "[IMPORTANT] Couldn't delete directory {}: {}", - directory.display(), - e - ); - return false; - } - true -} - -#[allow(dead_code)] -pub fn delete_user_directory(user_id: Uuid) { - let user_dir = Path::new(&get_directory()) - .join("users") - .join(user_id.to_string()); - let _ = delete_dir_recursive(&user_dir); -} - -pub fn load_file_buf(path: &str, name: &str) -> io::Result> { - let dir = Path::new(&get_directory()).join(path); - let file_path = dir.join(name); - - // Ensure the directory exists, create if necessary - if !dir.exists() { - if let Err(_) = fs::create_dir_all(&dir) { - return Err(io::Error::new( - io::ErrorKind::NotFound, - "Directory creation failed", - )); - } - } - - // Create the file if it doesn't exist - if !file_path.exists() { - return Err(io::Error::new( - io::ErrorKind::NotFound, - "File creation failed", - )); - } - - // Open the file and return a BufReader for efficient reading - let file = File::open(&file_path)?; - Ok(BufReader::new(file)) -} -pub fn has_file(path: &str, name: &str) -> bool { - let dir = Path::new(&get_directory()).join(path); - let file_path = dir.join(name); - - if !dir.exists() { - return false; - } - - if !file_path.exists() { - return false; - } - - true -} -pub fn has_dir(path: &str) -> bool { - let dir = Path::new(&get_directory()).join(path); - - if !dir.exists() { - return false; - } - - true -} - -pub fn load_file(path: &str, name: &str) -> String { - let dir = Path::new(&get_directory()).join(path); - let file_path = dir.join(name); - - if !dir.exists() { - if let Err(e) = fs::create_dir_all(&dir) { - log!("[IMPORTANT] Couldn't create directories: {}", e); - return String::new(); - } - return String::new(); - } - - if !file_path.exists() { - if let Err(e) = File::create(&file_path) { - log!("[IMPORTANT] Couldn't create file: {}", e); - } - return String::new(); - } - - let mut content = String::new(); - if let Ok(mut f) = File::open(&file_path) { - let _ = f.read_to_string(&mut content); - } - content -} - pub fn load_file_vec(path: &str, name: &str) -> Result, std::io::Error> { - let dir = Path::new(&get_directory()).join(path); + let dir = PathBuf::from(get_directory()).join(path); let file_path = dir.join(name); - std::fs::read(file_path) } -pub fn save_file(path: &str, name: &str, value: &str) { - let dir = Path::new(&get_directory()).join(path); - let file_path = dir.join(name); - - if !dir.exists() { - if let Err(e) = fs::create_dir_all(&dir) { - log!("[IMPORTANT] Couldn't create directories: {}", e); - return; - } - } - - if let Err(e) = fs::write(&file_path, value) { - log!( - "[IMPORTANT] Couldn't write file {}: {}", - file_path.display(), - e - ); - } -} - -pub fn get_children(path: &str) -> Vec { - let dir = Path::new(&get_directory()).join(path); - let mut children = Vec::new(); - if let Ok(entries) = fs::read_dir(&dir) { - for entry in entries { - if let Ok(entry) = entry { - children.push(entry.file_name().to_string_lossy().to_string()); - } - } - } - children -} - pub fn get_directory() -> String { WORKING_DIR.to_string_lossy().to_string() } - -pub fn working_dir() -> &'static Path { - &WORKING_DIR -} - -// Helper to download the zip file content to a file on disk -#[allow(dead_code)] -async fn download_zip(url: &str, as_name: &Path) -> Result<(), Box> { - let response = reqwest::get(url).await?; - - // Check for successful response status - if !response.status().is_success() { - return Err(format!("Failed to download file: Status {}", response.status()).into()); - } - - let mut zip_file = tokio::fs::File::create(as_name).await?; - let body = response.bytes().await?; - zip_file.write_all(&body).await?; - - Ok(()) -} - -#[allow(dead_code, deprecated)] -fn extract_zip_contents_to_folder( - zip_path: &Path, - target_dir: &Path, -) -> Result<(), Box> { - let file = File::open(zip_path)?; - let mut archive = ZipArchive::new(file)?; - - let staging_dir = target_dir.with_extension("staging"); - - let _ = fs::remove_dir_all(&staging_dir); - fs::create_dir_all(&staging_dir)?; - - let mut first_item_name: Option = None; - - for i in 0..archive.len() { - let mut file = archive.by_index(i)?; - let entry_path = staging_dir.join(file.sanitized_name()); - - if i == 0 { - if file.name().ends_with('/') || file.sanitized_name().components().count() == 1 { - first_item_name = Some(file.sanitized_name()); - } - } - - if file.name().ends_with('/') { - fs::create_dir_all(&entry_path)?; - } else { - if let Some(parent) = entry_path.parent() { - fs::create_dir_all(parent)?; - } - let mut out_file = File::create(entry_path)?; - io::copy(&mut file, &mut out_file)?; - } - } - - if let Some(root_path) = first_item_name { - let root_dir = staging_dir.join(&root_path); - - if root_dir.is_dir() { - let root_contents_count = fs::read_dir(&staging_dir)?.count(); - - if root_contents_count == 1 - || (root_contents_count > 1 && fs::metadata(&root_dir).is_ok()) - { - let _ = fs::remove_dir_all(target_dir); - fs::create_dir_all(target_dir)?; - - for entry in fs::read_dir(root_dir)? { - let entry = entry?; - let src = entry.path(); - let dest = target_dir.join(entry.file_name()); - - if let Err(_) = fs::rename(&src, &dest) { - if src.is_file() { - fs::copy(&src, &dest)?; - } else { - if entry.path().is_dir() { - fs::rename(&src, &dest)?; - } - } - } - } - - let _ = fs::remove_dir_all(&staging_dir); - return Ok(()); - } - } - } - - log!("Extracting directly (no single root folder detected)."); - let _ = fs::remove_dir_all(target_dir); - fs::rename(&staging_dir, target_dir)?; - - Ok(()) -} - -#[allow(dead_code)] -pub async fn download_and_extract_zip(url: &str, as_name: &str) { - let base_dir = PathBuf::from(get_directory()); - let zip_filename = format!("{}.zip", Uuid::new_v4()); - let zip_path = base_dir.join(&zip_filename); - let target_dir = base_dir.join(as_name); - - if let Err(e) = download_zip(url, &zip_path).await { - log!("Error downloading file: {}", e); - return; - } - - let zip_path_clone = zip_path.clone(); - let target_dir_clone = target_dir.clone(); - let extract_result = extract_zip_contents_to_folder(&zip_path_clone, &target_dir_clone); - if let Err(e) = extract_result { - log!("Panic during ZIP extraction: {}", e); - } - - if let Err(e) = tokio::fs::remove_file(&zip_path).await { - log!("Error cleaning up ZIP file {}: {}", zip_path.display(), e); - } -} diff --git a/src/util/logger.rs b/src/util/logger.rs index 9bd211a..c9c9719 100644 --- a/src/util/logger.rs +++ b/src/util/logger.rs @@ -10,7 +10,6 @@ use std::{ use ansi_term::Color; use mtp::codec::{CommunicationValue, DataTypeId, DataValue, Version}; -use crate::transport::connection::MtpValueCompat; use crate::util::file_util::get_directory; static LOGGER: OnceLock> = OnceLock::new(); @@ -223,7 +222,7 @@ pub fn log_cv_internal( let formatted = format_cv(cv); log_internal( - Some(cv.get_sender() as i64), + cv.sender().and_then(|sender| i64::try_from(sender).ok()), print_type.unwrap_or(PrintType::General), prefix, false, @@ -234,14 +233,14 @@ pub fn log_cv_internal( pub fn format_cv(cv: &CommunicationValue) -> String { let mut parts = Vec::new(); - let sender = cv.get_sender(); - let receiver = cv.get_receiver(); + let sender = cv.sender().filter(|sender| *sender != 0); + let receiver = cv.receiver().filter(|receiver| *receiver != 0); - if sender > 0 && receiver > 0 { + if let (Some(sender), Some(receiver)) = (sender, receiver) { parts.push(format!("{} > {}", sender, receiver)); - } else if sender > 0 { + } else if let Some(sender) = sender { parts.push(format!("{}", sender)); - } else if receiver > 0 { + } else if let Some(receiver) = receiver { parts.push(format!("> {}", receiver)); } @@ -249,7 +248,11 @@ pub fn format_cv(cv: &CommunicationValue) -> String { .get_comm_type_enum() .map(|kind| kind.to_string()) .unwrap_or_else(|| cv.get_type().to_string()); - parts.push(format!("{} (id={})", comm_type, cv.get_id())); + let id = cv + .id() + .map(|id| id.to_string()) + .unwrap_or_else(|| "missing".to_string()); + parts.push(format!("{} (id={})", comm_type, id)); let data = cv.data().unwrap_or(&[]); @@ -258,7 +261,7 @@ pub fn format_cv(cv: &CommunicationValue) -> String { Version(3, 0), ); - parts.push(format!("{}", formated_data)); + parts.push(formated_data.to_string()); parts.join(": ") } From 656b6acb01dc010d78926928b85e22af5ae94686 Mon Sep 17 00:00:00 2001 From: Rasensprenger Date: Thu, 20 Aug 2026 19:00:43 +0300 Subject: [PATCH 2/2] Update Rust crate sqlx to 0.9.0 --- Cargo.lock | 488 +++++++++-------------------------------------------- Cargo.toml | 2 +- 2 files changed, 78 insertions(+), 412 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2a4b6e1..05c3bf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -310,12 +310,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "const-oid" version = "0.10.2" @@ -479,25 +473,14 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid 0.9.6", - "pem-rfc7468 0.7.0", - "zeroize", -] - [[package]] name = "der" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ - "const-oid 0.10.2", - "pem-rfc7468 1.0.0", + "const-oid", + "pem-rfc7468", "zeroize", ] @@ -528,7 +511,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid 0.9.6", "crypto-common 0.1.7", "subtle", ] @@ -540,7 +522,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.1", - "const-oid 0.10.2", + "const-oid", "crypto-common 0.2.2", "ctutils", ] @@ -580,8 +562,8 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" dependencies = [ - "pkcs8 0.11.0", - "signature 3.0.0", + "pkcs8", + "signature", ] [[package]] @@ -594,7 +576,7 @@ dependencies = [ "ed25519", "serde", "sha2 0.11.0", - "signature 3.0.0", + "signature", "subtle", "zeroize", ] @@ -626,13 +608,12 @@ dependencies = [ [[package]] name = "etcetera" -version = "0.8.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" dependencies = [ "cfg-if", - "home", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -651,7 +632,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef975e30683b2d965054bb0a836f8973857c4ebf6acf274fe46617cd285060d8" dependencies = [ - "foldhash 0.2.0", + "foldhash", "libm", "portable-atomic", "siphasher", @@ -683,9 +664,9 @@ checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "flume" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" dependencies = [ "futures-core", "futures-sink", @@ -698,12 +679,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -944,13 +919,13 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.1.5", + "foldhash", ] [[package]] @@ -961,11 +936,11 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashlink" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.16.1", ] [[package]] @@ -980,31 +955,13 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac 0.12.1", -] - [[package]] name = "hkdf" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" dependencies = [ - "hmac 0.13.0", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", + "hmac", ] [[package]] @@ -1016,15 +973,6 @@ dependencies = [ "digest 0.11.3", ] -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "httlib-huffman" version = "0.3.4" @@ -1344,9 +1292,6 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] [[package]] name = "libc" @@ -1360,18 +1305,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" -[[package]] -name = "libredox" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" -dependencies = [ - "bitflags", - "libc", - "plain", - "redox_syscall 0.9.2", -] - [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -1411,12 +1344,12 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "md-5" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ "cfg-if", - "digest 0.10.7", + "digest 0.11.3", ] [[package]] @@ -1448,14 +1381,14 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d" dependencies = [ - "const-oid 0.10.2", + "const-oid", "crypto-common 0.2.2", "ctutils", "hybrid-array", "module-lattice", - "pkcs8 0.11.0", + "pkcs8", "shake", - "signature 3.0.0", + "signature", ] [[package]] @@ -1520,7 +1453,7 @@ dependencies = [ "mtp-common", "mtp-crypto", "mtp-transport", - "rand 0.10.2", + "rand", "tokio", ] @@ -1534,7 +1467,7 @@ dependencies = [ "mtp-common", "mtp-crypto", "mtp-type-map", - "rand 0.10.2", + "rand", "thiserror 2.0.20", ] @@ -1559,10 +1492,10 @@ dependencies = [ "chacha20poly1305", "ed25519-dalek", "getrandom 0.4.3", - "hkdf 0.13.0", + "hkdf", "ml-dsa", "mlkem-tls", - "rand 0.10.2", + "rand", "rand_core 0.6.4", "rustls", "serde", @@ -1578,7 +1511,7 @@ version = "0.3.0" source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "mtp-crypto", - "rand 0.10.2", + "rand", "thiserror 2.0.20", "zeroize", ] @@ -1592,7 +1525,7 @@ dependencies = [ "mtp-common", "mtp-crypto", "mtp-transport", - "rand 0.10.2", + "rand", "thiserror 2.0.20", "tokio", "tracing", @@ -1608,7 +1541,7 @@ dependencies = [ "mtp-codec", "mtp-common", "mtp-crypto", - "rand 0.10.2", + "rand", "rcgen", "rustls", "rustls-native-certs", @@ -1676,22 +1609,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.7", - "smallvec", - "zeroize", -] - [[package]] name = "num-conv" version = "0.2.2" @@ -1707,16 +1624,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" -dependencies = [ - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1724,7 +1631,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -1754,7 +1660,7 @@ dependencies = [ "http", "mtp", "once_cell", - "rand 0.10.2", + "rand", "rustls", "serde", "serde_json", @@ -1808,7 +1714,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link", ] @@ -1834,15 +1740,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "pem-rfc7468" version = "1.0.0" @@ -1864,35 +1761,14 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der 0.7.10", - "pkcs8 0.10.2", - "spki 0.7.3", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der 0.7.10", - "spki 0.7.3", -] - [[package]] name = "pkcs8" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ - "der 0.8.1", - "spki 0.8.0", + "der", + "spki", ] [[package]] @@ -1901,12 +1777,6 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - [[package]] name = "poly1305" version = "0.8.0" @@ -1939,15 +1809,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "proc-macro2" version = "1.0.107" @@ -1989,7 +1850,7 @@ dependencies = [ "fastbloom", "getrandom 0.4.3", "lru-slab", - "rand 0.10.2", + "rand", "rand_pcg", "ring", "rustc-hash", @@ -2032,17 +1893,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" -dependencies = [ - "libc", - "rand_chacha", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.10.2" @@ -2054,16 +1904,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -2112,15 +1952,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "redox_syscall" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c93da5bb2c5d4e6c0ef7abeead62c89169a0a4882bfb83ac892f2423aea2fe" -dependencies = [ - "bitflags", -] - [[package]] name = "ring" version = "0.17.14" @@ -2135,26 +1966,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid 0.9.6", - "digest 0.10.7", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "signature 2.2.0", - "spki 0.7.3", - "subtle", - "zeroize", -] - [[package]] name = "rustc-hash" version = "2.1.3" @@ -2364,18 +2175,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -2391,13 +2190,13 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.7" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -2459,16 +2258,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - [[package]] name = "signature" version = "3.0.0" @@ -2535,16 +2324,6 @@ dependencies = [ "lock_api", ] -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der 0.7.10", -] - [[package]] name = "spki" version = "0.8.0" @@ -2552,7 +2331,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", - "der 0.8.1", + "der", ] [[package]] @@ -2563,9 +2342,9 @@ checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" [[package]] name = "sqlx" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2576,12 +2355,13 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" dependencies = [ "base64 0.22.1", "bytes", + "cfg-if", "crc", "crossbeam-queue", "either", @@ -2590,12 +2370,11 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "hashlink", "indexmap", "log", "memchr", - "once_cell", "percent-encoding", "serde", "serde_json", @@ -2610,9 +2389,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" dependencies = [ "proc-macro2", "quote", @@ -2623,15 +2402,15 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" dependencies = [ + "cfg-if", "dotenvy", "either", "heck", "hex", - "once_cell", "proc-macro2", "quote", "serde", @@ -2642,57 +2421,42 @@ dependencies = [ "sqlx-postgres", "sqlx-sqlite", "syn 2.0.119", + "thiserror 2.0.20", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" dependencies = [ - "atoi", - "base64 0.22.1", "bitflags", "byteorder", "bytes", "crc", - "digest 0.10.7", + "digest 0.11.3", "dotenvy", "either", - "futures-channel", "futures-core", - "futures-io", "futures-util", "generic-array", - "hex", - "hkdf 0.12.4", - "hmac 0.12.1", - "itoa", "log", - "md-5", - "memchr", - "once_cell", "percent-encoding", - "rand 0.8.7", - "rsa", "serde", "sha1", - "sha2 0.10.9", - "smallvec", + "sha2 0.11.0", "sqlx-core", - "stringprep", "thiserror 2.0.20", "tracing", - "whoami", ] [[package]] name = "sqlx-postgres" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" dependencies = [ "atoi", "base64 0.22.1", @@ -2705,18 +2469,16 @@ dependencies = [ "futures-core", "futures-util", "hex", - "hkdf 0.12.4", - "hmac 0.12.1", - "home", + "hkdf", + "hmac", "itoa", "log", "md-5", "memchr", - "once_cell", - "rand 0.8.7", + "rand", "serde", "serde_json", - "sha2 0.10.9", + "sha2 0.11.0", "smallvec", "sqlx-core", "stringprep", @@ -2727,12 +2489,13 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" dependencies = [ "atoi", "flume", + "form_urlencoded", "futures-channel", "futures-core", "futures-executor", @@ -2742,7 +2505,6 @@ dependencies = [ "log", "percent-encoding", "serde", - "serde_urlencoded", "sqlx-core", "thiserror 2.0.20", "tracing", @@ -3114,12 +2876,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - [[package]] name = "wasm-bindgen" version = "0.2.127" @@ -3186,13 +2942,9 @@ dependencies = [ [[package]] name = "whoami" -version = "1.6.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" -dependencies = [ - "libredox", - "wasite", -] +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" [[package]] name = "winapi" @@ -3231,22 +2983,13 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "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.6", + "windows-targets", ] [[package]] @@ -3258,67 +3001,34 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "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.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[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.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[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.6" @@ -3331,48 +3041,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[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.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[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.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[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.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[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.6" @@ -3485,26 +3171,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zerocopy" -version = "0.8.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "zerofrom" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index 395ad06..09a50a8 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ rustls = { version = "0.23.42", default-features = false, features = [ "aws-lc-rs", "prefer-post-quantum", ] } -sqlx = { version = "0.8.6", features = ["mysql", "runtime-tokio", "migrate"] } +sqlx = { version = "0.9.0", features = ["mysql", "runtime-tokio", "migrate"] } tokio = { version = "*", features = ["full"] } tokio-util = { version = "0.7.19", features = ["rt"] } uuid = { version = "1.24.0", features = ["v4", "v7"] }