diff --git a/Cargo.lock b/Cargo.lock index 3b54c9d..a2d3f95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # 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" @@ -12,6 +18,17 @@ 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" @@ -195,6 +212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", + "zeroize", ] [[package]] @@ -215,6 +233,15 @@ 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" @@ -246,7 +273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures 0.2.17", ] @@ -269,7 +296,7 @@ checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", "chacha20 0.9.1", - "cipher", + "cipher 0.4.4", "poly1305", "zeroize", ] @@ -281,10 +308,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common 0.1.7", - "inout", + "inout 0.1.4", "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" @@ -322,6 +359,22 @@ 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" @@ -338,6 +391,12 @@ 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" @@ -371,6 +430,15 @@ 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" @@ -479,6 +547,12 @@ 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" @@ -543,6 +617,7 @@ dependencies = [ "const-oid 0.10.2", "crypto-common 0.2.2", "ctutils", + "zeroize", ] [[package]] @@ -608,6 +683,15 @@ 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" @@ -681,6 +765,16 @@ 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" @@ -1105,6 +1199,22 @@ 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]] @@ -1113,12 +1223,23 @@ 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]] @@ -1244,6 +1365,21 @@ 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" @@ -1348,6 +1484,12 @@ 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" @@ -1409,6 +1551,15 @@ 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" @@ -1425,12 +1576,28 @@ 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" @@ -1498,7 +1665,7 @@ dependencies = [ [[package]] name = "mtp" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" dependencies = [ "mtp-client", "mtp-codec", @@ -1514,7 +1681,7 @@ dependencies = [ [[package]] name = "mtp-client" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" dependencies = [ "mtp-codec", "mtp-common", @@ -1527,7 +1694,7 @@ dependencies = [ [[package]] name = "mtp-codec" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" dependencies = [ "base64 0.23.1", "byteorder", @@ -1541,7 +1708,7 @@ dependencies = [ [[package]] name = "mtp-common" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" dependencies = [ "quinn", "rustls", @@ -1552,7 +1719,7 @@ dependencies = [ [[package]] name = "mtp-crypto" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" dependencies = [ "argon2", "base64 0.22.1", @@ -1575,7 +1742,7 @@ dependencies = [ [[package]] name = "mtp-files" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" dependencies = [ "mtp-crypto", "rand 0.10.2", @@ -1586,7 +1753,7 @@ dependencies = [ [[package]] name = "mtp-host" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" dependencies = [ "mtp-codec", "mtp-common", @@ -1602,7 +1769,7 @@ dependencies = [ [[package]] name = "mtp-transport" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" dependencies = [ "async-trait", "mtp-codec", @@ -1622,7 +1789,7 @@ dependencies = [ [[package]] name = "mtp-type-map" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" dependencies = [ "serde", "serde_yaml", @@ -1631,7 +1798,7 @@ dependencies = [ [[package]] name = "mtp-webserver" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" +source = "git+https://git.methanium.net/Methanium/mtp.git#d11eb04d12e35dbfb42dd9a3f201d2da15f4af70" dependencies = [ "async-trait", "bytes", @@ -1755,15 +1922,17 @@ 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", - "zeroize", + "zip", ] [[package]] @@ -1824,6 +1993,16 @@ 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" @@ -1939,6 +2118,12 @@ 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" @@ -2121,6 +2306,44 @@ 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" @@ -2223,7 +2446,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "jni", "log", @@ -2299,7 +2522,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -2400,6 +2623,17 @@ 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" @@ -2479,6 +2713,12 @@ 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" @@ -2678,7 +2918,7 @@ dependencies = [ "rand 0.8.7", "rsa", "serde", - "sha1", + "sha1 0.10.7", "sha2 0.10.9", "smallvec", "sqlx-core", @@ -2766,6 +3006,24 @@ 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" @@ -2794,6 +3052,15 @@ 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" @@ -2805,6 +3072,27 @@ 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" @@ -2852,6 +3140,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", + "js-sys", "num-conv", "powerfmt", "serde_core", @@ -2958,12 +3247,56 @@ 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" @@ -2996,6 +3329,18 @@ 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" @@ -3108,6 +3453,15 @@ 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" @@ -3133,6 +3487,16 @@ 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" @@ -3165,6 +3529,16 @@ 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" @@ -3231,6 +3605,35 @@ 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" @@ -3579,8 +3982,81 @@ 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 7717f72..980aa16 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [ "crypto", "files", + "raw", "web-server", ] } @@ -18,6 +19,7 @@ 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", @@ -25,10 +27,11 @@ 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 2c05741..a1d4baf 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,2 @@ # 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 a297dcc..486541b 160000 --- a/mtp-type-maps +++ b/mtp-type-maps @@ -1 +1 @@ -Subproject commit a297dcce60bc6e84696c6a16f5fd510beb2ca643 +Subproject commit 486541b9483356ff49ff3ec7016f87d3ecbeaa0e diff --git a/src/config.rs b/src/config.rs index d3c7b29..66f5382 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,4 @@ use std::{env, time::Duration}; -use thiserror::Error; #[derive(Clone, Debug)] pub struct RateLimitConfig { @@ -10,26 +9,6 @@ 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 { "*" } @@ -47,75 +26,40 @@ impl Default for RateLimitConfig { } impl RateLimitConfig { - pub fn from_env() -> Result { + pub fn from_env() -> Self { let defaults = Self::default(); - Ok(Self { - window: env_duration("RATE_LIMIT_WINDOW_SECONDS", defaults.window)?, - general_requests: env_usize("RATE_LIMIT_GENERAL_REQUESTS", defaults.general_requests)?, + 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, + ), } } } -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_usize(name: &str, fallback: usize) -> usize { + env::var(name) + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|value| *value > 0) + .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)) +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) } diff --git a/src/db/iota_repo.rs b/src/db/iota_repo.rs index fb50432..3eedce8 100644 --- a/src/db/iota_repo.rs +++ b/src/db/iota_repo.rs @@ -48,12 +48,20 @@ 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.try_as_bytes()?) + .bind(public_key.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 0d10d4c..79b9034 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 (1..=MAX_PROTOCOL_ID).contains(&ts) { + if ts >= 1 && ts <= MAX_PROTOCOL_ID { return Ok(UserId::from(ts)); } @@ -335,6 +335,15 @@ 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)) @@ -351,6 +360,14 @@ 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. @@ -405,9 +422,8 @@ 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) + .bind(public_key.as_bytes()) .bind(id.0) .execute(&pool().await?) .await?; @@ -430,7 +446,6 @@ 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( @@ -457,7 +472,7 @@ pub async fn register_complete_user( ) .bind(id.0) .bind(username.as_bytes()) - .bind(&public_key_bytes) + .bind(public_key.as_bytes()) .bind(iota_id.0) .bind(token.as_bytes()) .execute(&mut *transaction) @@ -470,20 +485,20 @@ pub async fn register_complete_user( .bind(id.0) .fetch_optional(&mut *transaction) .await?; - let existing_matches = match existing { - Some(existing) => { - let existing = User::try_from(existing).map_err(OmegaError::from)?; - existing.iota_id == Some(iota_id) + match existing + .map(User::try_from) + .transpose() + .map_err(OmegaError::from)? + { + Some(existing) + if existing.iota_id == Some(iota_id) && existing.username == username - && existing.public_key.try_as_bytes()? == public_key_bytes - && existing.token == token + && existing.public_key.as_bytes() == public_key.as_bytes() + && existing.token == token => + { + Ok(()) } - None => false, - }; - if existing_matches { - Ok(()) - } else { - Err(insert_error.into()) + _ => Err(insert_error.into()), } } }; diff --git a/src/error.rs b/src/error.rs index 446afec..9911d25 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,34 +1,11 @@ -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")] @@ -68,8 +45,6 @@ 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 deleted file mode 100644 index a7a831d..0000000 --- a/src/identity.rs +++ /dev/null @@ -1,314 +0,0 @@ -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 e04238c..16b850e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,6 @@ mod api; mod config; mod db; pub mod error; -mod identity; mod models; mod server; mod sql; @@ -18,18 +17,45 @@ 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; -use zeroize::Zeroizing; + +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() + }) +} #[tokio::main] async fn main() { - if default_provider().install_default().is_err() { + if let Err(_) = default_provider().install_default() { println!("Error loading Provider"); return; } @@ -38,39 +64,6 @@ 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 { @@ -103,7 +96,7 @@ async fn main() { .unwrap_or(443); tokio::select! { - result = omikron_connection::start(port, state) => { + result = omikron_connection::start(port, OmegaState::new()) => { 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 da21449..291729d 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -11,8 +11,7 @@ fn serialize_public_key( where S: serde::Serializer, { - let encoded = key.try_to_base64().map_err(serde::ser::Error::custom)?; - serializer.serialize_str(&encoded) + serializer.serialize_str(&key.to_base64()) } pub use ids::{IotaId, OmikronId, UserId}; diff --git a/src/server/api.rs b/src/server/api.rs index 41e5d22..23debe4 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::identity::OmegaIdentity; +use crate::load_keyring; use crate::models::UserId; use crate::server::{ middleware, @@ -24,7 +24,6 @@ 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 { @@ -36,11 +35,11 @@ fn error_body(error: &OmegaError) -> String { }) } -fn user_response(user: crate::models::User) -> Result { - Ok(UserResponse { +fn user_response(user: crate::models::User) -> UserResponse { + UserResponse { status: "success", username: user.username, - public_key: user.public_key.try_to_base64()?, + public_key: user.public_key.to_base64(), user_id: user.id.0, iota_id: user.iota_id.map(|id| id.0), sub_level: user.sub_level, @@ -51,10 +50,10 @@ fn user_response(user: crate::models::User) -> Result { avatar: user .avatar .map(|value| base64::engine::general_purpose::STANDARD.encode(value)), - }) + } } -async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusCode, String)> { +async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> { match path_parts { ["api", "get", "omikron"] => { let connection = get_random_omikron() @@ -70,7 +69,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC json(&OmikronResponse { status: "success", id, - public_key: omikron.public_key.try_to_base64()?, + public_key: omikron.public_key.to_base64(), ip_address: omikron.ip_address, port: omikron.port, }), @@ -106,7 +105,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC json(&OmikronResponse { status: "success", id: omikron.id.0, - public_key: omikron.public_key.try_to_base64()?, + public_key: omikron.public_key.to_base64(), ip_address: omikron.ip_address, port: omikron.port, }), @@ -121,7 +120,12 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC .map(|(omikron_id, iotas)| { let iotas = iotas .into_iter() - .map(|(iota_id, users)| (iota_id.to_string(), users.into_iter().collect())) + .map(|(iota_id, users)| { + ( + iota_id.to_string(), + users.into_iter().map(i64::from).collect(), + ) + }) .collect(); (omikron_id.to_string(), iotas) }) @@ -142,7 +146,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC json(&IotaResponse { status: "success", iota_id: iota.id.0, - public_key: iota.public_key.try_to_base64()?, + public_key: iota.public_key.to_base64(), }), )) } @@ -154,7 +158,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC json(&UsernameResponse { status: "success", username: user.username, - public_key: user.public_key.try_to_base64()?, + public_key: user.public_key.to_base64(), user_id: user.id.0, iota_id: user.iota_id.map(|id| id.0), sub_level: user.sub_level, @@ -164,7 +168,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC } ["api", "get", "public_key"] => { let public_key = base64::engine::general_purpose::STANDARD - .encode(identity.public_key_bundle().try_as_bytes()?); + .encode(load_keyring().public_key_bundle().as_bytes()); Ok(( StatusCode::OK, json(&PublicKeyResponse { @@ -176,7 +180,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC ["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, @@ -185,17 +189,13 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC } } -pub async fn handle( - request: HttpRequest, - response: HttpResponse, - identity: Arc, -) -> HttpResponse { +pub async fn handle(request: HttpRequest, response: HttpResponse) -> 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( 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( 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( .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( .status(StatusCode::TEMPORARY_REDIRECT) .header("location", &location); } - let (status, body) = route(&path_parts, &identity) + let (status, body) = route(&path_parts) .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,7 +252,6 @@ pub async fn handle_pattern( request: HttpRequest, response: HttpResponse, _params: RouteParams, - identity: Arc, ) -> HttpResponse { - handle(request, response, identity).await + handle(request, response).await } diff --git a/src/server/middleware.rs b/src/server/middleware.rs index b279298..5f373ec 100644 --- a/src/server/middleware.rs +++ b/src/server/middleware.rs @@ -1,25 +1,14 @@ use dashmap::DashMap; use once_cell::sync::Lazy; use std::net::IpAddr; -use std::{collections::VecDeque, sync::OnceLock, time::Instant}; +use std::{collections::VecDeque, time::Instant}; use tokio::time::interval; static REQUESTS: Lazy>> = Lazy::new(DashMap::new); -static CONFIG: OnceLock = OnceLock::new(); +static CONFIG: Lazy = + Lazy::new(crate::config::RateLimitConfig::from_env); 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" @@ -34,15 +23,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(); } @@ -55,7 +44,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(); @@ -68,7 +57,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 64df9e9..2d01c5b 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 new file mode 100644 index 0000000..9ac4f06 --- /dev/null +++ b/src/server/server.rs @@ -0,0 +1,16 @@ +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 6089957..dd24ebf 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 deleted file mode 100644 index 5975888..0000000 --- a/src/server/web.rs +++ /dev/null @@ -1,47 +0,0 @@ -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 00be3c9..104e316 100644 --- a/src/sql/connection_status.rs +++ b/src/sql/connection_status.rs @@ -12,7 +12,11 @@ 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), @@ -34,18 +38,11 @@ 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 cac8ab6..be3e87c 100644 --- a/src/sql/user_online_tracker.rs +++ b/src/sql/user_online_tracker.rs @@ -275,6 +275,10 @@ 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); } @@ -331,6 +335,14 @@ 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( @@ -361,6 +373,15 @@ 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; @@ -475,6 +496,12 @@ 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 @@ -515,6 +542,15 @@ 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, @@ -585,6 +621,10 @@ 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); @@ -631,6 +671,29 @@ 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() } @@ -682,6 +745,10 @@ impl PresenceTracker { } result } + + pub fn remove_omikron_and_offline_users(&self, omikron_id: i64) -> RemovedOmikronState { + self.remove_omikron(omikron_id) + } } #[cfg(test)] @@ -689,10 +756,6 @@ 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(); @@ -702,7 +765,7 @@ mod tests { UserStatus::iota_offline ); - tracker.connect_iota(11, 42); + tracker.track_iota(11, 42); assert_eq!( tracker.resolve_public_state(7, 11), UserStatus::user_offline @@ -720,7 +783,7 @@ mod tests { #[test] fn session_must_match_assigned_iota_and_live_route() { let tracker = PresenceTracker::default(); - tracker.connect_iota(11, 42); + tracker.track_iota(11, 42); tracker.track_session(7, 3, 42, 12); assert!(!tracker.has_active_session_for_iota(7, 11)); assert_eq!( @@ -744,7 +807,7 @@ mod tests { #[test] fn resolver_covers_all_public_preference_states() { let tracker = PresenceTracker::default(); - tracker.connect_iota(11, 42); + tracker.track_iota(11, 42); tracker.track_session(7, 3, 42, 11); for (preference, expected) in [ @@ -763,20 +826,14 @@ mod tests { fn session_routes_replace_by_session_without_replacing_preferences() { let tracker = PresenceTracker::default(); tracker.set_preference(7, UserStatus::user_dnd); - tracker.connect_iota(11, 42); - tracker.connect_iota(12, 43); + tracker.track_iota(11, 42); + tracker.track_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 - .routes - .read() - .unwrap() - .sessions - .get(&(7, 3)) - .cloned(), + tracker.session_route(7, 3), Some(ClientSessionRoute { omikron_id: 42, iota_id: 11, @@ -808,21 +865,21 @@ mod tests { #[test] fn losing_one_iota_route_keeps_iota_online() { let tracker = PresenceTracker::default(); - tracker.connect_iota(11, 42); - tracker.connect_iota(11, 43); + tracker.track_iota(11, 42); + tracker.track_iota(11, 43); assert!(tracker.has_iota_route(11)); - assert!(!tracker.disconnect_iota(11, 42)); + assert!(!tracker.untrack_iota(11, 42)); assert!(tracker.has_iota_route(11)); - assert!(tracker.disconnect_iota(11, 43)); + assert!(tracker.untrack_iota(11, 43)); assert!(!tracker.has_iota_route(11)); } #[test] fn iota_connection_routes_track_primary_and_replacement() { let tracker = PresenceTracker::default(); - tracker.connect_iota(11, 42); - tracker.connect_iota(11, 43); + tracker.track_iota_connection(11, 42, true); + tracker.track_iota_connection(11, 43, false); assert_eq!(tracker.iota_connections(11), Some(vec![42, 43])); assert_eq!(tracker.primary_iota_route(11), Some(42)); @@ -904,8 +961,8 @@ mod tests { #[test] fn active_session_can_use_one_of_several_live_iota_routes() { let tracker = PresenceTracker::default(); - tracker.connect_iota(11, 42); - tracker.connect_iota(11, 43); + tracker.track_iota(11, 42); + tracker.track_iota(11, 43); tracker.track_session(7, 3, 43, 11); assert!(tracker.has_active_session_for_iota(7, 11)); @@ -916,13 +973,13 @@ mod tests { #[test] fn route_snapshot_replaces_only_one_omikron() { let tracker = PresenceTracker::default(); - tracker.connect_iota(11, 42); - tracker.connect_iota(12, 43); + tracker.track_iota(11, 42); + tracker.track_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_omikron_snapshot(42, &[12], &[(9, 5, 12)]); + tracker.replace_routes_owned_by(42, &[12], &[(9, 5, 12)]); assert!(!tracker.owns_session(7, 3, 42)); assert!(tracker.owns_session(8, 4, 43)); @@ -935,8 +992,8 @@ mod tests { fn omikron_cleanup_preserves_other_routes_and_preferences() { let tracker = PresenceTracker::default(); tracker.set_preference(7, UserStatus::user_dnd); - tracker.connect_iota(11, 42); - tracker.connect_iota(11, 43); + tracker.track_iota(11, 42); + tracker.track_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]); @@ -979,14 +1036,14 @@ mod tests { .cloned(), Some([20, 21].into_iter().collect()) ); - assert!(check_index_consistency(&tracker).is_ok()); + assert!(tracker.check_index_consistency().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!(check_index_consistency(&tracker).is_ok()); + assert!(tracker.check_index_consistency().is_ok()); assert!( tracker .routes @@ -1019,7 +1076,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!(check_index_consistency(&tracker).is_ok()); + assert!(tracker.check_index_consistency().is_ok()); } #[test] @@ -1036,6 +1093,6 @@ mod tests { .unwrap() .clear(); - assert!(check_index_consistency(&tracker).is_err()); + assert!(tracker.check_index_consistency().is_err()); } } diff --git a/src/state.rs b/src/state.rs index 215f1b3..02c899b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,10 +1,7 @@ -use crate::{ - config::OmegaConfig, identity::OmegaIdentity, sql::user_online_tracker::PresenceTracker, -}; +use crate::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 { @@ -14,50 +11,28 @@ 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 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; +impl Default for OmegaState { + fn default() -> Self { 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(), } } +} - 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, - )) - } - +impl OmegaState { pub fn issue_challenge( &self, operation: AccountChallengeOperation, @@ -68,6 +43,9 @@ impl OmegaState { self.challenges.insert( (operation, user_id, requester_iota_id), AccountChallenge { + operation, + user_id, + requester_iota_id, nonce, created_at: Instant::now(), }, @@ -90,6 +68,12 @@ impl OmegaState { } } +impl OmegaState { + pub fn new() -> Arc { + Arc::new(Self::default()) + } +} + #[cfg(test)] mod tests { use super::OmegaState; @@ -97,10 +81,10 @@ mod tests { #[test] fn state_instances_have_independent_presence_trackers() { - let first = OmegaState::test_state(); - let second = OmegaState::test_state(); + let first = OmegaState::new(); + let second = OmegaState::new(); - first.presence.connect_iota(11, 42); + first.presence.track_iota_connection(11, 42, true); assert!(first.presence.has_iota_route(11)); assert!(!second.presence.has_iota_route(11)); @@ -110,10 +94,10 @@ mod tests { #[test] fn two_session_private_and_public_presence_flow_is_authoritative() { - let state = OmegaState::test_state(); + let state = OmegaState::new(); state.presence.set_preference(7, UserStatus::user_online); state.presence.set_preference(8, UserStatus::user_online); - state.presence.connect_iota(11, 42); + state.presence.track_iota_connection(11, 42, true); 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 66d9eb3..6cada3e 100644 --- a/src/transport/connection.rs +++ b/src/transport/connection.rs @@ -2,94 +2,24 @@ pub(crate) use super::omikron_connection::{OmikronConnection, OmikronResult}; use mtp::codec::{CommunicationValue, DataValue}; -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; +pub(crate) trait MtpValueCompat { + fn get_id(&self) -> u32; + fn get_sender(&self) -> u64; + fn get_receiver(&self) -> u64; } -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()) - }) +impl MtpValueCompat for CommunicationValue { + fn get_id(&self) -> u32 { + self.id().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_sender(&self) -> u64 { + self.sender().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 get_receiver(&self) -> u64 { + self.receiver().unwrap_or_default() } - - 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 { @@ -116,65 +46,3 @@ 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 ae7215f..0374f78 100644 --- a/src/transport/handlers/account.rs +++ b/src/transport/handlers/account.rs @@ -1,5 +1,5 @@ use super::super::connection::{ - OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields, + MtpValueCompat, OmikronConnection, OmikronResult, OptionalDataValueCompat, }; use crate::{ db::{iota_repo, user_repo}, @@ -22,30 +22,24 @@ 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.require_id()?)) - .await + connection.send(&response.with_id(value.get_id())).await } pub async fn user( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { - value.require_id()?; - value.require_sender_i64()?; - let user_id = UserId::from(value.require_sender_i64()?); + let user_id = UserId::from(value.get_sender() as 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.require_sender_i64()?)), + iota_repo::delete_iota(IotaId::from(value.get_sender() as i64)), ) .await } @@ -54,8 +48,6 @@ 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() @@ -63,21 +55,18 @@ pub async fn release_from_iota( .filter(|id| *id > 0) else { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId) .await; }; - let requester = IotaId::from(value.require_sender_i64()?); + let requester = IotaId::from(value.get_sender() as i64); let Ok(user) = user_repo::get_by_user_id(UserId::from(user_id)).await else { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorNotFound) + .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) .await; }; if user.iota_id != Some(requester) { return connection - .send_error_response( - value.require_id()?, - CommunicationType::ErrorNotAuthenticated, - ) + .send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated) .await; } let previous_iota = user.iota_id; @@ -87,17 +76,14 @@ 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.require_id()?), - ) + .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) .await } Err(error) => { connection .send( &CommunicationValue::new(CommunicationType::ErrorInternal) - .with_id(value.require_id()?) + .with_id(value.get_id()) .add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())), ) .await @@ -118,8 +104,6 @@ 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() @@ -127,7 +111,7 @@ pub async fn attach_begin( .filter(|v| *v > 0) else { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId) .await; }; if user_repo::get_by_user_id(UserId::from(user_id)) @@ -135,10 +119,10 @@ pub async fn attach_begin( .is_err() { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorNotFound) + .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) .await; } - let requester = value.require_sender_i64()?; + let requester = value.get_sender() as i64; let nonce = connection .state() @@ -146,7 +130,7 @@ pub async fn attach_begin( connection .send( &CommunicationValue::new(CommunicationType::AttachUserChallenge) - .with_id(value.require_id()?) + .with_id(value.get_id()) .add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into())) .add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into())), ) @@ -157,8 +141,6 @@ 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() @@ -166,30 +148,24 @@ pub async fn attach_complete( .filter(|v| *v > 0) else { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId) .await; }; - let requester = value.require_sender_i64()?; + let requester = value.get_sender() as 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.require_id()?, - CommunicationType::ErrorInvalidChallenge, - ) + .send_error_response(value.get_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.require_id()?, - CommunicationType::ErrorInvalidChallenge, - ) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge) .await; }; if !connection.state().consume_challenge( @@ -199,15 +175,12 @@ pub async fn attach_complete( nonce, ) { return connection - .send_error_response( - value.require_id()?, - CommunicationType::ErrorInvalidChallenge, - ) + .send_error_response(value.get_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.require_id()?, CommunicationType::ErrorNotFound) + .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) .await; }; let payload = lifecycle_payload(b"tensamin:user-attach:v1\0", user_id, requester, nonce); @@ -215,10 +188,7 @@ 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.require_id()?, - CommunicationType::ErrorNotAuthenticated, - ) + .send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated) .await; } let previous_iota = user.iota_id; @@ -229,15 +199,12 @@ pub async fn attach_complete( } crate::transport::omikron_manager::publish_iota_user_snapshot(requester).await; connection - .send( - &CommunicationValue::new(CommunicationType::Success) - .with_id(value.require_id()?), - ) + .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) .await } Err(_) => { connection - .send_error_response(value.require_id()?, CommunicationType::ErrorInternal) + .send_error_response(value.get_id(), CommunicationType::ErrorInternal) .await } } @@ -258,7 +225,7 @@ async fn complete_delete( connection .send( &CommunicationValue::new(CommunicationType::Success) - .with_id(value.require_id()?) + .with_id(value.get_id()) .add_typed_default( DataType::CleanupPending, DataValue::Bool(cleanup_pending), @@ -268,14 +235,14 @@ async fn complete_delete( } Err(crate::error::OmegaError::NotFound) => { connection - .send_error_response(value.require_id()?, CommunicationType::ErrorNotFound) + .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) .await } Err(error) => { connection .send( &CommunicationValue::new(CommunicationType::ErrorInternal) - .with_id(value.require_id()?) + .with_id(value.get_id()) .add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())), ) .await @@ -287,8 +254,6 @@ 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() @@ -296,7 +261,7 @@ pub async fn delete_credential_begin( .filter(|v| *v > 0) else { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId) .await; }; if user_repo::get_by_user_id(UserId::from(user_id)) @@ -304,10 +269,10 @@ pub async fn delete_credential_begin( .is_err() { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorNotFound) + .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) .await; } - let requester = value.require_sender_i64()?; + let requester = value.get_sender() as i64; let nonce = connection .state() @@ -315,7 +280,7 @@ pub async fn delete_credential_begin( connection .send( &CommunicationValue::new(CommunicationType::DeleteUserCredentialChallenge) - .with_id(value.require_id()?) + .with_id(value.get_id()) .add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into())) .add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into())), ) @@ -326,8 +291,6 @@ 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() @@ -335,20 +298,17 @@ pub async fn delete_credential_complete( .filter(|v| *v > 0) else { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId) .await; }; - let requester = value.require_sender_i64()?; + let requester = value.get_sender() as 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.require_id()?, - CommunicationType::ErrorInvalidChallenge, - ) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge) .await; }; let (Some(signature), Some(pq_signature)) = ( @@ -356,10 +316,7 @@ pub async fn delete_credential_complete( value.get_data(DataType::PqSignature).as_bytes(), ) else { return connection - .send_error_response( - value.require_id()?, - CommunicationType::ErrorInvalidChallenge, - ) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge) .await; }; if !connection.state().consume_challenge( @@ -369,15 +326,12 @@ pub async fn delete_credential_complete( nonce, ) { return connection - .send_error_response( - value.require_id()?, - CommunicationType::ErrorInvalidChallenge, - ) + .send_error_response(value.get_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.require_id()?, CommunicationType::ErrorNotFound) + .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) .await; }; let payload = lifecycle_payload(b"tensamin:user-delete:v1\0", user_id, requester, nonce); @@ -385,10 +339,7 @@ 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.require_id()?, - CommunicationType::ErrorNotAuthenticated, - ) + .send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated) .await; } complete_delete(connection, value, user.id).await @@ -398,8 +349,6 @@ 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() @@ -407,31 +356,45 @@ pub async fn erase_hosted_user_data_ack( .filter(|v| *v > 0) else { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidUserId) .await; }; - let iota_id = IotaId::from(value.require_sender_i64()?); + let iota_id = IotaId::from(value.get_sender() as 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.require_id()?), - ) + .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) .await } Ok(false) => { connection - .send_error_response( - value.require_id()?, - CommunicationType::ErrorNotAuthenticated, - ) + .send_error_response(value.get_id(), CommunicationType::ErrorNotAuthenticated) .await } Err(_) => { connection - .send_error_response(value.require_id()?, CommunicationType::ErrorInternal) + .send_error_response(value.get_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 10df9e4..ad45c05 100644 --- a/src/transport/handlers/links.rs +++ b/src/transport/handlers/links.rs @@ -1,5 +1,5 @@ use super::super::connection::{ - OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields, + MtpValueCompat, OmikronConnection, OmikronResult, OptionalDataValueCompat, }; 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.require_id()?) + .with_id(value.get_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 59883c5..9405bf0 100644 --- a/src/transport/handlers/notifications.rs +++ b/src/transport/handlers/notifications.rs @@ -1,5 +1,5 @@ use super::super::connection::{ - OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields, + MtpValueCompat, OmikronConnection, OmikronResult, OptionalDataValueCompat, }; use crate::{db::notification_repo, log, models::UserId}; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; @@ -10,36 +10,35 @@ pub async fn get( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { - 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 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 response = CommunicationValue::new(CommunicationType::GetNotifications) - .with_id(request_id) + .with_id(value.get_id()) .add_typed_default(DataType::Notifications, DataValue::Array(notifications)); connection.send(&response).await } @@ -48,17 +47,19 @@ pub async fn read( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { - let request_id = value.require_id()?; - let receiver = value.require_sender_i64()?; + 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 Some(other) = value .get_data(DataType::SenderId) .as_number() - .and_then(|id| i64::try_from(id).ok()) - .filter(|id| *id > 0) + .map(|id| id as i64) else { - return connection - .send_error_response(request_id, CommunicationType::ErrorInvalidData) - .await; + return Ok(()); }; if let Err(error) = notification_repo::read_notification(UserId::from(receiver), UserId::from(other)).await @@ -70,7 +71,7 @@ pub async fn read( ); } else { let response = - CommunicationValue::new(CommunicationType::ReadNotification).with_id(request_id); + CommunicationValue::new(CommunicationType::ReadNotification).with_id(value.get_id()); let _ = connection.send(&response).await; let sync = CommunicationValue::new(CommunicationType::ReadNotification) .with_receiver(receiver as u64) @@ -84,54 +85,18 @@ pub async fn push( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { - 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 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 Some(sender) = value + let sender = value .get_data(DataType::SenderId) .as_number() - .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(()); - } + .map(|id| id as i64) + .unwrap_or(value.get_sender() as i64); if let Err(error) = notification_repo::add_notification(UserId::from(receiver), UserId::from(sender)).await { @@ -141,11 +106,9 @@ pub async fn push( error ); } else { - if let Some(request_id) = request_id { - let response = - CommunicationValue::new(CommunicationType::PushNotification).with_id(request_id); - let _ = connection.send(&response).await; - } + let response = + CommunicationValue::new(CommunicationType::PushNotification).with_id(value.get_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 a958a92..5c631bf 100644 --- a/src/transport/handlers/presence.rs +++ b/src/transport/handlers/presence.rs @@ -1,5 +1,5 @@ use super::super::connection::{ - OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields, + MtpValueCompat, OmikronConnection, OmikronResult, OptionalDataValueCompat, }; use crate::{ db::user_repo, log_in, models::IotaId, sql::connection_status::UserStatus, state::OmegaState, @@ -11,10 +11,7 @@ use std::{ }; fn parse_subscription(value: &CommunicationValue) -> Result<(i64, i64, Vec), &'static str> { - let Some(sender) = value.sender() else { - return Err("user_id"); - }; - let user_id = i64::try_from(sender) + let user_id = i64::try_from(value.get_sender()) .ok() .filter(|id| *id > 0) .ok_or("user_id")?; @@ -54,11 +51,13 @@ fn states_for_users(state: &OmegaState, users: &[crate::models::User]) -> HashMa users .iter() .map(|user| { - 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) + ( + user.id.0, + state.presence.resolve_public_state( + user.id.0, + user.iota_id.map(|id| id.0).unwrap_or_default(), + ), + ) }) .collect() } @@ -71,10 +70,9 @@ fn changed_states( let mut changes = users .iter() .filter_map(|user| { - let after = user - .iota_id - .map(|iota_id| state.presence.resolve_public_state(user.id.0, iota_id.0)) - .unwrap_or(UserStatus::user_offline); + let after = state + .presence + .resolve_public_state(user.id.0, user.iota_id.map(|id| id.0).unwrap_or_default()); (before.get(&user.id.0) != Some(&after)).then_some((user.id.0, after)) }) .collect::>(); @@ -98,6 +96,21 @@ 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 { @@ -130,6 +143,28 @@ 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, @@ -140,13 +175,13 @@ pub async fn state_subscribe( Ok(subscription) => subscription, Err("user_id") => { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorNoUserId) + .send_error_response(value.get_id(), CommunicationType::ErrorNoUserId) .await; } Err(detail) => { return connection .send_error_response_with_detail( - value.require_id()?, + value.get_id(), CommunicationType::ErrorInvalidData, detail, ) @@ -155,14 +190,14 @@ pub async fn state_subscribe( }; if !state.presence.owns_session(user_id, session_id, omikron_id) { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorNoIota) + .send_error_response(value.get_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.require_id()?)) + .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) .await } @@ -207,7 +242,7 @@ pub async fn user_connected( .filter(|id| *id > 0) else { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) .await; }; let Some(session_id) = value @@ -217,7 +252,7 @@ pub async fn user_connected( .filter(|id| *id > 0) else { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) .await; }; let Some(iota_id) = value @@ -227,25 +262,25 @@ pub async fn user_connected( .filter(|id| *id > 0) else { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) + .send_error_response(value.get_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.require_id()?, CommunicationType::ErrorNotFound) + .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) .await; } - Err(error) => return Err(error), + Err(error) => return Err(error.into()), }; let preferences = match user_repo::get_presence_preferences(&[user_id]).await { Ok(preferences) => preferences, - Err(error) => return Err(error), + Err(error) => return Err(error.into()), }; if user.iota_id.map(|id| id.0) != Some(iota_id) || !state.presence.has_iota_route(iota_id) { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorNoIota) + .send_error_response(value.get_id(), CommunicationType::ErrorNoIota) .await; } apply_preferences(&state, preferences); @@ -256,7 +291,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.require_id()?)) + .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) .await } @@ -274,7 +309,7 @@ pub async fn user_disconnected( .filter(|id| *id > 0) else { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) .await; }; let Some(session_id) = value @@ -284,7 +319,7 @@ pub async fn user_disconnected( .filter(|id| *id > 0) else { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) .await; }; if let Ok(user) = user_repo::get_by_user_id(user_id.into()).await { @@ -302,7 +337,137 @@ pub async fn user_disconnected( .remove_session(user_id, session_id, omikron_id); } connection - .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.require_id()?)) + .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()), + ), + ) .await } @@ -316,11 +481,10 @@ pub async fn iota_connected( let Some(iota_id) = value .get_data(DataType::IotaId) .as_number() - .and_then(|id| i64::try_from(id).ok()) - .filter(|id| *id > 0) + .map(|id| id as i64) else { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) .await; }; let users = user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await?; @@ -339,7 +503,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.require_id()?)) + .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) .await } @@ -353,11 +517,10 @@ pub async fn iota_disconnected( let Some(iota_id) = value .get_data(DataType::IotaId) .as_number() - .and_then(|id| i64::try_from(id).ok()) - .filter(|id| *id > 0) + .map(|id| id as i64) else { return connection - .send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) .await; }; let users = user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await?; @@ -367,7 +530,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.require_id()?)) + .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) .await } @@ -377,7 +540,7 @@ pub async fn sync_status( value: CommunicationValue, omikron_id: i64, ) -> OmikronResult<()> { - let request_id = value.require_id()?; + let request_id = value.get_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 2c2501a..8799893 100644 --- a/src/transport/handlers/register.rs +++ b/src/transport/handlers/register.rs @@ -1,5 +1,5 @@ use super::super::connection::{ - OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields, + MtpValueCompat, OmikronConnection, OmikronResult, OptionalDataValueCompat, }; 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.require_id()?, CommunicationType::ErrorInvalidData) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) .await; }; let (register_id, registration_token) = - user_repo::allocate_registration(IotaId::from(iota_id), value.require_id()?).await?; + user_repo::allocate_registration(IotaId::from(iota_id), value.get_id()).await?; let response = CommunicationValue::new(CommunicationType::GetRegister) - .with_id(value.require_id()?) + .with_id(value.get_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.require_id()?, CommunicationType::ErrorInvalidData) + .send_error_response(value.get_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.require_id()?) + .with_id(value.get_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.require_id()?) + .with_id(value.get_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.require_id()?, CommunicationType::ErrorInvalidData) + .send_error_response(value.get_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.require_id()?, CommunicationType::ErrorInvalidData) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) .await; }; @@ -136,17 +136,14 @@ pub async fn complete_user( { Ok(()) => { connection - .send( - &CommunicationValue::new(CommunicationType::Success) - .with_id(value.require_id()?), - ) + .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) .await } Err(error) => { connection .send( &CommunicationValue::new(CommunicationType::ErrorInternal) - .with_id(value.require_id()?) + .with_id(value.get_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 0b46254..e1fc12d 100644 --- a/src/transport/handlers/states.rs +++ b/src/transport/handlers/states.rs @@ -1,5 +1,5 @@ use super::super::connection::{ - OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields, + MtpValueCompat, OmikronConnection, OmikronResult, OptionalDataValueCompat, }; 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.require_id()?, + value.get_id(), CommunicationType::ErrorInvalidData, None, ) @@ -47,7 +47,7 @@ pub async fn get( if session_id.is_none() && !legacy_peer { return send_error( connection, - value.require_id()?, + value.get_id(), CommunicationType::ErrorInvalidData, None, ) @@ -60,7 +60,7 @@ pub async fn get( let DataValue::SignedNumber(id) = id else { return send_error( connection, - value.require_id()?, + value.get_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.require_id()?, + value.get_id(), CommunicationType::ErrorInvalidData, session_id, ) @@ -78,7 +78,7 @@ pub async fn get( if user_id <= 0 { return send_error( connection, - value.require_id()?, + value.get_id(), CommunicationType::ErrorInvalidData, session_id, ) @@ -95,7 +95,7 @@ pub async fn get( Err(_) => { return send_error( connection, - value.require_id()?, + value.get_id(), CommunicationType::ErrorInternal, session_id, ) @@ -110,10 +110,9 @@ pub async fn get( missing_user_ids.push(user_id); continue; }; - 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) + let status = state + .presence + .resolve_public_state(user_id, user.iota_id.map(|id| id.0).unwrap_or_default()) .to_string(); let mut map = Vec::new(); if let Some(kind) = DataType::UserId.try_to_id(&tm) { @@ -125,7 +124,7 @@ pub async fn get( states.push(DataValue::Container(map)); } let response = CommunicationValue::new(CommunicationType::GetStates) - .with_id(value.require_id()?) + .with_id(value.get_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 e6b676a..1c248fa 100644 --- a/src/transport/handlers/user_data.rs +++ b/src/transport/handlers/user_data.rs @@ -1,5 +1,5 @@ use super::super::connection::{ - OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields, + MtpValueCompat, OmikronConnection, OmikronResult, OptionalDataValueCompat, }; use crate::{ db::{iota_repo, user_repo}, @@ -29,16 +29,11 @@ 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() - .and_then(|id| i64::try_from(id).ok()) - .filter(|id| *id > 0) - { - user_repo::get_by_user_id(UserId::from(id)).await.ok() + 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() } else if let Some(name) = value.get_data(DataType::Username).as_str() { user_repo::get_by_username(name).await.ok() } else { @@ -46,7 +41,7 @@ pub async fn get_user( }; let Some(user) = user else { return connection - .send_error_response(request_id, CommunicationType::ErrorNotFound) + .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) .await; }; let id = user.id.0; @@ -57,11 +52,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(request_id) + .with_id(value.get_id()) .add_typed_default(DataType::Username, DataValue::Str(username)) .add_typed_default( DataType::PublicKey, - DataValue::Str(user.public_key.try_to_base64()?), + DataValue::Str(user.public_key.to_base64()), ) .add_typed_default(DataType::UserId, DataValue::SignedNumber(id.into())) .add_typed_default(DataType::Display, DataValue::Str(display)) @@ -84,7 +79,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 = sender == id; + let private_request = value.get_sender() as i64 == id; let resolved_status = if private_request { if !state .presence @@ -130,21 +125,13 @@ pub async fn get_iota( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { - 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)) + let found = if let Some(id) = value.get_data(DataType::IotaId).as_number() { + iota_repo::get_iota_by_id(IotaId::from(id as i64)) .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 Some(id) = i64::try_from(id).ok().filter(|id| *id > 0) - && let Ok(user) = user_repo::get_by_user_id(UserId::from(id)).await - { + if let Ok(user) = user_repo::get_by_user_id(UserId::from(id as i64)).await { match user.iota_id { Some(iota_id) => iota_repo::get_iota_by_id(iota_id) .await @@ -176,12 +163,12 @@ pub async fn get_iota( }; let Some((id, key, user_id, username)) = found else { return connection - .send_error_response(request_id, CommunicationType::ErrorNotFound) + .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) .await; }; let mut response = CommunicationValue::new(CommunicationType::GetIotaData) - .with_id(request_id) - .add_typed_default(DataType::PublicKey, DataValue::Str(key.try_to_base64()?)) + .with_id(value.get_id()) + .add_typed_default(DataType::PublicKey, DataValue::Str(key.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 { @@ -198,8 +185,7 @@ async fn update_user( connection: Arc, value: CommunicationValue, ) -> OmikronResult<()> { - let request_id = value.require_id()?; - let id = UserId::from(value.require_sender_i64()?); + let id = UserId::from(value.get_sender() as 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()) @@ -207,55 +193,56 @@ async fn update_user( .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(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(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(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(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(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(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(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(key) = value + if error.is_none() { + if 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(request_id)).await + connection.send(&response.with_id(value.get_id())).await } pub async fn change_user( @@ -269,35 +256,34 @@ 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.require_id()?, CommunicationType::ErrorInvalidData) + .send_error_response(value.get_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.require_id()?, CommunicationType::ErrorInvalidData) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) .await; }; - let user_id = UserId::from(value.require_sender_i64()?); + let user_id = UserId::from(value.get_sender() as i64); let user = match user_repo::get_by_user_id(user_id).await { Ok(user) => user, Err(_) => { return connection - .send_error_response(request_id, CommunicationType::ErrorNotFound) + .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) .await; } }; if user.token != reset { return connection - .send_error_response(request_id, CommunicationType::ErrorInvalidChallenge) + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge) .await; } let result = - match user_repo::change_iota_id(user_id, Some(IotaId::from(value.require_sender_i64()?))) + match user_repo::change_iota_id(user_id, Some(IotaId::from(value.get_sender() as i64))) .await { Ok(()) => user_repo::change_token(user_id, new_token.to_owned()).await, @@ -308,5 +294,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(request_id)).await + connection.send(&response.with_id(value.get_id())).await } diff --git a/src/transport/omikron_connection.rs b/src/transport/omikron_connection.rs index 83a9df7..82b9ece 100644 --- a/src/transport/omikron_connection.rs +++ b/src/transport/omikron_connection.rs @@ -1,13 +1,12 @@ use super::capabilities::{OmegaCapabilities, PeerCapabilities}; use crate::models::OmikronId; use crate::{ - log, log_cv_in, log_cv_out, log_err, log_in, server, + load_keyring, 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, mapref::entry::Entry}; +use dashmap::DashMap; use mtp::{ codec::{CommunicationType, CommunicationValue}, crypto::PublicKeyBundle, @@ -17,21 +16,19 @@ use mtp::{ use std::{ net::{IpAddr, Ipv4Addr}, sync::{ - Arc, Mutex as StdMutex, - atomic::{AtomicU32, AtomicUsize, Ordering}, + Arc, + atomic::{AtomicUsize, Ordering}, }, time::{Duration, Instant}, }; use tokio::{ - sync::{Mutex, Semaphore, mpsc, oneshot}, + sync::{Mutex, mpsc}, 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); @@ -50,27 +47,16 @@ impl Drop for ConnectionLimitGuard { pub type OmikronResult = crate::error::Result; pub struct WaitingTask { - pub sender: oneshot::Sender, + pub task: Box, CommunicationValue) -> bool + Send + Sync>, 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 { @@ -78,9 +64,6 @@ 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(); } } @@ -92,24 +75,17 @@ impl OmikronConnection { state: Arc, authenticated: bool, ) -> Option> { - if !authenticated || id == 0 || i64::try_from(id).is_err() { + if !authenticated { 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, })) } @@ -118,115 +94,6 @@ 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, @@ -250,16 +117,12 @@ impl OmikronConnection { self.clone().cleanup().await; return; } - self.start_ordered_worker(); - let cleanup_conn = Arc::downgrade(&self); + let cleanup_conn = self.clone(); *self.cleanup_handle.lock().unwrap() = Some(tokio::spawn(async move { let mut ticker = interval(CLEANUP_INTERVAL); loop { ticker.tick().await; - let Some(connection) = cleanup_conn.upgrade() else { - break; - }; - connection + cleanup_conn .waiting_tasks .retain(|_, task| task.inserted_at.elapsed() < MAX_WAITING_AGE); } @@ -301,81 +164,16 @@ impl OmikronConnection { async fn process_message(self: Arc, value: CommunicationValue) -> OmikronResult<()> { log_cv_in!(PrintType::Omikron, &value); - if Self::correlation_response(&value) - && let Some(message_id) = value.id() + if value.is_type(CommunicationType::Relay) { + return self.dispatch(value).await; + } + if let Some(message_id) = value.id() && let Some((_, task)) = self.waiting_tasks.remove(&message_id) { - let _ = task.sender.send(value); + let _ = (task.task)(self.clone(), value); return Ok(()); } - - 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, - } + self.dispatch(value).await } async fn dispatch(self: Arc, value: CommunicationValue) -> OmikronResult<()> { @@ -543,39 +341,33 @@ impl OmikronConnection { value: &CommunicationValue, timeout: Duration, ) -> OmikronResult { - 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); + 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(), + }, + ); - if let Err(error) = self.clone().send(&outbound).await { + if let Err(error) = self.clone().send(value).await { self.waiting_tasks.remove(&message_id); return Err(error); } - 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(), - )) - } + 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(), + )), Err(_) => { self.waiting_tasks.remove(&message_id); Err(crate::error::OmegaError::Transport( @@ -619,7 +411,6 @@ 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 { @@ -650,8 +441,7 @@ pub async fn get_by_omikron_id( description: Option, ) -> Option { PeerCapabilities::from_identification_description(description.as_deref()).ok()?; - 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)) + crate::db::omikron_repo::get_omikron_by_id(OmikronId::from(omikron_id as i64)) .await .ok() .map(|omikron| omikron.public_key) @@ -663,7 +453,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::web::build_web_config(state.identity.clone())? + let web_config = server::server::build_web_config()? .serve_tcp_https(true) .max_tcp_connections(256); let ip = IpAddr::from(Ipv4Addr::new(0, 0, 0, 0)); @@ -688,7 +478,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(connection_limit_guard); + drop(ConnectionLimitGuard(peer_ip)); log_err!( 0, PrintType::Omega, @@ -737,7 +526,6 @@ 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,11 +64,12 @@ 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) - && 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) { + if let Some(iota_id) = user.iota_id + && let Some(users) = iotas.get_mut(&iota_id.0) + { + users.push(user.id.0); + } } } } @@ -96,10 +97,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()) - && let Some(connection) = get_connected_omikron(key) - { - return Ok(connection); + if let Some(key) = keys.into_iter().choose(&mut rand::rng()) { + if 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 0cbd1f2..3d76705 100644 --- a/src/transport/relay_router.rs +++ b/src/transport/relay_router.rs @@ -21,6 +21,14 @@ 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 { @@ -32,6 +40,12 @@ impl RouteTarget { _ => None, } } + + pub const fn id(self) -> u64 { + match self { + Self::User(id) | Self::Iota(id) => id, + } + } } #[derive(Debug, Error)] @@ -169,11 +183,10 @@ mod tests { use mtp::codec::DataValue; fn wire(target: RouteTarget) -> u64 { - let (kind, id) = match target { - RouteTarget::User(id) => (USER_TARGET_KIND, id), - RouteTarget::Iota(id) => (IOTA_TARGET_KIND, id), + let Some(value) = target.wire_id() else { + panic!("valid route target was rejected"); }; - kind | id + value } fn relay_frame() -> CommunicationValue { diff --git a/src/util/file_util.rs b/src/util/file_util.rs index 05b9018..8630489 100644 --- a/src/util/file_util.rs +++ b/src/util/file_util.rs @@ -1,15 +1,293 @@ use once_cell::sync::Lazy; -use std::path::PathBuf; +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; static WORKING_DIR: Lazy = Lazy::new(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))); -pub fn load_file_vec(path: &str, name: &str) -> Result, std::io::Error> { - let dir = PathBuf::from(get_directory()).join(path); +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 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 c9c9719..9bd211a 100644 --- a/src/util/logger.rs +++ b/src/util/logger.rs @@ -10,6 +10,7 @@ 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(); @@ -222,7 +223,7 @@ pub fn log_cv_internal( let formatted = format_cv(cv); log_internal( - cv.sender().and_then(|sender| i64::try_from(sender).ok()), + Some(cv.get_sender() as i64), print_type.unwrap_or(PrintType::General), prefix, false, @@ -233,14 +234,14 @@ pub fn log_cv_internal( pub fn format_cv(cv: &CommunicationValue) -> String { let mut parts = Vec::new(); - let sender = cv.sender().filter(|sender| *sender != 0); - let receiver = cv.receiver().filter(|receiver| *receiver != 0); + let sender = cv.get_sender(); + let receiver = cv.get_receiver(); - if let (Some(sender), Some(receiver)) = (sender, receiver) { + if sender > 0 && receiver > 0 { parts.push(format!("{} > {}", sender, receiver)); - } else if let Some(sender) = sender { + } else if sender > 0 { parts.push(format!("{}", sender)); - } else if let Some(receiver) = receiver { + } else if receiver > 0 { parts.push(format!("> {}", receiver)); } @@ -248,11 +249,7 @@ pub fn format_cv(cv: &CommunicationValue) -> String { .get_comm_type_enum() .map(|kind| kind.to_string()) .unwrap_or_else(|| cv.get_type().to_string()); - let id = cv - .id() - .map(|id| id.to_string()) - .unwrap_or_else(|| "missing".to_string()); - parts.push(format!("{} (id={})", comm_type, id)); + parts.push(format!("{} (id={})", comm_type, cv.get_id())); let data = cv.data().unwrap_or(&[]); @@ -261,7 +258,7 @@ pub fn format_cv(cv: &CommunicationValue) -> String { Version(3, 0), ); - parts.push(formated_data.to_string()); + parts.push(format!("{}", formated_data)); parts.join(": ") }