diff --git a/Cargo.lock b/Cargo.lock index 4ce5e8d..a20fbae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,27 +18,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "aead" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" -dependencies = [ - "crypto-common 0.2.2", - "inout 0.2.2", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher 0.4.4", - "cpufeatures 0.2.17", -] - [[package]] name = "aes" version = "0.9.1" @@ -50,20 +29,6 @@ dependencies = [ "cpufeatures 0.3.0", ] -[[package]] -name = "aes-gcm" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" -dependencies = [ - "aead 0.6.1", - "aes 0.9.1", - "cipher 0.5.2", - "ctr", - "ghash", - "subtle", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -79,21 +44,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - [[package]] name = "asn1-rs" version = "0.7.2" @@ -133,148 +83,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand 2.5.0", - "futures-lite 2.6.1", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-global-executor" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -dependencies = [ - "async-channel 2.5.0", - "async-executor", - "async-io 2.6.0", - "async-lock 3.4.2", - "blocking", - "futures-lite 2.6.1", - "once_cell", -] - -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.28", - "slab", - "socket2 0.4.10", - "waker-fn", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite 2.6.1", - "parking", - "polling 3.11.0", - "rustix 1.1.4", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener 5.4.1", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-std" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" -dependencies = [ - "async-channel 1.9.0", - "async-global-executor", - "async-io 2.6.0", - "async-lock 3.4.2", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite 2.6.1", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - [[package]] name = "async-trait" version = "0.1.91" @@ -283,7 +91,7 @@ checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", - "syn 3.0.1", + "syn 3.0.2", ] [[package]] @@ -352,12 +160,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.13.1" @@ -383,19 +185,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", -] - -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel 2.5.0", - "async-task", - "futures-io", - "futures-lite 2.6.1", - "piper", + "zeroize", ] [[package]] @@ -477,7 +267,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ - "aead 0.5.2", + "aead", "chacha20 0.9.1", "cipher 0.4.4", "poly1305", @@ -501,7 +291,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "block-buffer 0.12.1", "crypto-common 0.2.2", "inout 0.2.2", ] @@ -554,9 +343,9 @@ checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "constant_time_eq" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "core-foundation" @@ -669,15 +458,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "ctr" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" -dependencies = [ - "cipher 0.5.2", -] - [[package]] name = "ctutils" version = "0.4.2" @@ -697,7 +477,7 @@ dependencies = [ "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest 0.10.7", - "fiat-crypto 0.2.9", + "fiat-crypto", "rustc_version", "subtle", "zeroize", @@ -781,17 +561,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "digest" version = "0.10.7" @@ -814,6 +583,7 @@ dependencies = [ "const-oid 0.10.2", "crypto-common 0.2.2", "ctutils", + "zeroize", ] [[package]] @@ -869,17 +639,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "ed448-goldilocks" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87b5fa9e9e3dd5fe1369f380acd3dcdfa766dbd0a1cd5b048fb40e38a6a78e79" -dependencies = [ - "fiat-crypto 0.1.20", - "hex", - "subtle", -] - [[package]] name = "either" version = "1.16.0" @@ -925,12 +684,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - [[package]] name = "event-listener" version = "5.4.1" @@ -942,16 +695,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener 5.4.1", - "pin-project-lite", -] - [[package]] name = "fastbloom" version = "0.17.0" @@ -964,27 +707,12 @@ dependencies = [ "siphasher", ] -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - [[package]] name = "fastrand" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" -[[package]] -name = "fiat-crypto" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" - [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1110,34 +838,6 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand 2.5.0", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - [[package]] name = "futures-macro" version = "0.3.33" @@ -1201,18 +901,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - [[package]] name = "getrandom" version = "0.4.3" @@ -1222,32 +910,11 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi 6.0.0", + "r-efi", "rand_core 0.10.1", "wasm-bindgen", ] -[[package]] -name = "ghash" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" -dependencies = [ - "polyval", -] - -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "h2" version = "0.4.15" @@ -1274,7 +941,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10872b55cfb02a821b69dc7cf8dc6a71d6af25eb9a79662bec4a9d016056b3be" dependencies = [ "bytes", - "fastrand 2.5.0", + "fastrand", "futures-util", "http", "pin-project-lite", @@ -1361,18 +1028,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - [[package]] name = "hex" version = "0.4.3" @@ -1481,9 +1136,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -1532,7 +1187,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.5", + "socket2", "system-configuration", "tokio", "tower-service", @@ -1671,26 +1326,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ipnet" version = "2.12.0" @@ -1773,12 +1408,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" - [[package]] name = "keccak" version = "0.1.6" @@ -1798,15 +1427,6 @@ dependencies = [ "cpufeatures 0.3.0", ] -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -1840,7 +1460,7 @@ version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ - "bitflags 2.13.1", + "bitflags", "libc", "plain", "redox_syscall 0.9.0", @@ -1856,18 +1476,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - [[package]] name = "litemap" version = "0.8.2" @@ -1888,9 +1496,6 @@ name = "log" version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" -dependencies = [ - "value-bag", -] [[package]] name = "lru-slab" @@ -1900,12 +1505,11 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "lzma-rust2" -version = "0.13.0" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" +checksum = "ca93e534d1142d1d0dcca6d25fe302508a5dfb40b302802904577725ea0b695b" dependencies = [ - "crc", - "sha2 0.10.9", + "sha2 0.11.0", ] [[package]] @@ -2252,31 +1856,25 @@ dependencies = [ name = "omega" version = "0.1.0" dependencies = [ - "aes-gcm", "ansi_term", - "anyhow", "base64", "bytes", "dashmap", "dotenv", - "hex", - "hkdf 0.12.4", "http", - "json", "mtp", "once_cell", - "rand 0.8.7", - "rand_core 0.6.4", + "rand 0.10.2", "reqwest", "rustls", - "sha2 0.10.9", + "serde", + "serde_json", "sqlx", "strum", "strum_macros", "thiserror 2.0.19", "tokio", "uuid", - "x448", "zip", ] @@ -2329,12 +1927,12 @@ dependencies = [ [[package]] name = "pbkdf2" -version = "0.12.2" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ - "digest 0.10.7", - "hmac 0.12.1", + "digest 0.11.3", + "hmac 0.13.0", ] [[package]] @@ -2368,23 +1966,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" -dependencies = [ - "atomic-waker", - "fastrand 2.5.0", - "futures-io", -] - [[package]] name = "pkcs1" version = "0.7.5" @@ -2428,36 +2009,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi 0.5.2", - "pin-project-lite", - "rustix 1.1.4", - "windows-sys 0.61.2", -] - [[package]] name = "poly1305" version = "0.8.0" @@ -2466,18 +2017,7 @@ checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ "cpufeatures 0.2.17", "opaque-debug", - "universal-hash 0.5.1", -] - -[[package]] -name = "polyval" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" -dependencies = [ - "cpubits", - "cpufeatures 0.3.0", - "universal-hash 0.6.1", + "universal-hash", ] [[package]] @@ -2539,7 +2079,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.5", + "socket2", "thiserror 2.0.19", "tokio", "tracing", @@ -2580,7 +2120,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.5", + "socket2", "tracing", "windows-sys 0.61.2", ] @@ -2594,12 +2134,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "r-efi" version = "6.0.0" @@ -2638,12 +2172,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" - [[package]] name = "rand_core" version = "0.6.4" @@ -2689,7 +2217,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.1", + "bitflags", ] [[package]] @@ -2698,7 +2226,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" dependencies = [ - "bitflags 2.13.1", + "bitflags", ] [[package]] @@ -2797,33 +2325,6 @@ dependencies = [ "nom", ] -[[package]] -name = "rustix" -version = "0.37.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.13.1", - "errno", - "libc", - "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", -] - [[package]] name = "rustls" version = "0.23.42" @@ -2943,7 +2444,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.1", + "bitflags", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -2993,14 +2494,14 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.1", + "syn 3.0.2", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -3045,6 +2546,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" @@ -3167,16 +2679,6 @@ dependencies = [ "serde", ] -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.6.5" @@ -3241,14 +2743,12 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "async-io 1.13.0", - "async-std", "base64", "bytes", "crc", "crossbeam-queue", "either", - "event-listener 5.4.1", + "event-listener", "futures-core", "futures-intrusive", "futures-io", @@ -3265,6 +2765,8 @@ dependencies = [ "sha2 0.10.9", "smallvec", "thiserror 2.0.19", + "tokio", + "tokio-stream", "tracing", "url", ] @@ -3288,7 +2790,6 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ - "async-std", "dotenvy", "either", "heck", @@ -3304,6 +2805,7 @@ dependencies = [ "sqlx-postgres", "sqlx-sqlite", "syn 2.0.119", + "tokio", "url", ] @@ -3315,7 +2817,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64", - "bitflags 2.13.1", + "bitflags", "byteorder", "bytes", "crc", @@ -3339,7 +2841,7 @@ dependencies = [ "rand 0.8.7", "rsa", "serde", - "sha1", + "sha1 0.10.7", "sha2 0.10.9", "smallvec", "sqlx-core", @@ -3357,7 +2859,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64", - "bitflags 2.13.1", + "bitflags", "byteorder", "crc", "dotenvy", @@ -3429,15 +2931,15 @@ dependencies = [ [[package]] name = "strum" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" [[package]] name = "strum_macros" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ "heck", "proc-macro2", @@ -3464,9 +2966,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5edbec4ed188954a10c12c038215f8ce7606b2d5c973cd8dc43e8795065c5f2f" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" dependencies = [ "proc-macro2", "quote", @@ -3499,7 +3001,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.13.1", + "bitflags", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3551,16 +3053,17 @@ checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 3.0.1", + "syn 3.0.2", ] [[package]] name = "time" -version = "0.3.53" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", + "js-sys", "num-conv", "powerfmt", "serde_core", @@ -3576,9 +3079,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -3611,9 +3114,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.53.0" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -3621,7 +3124,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.5", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -3647,6 +3150,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -3681,7 +3195,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.1", + "bitflags", "bytes", "futures-util", "http", @@ -3743,6 +3257,12 @@ 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" @@ -3786,16 +3306,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "universal-hash" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" -dependencies = [ - "crypto-common 0.2.2", - "ctutils", -] - [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -3843,12 +3353,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "value-bag" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd4ec1eb1d240636e354a30110a1dfcb37047169a4d9bd6d9d3469df574b5c4" - [[package]] name = "vcpkg" version = "0.2.15" @@ -3861,12 +3365,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - [[package]] name = "walkdir" version = "2.5.0" @@ -3892,15 +3390,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasite" version = "0.1.0" @@ -4215,12 +3704,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - [[package]] name = "writeable" version = "0.6.3" @@ -4241,7 +3724,7 @@ dependencies = [ "rustls-native-certs", "rustls-pki-types", "sha2 0.11.0", - "socket2 0.6.5", + "socket2", "thiserror 2.0.19", "time", "tokio", @@ -4275,17 +3758,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "x448" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd07d4fae29e07089dbcacf7077cd52dce7760125ca9a4dd5a35ca603ffebb" -dependencies = [ - "ed448-goldilocks", - "hex", - "rand_core 0.5.1", -] - [[package]] name = "x509-parser" version = "0.18.1" @@ -4340,18 +3812,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.54" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.54" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" dependencies = [ "proc-macro2", "quote", @@ -4434,26 +3906,26 @@ dependencies = [ [[package]] name = "zip" -version = "6.0.0" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ - "aes 0.8.4", - "arbitrary", + "aes", "bzip2", "constant_time_eq", "crc32fast", "deflate64", "flate2", - "getrandom 0.3.4", - "hmac 0.12.1", + "getrandom 0.4.3", + "hmac 0.13.0", "indexmap", "lzma-rust2", "memchr", "pbkdf2", "ppmd-rust", - "sha1", + "sha1 0.11.0", "time", + "typed-path", "zeroize", "zopfli", "zstd", diff --git a/Cargo.toml b/Cargo.toml index e601d1a..3b4f6a3 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,33 +10,27 @@ mtp = { git = "https://git.methanium.net/methanium/mtp", features = [ "web-server", ] } -aes-gcm = "*" ansi_term = "0.12.1" -anyhow = "1.0.101" base64 = "0.22.1" bytes = "1" -dashmap = "6.1.0" +dashmap = "6.2.1" dotenv = "0.15.0" -hex = "0.4.3" -hkdf = "0.12.4" http = "1" -json = "0.12.4" -once_cell = "1.21.3" -rand = "0.8" -rand_core = { version = "0.6", features = ["getrandom", "std"] } -reqwest = { version = "0.13.2" } -rustls = { version = "0.23.37", default-features = false, features = [ +once_cell = "1.21.4" +rand = "0.10.2" +reqwest = "0.13.4" +rustls = { version = "0.23.42", default-features = false, features = [ "std", "tls12", "aws-lc-rs", "prefer-post-quantum", ] } -sha2 = "0.10.9" -sqlx = { version = "0.8.6", features = ["mysql", "runtime-async-std"] } -strum = "0.27.2" -strum_macros = "0.27.2" +sqlx = { version = "0.8.6", features = ["mysql", "runtime-tokio", "migrate"] } +strum = "0.28.0" +strum_macros = "0.28.0" tokio = { version = "*", features = ["full"] } -uuid = { version = "1.19.0", features = ["v4"] } -x448 = "0.6.0" -zip = "6.0.0" -thiserror = "2.0.18" +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" diff --git a/migrations/001.sql b/migrations/001.sql new file mode 100644 index 0000000..dc3a096 --- /dev/null +++ b/migrations/001.sql @@ -0,0 +1,44 @@ +CREATE TABLE IF NOT EXISTS users ( + id BIGINT NOT NULL PRIMARY KEY, + iota_id BIGINT NOT NULL, + username VARBINARY(255) NOT NULL, + display VARBINARY(255), + status VARBINARY(255), + about VARBINARY(1000), + avatar BLOB, + sub_level INT NOT NULL DEFAULT 0, + sub_end BIGINT NOT NULL DEFAULT 0, + public_key BLOB NOT NULL, + token BLOB NOT NULL, + UNIQUE KEY uk_users_username (username), + UNIQUE KEY uk_users_iota_id (iota_id) +); + +CREATE TABLE IF NOT EXISTS iotas ( + id BIGINT NOT NULL PRIMARY KEY, + public_key BLOB NOT NULL +); + +CREATE TABLE IF NOT EXISTS omikrons ( + id BIGINT NOT NULL PRIMARY KEY, + public_key BLOB NOT NULL, + location VARBINARY(255) NOT NULL, + ip_address VARBINARY(45) NOT NULL, + port INT NOT NULL +); + +CREATE TABLE IF NOT EXISTS notifications ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + sender_id BIGINT NOT NULL, + receiver_id BIGINT NOT NULL, + amount BIGINT NOT NULL DEFAULT 1, + UNIQUE KEY uk_notifications_sender_receiver (sender_id, receiver_id), + INDEX idx_notifications_receiver (receiver_id) +); + +CREATE TABLE IF NOT EXISTS short_links ( + short_key VARCHAR(12) CHARACTER SET ascii COLLATE ascii_bin NOT NULL PRIMARY KEY, + long_url TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_short_links_created_at (created_at) +); diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..4c6f2cd --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1 @@ +pub mod response; diff --git a/src/api/response.rs b/src/api/response.rs new file mode 100644 index 0000000..e8489b5 --- /dev/null +++ b/src/api/response.rs @@ -0,0 +1,70 @@ +use serde::Serialize; +use std::collections::BTreeMap; + +#[derive(Serialize)] +pub struct StatusResponse { + pub status: &'static str, +} + +#[derive(Serialize)] +pub struct OmikronResponse { + pub status: &'static str, + pub id: i64, + pub public_key: String, + pub ip_address: String, + pub port: u16, +} + +#[derive(Serialize)] +pub struct IotaResponse { + pub status: &'static str, + pub iota_id: i64, + pub public_key: String, +} + +#[derive(Serialize)] +pub struct UserResponse { + pub status: &'static str, + pub username: String, + pub public_key: String, + pub user_id: i64, + pub iota_id: i64, + pub sub_level: i32, + pub sub_end: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub display: Option, + #[serde(rename = "status_message", skip_serializing_if = "Option::is_none")] + pub status_message: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub about: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar: Option, +} + +#[derive(Serialize)] +pub struct UsernameResponse { + pub status: &'static str, + pub username: String, + pub public_key: String, + pub user_id: i64, + pub iota_id: i64, + pub sub_level: i32, + pub sub_end: i64, +} + +#[derive(Serialize)] +pub struct ConnectionsResponse { + pub status: &'static str, + #[serde(flatten)] + pub connections: BTreeMap>>, +} + +#[derive(Serialize)] +pub struct PublicKeyResponse { + pub status: &'static str, + pub public_key: String, +} + +pub fn json(value: &T) -> String { + serde_json::to_string(value).unwrap_or_else(|_| "{\"status\":\"error\"}".to_string()) +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..5ba9dc2 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,65 @@ +use std::{env, time::Duration}; + +#[derive(Clone, Debug)] +pub struct RateLimitConfig { + pub window: Duration, + pub general_requests: usize, + pub registration_requests: usize, + pub transport_connections: usize, + pub transport_connections_per_ip: usize, +} + +pub fn cors_origin() -> String { + env::var("CORS_ORIGIN").unwrap_or_else(|_| "https://tensamin.net".to_string()) +} + +impl Default for RateLimitConfig { + fn default() -> Self { + Self { + window: Duration::from_secs(60), + general_requests: 120, + registration_requests: 20, + transport_connections: 512, + transport_connections_per_ip: 32, + } + } +} + +impl RateLimitConfig { + pub fn from_env() -> Self { + let defaults = Self::default(); + Self { + window: env_duration("RATE_LIMIT_WINDOW_SECONDS", defaults.window), + general_requests: env_usize("RATE_LIMIT_GENERAL_REQUESTS", defaults.general_requests), + 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, + ), + } + } +} + +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_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 new file mode 100644 index 0000000..a5952f1 --- /dev/null +++ b/src/db/iota_repo.rs @@ -0,0 +1,53 @@ +use crate::{ + db::pool, + error::{OmegaError, Result}, + models::{Iota, IotaId}, +}; +use mtp::crypto::PublicKeyBundle; +use sqlx::Row; + +pub async fn get_iota_by_id(id: IotaId) -> Result { + let row = sqlx::query("SELECT id, public_key FROM iotas WHERE id = ?") + .bind(id.0) + .fetch_optional(&pool().await?) + .await? + .ok_or(OmegaError::NotFound)?; + let key = PublicKeyBundle::from_bytes(&row.get::, _>("public_key")) + .map_err(|error| OmegaError::Validation(error.to_string()))?; + Ok(Iota { + id: row.get::("id").into(), + public_key: key, + }) +} + +pub async fn create_new_iota(public_key: PublicKeyBundle) -> Result { + let id = crate::db::user_repo::get_register_id().await?; + let iota_id = IotaId::from(id.0); + register_complete_iota(iota_id, public_key).await?; + Ok(iota_id) +} + +pub async fn register_complete_iota(id: IotaId, public_key: PublicKeyBundle) -> Result<()> { + sqlx::query("INSERT INTO iotas (id, public_key) VALUES (?, ?)") + .bind(id.0) + .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) + .execute(&pool().await?) + .await?; + Ok(()) +} diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..f8afccb --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,38 @@ +use crate::error::{OmegaError, Result}; +use once_cell::sync::Lazy; +use sqlx::{MySql, Pool, mysql::MySqlPoolOptions}; +use std::{env, sync::Arc}; +use tokio::sync::RwLock; + +pub mod iota_repo; +pub mod notification_repo; +pub mod omikron_repo; +pub mod short_link_repo; +pub mod user_repo; + +pub type DbPool = Pool; + +static POOL: Lazy>>> = Lazy::new(|| Arc::new(RwLock::new(None))); + +pub async fn pool() -> Result { + POOL.read() + .await + .as_ref() + .cloned() + .ok_or(OmegaError::DatabaseNotInitialized) +} + +pub async fn initialize() -> Result<()> { + let url = + env::var("DB_URL").map_err(|_| OmegaError::Validation("DB_URL is not set".to_string()))?; + let pool = MySqlPoolOptions::new() + .max_connections(200) + .connect(&url) + .await?; + sqlx::migrate!() + .run(&pool) + .await + .map_err(|error| OmegaError::Validation(format!("database migration failed: {error}")))?; + *POOL.write().await = Some(pool); + Ok(()) +} diff --git a/src/db/notification_repo.rs b/src/db/notification_repo.rs new file mode 100644 index 0000000..b317472 --- /dev/null +++ b/src/db/notification_repo.rs @@ -0,0 +1,38 @@ +use crate::{ + db::pool, + error::Result, + models::{Notification, UserId}, +}; +use sqlx::Row; + +pub async fn add_notification(sender_id: UserId, receiver_id: UserId) -> Result<()> { + sqlx::query("INSERT INTO notifications (sender_id, receiver_id, amount) VALUES (?, ?, 1) ON DUPLICATE KEY UPDATE amount = amount + 1").bind(sender_id.0).bind(receiver_id.0).execute(&pool().await?).await?; + Ok(()) +} + +pub async fn read_notification(sender_id: UserId, receiver_id: UserId) -> Result<()> { + sqlx::query("DELETE FROM notifications WHERE sender_id = ? AND receiver_id = ?") + .bind(sender_id.0) + .bind(receiver_id.0) + .execute(&pool().await?) + .await?; + Ok(()) +} + +pub async fn get_notifications(receiver_id: UserId) -> Result> { + let rows = sqlx::query( + "SELECT id, sender_id, receiver_id, amount FROM notifications WHERE receiver_id = ?", + ) + .bind(receiver_id.0) + .fetch_all(&pool().await?) + .await?; + Ok(rows + .into_iter() + .map(|row| Notification { + id: row.get("id"), + sender_id: row.get::("sender_id").into(), + receiver_id: row.get::("receiver_id").into(), + amount: row.get("amount"), + }) + .collect()) +} diff --git a/src/db/omikron_repo.rs b/src/db/omikron_repo.rs new file mode 100644 index 0000000..03e1661 --- /dev/null +++ b/src/db/omikron_repo.rs @@ -0,0 +1,29 @@ +use crate::{ + db::pool, + error::{OmegaError, Result}, + models::{Omikron, OmikronId}, +}; +use mtp::crypto::PublicKeyBundle; +use sqlx::Row; + +pub async fn get_omikron_by_id(id: OmikronId) -> Result { + let row = + sqlx::query("SELECT id, public_key, location, ip_address, port FROM omikrons WHERE id = ?") + .bind(id.0) + .fetch_optional(&pool().await?) + .await? + .ok_or(OmegaError::NotFound)?; + let key = PublicKeyBundle::from_bytes(&row.get::, _>("public_key")) + .map_err(|error| OmegaError::Validation(error.to_string()))?; + let location = String::from_utf8(row.get("location")) + .map_err(|error| OmegaError::Validation(format!("invalid location UTF-8: {error}")))?; + let ip_address = String::from_utf8(row.get("ip_address")) + .map_err(|error| OmegaError::Validation(format!("invalid IP address UTF-8: {error}")))?; + Ok(Omikron { + id: row.get::("id").into(), + public_key: key, + location, + ip_address, + port: row.get::("port") as u16, + }) +} diff --git a/src/db/short_link_repo.rs b/src/db/short_link_repo.rs new file mode 100644 index 0000000..24d85ac --- /dev/null +++ b/src/db/short_link_repo.rs @@ -0,0 +1,35 @@ +use crate::{db::pool, error::Result}; + +pub async fn count() -> Result { + let count = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM short_links") + .fetch_one(&pool().await?) + .await?; + Ok(count.max(0) as u64) +} + +pub async fn insert(short_key: &str, long_url: &str) -> Result { + let result = sqlx::query("INSERT IGNORE INTO short_links (short_key, long_url) VALUES (?, ?)") + .bind(short_key) + .bind(long_url) + .execute(&pool().await?) + .await?; + Ok(result.rows_affected() == 1) +} + +pub async fn get(short_key: &str) -> Result> { + Ok( + sqlx::query_scalar::<_, String>("SELECT long_url FROM short_links WHERE short_key = ?") + .bind(short_key) + .fetch_optional(&pool().await?) + .await?, + ) +} + +pub async fn delete_expired() -> Result { + let result = sqlx::query( + "DELETE FROM short_links WHERE created_at < CURRENT_TIMESTAMP - INTERVAL 7 DAY", + ) + .execute(&pool().await?) + .await?; + Ok(result.rows_affected()) +} diff --git a/src/db/user_repo.rs b/src/db/user_repo.rs new file mode 100644 index 0000000..ca71dd3 --- /dev/null +++ b/src/db/user_repo.rs @@ -0,0 +1,194 @@ +use crate::{ + db::pool, + error::{OmegaError, Result}, + models::{IotaId, User, UserId}, +}; +use mtp::crypto::PublicKeyBundle; +use sqlx::FromRow; + +pub async fn get_register_id() -> Result { + let bytes = *uuid::Uuid::now_v7().as_bytes(); + let id = + i64::from_be_bytes(bytes[8..].try_into().map_err(|_| { + OmegaError::Validation("generated ID has an invalid length".to_string()) + })?) & i64::MAX; + Ok(UserId::from(id.max(1))) +} + +const USER_BY_USERNAME_QUERY: &str = "SELECT id, iota_id, username, display, status, about, avatar, sub_level, sub_end, public_key, token FROM users WHERE username = ?"; +const USER_BY_ID_QUERY: &str = "SELECT id, iota_id, username, display, status, about, avatar, sub_level, sub_end, public_key, token FROM users WHERE id = ?"; +const USERS_BY_IOTA_ID_QUERY: &str = "SELECT id, iota_id, username, display, status, about, avatar, sub_level, sub_end, public_key, token FROM users WHERE iota_id = ?"; + +#[derive(FromRow)] +struct UserRow { + id: i64, + iota_id: i64, + username: Vec, + display: Option>, + status: Option>, + about: Option>, + avatar: Option>, + sub_level: i32, + sub_end: i64, + public_key: Vec, + token: Vec, +} + +impl TryFrom for User { + type Error = sqlx::Error; + + fn try_from(row: UserRow) -> std::result::Result { + let public_key = PublicKeyBundle::from_bytes(&row.public_key) + .map_err(|error| sqlx::Error::Decode(Box::new(error)))?; + let decode = + |value| String::from_utf8(value).map_err(|error| sqlx::Error::Decode(Box::new(error))); + Ok(User { + id: row.id.into(), + iota_id: row.iota_id.into(), + username: decode(row.username)?, + display: row.display.map(decode).transpose()?, + status: row.status.map(decode).transpose()?, + about: row.about.map(decode).transpose()?, + avatar: row.avatar, + sub_level: row.sub_level, + sub_end: row.sub_end, + public_key, + token: decode(row.token)?, + }) + } +} + +pub async fn get_by_username(username: &str) -> Result { + let row = sqlx::query_as::<_, UserRow>(USER_BY_USERNAME_QUERY) + .bind(username) + .fetch_optional(&pool().await?) + .await? + .ok_or(OmegaError::NotFound)?; + row.try_into().map_err(OmegaError::from) +} + +pub async fn get_by_user_id(id: UserId) -> Result { + let row = sqlx::query_as::<_, UserRow>(USER_BY_ID_QUERY) + .bind(id.0) + .fetch_optional(&pool().await?) + .await? + .ok_or(OmegaError::NotFound)?; + row.try_into().map_err(OmegaError::from) +} + +pub async fn get_users_by_iota_id(id: IotaId) -> Result> { + let rows = sqlx::query_as::<_, UserRow>(USERS_BY_IOTA_ID_QUERY) + .bind(id.0) + .fetch_all(&pool().await?) + .await?; + rows.into_iter() + .map(|row| row.try_into().map_err(OmegaError::from)) + .collect() +} + +async fn update( + id: UserId, + query: &'static str, + value: impl Send + sqlx::Encode<'static, sqlx::MySql> + sqlx::Type + 'static, +) -> Result<()> { + sqlx::query(query) + .bind(value) + .bind(id.0) + .execute(&pool().await?) + .await?; + Ok(()) +} + +pub async fn change_username(id: UserId, value: String) -> Result<()> { + update( + id, + "UPDATE users SET username = ? WHERE id = ?", + value.into_bytes(), + ) + .await +} +pub async fn change_display_name(id: UserId, value: String) -> Result<()> { + update( + id, + "UPDATE users SET display = ? WHERE id = ?", + value.into_bytes(), + ) + .await +} +pub async fn change_avatar(id: UserId, value: String) -> Result<()> { + update( + id, + "UPDATE users SET avatar = ? WHERE id = ?", + value.into_bytes(), + ) + .await +} +pub async fn change_about(id: UserId, value: String) -> Result<()> { + update( + id, + "UPDATE users SET about = ? WHERE id = ?", + value.into_bytes(), + ) + .await +} +pub async fn change_status(id: UserId, value: String) -> Result<()> { + update( + id, + "UPDATE users SET status = ? WHERE id = ?", + value.into_bytes(), + ) + .await +} + +pub async fn change_iota_id(id: UserId, value: IotaId) -> Result<()> { + sqlx::query("UPDATE users SET iota_id = ? WHERE id = ?") + .bind(value.0) + .bind(id.0) + .execute(&pool().await?) + .await?; + Ok(()) +} +pub async fn change_token(id: UserId, value: String) -> Result<()> { + update( + id, + "UPDATE users SET token = ? WHERE id = ?", + value.into_bytes(), + ) + .await +} +pub async fn delete_user(id: UserId) -> Result<()> { + sqlx::query("DELETE FROM users WHERE id = ?") + .bind(id.0) + .execute(&pool().await?) + .await?; + Ok(()) +} + +pub async fn change_keys(id: UserId, public_key: PublicKeyBundle) -> Result<()> { + sqlx::query("UPDATE users SET public_key = ? WHERE id = ?") + .bind(public_key.as_bytes()) + .bind(id.0) + .execute(&pool().await?) + .await?; + Ok(()) +} + +pub async fn register_complete_user( + id: UserId, + username: String, + public_key: PublicKeyBundle, + iota_id: IotaId, + token: String, +) -> Result<()> { + sqlx::query( + "INSERT INTO users (id, username, public_key, iota_id, token) VALUES (?, ?, ?, ?, ?)", + ) + .bind(id.0) + .bind(username.into_bytes()) + .bind(public_key.as_bytes()) + .bind(iota_id.0) + .bind(token.into_bytes()) + .execute(&pool().await?) + .await?; + Ok(()) +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..9911d25 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,57 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum OmegaError { + #[error("database pool is not initialized")] + DatabaseNotInitialized, + #[error("database error: {0}")] + Database(sqlx::Error), + #[error("invalid input: {0}")] + Validation(String), + #[error("resource not found")] + NotFound, + #[error("transport error: {0}")] + Transport(String), + #[error("not connected")] + NotConnected, + #[error("not authenticated")] + NotAuthenticated, + #[error("invalid response")] + InvalidResponse, + #[error("authentication failed")] + AuthenticationFailed, + #[error("send error: {0}")] + SendError(String), + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), +} + +pub type Result = std::result::Result; + +impl From for OmegaError { + fn from(error: sqlx::Error) -> Self { + if matches!(error, sqlx::Error::RowNotFound) { + Self::NotFound + } else { + Self::Database(error) + } + } +} + +impl OmegaError { + pub fn status_code(&self) -> http::StatusCode { + match self { + Self::Validation(_) => http::StatusCode::BAD_REQUEST, + Self::NotFound => http::StatusCode::NOT_FOUND, + Self::DatabaseNotInitialized + | Self::Database(_) + | Self::Transport(_) + | Self::NotConnected + | Self::NotAuthenticated + | Self::InvalidResponse + | Self::AuthenticationFailed + | Self::SendError(_) + | Self::Io(_) => http::StatusCode::INTERNAL_SERVER_ERROR, + } + } +} diff --git a/src/main.rs b/src/main.rs index 9e35297..251713e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,30 +1,41 @@ +mod api; +mod config; +mod db; +pub mod error; +mod models; mod server; mod sql; mod transport; mod util; -use crate::sql::sql::initialize_db; -use crate::sql::sql::print_users; +pub use error::{OmegaError, Result}; + +use crate::db::initialize; use crate::transport::omikron_connection; use crate::util::file_util::get_directory; use crate::util::logger::PrintType; use crate::util::logger::startup; use dotenv::from_path; -use mtp::files::{load_keyring_raw, save_keyring_raw, save_public_key_bundle}; use mtp::crypto::Keyring; +use mtp::files::{load_keyring_raw, save_keyring_raw, save_public_key_bundle}; use once_cell::sync::Lazy; use rustls::crypto::aws_lc_rs::default_provider; use std::env; use std::path::Path; +use std::time::Duration; +use tokio::time::interval; const KEYRING_PATH: &str = "./omega.mk"; static KEYRING: Lazy = Lazy::new(|| { load_keyring_raw(KEYRING_PATH).unwrap_or_else(|_| { let kr = Keyring::generate(); - save_keyring_raw(&kr, KEYRING_PATH).expect("Failed to save generated keyring"); - save_public_key_bundle(&kr.public_key_bundle(), KEYRING_PATH) - .expect("Failed to save generated public key bundle"); + 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 }) @@ -34,7 +45,10 @@ pub fn get_keyring() -> &'static Keyring { &KEYRING } pub fn load_keyring() -> Keyring { - Keyring::from_bytes(&KEYRING.to_bytes()).unwrap() + Keyring::from_bytes(&KEYRING.to_bytes()).unwrap_or_else(|error| { + eprintln!("Failed to clone keyring: {error}"); + Keyring::generate() + }) } #[tokio::main] @@ -50,7 +64,7 @@ async fn main() { log!("Started"); log!(" .env"); - if let Err(e) = initialize_db().await { + if let Err(e) = initialize().await { log!("[FATAL] Database initialization failed: {}", e); log!( "[FATAL] Please ensure the database is running and the .env file is configured correctly." @@ -59,12 +73,21 @@ async fn main() { } else { log!(" DB"); } - if let Err(e) = print_users().await { - log!("[ERROR] Failed to print users: {}", e); - } else { - log!(" Users"); - } - + let rate_limit_cleanup = crate::server::middleware::spawn_cleanup_task(); + let short_link_cleanup = tokio::spawn(async { + let mut ticker = interval(Duration::from_secs(24 * 60 * 60)); + loop { + ticker.tick().await; + if let Err(error) = crate::db::short_link_repo::delete_expired().await { + log_err!( + 0, + PrintType::General, + "Short-link cleanup failed: {}", + error + ); + } + } + }); let port: u16 = env::var("PORT") .ok() .and_then(|s| s.parse().ok()) @@ -80,4 +103,6 @@ async fn main() { log!("Shutting down on signal..."); } } + rate_limit_cleanup.abort(); + short_link_cleanup.abort(); } diff --git a/src/models/ids.rs b/src/models/ids.rs new file mode 100644 index 0000000..7eda84f --- /dev/null +++ b/src/models/ids.rs @@ -0,0 +1,30 @@ +use std::fmt::{Display, Formatter}; + +macro_rules! id_type { + ($name:ident) => { + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Serialize)] + pub struct $name(pub i64); + + impl From for $name { + fn from(value: i64) -> Self { + Self(value) + } + } + + impl From<$name> for i64 { + fn from(value: $name) -> Self { + value.0 + } + } + + impl Display for $name { + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(formatter) + } + } + }; +} + +id_type!(UserId); +id_type!(IotaId); +id_type!(OmikronId); diff --git a/src/models/iota.rs b/src/models/iota.rs new file mode 100644 index 0000000..3a93bcb --- /dev/null +++ b/src/models/iota.rs @@ -0,0 +1,9 @@ +use super::IotaId; +use mtp::crypto::PublicKeyBundle; + +#[derive(Clone, Debug, serde::Serialize)] +pub struct Iota { + pub id: IotaId, + #[serde(serialize_with = "crate::models::serialize_public_key")] + pub public_key: PublicKeyBundle, +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..291729d --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,21 @@ +mod ids; +mod iota; +mod notification; +mod omikron; +mod user; + +fn serialize_public_key( + key: &mtp::crypto::PublicKeyBundle, + serializer: S, +) -> std::result::Result +where + S: serde::Serializer, +{ + serializer.serialize_str(&key.to_base64()) +} + +pub use ids::{IotaId, OmikronId, UserId}; +pub use iota::Iota; +pub use notification::Notification; +pub use omikron::Omikron; +pub use user::User; diff --git a/src/models/notification.rs b/src/models/notification.rs new file mode 100644 index 0000000..8dee06e --- /dev/null +++ b/src/models/notification.rs @@ -0,0 +1,9 @@ +use super::UserId; + +#[derive(Clone, Debug, serde::Serialize)] +pub struct Notification { + pub id: i64, + pub sender_id: UserId, + pub receiver_id: UserId, + pub amount: i64, +} diff --git a/src/models/omikron.rs b/src/models/omikron.rs new file mode 100644 index 0000000..3b47d44 --- /dev/null +++ b/src/models/omikron.rs @@ -0,0 +1,12 @@ +use super::OmikronId; +use mtp::crypto::PublicKeyBundle; + +#[derive(Clone, Debug, serde::Serialize)] +pub struct Omikron { + pub id: OmikronId, + #[serde(serialize_with = "crate::models::serialize_public_key")] + pub public_key: PublicKeyBundle, + pub location: String, + pub ip_address: String, + pub port: u16, +} diff --git a/src/models/user.rs b/src/models/user.rs new file mode 100644 index 0000000..0dda84c --- /dev/null +++ b/src/models/user.rs @@ -0,0 +1,19 @@ +use super::{IotaId, UserId}; +use mtp::crypto::PublicKeyBundle; + +#[derive(Clone, Debug, serde::Serialize)] +pub struct User { + pub id: UserId, + pub iota_id: IotaId, + pub username: String, + pub display: Option, + pub status: Option, + pub about: Option, + pub avatar: Option>, + pub sub_level: i32, + pub sub_end: i64, + #[serde(serialize_with = "crate::models::serialize_public_key")] + pub public_key: PublicKeyBundle, + #[serde(skip_serializing)] + pub token: String, +} diff --git a/src/server/api.rs b/src/server/api.rs index 6471398..6dc01aa 100644 --- a/src/server/api.rs +++ b/src/server/api.rs @@ -1,356 +1,240 @@ +use crate::api::response::{ + ConnectionsResponse, IotaResponse, OmikronResponse, PublicKeyResponse, StatusResponse, + UserResponse, UsernameResponse, json, +}; +use crate::db::{ + iota_repo::get_iota_by_id, + omikron_repo::get_omikron_by_id, + user_repo::{get_by_user_id, get_by_username}, +}; +use crate::error::{OmegaError, Result}; use crate::load_keyring; -use crate::sql::sql; -use crate::sql::sql::{get_by_user_id, get_iota_by_id, get_omikron_by_id}; +use crate::models::UserId; +use crate::server::{ + middleware, + validation::{parse_positive_id, validate_non_empty}, +}; use crate::sql::user_online_tracker::{get_all_connections, get_iota_primary_omikron_connection}; use crate::transport::omikron_manager::get_random_omikron; use crate::util::file_util::get_directory; use base64::Engine as _; use bytes::Bytes; use http::{Method, StatusCode}; -use json::JsonValue; use mtp::webserver::{Http3Request, Http3Response, RouteParams}; +use std::collections::BTreeMap; + +fn error_body(error: &OmegaError) -> String { + json(&StatusResponse { + status: match error { + OmegaError::Validation(_) => "error_bad_request", + OmegaError::NotFound => "error_not_found", + _ => "error", + }, + }) +} + +fn user_response(user: crate::models::User) -> UserResponse { + UserResponse { + status: "success", + username: user.username, + public_key: user.public_key.to_base64(), + user_id: user.id.0, + iota_id: user.iota_id.0, + sub_level: user.sub_level, + sub_end: user.sub_end, + display: user.display, + status_message: user.status, + about: user.about, + avatar: user + .avatar + .map(|value| base64::engine::general_purpose::STANDARD.encode(value)), + } +} + +async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> { + match path_parts { + ["api", "get", "omikron"] => { + let connection = get_random_omikron() + .await + .map_err(|_| OmegaError::NotFound)?; + let id = connection + .get_omikron_id() + .await + .ok_or(OmegaError::NotFound)?; + let omikron = get_omikron_by_id(id.into()).await?; + Ok(( + StatusCode::OK, + json(&OmikronResponse { + status: "success", + id, + public_key: omikron.public_key.to_base64(), + ip_address: omikron.ip_address, + port: omikron.port, + }), + )) + } + ["api", "get", "omikron", id] => { + let id = parse_positive_id(id)?; + let omikron = match get_omikron_by_id(id.into()).await { + Ok(value) => value, + Err(_) => { + let fallback_id = + if let Some(fallback_id) = get_iota_primary_omikron_connection(id) { + fallback_id + } else { + let user = get_by_user_id(UserId::from(id)).await?; + get_iota_primary_omikron_connection(user.iota_id.0) + .ok_or(OmegaError::NotFound)? + }; + get_omikron_by_id(fallback_id.into()).await? + } + }; + Ok(( + StatusCode::OK, + json(&OmikronResponse { + status: "success", + id: omikron.id.0, + public_key: omikron.public_key.to_base64(), + ip_address: omikron.ip_address, + port: omikron.port, + }), + )) + } + ["api", "get", "connections"] => { + let connections = get_all_connections() + .await + .map_err(|_| OmegaError::Transport("failed to load connections".to_string()))?; + let connections = connections + .into_iter() + .map(|(omikron_id, iotas)| { + let iotas = iotas + .into_iter() + .map(|(iota_id, users)| { + ( + iota_id.to_string(), + users.into_iter().map(i64::from).collect(), + ) + }) + .collect(); + (omikron_id.to_string(), iotas) + }) + .collect::>(); + Ok(( + StatusCode::OK, + json(&ConnectionsResponse { + status: "success", + connections, + }), + )) + } + ["api", "get", "iota", id] => { + let id = parse_positive_id(id)?; + let iota = get_iota_by_id(id.into()).await?; + Ok(( + StatusCode::OK, + json(&IotaResponse { + status: "success", + iota_id: iota.id.0, + public_key: iota.public_key.to_base64(), + }), + )) + } + ["api", "get", "id", username] => { + validate_non_empty(username, "username", 15)?; + let user = get_by_username(username).await?; + Ok(( + StatusCode::OK, + json(&UsernameResponse { + status: "success", + username: user.username, + public_key: user.public_key.to_base64(), + user_id: user.id.0, + iota_id: user.iota_id.0, + sub_level: user.sub_level, + sub_end: user.sub_end, + }), + )) + } + ["api", "get", "public_key"] => { + let public_key = base64::engine::general_purpose::STANDARD + .encode(load_keyring().public_key_bundle().as_bytes()); + Ok(( + StatusCode::OK, + json(&PublicKeyResponse { + status: "success", + public_key, + }), + )) + } + ["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::INTERNAL_SERVER_ERROR, + json(&StatusResponse { status: "error" }), + )), + } +} pub async fn handle(request: Http3Request, response: Http3Response) -> Http3Response { let method = request.method; let path = request.uri.path().to_string(); - let body_string = request - .body - .map(|body| String::from_utf8_lossy(&body).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()) + .body(json(&StatusResponse { + status: "error_rate_limited", + })); + } if method == Method::OPTIONS { return response .status(StatusCode::OK) - .header("access-control-allow-origin", "*") + .header("access-control-allow-origin", &crate::config::cors_origin()) .header("access-control-allow-methods", "GET, POST, OPTIONS") .header("access-control-allow-headers", "*"); } - - let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); - - let _body: Option = if let Some(ref bs) = body_string { - if let Ok(body_json) = json::parse(bs) { - Some(body_json) - } else { - None - } - } else { - None - }; - - let (status, body_text) = match path_parts.as_slice() { - // ================================================== - // DOWNLOAD IOTA FRONTEND - // ================================================== - ["api", "download", "iota_frontend"] => { - let file_path = format!("{}/downloads/iota_frontend.zip", get_directory()); - - match std::fs::read(file_path) { - Ok(file_bytes) => { - return response - .status(StatusCode::OK) - .header("access-control-allow-origin", "*") - .header("content-type", "application/zip") - .header( - "content-disposition", - "attachment; filename=\"iota_frontend.zip\"", - ) - .body(Bytes::from(file_bytes)); - } - Err(_) => { - let mut res = JsonValue::new_object(); - res["status"] = "error_not_found".into(); - return response - .status(StatusCode::NOT_FOUND) - .header("access-control-allow-origin", "*") - .body(res.dump()); - } - } - } - - // ================================================== - // GET RANDOM OMIKRON - // ================================================== - ["api", "get", "omikron"] => { - if let Ok(omikron_conn) = get_random_omikron().await { - if let Some(id) = omikron_conn.get_omikron_id().await { - if let Ok((public_key, ip_address, port)) = sql::get_omikron_by_id(id).await { - let mut res = JsonValue::new_object(); - res["status"] = "success".into(); - res["id"] = id.into(); - res["public_key"] = public_key.to_base64().into(); - res["ip_address"] = ip_address.into(); - res["port"] = port.into(); - - (StatusCode::OK, res.dump()) - } else { - let mut res = JsonValue::new_object(); - res["status"] = "error".into(); - (StatusCode::INTERNAL_SERVER_ERROR, res.dump()) - } - } else { - let mut res = JsonValue::new_object(); - res["status"] = "error".into(); - (StatusCode::INTERNAL_SERVER_ERROR, res.dump()) - } - } else { - let mut res = JsonValue::new_object(); - res["status"] = "error_not_found".into(); - (StatusCode::NOT_FOUND, res.dump()) - } - } - - // ================================================== - // GET OMIKRON BY ID - // ================================================== - ["api", "get", "omikron", id] => { - let id = id.parse::().unwrap_or(0); - - if id == 0 { - let mut res = JsonValue::new_object(); - res["status"] = "error_bad_request".into(); - (StatusCode::BAD_REQUEST, res.dump()) - } else if let Ok((public_key, ip_address, port)) = get_omikron_by_id(id).await { - let mut res = JsonValue::new_object(); - res["status"] = "success".into(); - res["id"] = id.into(); - res["public_key"] = public_key.to_base64().into(); - res["ip_address"] = ip_address.into(); - res["port"] = port.into(); - (StatusCode::OK, res.dump()) - } else if let Some(omikron_id) = get_iota_primary_omikron_connection(id) { - if let Ok((public_key, ip_address, port)) = get_omikron_by_id(omikron_id).await { - let mut res = JsonValue::new_object(); - res["status"] = "success".into(); - res["id"] = omikron_id.into(); - res["public_key"] = public_key.to_base64().into(); - res["ip_address"] = ip_address.into(); - res["port"] = port.into(); - (StatusCode::OK, res.dump()) - } else { - let mut res = JsonValue::new_object(); - res["status"] = "error_not_found".into(); - (StatusCode::NOT_FOUND, res.dump()) - } - } else if let Ok((_, iota_id, _, _, _, _, _, _, _, _, _, _)) = get_by_user_id(id).await - { - if let Some(omikron_id) = get_iota_primary_omikron_connection(iota_id) { - if let Ok((public_key, ip_address, port)) = get_omikron_by_id(omikron_id).await - { - let mut res = JsonValue::new_object(); - res["status"] = "success".into(); - res["id"] = omikron_id.into(); - res["public_key"] = public_key.to_base64().into(); - res["ip_address"] = ip_address.into(); - res["port"] = port.into(); - (StatusCode::OK, res.dump()) - } else { - let mut res = JsonValue::new_object(); - res["status"] = "error_not_found".into(); - (StatusCode::NOT_FOUND, res.dump()) - } - } else { - let mut res = JsonValue::new_object(); - res["status"] = "error_not_found".into(); - (StatusCode::NOT_FOUND, res.dump()) - } - } else { - let mut res = JsonValue::new_object(); - res["status"] = "error_not_found".into(); - (StatusCode::NOT_FOUND, res.dump()) - } - } - - ["api", "get", "connections"] => { - if let Ok(connections) = get_all_connections().await { - let mut res = JsonValue::new_object(); - res["status"] = "success".into(); - - for (omikron_id, iota_map) in connections { - let mut omikron_obj = JsonValue::new_object(); - for (iota_id, user_ids) in iota_map { - let mut user_arr = JsonValue::new_array(); - for user_id in user_ids { - let _ = user_arr.push(user_id); - } - omikron_obj[&iota_id.to_string()] = user_arr; - } - res[&omikron_id.to_string()] = omikron_obj; - } - - (StatusCode::OK, res.dump()) - } else { - let mut res = JsonValue::new_object(); - res["status"] = "error".into(); - (StatusCode::INTERNAL_SERVER_ERROR, res.dump()) - } - } - - // ================================================== - // GET IOTA BY ID - // ================================================== - ["api", "get", "iota", id] => { - let id: i64 = id.parse().unwrap_or(0); - - if id == 0 { - let mut res = JsonValue::new_object(); - res["status"] = "error_bad_request".into(); - (StatusCode::BAD_REQUEST, res.dump()) - } else if let Ok((id, public_key)) = get_iota_by_id(id).await { - let mut res = JsonValue::new_object(); - res["status"] = "success".into(); - res["iota_id"] = id.into(); - res["public_key"] = public_key.to_base64().into(); - (StatusCode::OK, res.dump()) - } else { - let mut res = JsonValue::new_object(); - res["status"] = "error_not_found".into(); - (StatusCode::NOT_FOUND, res.dump()) - } - } - - // ================================================== - // GET ID BY USERNAME - // ================================================== - ["api", "get", "id", username] => { - if username.is_empty() { - let mut res = JsonValue::new_object(); - res["status"] = "error_bad_request".into(); - (StatusCode::BAD_REQUEST, res.dump()) - } else if let Ok(( - id, - iota_id, - username, - _, - _, - _, - _, - sub_level, - sub_end, - public_key, - _, - _, - )) = sql::get_by_username(username).await - { - let mut res = JsonValue::new_object(); - res["status"] = "success".into(); - res["username"] = username.into(); - res["public_key"] = public_key.to_base64().into(); - res["user_id"] = id.into(); - res["iota_id"] = iota_id.into(); - res["sub_level"] = sub_level.into(); - res["sub_end"] = sub_end.into(); - - (StatusCode::OK, res.dump()) - } else { - let mut res = JsonValue::new_object(); - res["status"] = "error_not_found".into(); - (StatusCode::OK, res.dump()) - } - } - - // ================================================== - // GET SERVER PUBLIC KEY - // ================================================== - ["api", "get", "public_key"] => { - let mut res = JsonValue::new_object(); - res["status"] = "success".into(); - let bundle = load_keyring().public_key_bundle(); - res["public_key"] = base64::engine::general_purpose::STANDARD - .encode(bundle.as_bytes()) - .into(); - (StatusCode::OK, res.dump()) - } - - // ================================================== - // GET USER BY ID - // ================================================== - ["api", "get", "user", id] => { - let id: i64 = id.parse().unwrap_or(0); - - if id == 0 { - let mut res = JsonValue::new_object(); - res["status"] = "error_bad_request".into(); - (StatusCode::BAD_REQUEST, res.dump()) - } else if let Ok(( - id, - iota_id, - username, - display, - status_msg, - about, - avatar, - sub_level, - sub_end, - public_key, - _, - _, - )) = sql::get_by_user_id(id).await - { - let mut res = JsonValue::new_object(); - res["status"] = "success".into(); - res["username"] = username.into(); - res["public_key"] = public_key.to_base64().into(); - res["user_id"] = id.into(); - res["iota_id"] = iota_id.into(); - res["sub_level"] = sub_level.into(); - res["sub_end"] = sub_end.into(); - - if let Some(display) = display { - res["display"] = display.into(); - } - if let Some(status_msg) = status_msg { - res["status_message"] = status_msg.into(); - } - if let Some(about) = about { - res["about"] = about.into(); - } - if let Some(avatar) = avatar { - res["avatar"] = base64::engine::general_purpose::STANDARD - .encode(avatar) - .into(); - } - - (StatusCode::OK, res.dump()) - } else { - let mut res = JsonValue::new_object(); - res["status"] = "error_not_found".into(); - (StatusCode::OK, res.dump()) - } - } - - // ================================================== - // DIRECT - SHORT LINK RESOLUTION - // ================================================== - ["direct", short @ ..] => { - let short_str = short.join("/"); - let short = short_str.replace("/", ""); - if let Ok(long) = crate::server::short_link::get_short_link(&short).await { - return response - .status(StatusCode::TEMPORARY_REDIRECT) - .header("location", &long); - } else { - return response - .status(StatusCode::TEMPORARY_REDIRECT) - .header("location", "https://tensamin.net"); - } - } - - // ================================================== - // DEFAULT - // ================================================== - _ => { - let mut res = JsonValue::new_object(); - res["status"] = "error".into(); - (StatusCode::INTERNAL_SERVER_ERROR, res.dump()) - } - }; - + let path_parts: Vec<&str> = path.split('/').filter(|part| !part.is_empty()).collect(); + if let ["api", "download", "iota_frontend"] = path_parts.as_slice() { + let file_path = format!("{}/downloads/iota_frontend.zip", get_directory()); + return match std::fs::read(file_path) { + Ok(bytes) => response + .status(StatusCode::OK) + .header("access-control-allow-origin", &crate::config::cors_origin()) + .header("content-type", "application/zip") + .header( + "content-disposition", + "attachment; filename=\"iota_frontend.zip\"", + ) + .body(Bytes::from(bytes)), + Err(_) => response + .status(StatusCode::NOT_FOUND) + .header("access-control-allow-origin", &crate::config::cors_origin()) + .body(json(&StatusResponse { + status: "error_not_found", + })), + }; + } + if let ["direct", short @ ..] = path_parts.as_slice() { + let short = short.join(""); + let location = crate::server::short_link::get_short_link(&short) + .await + .unwrap_or_else(|_| "https://tensamin.net".to_string()); + return response + .status(StatusCode::TEMPORARY_REDIRECT) + .header("location", &location); + } + 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", "*") + .header("access-control-allow-origin", &crate::config::cors_origin()) .header("access-control-allow-headers", "*") .header("access-control-allow-methods", "GET, POST, OPTIONS") - .body(body_text) + .body(body) } pub async fn handle_pattern( diff --git a/src/server/middleware.rs b/src/server/middleware.rs new file mode 100644 index 0000000..5f373ec --- /dev/null +++ b/src/server/middleware.rs @@ -0,0 +1,84 @@ +use dashmap::DashMap; +use once_cell::sync::Lazy; +use std::net::IpAddr; +use std::{collections::VecDeque, time::Instant}; +use tokio::time::interval; + +static REQUESTS: Lazy>> = Lazy::new(DashMap::new); +static CONFIG: Lazy = + Lazy::new(crate::config::RateLimitConfig::from_env); +const MAX_TRACKED_CLIENT_BUCKETS: usize = 100_000; + +pub fn allow(remote_addr: IpAddr, path: &str) -> bool { + let key = if path.contains("register") { + "registration" + } else { + "general" + }; + let map_key = (remote_addr, key.to_string()); + if REQUESTS.len() >= MAX_TRACKED_CLIENT_BUCKETS { + cleanup_expired(); + if REQUESTS.len() >= MAX_TRACKED_CLIENT_BUCKETS && !REQUESTS.contains_key(&map_key) { + return false; + } + } + let limit = if key == "registration" { + CONFIG.registration_requests + } else { + 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) + { + entries.pop_front(); + } + if entries.len() >= limit { + return false; + } + entries.push_back(now); + true +} + +pub fn spawn_cleanup_task() -> tokio::task::JoinHandle<()> { + tokio::spawn(async { + let mut ticker = interval(CONFIG.window); + loop { + ticker.tick().await; + cleanup_expired(); + } + }) +} + +fn cleanup_expired() { + let now = Instant::now(); + REQUESTS.retain(|_, entries| { + while entries + .front() + .is_some_and(|time| now.duration_since(*time) >= CONFIG.window) + { + entries.pop_front(); + } + !entries.is_empty() + }); +} + +#[cfg(test)] +mod tests { + use super::allow; + use std::net::{IpAddr, Ipv4Addr}; + + #[test] + fn tracks_clients_independently() { + let first: IpAddr = "192.0.2.10" + .parse() + .unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)); + let second: IpAddr = "192.0.2.11" + .parse() + .unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)); + assert!(allow(first, "/api/get/user/1")); + assert!(allow(second, "/api/get/user/1")); + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs index 59e9769..2d01c5b 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,4 +1,6 @@ pub mod api; pub mod index; +pub mod middleware; pub mod server; pub mod short_link; +pub mod validation; diff --git a/src/server/short_link.rs b/src/server/short_link.rs index b3f91bd..dd24ebf 100644 --- a/src/server/short_link.rs +++ b/src/server/short_link.rs @@ -1,37 +1,27 @@ -use dashmap::DashMap; -use once_cell::sync::Lazy; -use rand::{Rng, thread_rng}; - -static LINKS: Lazy> = Lazy::new(DashMap::new); +use crate::db::short_link_repo; +use rand::RngExt; const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPRSTUVWXYZ1234567890"; pub async fn add_short_link(long: &str) -> Result { - let raw = generate_unique_short_link().await; - LINKS.insert(raw.clone(), long.to_string()); - - Ok(format!( - "hmtps://omega.tensamin.net/direct/{}", - format_with_dashes(&raw) - )) -} - -async fn generate_unique_short_link() -> String { loop { - let short = generate_short_link().await; - if !LINKS.contains_key(&short) { - return short; + let raw = generate_short_link().await; + if short_link_repo::insert(&raw, long).await.map_err(|_| ())? { + return Ok(format!( + "https://omega.tensamin.net/direct/{}", + format_with_dashes(&raw) + )); } } } pub async fn generate_short_link() -> String { - let len = short_length(); + let len = short_length().await; - let mut rng = thread_rng(); + let mut rng = rand::rng(); (0..len) .map(|_| { - let idx = rng.gen_range(0..CHARSET.len()); + let idx = rng.random_range(0..CHARSET.len()); CHARSET[idx] as char }) .collect() @@ -46,17 +36,17 @@ pub async fn get_short_link(short: &str) -> Result { let frag = short.replace(key, ""); let normalized = normalize_short(&key); - if let Ok(t) = LINKS.get(&normalized).map(|v| v.value().clone()).ok_or(()) { - Ok(format!("{}{}", t, frag)) - } else { - Err(()) - } + let target = short_link_repo::get(&normalized) + .await + .map_err(|_| ())? + .ok_or(())?; + Ok(format!("{}{}", target, frag)) } /* ---------------- helpers ---------------- */ -fn short_length() -> usize { - let count = LINKS.len(); +async fn short_length() -> usize { + let count = short_link_repo::count().await.unwrap_or(0); match count { 0..=1_999 => 4, diff --git a/src/server/validation.rs b/src/server/validation.rs new file mode 100644 index 0000000..b80799c --- /dev/null +++ b/src/server/validation.rs @@ -0,0 +1,42 @@ +use crate::error::{OmegaError, Result}; + +pub fn parse_positive_id(value: &str) -> Result { + let id = value + .parse::() + .map_err(|_| OmegaError::Validation("ID must be a positive integer".to_string()))?; + if id <= 0 { + return Err(OmegaError::Validation( + "ID must be a positive integer".to_string(), + )); + } + Ok(id) +} + +pub fn validate_non_empty(value: &str, field: &str, max_len: usize) -> Result<()> { + let length = value.chars().count(); + if length == 0 || length > max_len { + return Err(OmegaError::Validation(format!( + "{field} must contain 1 to {max_len} characters" + ))); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{parse_positive_id, validate_non_empty}; + + #[test] + fn rejects_invalid_ids() { + assert!(parse_positive_id("bad").is_err()); + assert!(parse_positive_id("0").is_err()); + assert_eq!(parse_positive_id("7").expect("valid test ID"), 7); + } + + #[test] + fn enforces_string_bounds() { + assert!(validate_non_empty("", "username", 15).is_err()); + assert!(validate_non_empty("abcdefghijklmnop", "username", 15).is_err()); + assert!(validate_non_empty("alice", "username", 15).is_ok()); + } +} diff --git a/src/sql/mod.rs b/src/sql/mod.rs index 91ba841..bec658c 100644 --- a/src/sql/mod.rs +++ b/src/sql/mod.rs @@ -1,3 +1,2 @@ pub mod connection_status; -pub mod sql; pub mod user_online_tracker; diff --git a/src/sql/sql.rs b/src/sql/sql.rs deleted file mode 100644 index 91cd5ba..0000000 --- a/src/sql/sql.rs +++ /dev/null @@ -1,778 +0,0 @@ -use crate::log; -use mtp::crypto::PublicKeyBundle; -use once_cell::sync::Lazy; -use sqlx::{MySql, Pool, Row, mysql::MySqlPoolOptions}; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::{ - env, - sync::Arc, - time::{SystemTime, UNIX_EPOCH}, -}; -use tokio::sync::RwLock; - -/* -use crate::sql::{ - iota_omikron_tracker::{get_omikron_for_iota, track_iota_omikron, untrack_iota}, - sql::{ - change_about, change_avatar, change_display_name, change_iota_id, change_iota_key, - change_keys, change_status, change_username, get_by_id, get_by_username, get_iota_by_id, - get_register_id, register_complete_iota, register_complete_user, - }, -}; - */ - -static SQL_DB: Lazy>>>> = Lazy::new(|| Arc::new(RwLock::new(None))); - -pub async fn connect() -> Result, sqlx::Error> { - let url = env::var("DB_URL").expect("DB_URL is not set"); - - MySqlPoolOptions::new() - .max_connections(200) - .connect(&url) - .await -} -// Omega -// - Omikron -// - Iota -// - User -// - User -// - Iota -// - User -// - User -// - Omikron -// - Iota -// - User -// - User -// - Iota -// - User -// - User -pub async fn initialize_db() -> Result<(), sqlx::Error> { - let pool = connect().await?; - let mut db_lock = SQL_DB.write().await; - // create tables - // with indexes - let _ = sqlx::query( - "CREATE TABLE IF NOT EXISTS - users ( - id BIGINT NOT NULL PRIMARY KEY, - username VARCHAR(15) NOT NULL UNIQUE COLLATE utf8mb4_bin, - display VARCHAR(15) COLLATE utf8mb4_bin, - status VARCHAR(15) COLLATE utf8mb4_bin, - about VARCHAR(200) COLLATE utf8mb4_bin, - avatar MEDIUMBLOB, - sub_level INT(11) NOT NULL DEFAULT 0, - sub_end BIGINT(20) NOT NULL DEFAULT 0, - public_key BLOB NOT NULL, - private_key_hash TEXT NOT NULL COLLATE utf8mb4_bin DEFAULT '', - iota_id BIGINT NOT NULL, - token VARCHAR(256) NOT NULL UNIQUE COLLATE utf8mb4_bin - )", - ) - .execute(&pool) - .await; - let _ = sqlx::query( - "CREATE TABLE IF NOT EXISTS - iotas ( - id BIGINT NOT NULL PRIMARY KEY, - public_key BLOB NOT NULL - )", - ) - .execute(&pool) - .await; - let _ = sqlx::query( - "CREATE TABLE IF NOT EXISTS - omikrons ( - id BIGINT NOT NULL PRIMARY KEY, - public_key BLOB NOT NULL, - location VARCHAR(255) NOT NULL COLLATE utf8mb4_bin, - ip_address VARCHAR(255) NOT NULL COLLATE utf8mb4_bin, - port INT(11) NOT NULL DEFAULT 959 - )", - ) - .execute(&pool) - .await; - // Retrofits `port` onto omikrons tables created before this column existed; - // `CREATE TABLE IF NOT EXISTS` above is a no-op against an already-existing table. - let _ = sqlx::query( - "ALTER TABLE omikrons ADD COLUMN IF NOT EXISTS port INT(11) NOT NULL DEFAULT 959", - ) - .execute(&pool) - .await; - let _ = sqlx::query( - "CREATE TABLE IF NOT EXISTS - notifications ( - id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT, - sender_id BIGINT NOT NULL, - receiver_id BIGINT NOT NULL, - amount BIGINT NOT NULL DEFAULT 0 - )", - ) - .execute(&pool) - .await; - *db_lock = Some(pool); - Ok(()) -} - -// ========================================================================================== -// REGISTER -// ========================================================================================== -pub static CURRENT_MILLI_USED: Lazy> = Lazy::new(|| Arc::new(AtomicU64::new(0))); -pub static CURRENT_REGISTER_PROCESS: Lazy>>> = - Lazy::new(|| Arc::new(RwLock::new(Vec::new()))); - -pub async fn get_register_id() -> u64 { - let mut current_time = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as u64; - - loop { - let current_locked = CURRENT_MILLI_USED.load(Ordering::SeqCst); - - if current_locked < current_time { - let result = CURRENT_MILLI_USED.compare_exchange( - current_locked, // expected value - current_time, // new value - Ordering::SeqCst, // acquire/release ordering - Ordering::SeqCst, // failure ordering - ); - - match result { - Ok(_) => { - CURRENT_REGISTER_PROCESS.write().await.push(current_time); - return current_time; - } - Err(_) => { - continue; - } - } - } else { - current_time = current_locked + 1; - } - } -} - -// ========================================================================================== -// USERS -// ========================================================================================== - -pub async fn get_by_username( - username: &str, -) -> Result< - ( - i64, - i64, - String, - Option, - Option, - Option, - Option>, - i32, - i64, - PublicKeyBundle, - String, - String, - ), - sqlx::Error, -> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - let row = sqlx::query( - "SELECT id, iota_id, username, display, status, about, avatar, sub_level, sub_end, public_key, private_key_hash, token FROM users WHERE username = ?", - ) - .bind(username) - .fetch_optional(&pool) - .await?; - - match row { - Some(row) => { - let id: i64 = row.get("id"); - let iota_id: i64 = row.get("iota_id"); - let username: Vec = row.get("username"); - let display: Option> = row.get("display"); - let status: Option> = row.get("status"); - let about: Option> = row.get("about"); - let avatar: Option> = row.get("avatar"); - let sub_level: i32 = row.get("sub_level"); - let sub_end: i64 = row.get("sub_end"); - let public_key = PublicKeyBundle::from_bytes(&row.get::, _>("public_key")) - .map_err(|e| sqlx::Error::Decode(Box::new(e)))?; - let private_key_hash: Vec = row.get("private_key_hash"); - let token: Vec = row.get("token"); - - Ok(( - id, - iota_id, - String::from_utf8_lossy(&username).to_string(), - display.map(|d| String::from_utf8_lossy(&d).to_string()), - status.map(|s| String::from_utf8_lossy(&s).to_string()), - about.map(|a| String::from_utf8_lossy(&a).to_string()), - avatar, - sub_level, - sub_end, - public_key, - String::from_utf8_lossy(&private_key_hash).to_string(), - String::from_utf8_lossy(&token).to_string(), - )) - } - _ => Err(sqlx::Error::RowNotFound), - } -} - -pub async fn get_by_user_id( - id: i64, -) -> Result< - ( - i64, - i64, - String, - Option, - Option, - Option, - Option>, - i32, - i64, - PublicKeyBundle, - String, - String, - ), - sqlx::Error, -> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - let row = sqlx::query( - "SELECT id, iota_id, username, display, status, about, avatar, sub_level, sub_end, public_key, private_key_hash, token FROM users WHERE id = ?", - ) - .bind(id) - .fetch_optional(&pool) - .await?; - - match row { - Some(row) => { - let id: i64 = row.get("id"); - let iota_id: i64 = row.get("iota_id"); - let username: Vec = row.get("username"); - let display: Option> = row.get("display"); - let status: Option> = row.get("status"); - let about: Option> = row.get("about"); - let avatar: Option> = row.get("avatar"); - let sub_level: i32 = row.get("sub_level"); - let sub_end: i64 = row.get("sub_end"); - let public_key = PublicKeyBundle::from_bytes(&row.get::, _>("public_key")) - .map_err(|e| sqlx::Error::Decode(Box::new(e)))?; - let private_key_hash: Vec = row.get("private_key_hash"); - let token: Vec = row.get("token"); - - Ok(( - id, - iota_id, - String::from_utf8_lossy(&username).to_string(), - display.map(|d| String::from_utf8_lossy(&d).to_string()), - status.map(|s| String::from_utf8_lossy(&s).to_string()), - about.map(|a| String::from_utf8_lossy(&a).to_string()), - avatar, - sub_level, - sub_end, - public_key, - String::from_utf8_lossy(&private_key_hash).to_string(), - String::from_utf8_lossy(&token).to_string(), - )) - } - _ => Err(sqlx::Error::RowNotFound), - } -} - -pub async fn get_users_by_iota_id( - iota_id_param: i64, -) -> Result< - Vec<( - i64, - i64, - String, - Option, - Option, - Option, - Option>, - i32, - i64, - PublicKeyBundle, - String, - String, - )>, - sqlx::Error, -> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - let rows = sqlx::query( - "SELECT id, iota_id, username, display, status, about, avatar, sub_level, sub_end, public_key, private_key_hash, token FROM users WHERE iota_id = ?", - ) - .bind(iota_id_param) - .fetch_all(&pool) - .await?; - - let mut users = Vec::new(); - for row in rows { - let id: i64 = row.get("id"); - let iota_id: i64 = row.get("iota_id"); - let username: Vec = row.get("username"); - let display: Option> = row.get("display"); - let status: Option> = row.get("status"); - let about: Option> = row.get("about"); - let avatar: Option> = row.get("avatar"); - let sub_level: i32 = row.get("sub_level"); - let sub_end: i64 = row.get("sub_end"); - let public_key = PublicKeyBundle::from_bytes(&row.get::, _>("public_key")) - .map_err(|e| sqlx::Error::Decode(Box::new(e)))?; - let private_key_hash: Vec = row.get("private_key_hash"); - let token: Vec = row.get("token"); - - users.push(( - id, - iota_id, - String::from_utf8_lossy(&username).to_string(), - display.map(|d| String::from_utf8_lossy(&d).to_string()), - status.map(|s| String::from_utf8_lossy(&s).to_string()), - about.map(|a| String::from_utf8_lossy(&a).to_string()), - avatar, - sub_level, - sub_end, - public_key, - String::from_utf8_lossy(&private_key_hash).to_string(), - String::from_utf8_lossy(&token).to_string(), - )); - } - - Ok(users) -} - -pub async fn change_username(id: i64, new_username: String) -> Result<(), sqlx::Error> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - sqlx::query("UPDATE users SET username = ? WHERE id = ?") - .bind(new_username.as_bytes().to_vec()) - .bind(id) - .execute(&pool) - .await?; - - Ok(()) -} - -pub async fn change_display_name(id: i64, new_display: String) -> Result<(), sqlx::Error> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - sqlx::query("UPDATE users SET display = ? WHERE id = ?") - .bind(new_display.as_bytes().to_vec()) - .bind(id) - .execute(&pool) - .await?; - - Ok(()) -} - -pub async fn change_avatar(id: i64, new_avatar: String) -> Result<(), sqlx::Error> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - sqlx::query("UPDATE users SET avatar = ? WHERE id = ?") - .bind(new_avatar.as_bytes().to_vec()) - .bind(id) - .execute(&pool) - .await?; - - Ok(()) -} - -pub async fn change_about(id: i64, new_about: String) -> Result<(), sqlx::Error> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - sqlx::query("UPDATE users SET about = ? WHERE id = ?") - .bind(new_about.as_bytes().to_vec()) - .bind(id) - .execute(&pool) - .await?; - - Ok(()) -} - -pub async fn change_status(id: i64, new_status: String) -> Result<(), sqlx::Error> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - sqlx::query("UPDATE users SET status = ? WHERE id = ?") - .bind(new_status.as_bytes().to_vec()) - .bind(id) - .execute(&pool) - .await?; - - Ok(()) -} - -pub async fn delete_user(id: i64) -> Result<(), sqlx::Error> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - sqlx::query("DELETE FROM users WHERE id = ?") - .bind(id) - .execute(&pool) - .await?; - - Ok(()) -} -pub async fn change_iota_id(id: i64, new_iota_id: i64) -> Result<(), sqlx::Error> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - sqlx::query("UPDATE users SET iota_id = ? WHERE id = ?") - .bind(new_iota_id) - .bind(id) - .execute(&pool) - .await?; - - Ok(()) -} - -pub async fn change_keys( - id: i64, - new_public_key: PublicKeyBundle, - new_private_key_hash: String, -) -> Result<(), sqlx::Error> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - sqlx::query("UPDATE users SET public_key = ?, private_key_hash = ? WHERE id = ?") - .bind(new_public_key.as_bytes()) - .bind(new_private_key_hash.as_bytes().to_vec()) - .bind(id) - .execute(&pool) - .await?; - - Ok(()) -} -pub async fn change_token(id: i64, new_token: String) -> Result<(), sqlx::Error> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - sqlx::query("UPDATE users SET token = ? WHERE id = ?") - .bind(new_token.as_bytes().to_vec()) - .bind(id) - .execute(&pool) - .await?; - - Ok(()) -} -pub async fn register_complete_user( - id: i64, - username: String, - public_key: PublicKeyBundle, - iota_id: i64, - token: String, -) -> Result<(), sqlx::Error> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - sqlx::query( - "INSERT INTO users (id, username, public_key, iota_id, token) VALUES (?, ?, ?, ?, ?)", - ) - .bind(id) - .bind(username.as_bytes().to_vec()) - .bind(public_key.as_bytes()) - .bind(iota_id) - .bind(token.as_bytes().to_vec()) - .execute(&pool) - .await?; - - Ok(()) -} -pub async fn print_users() -> Result<(), Box> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - log!("Printing users..."); - for row in sqlx::query( - "SELECT id, iota_id, username, display, status, about, sub_level, sub_end, public_key, private_key_hash, token FROM users", - ) - .fetch_all(&pool) - .await? - .iter() - { - let id: i64 = row.get("id"); - let iota_id: i64 = row.get("iota_id"); - let username: Vec = row.get("username"); - let display: Option> = row.get("display"); - let status: Option> = row.get("status"); - let about: Option> = row.get("about"); - let sub_level: i32 = row.get("sub_level"); - let sub_end: i64 = row.get("sub_end"); - - log!( - "User: {:?}", - ( - id, - iota_id, - String::from_utf8_lossy(&username).to_string(), - display.map_or("".to_string(), |d| String::from_utf8_lossy(&d).to_string()), - status.map_or("".to_string(), |s| String::from_utf8_lossy(&s).to_string()), - about.map_or("".to_string(), |a| String::from_utf8_lossy(&a).to_string()), - sub_level, - sub_end - ) - ); - } - - Ok(()) -} - -// ========================================================================================== -// IOTA -// ========================================================================================== -pub async fn create_new_iota(public_key: PublicKeyBundle) -> Result { - let new_id = get_register_id().await as i64; - register_complete_iota(new_id, public_key).await?; - Ok(new_id) -} - -pub async fn register_complete_iota( - id: i64, - public_key: PublicKeyBundle, -) -> Result<(), sqlx::Error> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - sqlx::query("INSERT INTO iotas (id, public_key) VALUES (?, ?)") - .bind(id) - .bind(public_key.as_bytes()) - .execute(&pool) - .await?; - - Ok(()) -} - -pub async fn get_iota_by_id(id: i64) -> Result<(i64, PublicKeyBundle), sqlx::Error> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - let result = - sqlx::query_as::<_, (i64, Vec)>("SELECT id, public_key FROM iotas WHERE id = ?") - .bind(id) - .fetch_optional(&pool) - .await; - - match result { - Ok(optional_row) => match optional_row { - Some((id_i64, public_key)) => { - let bundle = PublicKeyBundle::from_bytes(&public_key) - .map_err(|e| sqlx::Error::Decode(Box::new(e)))?; - Ok((id_i64, bundle)) - } - _ => Err(sqlx::Error::RowNotFound), - }, - Err(e) => Err(e), - } -} - -pub async fn change_iota_key(id: i64, new_key: PublicKeyBundle) -> Result<(), sqlx::Error> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - sqlx::query("UPDATE iotas SET public_key = ? WHERE id = ?") - .bind(new_key.as_bytes()) - .bind(id) - .execute(&pool) - .await?; - - Ok(()) -} - -pub async fn delete_iota(id: i64) -> Result<(), sqlx::Error> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - sqlx::query("DELETE FROM iotas WHERE id = ?") - .bind(id) - .execute(&pool) - .await?; - - Ok(()) -} - -// ========================================================================================== -// OMIKRONS -// ========================================================================================== - -pub async fn get_omikron_by_id(id: i64) -> Result<(PublicKeyBundle, String, u16), sqlx::Error> { - let pool = { - let db_lock = SQL_DB.read().await; - db_lock - .as_ref() - .cloned() - .expect("Database pool not initialized") - }; - - let row = sqlx::query_as::<_, (Vec, Vec, i32)>( - "SELECT public_key, ip_address, port FROM omikrons WHERE id = ?", - ) - .bind(id) - .fetch_optional(&pool) - .await?; - - match row { - Some((public_key, ip_address, port)) => { - let bundle = PublicKeyBundle::from_bytes(&public_key) - .map_err(|e| sqlx::Error::Decode(Box::new(e)))?; - Ok(( - bundle, - String::from_utf8_lossy(&ip_address).to_string(), - port as u16, - )) - } - _ => Err(sqlx::Error::RowNotFound), - } -} - -// ========================================================================================== -// PHI -// ========================================================================================== - -pub async fn add_notification(sender_id: i64, receiver_id: i64) -> Result<(), sqlx::Error> { - let db_lock = SQL_DB.read().await; - let pool = db_lock.as_ref().expect("Database pool is not initialized"); - - sqlx::query( - r#" - INSERT INTO notifications (sender_id, receiver_id, amount) - VALUES (?, ?, 1) - ON DUPLICATE KEY UPDATE amount = amount + 1 - "#, - ) - .bind(sender_id) - .bind(receiver_id) - .execute(pool) - .await?; - - Ok(()) -} -pub async fn read_notification(sender_id: i64, receiver_id: i64) -> Result<(), sqlx::Error> { - let db_lock = SQL_DB.read().await; - let pool = db_lock.as_ref().expect("Database pool is not initialized"); - - sqlx::query( - r#" - DELETE FROM notifications WHERE sender_id = ? AND receiver_id = ? - "#, - ) - .bind(sender_id) - .bind(receiver_id) - .execute(pool) - .await?; - - Ok(()) -} -pub async fn get_notifications(user_id: i64) -> Result, sqlx::Error> { - let db_lock = SQL_DB.read().await; - let pool = db_lock.as_ref().expect("Database pool is not initialized"); - - sqlx::query_as::<_, (i64, i64)>( - r#" - SELECT sender_id, amount FROM notifications WHERE receiver_id = ? - "#, - ) - .bind(user_id) - .fetch_all(pool) - .await -} diff --git a/src/sql/user_online_tracker.rs b/src/sql/user_online_tracker.rs index 288fcdc..a0c2cd8 100644 --- a/src/sql/user_online_tracker.rs +++ b/src/sql/user_online_tracker.rs @@ -1,4 +1,5 @@ -use crate::sql; +use crate::db::user_repo; +use crate::models::IotaId; use crate::sql::connection_status::UserStatus; use dashmap::DashMap; use once_cell::sync::Lazy; @@ -33,27 +34,30 @@ pub fn track_iota_connection(iota_id: i64, omikron_id: i64, primary: bool) { } pub fn untrack_iota_connection(iota_id: i64, omikron_id: i64) -> bool { - let connections_empty = if let Some(r) = IOTA_OMIKRON_CONNECTIONS.get(&iota_id) { - let mut vec = r.value().clone(); - vec.retain(|&id| id != omikron_id); - let empty = vec.is_empty(); - drop(r); - IOTA_OMIKRON_CONNECTIONS.insert(iota_id, vec); - empty - } else { - false - }; - - if let Some(primary_ref) = IOTA_PRIMARY_OMIKRON_CONNECTION.get(&iota_id) { - let primary_id = *primary_ref.value(); - drop(primary_ref); - if primary_id == omikron_id { - IOTA_PRIMARY_OMIKRON_CONNECTION.remove(&iota_id); - } + let mut replacement = None; + let mut connections_empty = false; + if let Some(mut entry) = IOTA_OMIKRON_CONNECTIONS.get_mut(&iota_id) { + entry.retain(|&id| id != omikron_id); + connections_empty = entry.is_empty(); + replacement = entry.first().copied(); } if connections_empty { - IOTA_OMIKRON_CONNECTIONS.remove(&iota_id); + IOTA_OMIKRON_CONNECTIONS.remove_if(&iota_id, |_, connections| connections.is_empty()); + } + + if IOTA_PRIMARY_OMIKRON_CONNECTION + .get(&iota_id) + .is_some_and(|primary| *primary == omikron_id) + { + match replacement { + Some(omikron_id) => { + IOTA_PRIMARY_OMIKRON_CONNECTION.insert(iota_id, omikron_id); + } + None => { + IOTA_PRIMARY_OMIKRON_CONNECTION.remove(&iota_id); + } + } } connections_empty @@ -87,9 +91,9 @@ pub async fn get_all_connections() let iota_id = *entry.key(); let omikron_ids = entry.value().clone(); - if let Ok(users) = sql::sql::get_users_by_iota_id(iota_id.try_into().unwrap()).await { + if let Ok(users) = user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await { for user in users { - let user_id = user.0 as i64; + let user_id = user.id.0; if let Some(conn) = USER_STATUS_MAP.get(&user_id) { let user_omikron_id = conn.omikron_id; if omikron_ids.contains(&user_omikron_id) { @@ -117,6 +121,12 @@ pub fn track_user_status(user_id: i64, status: UserStatus, omikron_id: i64) { ); } +pub fn untrack_user_status(user_id: i64, omikron_id: i64) { + USER_STATUS_MAP.remove_if(&user_id, |_, connection| { + connection.omikron_id == omikron_id + }); +} + pub fn get_user_status(user_id: i64) -> Option { USER_STATUS_MAP.get(&user_id).map(|v| v.clone()) } @@ -128,57 +138,38 @@ pub fn untrack_many_users(user_ids: &[i64]) { } pub async fn untrack_omikron(omikron_id: i64) { - let primary_keys_to_remove: Vec = IOTA_PRIMARY_OMIKRON_CONNECTION - .iter() - .filter(|entry| *entry.value() == omikron_id) - .map(|entry| *entry.key()) - .collect(); - - for key in primary_keys_to_remove { - IOTA_PRIMARY_OMIKRON_CONNECTION.remove(&key); - } - let mut offline_iotas = Vec::new(); - let mut primary_to_remove = Vec::new(); + let mut primary_replacements = Vec::new(); - // Collect iotas and primary info first - for r in IOTA_OMIKRON_CONNECTIONS.iter() { - let iota_id = *r.key(); - let mut connections = r.value().clone(); - connections.retain(|&id| id != omikron_id); - - if connections.is_empty() { + for mut entry in IOTA_OMIKRON_CONNECTIONS.iter_mut() { + let iota_id = *entry.key(); + entry.retain(|&id| id != omikron_id); + if entry.is_empty() { offline_iotas.push(iota_id); - } - - if IOTA_PRIMARY_OMIKRON_CONNECTION + } else if IOTA_PRIMARY_OMIKRON_CONNECTION .get(&iota_id) - .map(|p| *p == omikron_id) - .unwrap_or(false) + .is_some_and(|primary| *primary == omikron_id) { - primary_to_remove.push(iota_id); + primary_replacements.push((iota_id, entry[0])); } - - // Update the connections vector after filtering - IOTA_OMIKRON_CONNECTIONS.insert(iota_id, connections); } - // Step 2: Remove primary connections safely - for iota_id in primary_to_remove { - IOTA_PRIMARY_OMIKRON_CONNECTION.remove(&iota_id); + for iota_id in &offline_iotas { + IOTA_OMIKRON_CONNECTIONS.remove_if(iota_id, |_, connections| connections.is_empty()); + IOTA_PRIMARY_OMIKRON_CONNECTION.remove(iota_id); + } + + for (iota_id, replacement) in primary_replacements { + IOTA_PRIMARY_OMIKRON_CONNECTION.insert(iota_id, replacement); } - // Step 3: Remove users that were on this omikron USER_STATUS_MAP.retain(|_, status| status.omikron_id != omikron_id); - // Step 4: For offline iotas, remove associated users from USER_STATUS_MAP for iota_id in offline_iotas { - if let Ok(users) = sql::sql::get_users_by_iota_id(iota_id.try_into().unwrap()).await { + if let Ok(users) = user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await { for user in users { - USER_STATUS_MAP.remove(&(user.0 as i64)); + USER_STATUS_MAP.remove(&user.id.0); } } - // Finally remove the empty connections vector - IOTA_OMIKRON_CONNECTIONS.remove(&iota_id); } } diff --git a/src/transport/connection.rs b/src/transport/connection.rs new file mode 100644 index 0000000..eff0bb1 --- /dev/null +++ b/src/transport/connection.rs @@ -0,0 +1 @@ +pub(crate) use super::omikron_connection::OmikronConnection; diff --git a/src/transport/handlers/account.rs b/src/transport/handlers/account.rs new file mode 100644 index 0000000..1927b14 --- /dev/null +++ b/src/transport/handlers/account.rs @@ -0,0 +1,43 @@ +use super::super::omikron_connection::{OmikronConnection, OmikronResult}; +use crate::{ + db::{iota_repo, user_repo}, + models::{IotaId, UserId}, +}; +use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; +use std::sync::Arc; + +async fn delete( + connection: Arc, + value: CommunicationValue, + result: impl std::future::Future>, +) -> OmikronResult<()> { + let response = match result.await { + Ok(()) => CommunicationValue::new(CommunicationType::Success), + Err(error) => CommunicationValue::new(CommunicationType::ErrorInternal) + .add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())), + }; + connection.send(&response.with_id(value.get_id())).await +} + +pub async fn user( + connection: Arc, + value: CommunicationValue, +) -> OmikronResult<()> { + delete( + connection, + value.clone(), + user_repo::delete_user(UserId::from(value.get_sender() as i64)), + ) + .await +} +pub async fn iota( + connection: Arc, + value: CommunicationValue, +) -> OmikronResult<()> { + delete( + connection, + value.clone(), + iota_repo::delete_iota(IotaId::from(value.get_sender() as i64)), + ) + .await +} diff --git a/src/transport/handlers/calls.rs b/src/transport/handlers/calls.rs new file mode 100644 index 0000000..9b16182 --- /dev/null +++ b/src/transport/handlers/calls.rs @@ -0,0 +1 @@ +/* Call and WebRTC commands are reserved for the transport extensions that define those protocol values. */ diff --git a/src/transport/handlers/links.rs b/src/transport/handlers/links.rs new file mode 100644 index 0000000..a43cb99 --- /dev/null +++ b/src/transport/handlers/links.rs @@ -0,0 +1,21 @@ +use super::super::omikron_connection::{OmikronConnection, OmikronResult}; +use crate::server::short_link::add_short_link; +use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; +use std::sync::Arc; + +pub async fn shorten( + connection: Arc, + value: CommunicationValue, +) -> OmikronResult<()> { + let link = value + .get_data(DataType::Link) + .as_str() + .ok_or(crate::error::OmegaError::InvalidResponse)?; + let short = add_short_link(link) + .await + .map_err(|_| crate::error::OmegaError::Transport("short link error".to_string()))?; + let response = CommunicationValue::new(CommunicationType::ShortenLink) + .with_id(value.get_id()) + .add_typed_default(DataType::Link, DataValue::Str(short)); + connection.send(&response).await +} diff --git a/src/transport/handlers/messaging.rs b/src/transport/handlers/messaging.rs new file mode 100644 index 0000000..38c66d6 --- /dev/null +++ b/src/transport/handlers/messaging.rs @@ -0,0 +1 @@ +/* Message delivery remains in the connection dispatcher until the protocol exposes a separate message handler contract. */ diff --git a/src/transport/handlers/mod.rs b/src/transport/handlers/mod.rs new file mode 100644 index 0000000..cb506cf --- /dev/null +++ b/src/transport/handlers/mod.rs @@ -0,0 +1,9 @@ +pub mod account; +pub mod calls; +pub mod links; +pub mod messaging; +pub mod notifications; +pub mod presence; +pub mod register; +pub mod states; +pub mod user_data; diff --git a/src/transport/handlers/notifications.rs b/src/transport/handlers/notifications.rs new file mode 100644 index 0000000..5f23474 --- /dev/null +++ b/src/transport/handlers/notifications.rs @@ -0,0 +1,116 @@ +use super::super::omikron_connection::{OmikronConnection, OmikronResult}; +use crate::{db::notification_repo, log, models::UserId}; +use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; +use mtp::type_map::TypeMap; +use std::sync::Arc; + +pub async fn get( + connection: Arc, + value: CommunicationValue, +) -> OmikronResult<()> { + let notifications = + match notification_repo::get_notifications(UserId::from(value.get_sender() as i64)).await { + Ok(items) => items + .into_iter() + .map(|item| { + let tm = TypeMap::latest(); + let Some(sender) = DataType::SenderId.try_to_id(&tm) else { + return DataValue::Container(Vec::new()); + }; + let Some(amount) = DataType::Amount.try_to_id(&tm) else { + return DataValue::Container(Vec::new()); + }; + DataValue::Container(vec![ + (sender, DataValue::SignedNumber(item.sender_id.0.into())), + (amount, DataValue::SignedNumber(item.amount.into())), + ]) + }) + .collect(), + Err(error) => { + log!( + crate::util::logger::PrintType::General, + "SQL get_notifications error: {}", + error + ); + Vec::new() + } + }; + let response = CommunicationValue::new(CommunicationType::GetNotifications) + .with_id(value.get_id()) + .add_typed_default(DataType::Notifications, DataValue::Array(notifications)); + connection.send(&response).await +} + +pub async fn read( + connection: Arc, + value: CommunicationValue, +) -> OmikronResult<()> { + let receiver = match value.get_sender() { + sender if sender > 0 => sender as i64, + _ => match value.get_data(DataType::ReceiverId).as_number() { + Some(id) => id as i64, + None => return Ok(()), + }, + }; + let Some(other) = value + .get_data(DataType::SenderId) + .as_number() + .map(|id| id as i64) + else { + return Ok(()); + }; + if let Err(error) = + notification_repo::read_notification(UserId::from(receiver), UserId::from(other)).await + { + log!( + crate::util::logger::PrintType::General, + "SQL read_notification error: {}", + error + ); + } else { + let response = + 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) + .add_typed_default(DataType::SenderId, DataValue::SignedNumber(other.into())); + crate::transport::omikron_manager::send_to_user(receiver, &sync).await; + } + Ok(()) +} + +pub async fn push( + connection: Arc, + value: CommunicationValue, +) -> OmikronResult<()> { + let receiver = match value.get_receiver() { + receiver if receiver > 0 => receiver as i64, + _ => match value.get_data(DataType::ReceiverId).as_number() { + Some(id) => id as i64, + None => return Ok(()), + }, + }; + let sender = value + .get_data(DataType::SenderId) + .as_number() + .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 + { + log!( + crate::util::logger::PrintType::General, + "SQL add_notification error: {}", + error + ); + } else { + 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())); + crate::transport::omikron_manager::send_to_user(receiver, &push).await; + } + Ok(()) +} diff --git a/src/transport/handlers/presence.rs b/src/transport/handlers/presence.rs new file mode 100644 index 0000000..1dd10d0 --- /dev/null +++ b/src/transport/handlers/presence.rs @@ -0,0 +1,128 @@ +use super::super::omikron_connection::{OmikronConnection, OmikronResult}; +use crate::{ + db::user_repo, + log_in, + models::IotaId, + sql::{connection_status::UserStatus, user_online_tracker}, +}; +use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; +use std::sync::Arc; + +pub async fn user_connected( + _connection: Arc, + value: CommunicationValue, + omikron_id: i64, +) -> OmikronResult<()> { + log_in!(crate::util::logger::PrintType::Omega, "User connected"); + if let Some(user_id) = value.get_data(DataType::UserId).as_number() { + let status = value + .get_data(DataType::UserState) + .as_str() + .and_then(UserStatus::from_str) + .unwrap_or(UserStatus::user_online); + if let Ok(user_id) = i64::try_from(user_id) { + user_online_tracker::track_user_status(user_id, status, omikron_id); + } + } + Ok(()) +} + +pub async fn user_disconnected( + _: Arc, + value: CommunicationValue, + omikron_id: i64, +) -> OmikronResult<()> { + log_in!(crate::util::logger::PrintType::Omega, "User disconnected"); + if let Some(user_id) = value.get_data(DataType::UserId).as_number() { + user_online_tracker::untrack_user_status(user_id as i64, omikron_id); + } + Ok(()) +} + +pub async fn iota_connected( + connection: Arc, + value: CommunicationValue, + omikron_id: i64, +) -> OmikronResult<()> { + log_in!(crate::util::logger::PrintType::Omega, "IOTA connected"); + let Some(iota_id) = value + .get_data(DataType::IotaId) + .as_number() + .map(|id| id as i64) + else { + return Ok(()); + }; + user_online_tracker::track_iota_connection(iota_id, omikron_id, true); + let mut user_ids = Vec::new(); + match user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await { + Ok(users) => { + for user in users { + user_ids.push(DataValue::SignedNumber(user.id.0.into())); + user_online_tracker::track_user_status( + user.id.0, + UserStatus::user_offline, + omikron_id, + ); + } + } + Err(_) => log_in!( + crate::util::logger::PrintType::General, + "SQL error loading users for IOTA" + ), + } + let response = CommunicationValue::new(CommunicationType::IotaUserData) + .with_id(value.get_id()) + .add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into())) + .add_typed_default(DataType::UserIds, DataValue::Array(user_ids)); + let _ = connection.send(&response).await; + Ok(()) +} + +pub async fn iota_disconnected( + _: Arc, + value: CommunicationValue, + omikron_id: i64, +) -> OmikronResult<()> { + log_in!(crate::util::logger::PrintType::Omega, "IOTA disconnected"); + let Some(iota_id) = value + .get_data(DataType::IotaId) + .as_number() + .map(|id| id as i64) + else { + return Ok(()); + }; + if user_online_tracker::untrack_iota_connection(iota_id, omikron_id) { + if let Ok(users) = user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await { + user_online_tracker::untrack_many_users( + &users.iter().map(|user| user.id.0).collect::>(), + ); + } + } + Ok(()) +} + +pub async fn sync_status( + _: Arc, + value: CommunicationValue, + omikron_id: i64, +) -> OmikronResult<()> { + if let DataValue::Array(ids) = value.get_data(DataType::UserIds) { + for id in ids { + if let DataValue::SignedNumber(id) = id { + user_online_tracker::track_user_status( + *id as i64, + UserStatus::user_offline, + omikron_id, + ); + } + } + } + if let DataValue::Array(ids) = value.get_data(DataType::IotaIds) { + for id in ids { + if let DataValue::SignedNumber(id) = id { + user_online_tracker::track_iota_connection(*id as i64, omikron_id, true); + } + } + } + Ok(()) +} diff --git a/src/transport/handlers/register.rs b/src/transport/handlers/register.rs new file mode 100644 index 0000000..23bf3e7 --- /dev/null +++ b/src/transport/handlers/register.rs @@ -0,0 +1,151 @@ +use super::super::omikron_connection::{OmikronConnection, OmikronResult}; +use crate::{ + db::{iota_repo, user_repo}, + models::{IotaId, UserId}, +}; +use mtp::{ + codec::{CommunicationType, CommunicationValue, DataType, DataValue}, + crypto::PublicKeyBundle, +}; +use std::sync::Arc; + +pub async fn get_register( + connection: Arc, + value: CommunicationValue, +) -> OmikronResult<()> { + let register_id = user_repo::get_register_id().await?; + let response = CommunicationValue::new(CommunicationType::GetRegister) + .with_id(value.get_id()) + .add_typed_default( + DataType::UserId, + DataValue::SignedNumber(register_id.0.into()), + ); + connection.send(&response).await +} + +pub async fn complete_iota( + connection: Arc, + value: CommunicationValue, +) -> OmikronResult<()> { + let iota_id = value + .get_data(DataType::IotaId) + .as_number() + .map(|id| id as i64); + let public_key = value + .get_data(DataType::PublicKey) + .as_str() + .and_then(|key| PublicKeyBundle::from_base64(key).ok()); + let Some(public_key) = public_key else { + return connection + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) + .await; + }; + match iota_id { + Some(iota_id) => { + match iota_repo::register_complete_iota(IotaId::from(iota_id), public_key).await { + Ok(()) => { + connection + .send( + &CommunicationValue::new(CommunicationType::Success) + .with_id(value.get_id()), + ) + .await + } + Err(error) => { + connection + .send( + &CommunicationValue::new(CommunicationType::ErrorInternal) + .with_id(value.get_id()) + .add_typed_default( + DataType::ErrorType, + DataValue::Str(error.to_string()), + ), + ) + .await + } + } + } + None => match iota_repo::create_new_iota(public_key).await { + Ok(id) => { + connection + .send( + &CommunicationValue::new(CommunicationType::CompleteRegisterIota) + .with_id(value.get_id()) + .add_typed_default( + DataType::IotaId, + DataValue::SignedNumber(id.0.into()), + ), + ) + .await + } + Err(error) => { + connection + .send( + &CommunicationValue::new(CommunicationType::ErrorInternal) + .with_id(value.get_id()) + .add_typed_default( + DataType::ErrorType, + DataValue::Str(error.to_string()), + ), + ) + .await + } + }, + } +} + +pub async fn complete_user( + connection: Arc, + value: CommunicationValue, +) -> OmikronResult<()> { + let user_id = value + .get_data(DataType::UserId) + .as_number() + .map(|id| id as i64); + let username = value + .get_data(DataType::Username) + .as_str() + .map(str::to_owned); + let public_key = value + .get_data(DataType::PublicKey) + .as_str() + .and_then(|key| PublicKeyBundle::from_base64(key).ok()); + let reset_token = value + .get_data(DataType::ResetToken) + .as_str() + .map(str::to_owned); + let Some((user_id, username, public_key, reset_token)) = user_id + .zip(username) + .zip(public_key) + .zip(reset_token) + .map(|(((id, name), key), token)| (id, name, key, token)) + else { + return connection + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) + .await; + }; + match user_repo::register_complete_user( + UserId::from(user_id), + username, + public_key, + IotaId::from(value.get_sender() as i64), + reset_token, + ) + .await + { + Ok(()) => { + connection + .send(&CommunicationValue::new(CommunicationType::Success).with_id(value.get_id())) + .await + } + Err(error) => { + connection + .send( + &CommunicationValue::new(CommunicationType::ErrorInternal) + .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 new file mode 100644 index 0000000..be116b1 --- /dev/null +++ b/src/transport/handlers/states.rs @@ -0,0 +1,46 @@ +use super::super::omikron_connection::{OmikronConnection, OmikronResult}; +use crate::sql::{connection_status::UserStatus, user_online_tracker}; +use mtp::{ + codec::{CommunicationType, CommunicationValue, DataType, DataValue}, + type_map::TypeMap, +}; +use std::sync::Arc; + +pub async fn get( + connection: Arc, + value: CommunicationValue, +) -> OmikronResult<()> { + let DataValue::Array(ids) = value.get_data(DataType::UserIds) else { + return Ok(()); + }; + let tm = TypeMap::latest(); + let states = ids + .iter() + .filter_map(|id| { + let DataValue::SignedNumber(id) = id else { + return None; + }; + let status = user_online_tracker::get_user_status(*id as i64) + .map(|status| { + if status.connection_type == UserStatus::user_invisible { + UserStatus::user_offline.to_string() + } else { + status.connection_type.to_string() + } + }) + .unwrap_or_else(|| UserStatus::iota_offline.to_string()); + let mut map = Vec::new(); + if let Some(kind) = DataType::UserId.try_to_id(&tm) { + map.push((kind, DataValue::SignedNumber((*id as i64).into()))); + } + if let Some(kind) = DataType::UserState.try_to_id(&tm) { + map.push((kind, DataValue::Str(status))); + } + Some(DataValue::Container(map)) + }) + .collect(); + let response = CommunicationValue::new(CommunicationType::GetStates) + .with_id(value.get_id()) + .add_typed_default(DataType::UserStates, DataValue::Array(states)); + connection.send(&response).await +} diff --git a/src/transport/handlers/user_data.rs b/src/transport/handlers/user_data.rs new file mode 100644 index 0000000..27c9dbb --- /dev/null +++ b/src/transport/handlers/user_data.rs @@ -0,0 +1,271 @@ +use super::super::omikron_connection::{OmikronConnection, OmikronResult}; +use crate::{ + db::{iota_repo, user_repo}, + models::{IotaId, UserId}, + sql::{connection_status::UserStatus, user_online_tracker}, +}; +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use mtp::{ + codec::{CommunicationType, CommunicationValue, DataType, DataValue}, + crypto::PublicKeyBundle, +}; +use std::sync::Arc; + +fn connections(iota_id: i64) -> DataValue { + DataValue::Array( + user_online_tracker::get_iota_omikron_connections(iota_id) + .unwrap_or_default() + .into_iter() + .map(|id| DataValue::SignedNumber(id.into())) + .collect(), + ) +} + +pub async fn get_user( + connection: Arc, + value: CommunicationValue, +) -> OmikronResult<()> { + 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 { + None + }; + let Some(user) = user else { + return connection + .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) + .await; + }; + let id = user.id.0; + let iota_id = user.iota_id.0; + let username = user.username.clone(); + let display = user + .display + .filter(|name| !name.is_empty()) + .unwrap_or_else(|| username.clone()); + let mut response = CommunicationValue::new(CommunicationType::GetUserData) + .with_id(value.get_id()) + .add_typed_default(DataType::Username, DataValue::Str(username)) + .add_typed_default( + DataType::PublicKey, + DataValue::Str(user.public_key.to_base64()), + ) + .add_typed_default(DataType::UserId, DataValue::SignedNumber(id.into())) + .add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into())) + .add_typed_default(DataType::Display, DataValue::Str(display)) + .add_typed_default( + DataType::SubLevel, + DataValue::SignedNumber(user.sub_level as i128), + ) + .add_typed_default( + DataType::SubEnd, + DataValue::SignedNumber(user.sub_end.into()), + ); + if let Some(status) = user.status.filter(|value| !value.is_empty()) { + response = response.add_typed_default(DataType::Status, DataValue::Str(status)); + } + if let Some(about) = user.about.filter(|value| !value.is_empty()) { + response = response.add_typed_default(DataType::About, DataValue::Str(about)); + } + if let Some(avatar) = user.avatar { + response = + response.add_typed_default(DataType::Avatar, DataValue::Str(STANDARD.encode(avatar))); + } + let online = user_online_tracker::get_user_status(id); + response = response + .add_typed_default( + DataType::OnlineStatus, + DataValue::Str( + online + .as_ref() + .map(|status| { + if status.connection_type == UserStatus::user_invisible { + UserStatus::user_offline.to_string() + } else { + status.connection_type.to_string() + } + }) + .unwrap_or_else(|| UserStatus::iota_offline.to_string()), + ), + ) + .add_typed_default(DataType::OmikronConnections, connections(iota_id)); + if let Some(status) = online { + response = response.add_typed_default( + DataType::OmikronId, + DataValue::SignedNumber(status.omikron_id.into()), + ); + } + connection.send(&response).await +} + +pub async fn get_iota( + connection: Arc, + value: CommunicationValue, +) -> OmikronResult<()> { + let found = if let Some(id) = value.get_data(DataType::IotaId).as_number() { + iota_repo::get_iota_by_id(IotaId::from(id as i64)) + .await + .ok() + .map(|iota| (iota.id.0, iota.public_key, None, None)) + } else if let Some(id) = value.get_data(DataType::UserId).as_number() { + if let Ok(user) = user_repo::get_by_user_id(UserId::from(id as i64)).await { + iota_repo::get_iota_by_id(user.iota_id) + .await + .ok() + .map(|iota| (iota.id.0, iota.public_key, Some(user.id.0), None)) + } else { + None + } + } else if let Some(name) = value.get_data(DataType::Username).as_str() { + if let Ok(user) = user_repo::get_by_username(name).await { + iota_repo::get_iota_by_id(user.iota_id) + .await + .ok() + .map(|iota| { + ( + iota.id.0, + iota.public_key, + Some(user.id.0), + Some(name.to_owned()), + ) + }) + } else { + None + } + } else { + None + }; + let Some((id, key, user_id, username)) = found else { + return connection + .send_error_response(value.get_id(), CommunicationType::ErrorNotFound) + .await; + }; + let mut response = CommunicationValue::new(CommunicationType::GetIotaData) + .with_id(value.get_id()) + .add_typed_default(DataType::PublicKey, DataValue::Str(key.to_base64())) + .add_typed_default(DataType::IotaId, DataValue::SignedNumber(id.into())) + .add_typed_default(DataType::OmikronConnections, connections(id)); + if let Some(user_id) = user_id { + response = + response.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into())); + } + if let Some(username) = username { + response = response.add_typed_default(DataType::Username, DataValue::Str(username)); + } + connection.send(&response).await +} + +async fn update_user( + connection: Arc, + value: CommunicationValue, +) -> OmikronResult<()> { + 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()) + .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() { + 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() { + 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() { + 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() { + 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()); + } + } + let response = match error { + None => CommunicationValue::new(CommunicationType::Success), + Some(error) => CommunicationValue::new(CommunicationType::ErrorInternal) + .add_typed_default(DataType::ErrorType, DataValue::Str(error)), + }; + connection.send(&response.with_id(value.get_id())).await +} + +pub async fn change_user( + connection: Arc, + value: CommunicationValue, +) -> OmikronResult<()> { + update_user(connection, value).await +} + +pub async fn change_iota( + connection: Arc, + value: CommunicationValue, +) -> OmikronResult<()> { + let Some(reset) = value.get_data(DataType::ResetToken).as_str() else { + return connection + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) + .await; + }; + let Some(new_token) = value.get_data(DataType::NewToken).as_str() else { + return connection + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) + .await; + }; + 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(value.get_id(), CommunicationType::ErrorNotFound) + .await; + } + }; + if user.token != reset { + return connection + .send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge) + .await; + } + let result = + match user_repo::change_iota_id(user_id, IotaId::from(value.get_sender() as i64)).await { + Ok(()) => user_repo::change_token(user_id, new_token.to_owned()).await, + Err(error) => Err(error), + }; + let response = match result { + Ok(()) => CommunicationValue::new(CommunicationType::Success), + Err(error) => CommunicationValue::new(CommunicationType::ErrorInternal) + .add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())), + }; + connection.send(&response.with_id(value.get_id())).await +} diff --git a/src/transport/mod.rs b/src/transport/mod.rs index 4f3953a..8171bf2 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -1,2 +1,4 @@ +pub mod connection; +pub mod handlers; pub mod omikron_connection; pub mod omikron_manager; diff --git a/src/transport/omikron_connection.rs b/src/transport/omikron_connection.rs index 6a5517e..b2b01b3 100644 --- a/src/transport/omikron_connection.rs +++ b/src/transport/omikron_connection.rs @@ -1,23 +1,22 @@ +use crate::models::OmikronId; use crate::{ - load_keyring, log, log_cv_in, log_cv_out, log_err, log_in, - server::{self, short_link::add_short_link}, - sql::{ - connection_status::UserStatus, - sql::{self, get_by_user_id, get_by_username, get_iota_by_id}, - user_online_tracker::{self}, - }, + load_keyring, log, log_cv_in, log_cv_out, log_err, log_in, server, transport::omikron_manager, util::{file_util::load_file_vec, logger::PrintType}, }; -use base64::{Engine as _, engine::general_purpose::STANDARD}; use dashmap::DashMap; -use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; -use mtp::host::{AuthenticationPolicy, HostConfig, Policy, SendMode}; -use mtp::webserver::{MTPWebServer, WebMtpReceiver, WebMtpSender}; -use mtp::crypto::PublicKeyBundle; -use std::net::{IpAddr, Ipv4Addr}; +use mtp::{ + codec::{CommunicationType, CommunicationValue, DataType, DataValue}, + crypto::PublicKeyBundle, + host::{AuthenticationPolicy, HostConfig, Policy, SendMode}, + webserver::{MTPWebServer, WebMtpReceiver, WebMtpSender}, +}; use std::{ - sync::Arc, + net::{IpAddr, Ipv4Addr}, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, time::{Duration, Instant}, }; use tokio::{ @@ -27,25 +26,24 @@ use tokio::{ 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 ACTIVE_CONNECTIONS_BY_IP: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(DashMap::new); -#[derive(Debug, thiserror::Error)] -pub enum OmikronError { - #[error("Not connected")] - NotConnected, - #[error("Not authenticated")] - NotAuthenticated, - #[error("Invalid response")] - InvalidResponse, - #[error("Authentication failed")] - AuthenticationFailed, - #[error("SQL error: {0}")] - Sql(String), - #[error("Send error: {0}")] - Send(String), +struct ConnectionLimitGuard(Option); +impl Drop for ConnectionLimitGuard { + fn drop(&mut self) { + ACTIVE_CONNECTIONS.fetch_sub(1, Ordering::AcqRel); + if let Some(ip) = self.0 { + if let Some(mut count) = ACTIVE_CONNECTIONS_BY_IP.get_mut(&ip) { + *count = count.saturating_sub(1); + } + ACTIVE_CONNECTIONS_BY_IP.remove_if(&ip, |_, count| *count == 0); + } + } } -pub type OmikronResult = Result; - +pub type OmikronResult = crate::error::Result; pub struct WaitingTask { pub task: Box, CommunicationValue) -> bool + Send + Sync>, pub inserted_at: Instant, @@ -58,7 +56,6 @@ pub struct OmikronConnection { waiting_tasks: DashMap, cleanup_handle: std::sync::Mutex>>, } - impl Drop for OmikronConnection { fn drop(&mut self) { if let Some(handle) = self.cleanup_handle.lock().unwrap().take() { @@ -69,15 +66,13 @@ impl Drop for OmikronConnection { impl OmikronConnection { pub fn new(sender: WebMtpSender, id: u64) -> Arc { - let conn = Arc::new(Self { + Arc::new(Self { id, sender: Mutex::new(Some(sender)), ping: RwLock::new(-1), waiting_tasks: DashMap::new(), cleanup_handle: std::sync::Mutex::new(None), - }); - - conn + }) } pub async fn handle(self: Arc, receiver: &mut WebMtpReceiver) { @@ -86,28 +81,24 @@ impl OmikronConnection { PrintType::Omega, "Omikron connection started" ); - let cleanup_conn = self.clone(); - let cleanup_handle = tokio::spawn(async move { + *self.cleanup_handle.lock().unwrap() = Some(tokio::spawn(async move { let mut ticker = interval(CLEANUP_INTERVAL); loop { ticker.tick().await; cleanup_conn .waiting_tasks - .retain(|_, v| v.inserted_at.elapsed() < MAX_WAITING_AGE); + .retain(|_, task| task.inserted_at.elapsed() < MAX_WAITING_AGE); } - }); - *self.cleanup_handle.lock().unwrap() = Some(cleanup_handle); - - while let Ok(cv) = receiver.receive().await { - if let Err(e) = self.clone().process_message(cv).await { - log_err!(0, PrintType::Omega, "Error processing message: {}", e); - if matches!(e, OmikronError::NotConnected) { + })); + while let Ok(value) = receiver.receive().await { + if let Err(error) = self.clone().process_message(value).await { + log_err!(0, PrintType::Omega, "Error processing message: {}", error); + if matches!(error, crate::error::OmegaError::NotConnected) { break; } } } - self.clone().cleanup().await; log_in!( self.id as i64, @@ -116,845 +107,120 @@ impl OmikronConnection { ); } - async fn process_message(self: Arc, cv: CommunicationValue) -> OmikronResult<()> { - if !cv.is_type(CommunicationType::Pong) && !cv.is_type(CommunicationType::Ping) { - log_cv_in!(PrintType::Omikron, &cv); + async fn process_message(self: Arc, value: CommunicationValue) -> OmikronResult<()> { + if !value.is_type(CommunicationType::Pong) && !value.is_type(CommunicationType::Ping) { + log_cv_in!(PrintType::Omikron, &value); } - - let msg_id = cv.get_id(); - - if let Some((_, task)) = self.waiting_tasks.remove(&msg_id) { - let _ = (task.task)(self.clone(), cv); + if let Some((_, task)) = self.waiting_tasks.remove(&value.get_id()) { + let _ = (task.task)(self.clone(), value); return Ok(()); } - - if cv.is_type(CommunicationType::Ping) { - return self.handle_ping(cv).await; + if value.is_type(CommunicationType::Ping) { + return self.ping(value).await; } - - let omikron_id = self.id as i64; - self.clone().handle_authenticated(cv, omikron_id).await + self.dispatch(value).await } - async fn handle_authenticated( - self: Arc, - cv: CommunicationValue, - omikron_id: i64, - ) -> OmikronResult<()> { - let comm_type = cv.get_comm_type_enum(); - match comm_type { - Some(CommunicationType::ShortenLink) => self.handle_shorten_link(cv).await, - + async fn dispatch(self: Arc, value: CommunicationValue) -> OmikronResult<()> { + let id = self.id as i64; + match value.get_comm_type_enum() { + Some(CommunicationType::ShortenLink) => { + crate::transport::handlers::links::shorten(self, value).await + } Some(CommunicationType::UserConnected) => { - self.handle_user_connected(cv, omikron_id).await; - Ok(()) + crate::transport::handlers::presence::user_connected(self, value, id).await } Some(CommunicationType::UserDisconnected) => { - self.handle_user_disconnected(cv, omikron_id).await; - Ok(()) + crate::transport::handlers::presence::user_disconnected(self, value, id).await } Some(CommunicationType::IotaConnected) => { - self.handle_iota_connected(cv, omikron_id).await; - Ok(()) + crate::transport::handlers::presence::iota_connected(self, value, id).await } Some(CommunicationType::IotaDisconnected) => { - self.handle_iota_disconnected(cv, omikron_id).await; - Ok(()) + crate::transport::handlers::presence::iota_disconnected(self, value, id).await } Some(CommunicationType::SyncClientIotaStatus) => { - self.handle_sync_status(cv, omikron_id).await; - Ok(()) + crate::transport::handlers::presence::sync_status(self, value, id).await + } + Some(CommunicationType::GetUserData) => { + crate::transport::handlers::user_data::get_user(self, value).await + } + Some(CommunicationType::GetIotaData) => { + crate::transport::handlers::user_data::get_iota(self, value).await + } + Some(CommunicationType::ChangeUserData) => { + crate::transport::handlers::user_data::change_user(self, value).await + } + Some(CommunicationType::ChangeIotaData) => { + crate::transport::handlers::user_data::change_iota(self, value).await + } + Some(CommunicationType::GetRegister) => { + crate::transport::handlers::register::get_register(self, value).await } - - Some(CommunicationType::GetUserData) => self.handle_get_user_data(cv).await, - Some(CommunicationType::GetIotaData) => self.handle_get_iota_data(cv).await, - - Some(CommunicationType::GetRegister) => self.handle_get_register(cv).await, Some(CommunicationType::CompleteRegisterIota) => { - self.handle_complete_register_iota(cv).await + crate::transport::handlers::register::complete_iota(self, value).await } Some(CommunicationType::CompleteRegisterUser) => { - self.handle_complete_register_user(cv).await + crate::transport::handlers::register::complete_user(self, value).await + } + Some(CommunicationType::DeleteUser) => { + crate::transport::handlers::account::user(self, value).await + } + Some(CommunicationType::DeleteIota) => { + crate::transport::handlers::account::iota(self, value).await + } + Some(CommunicationType::GetNotifications) => { + crate::transport::handlers::notifications::get(self, value).await + } + Some(CommunicationType::ReadNotification) => { + crate::transport::handlers::notifications::read(self, value).await + } + Some(CommunicationType::PushNotification) => { + crate::transport::handlers::notifications::push(self, value).await + } + Some(CommunicationType::GetStates) => { + crate::transport::handlers::states::get(self, value).await } - - Some(CommunicationType::ChangeUserData) => self.handle_change_user_data(cv).await, - Some(CommunicationType::ChangeIotaData) => self.handle_change_iota_data(cv).await, - Some(CommunicationType::DeleteUser) => self.handle_delete_user(cv).await, - Some(CommunicationType::DeleteIota) => self.handle_delete_iota(cv).await, - - Some(CommunicationType::GetNotifications) => self.handle_get_notifications(cv).await, - Some(CommunicationType::ReadNotification) => self.handle_read_notification(cv).await, - Some(CommunicationType::PushNotification) => self.handle_push_notification(cv).await, - Some(CommunicationType::GetStates) => self.handle_get_states(cv).await, - _ => { log_err!( 0, PrintType::Omega, "Unknown message type: {:?}", - cv.get_type() + value.get_type() ); Ok(()) } } } - async fn handle_shorten_link(self: Arc, cv: CommunicationValue) -> OmikronResult<()> { - let link = cv - .get_data(DataType::Link) - .as_str() - .ok_or(OmikronError::InvalidResponse)?; - - let short = add_short_link(link) - .await - .map_err(|_| OmikronError::Sql("Shortend link Error".to_string()))?; - - let response = CommunicationValue::new(CommunicationType::ShortenLink) - .with_id(cv.get_id()) - .add_typed_default(DataType::Link, DataValue::Str(short)); - - self.send(&response).await - } - - async fn handle_user_connected(self: Arc, cv: CommunicationValue, omikron_id: i64) { - log_in!(PrintType::Omega, "User connected"); - if let Some(user_id) = cv.get_data(DataType::UserId).as_number() { - let status = cv - .get_data(DataType::UserState) - .as_str() - .and_then(|s| UserStatus::from_str(s)) - .unwrap_or(UserStatus::user_online); - user_online_tracker::track_user_status(user_id.try_into().unwrap(), status, omikron_id); - } - } - - async fn handle_user_disconnected(self: Arc, cv: CommunicationValue, _omikron_id: i64) { - log_in!(PrintType::Omega, "User disconnected"); - if let Some(user_id) = cv.get_data(DataType::UserId).as_number() { - if let Some(status) = user_online_tracker::get_user_status(user_id as i64) { - user_online_tracker::track_user_status( - user_id as i64, - UserStatus::user_offline, - status.omikron_id, - ); - } - } - } - - async fn handle_iota_connected(self: Arc, cv: CommunicationValue, omikron_id: i64) { - log_in!(PrintType::Omega, "IOTA connected"); - let iota_id = match cv.get_data(DataType::IotaId).as_number() { - Some(id) => id as i64, - None => return, - }; - user_online_tracker::track_iota_connection(iota_id, omikron_id, true); - - let mut user_ids = Vec::new(); - if let Ok(users) = sql::get_users_by_iota_id(iota_id.try_into().unwrap()).await { - for (user_id, _, _, _, _, _, _, _, _, _, _, _) in users { - user_ids.push(DataValue::SignedNumber(user_id.try_into().unwrap())); - user_online_tracker::track_user_status( - user_id.try_into().unwrap(), - UserStatus::user_offline, - omikron_id, - ); - } - } else { - log_in!(PrintType::General, "SQL error loading users for IOTA"); - } - - let response = CommunicationValue::new(CommunicationType::IotaUserData) - .with_id(cv.get_id()) - .add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into())) - .add_typed_default(DataType::UserIds, DataValue::Array(user_ids)); - - let _ = self.send(&response).await; - } - - async fn handle_iota_disconnected(self: Arc, cv: CommunicationValue, omikron_id: i64) { - log_in!(PrintType::Omega, "IOTA disconnected"); - let iota_id = match cv.get_data(DataType::IotaId).as_number() { - Some(id) => id as i64, - None => return, - }; - let iota_offline = user_online_tracker::untrack_iota_connection(iota_id, omikron_id); - if iota_offline { - if let Ok(users) = sql::get_users_by_iota_id(iota_id.try_into().unwrap()).await { - let user_ids: Vec = users.iter().map(|u| u.0.try_into().unwrap()).collect(); - user_online_tracker::untrack_many_users(&user_ids); - } - } - } - - async fn handle_sync_status(self: Arc, cv: CommunicationValue, omikron_id: i64) { - if let DataValue::Array(user_ids) = cv.get_data(DataType::UserIds) { - for user_id_val in user_ids { - if let DataValue::SignedNumber(user_id) = user_id_val { - user_online_tracker::track_user_status( - *user_id as i64, - UserStatus::user_offline, - omikron_id, - ); - } - } - } - - if let DataValue::Array(iota_ids) = cv.get_data(DataType::IotaIds) { - for iota_id_val in iota_ids { - if let DataValue::SignedNumber(iota_id) = iota_id_val { - user_online_tracker::track_iota_connection(*iota_id as i64, omikron_id, true); - } - } - } - } - - async fn handle_get_user_data(self: Arc, cv: CommunicationValue) -> OmikronResult<()> { - if let Some(user_id) = cv.get_data(DataType::UserId).as_number() { - if let Ok(user_data) = get_by_user_id(user_id as i64).await { - let response = self - .clone() - .build_user_data_response(cv.get_id(), user_data) - .await; - return self.send(&response).await; - } - } - - if let Some(username) = cv.get_data(DataType::Username).as_str() { - if let Ok(user_data) = get_by_username(username).await { - let response = self - .clone() - .build_user_data_response(cv.get_id(), user_data) - .await; - return self.send(&response).await; - } - } - - let response = - CommunicationValue::new(CommunicationType::ErrorNotFound).with_id(cv.get_id()); - self.send(&response).await - } - - async fn build_user_data_response( - self: Arc, - msg_id: u32, - user: ( - i64, - i64, - String, - Option, - Option, - Option, - Option>, - i32, - i64, - PublicKeyBundle, - String, - String, - ), - ) -> CommunicationValue { - let ( - id, - iota_id, - username, - display, - status, - about, - avatar, - sub_level, - sub_end, - public_key, - _, - _, - ) = user; - - let mut response = CommunicationValue::new(CommunicationType::GetUserData) - .with_id(msg_id) - .add_typed_default(DataType::Username, DataValue::Str(username.clone())) - .add_typed_default(DataType::PublicKey, DataValue::Str(public_key.to_base64())) - .add_typed_default(DataType::UserId, DataValue::SignedNumber(id.into())) - .add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into())) - .add_typed_default( - DataType::SubLevel, - DataValue::SignedNumber(sub_level as i128), - ) - .add_typed_default(DataType::SubEnd, DataValue::SignedNumber(sub_end.into())); - - let display_name = display.filter(|d| !d.is_empty()).unwrap_or(username); - response = response.add_typed_default(DataType::Display, DataValue::Str(display_name)); - - if let Some(s) = status.filter(|s| !s.is_empty()) { - response = response.add_typed_default(DataType::Status, DataValue::Str(s)); - } - if let Some(a) = about.filter(|a| !a.is_empty()) { - response = response.add_typed_default(DataType::About, DataValue::Str(a)); - } - if let Some(av) = avatar { - response = - response.add_typed_default(DataType::Avatar, DataValue::Str(STANDARD.encode(av))); - } - - let user_status = user_online_tracker::get_user_status(id); - let iota_connections = - user_online_tracker::get_iota_omikron_connections(iota_id).unwrap_or_default(); - - if let Some(us) = user_status { - let display_status = if us.connection_type == UserStatus::user_invisible { - UserStatus::user_offline - } else { - us.connection_type.clone() - }; - response = response.add_typed_default( - DataType::OnlineStatus, - DataValue::Str(display_status.to_string()), - ); - response = response.add_typed_default( - DataType::OmikronId, - DataValue::SignedNumber(us.omikron_id.into()), - ); - } else { - response = response.add_typed_default( - DataType::OnlineStatus, - DataValue::Str(UserStatus::iota_offline.to_string()), - ); - } - - response = response.add_typed_default( - DataType::OmikronConnections, - DataValue::Array( - iota_connections - .into_iter() - .map(|id| DataValue::SignedNumber(id as i128)) - .collect(), - ), - ); - - response - } - - async fn handle_get_iota_data(self: Arc, cv: CommunicationValue) -> OmikronResult<()> { - if let Some(iota_id) = cv.get_data(DataType::IotaId).as_number() { - if let Ok((iota_id, public_key)) = get_iota_by_id(iota_id as i64).await { - let response = self - .clone() - .build_iota_data_response(cv.get_id(), iota_id, public_key, None, None) - .await; - return self.send(&response).await; - } - } - - if let Some(user_id) = cv.get_data(DataType::UserId).as_number() { - if let Ok((_, iota_id, _, _, _, _, _, _, _, _, _, _)) = - get_by_user_id(user_id as i64).await - { - if let Ok((iota_id, public_key)) = get_iota_by_id(iota_id).await { - let response = self - .clone() - .build_iota_data_response( - cv.get_id(), - iota_id, - public_key, - Some(user_id as i64), - None, - ) - .await; - return self.send(&response).await; - } - } - } - - if let Some(username) = cv.get_data(DataType::Username).as_str() { - if let Ok((user_id, iota_id, _, _, _, _, _, _, _, _, _, _)) = - get_by_username(username).await - { - if let Ok((iota_id, public_key)) = get_iota_by_id(iota_id).await { - let response = self - .clone() - .build_iota_data_response( - cv.get_id(), - iota_id, - public_key, - Some(user_id), - Some(username.to_string()), - ) - .await; - return self.send(&response).await; - } - } - } - - let response = - CommunicationValue::new(CommunicationType::ErrorNotFound).with_id(cv.get_id()); - self.send(&response).await - } - - async fn build_iota_data_response( - self: Arc, - msg_id: u32, - iota_id: i64, - public_key: PublicKeyBundle, - user_id: Option, - username: Option, - ) -> CommunicationValue { - let mut response = CommunicationValue::new(CommunicationType::GetIotaData) - .with_id(msg_id) - .add_typed_default(DataType::PublicKey, DataValue::Str(public_key.to_base64())) - .add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into())); - - if let Some(uid) = user_id { - response = - response.add_typed_default(DataType::UserId, DataValue::SignedNumber(uid.into())); - } - if let Some(uname) = username { - response = response.add_typed_default(DataType::Username, DataValue::Str(uname)); - } - - let iota_connections = - user_online_tracker::get_iota_omikron_connections(iota_id).unwrap_or_default(); - - response.add_typed_default( - DataType::OmikronConnections, - DataValue::Array( - iota_connections - .into_iter() - .map(|id| DataValue::SignedNumber(id as i128)) - .collect(), - ), - ) - } - - async fn handle_get_register(self: Arc, cv: CommunicationValue) -> OmikronResult<()> { - let register_id = sql::get_register_id().await; - let response = CommunicationValue::new(CommunicationType::GetRegister) - .with_id(cv.get_id()) - .add_typed_default( - DataType::UserId, - DataValue::SignedNumber(register_id as i128), - ); - self.send(&response).await - } - - async fn handle_complete_register_iota( - self: Arc, - cv: CommunicationValue, - ) -> OmikronResult<()> { - let iota_id_opt = cv.get_data(DataType::IotaId).as_number().map(|n| n as i64); - - let public_key = cv - .get_data(DataType::PublicKey) - .as_str() - .and_then(|s| PublicKeyBundle::from_base64(s).ok()); - - if let Some(public_key) = public_key { - if let Some(iota_id) = iota_id_opt { - match sql::register_complete_iota(iota_id, public_key).await { - Ok(_) => { - let response = CommunicationValue::new(CommunicationType::Success) - .with_id(cv.get_id()); - self.send(&response).await - } - Err(e) => { - let response = CommunicationValue::new(CommunicationType::ErrorInternal) - .with_id(cv.get_id()) - .add_typed_default(DataType::ErrorType, DataValue::Str(e.to_string())); - self.send(&response).await - } - } - } else { - match sql::create_new_iota(public_key).await { - Ok(new_iota_id) => { - let response = - CommunicationValue::new(CommunicationType::CompleteRegisterIota) - .with_id(cv.get_id()) - .add_typed_default( - DataType::IotaId, - DataValue::SignedNumber(new_iota_id.into()), - ); - self.send(&response).await - } - Err(e) => { - let response = CommunicationValue::new(CommunicationType::ErrorInternal) - .with_id(cv.get_id()) - .add_typed_default(DataType::ErrorType, DataValue::Str(e.to_string())); - self.send(&response).await - } - } - } - } else { - self.send_error_response(cv.get_id(), CommunicationType::ErrorInvalidData) - .await - } - } - - async fn handle_complete_register_user( - self: Arc, - cv: CommunicationValue, - ) -> OmikronResult<()> { - let user_id = cv.get_data(DataType::UserId).as_number().map(|n| n as i64); - let username = cv - .get_data(DataType::Username) - .as_str() - .map(|s| s.to_string()); - let public_key = cv - .get_data(DataType::PublicKey) - .as_str() - .and_then(|s| PublicKeyBundle::from_base64(s).ok()); - let iota_id = cv.get_sender(); - let reset_token = cv - .get_data(DataType::ResetToken) - .as_str() - .map(|s| s.to_string()); - - if let (Some(uid), Some(uname), Some(pk), Some(rt)) = - (user_id, username, public_key, reset_token) - { - match sql::register_complete_user(uid, uname, pk, iota_id as i64, rt).await { - Ok(_) => { - let response = - CommunicationValue::new(CommunicationType::Success).with_id(cv.get_id()); - self.send(&response).await - } - Err(e) => { - let response = CommunicationValue::new(CommunicationType::ErrorInternal) - .with_id(cv.get_id()) - .add_typed_default(DataType::ErrorType, DataValue::Str(e.to_string())); - self.send(&response).await - } - } - } else { - self.send_error_response(cv.get_id(), CommunicationType::ErrorInvalidData) - .await - } - } - - async fn handle_change_user_data(self: Arc, cv: CommunicationValue) -> OmikronResult<()> { - let user_id = cv.get_sender() as i64; - let mut success = true; - let mut error_message = String::new(); - - if let Some(username) = cv.get_data(DataType::Username).as_str() { - if let Err(e) = sql::change_username(user_id, username.to_string()).await { - success = false; - error_message = e.to_string(); - } - } - if let Some(display) = cv.get_data(DataType::Display).as_str() { - if let Err(e) = sql::change_display_name(user_id, display.to_string()).await { - success = false; - error_message = e.to_string(); - } - } - if let Some(avatar) = cv.get_data(DataType::Avatar).as_str() { - if let Err(e) = sql::change_avatar(user_id, avatar.to_string()).await { - success = false; - error_message = e.to_string(); - } - } - if let Some(about) = cv.get_data(DataType::About).as_str() { - if let Err(e) = sql::change_about(user_id, about.to_string()).await { - success = false; - error_message = e.to_string(); - } - } - if let Some(status) = cv.get_data(DataType::Status).as_str() { - if let Err(e) = sql::change_status(user_id, status.to_string()).await { - success = false; - error_message = e.to_string(); - } - } - if let (Some(public_key), Some(private_key_hash)) = ( - cv.get_data(DataType::PublicKey) - .as_str() - .and_then(|s| PublicKeyBundle::from_base64(s).ok()), - cv.get_data(DataType::PrivateKeyHash).as_str(), - ) { - if let Err(e) = - sql::change_keys(user_id, public_key, private_key_hash.to_string()).await - { - success = false; - error_message = e.to_string(); - } - } - - if success { - let response = CommunicationValue::new(CommunicationType::Success).with_id(cv.get_id()); - self.send(&response).await - } else { - let response = CommunicationValue::new(CommunicationType::ErrorInternal) - .with_id(cv.get_id()) - .add_typed_default(DataType::ErrorType, DataValue::Str(error_message)); - self.send(&response).await - } - } - - async fn handle_change_iota_data(self: Arc, cv: CommunicationValue) -> OmikronResult<()> { - let user_id = cv.get_sender() as i64; - - if let (iota_id, Some(reset_token), Some(new_token)) = ( - cv.get_sender(), - cv.get_data(DataType::ResetToken).as_str(), - cv.get_data(DataType::NewToken).as_str(), - ) { - match sql::get_by_user_id(user_id).await { - Ok(user) => { - let current_token = user.11; - if current_token == reset_token { - let mut success = true; - let mut error_message = String::new(); - - if let Err(e) = sql::change_iota_id(user_id, iota_id as i64).await { - success = false; - error_message = e.to_string(); - } - if success { - if let Err(e) = sql::change_token(user_id, new_token.to_string()).await - { - success = false; - error_message = e.to_string(); - } - } - - if success { - let response = CommunicationValue::new(CommunicationType::Success) - .with_id(cv.get_id()); - self.send(&response).await - } else { - let response = - CommunicationValue::new(CommunicationType::ErrorInternal) - .with_id(cv.get_id()) - .add_typed_default( - DataType::ErrorType, - DataValue::Str(error_message), - ); - self.send(&response).await - } - } else { - self.send_error_response( - cv.get_id(), - CommunicationType::ErrorInvalidChallenge, - ) - .await - } - } - Err(_) => { - self.send_error_response(cv.get_id(), CommunicationType::ErrorNotFound) - .await - } - } - } else { - self.send_error_response(cv.get_id(), CommunicationType::ErrorInvalidData) - .await - } - } - - async fn handle_delete_user(self: Arc, cv: CommunicationValue) -> OmikronResult<()> { - let user_id = cv.get_sender() as i64; - match sql::delete_user(user_id).await { - Ok(_) => { - let response = - CommunicationValue::new(CommunicationType::Success).with_id(cv.get_id()); - self.send(&response).await - } - Err(e) => { - let response = CommunicationValue::new(CommunicationType::ErrorInternal) - .with_id(cv.get_id()) - .add_typed_default(DataType::ErrorType, DataValue::Str(e.to_string())); - self.send(&response).await - } - } - } - - async fn handle_delete_iota(self: Arc, cv: CommunicationValue) -> OmikronResult<()> { - let iota_id = cv.get_sender(); - match sql::delete_iota(iota_id as i64).await { - Ok(_) => { - let response = - CommunicationValue::new(CommunicationType::Success).with_id(cv.get_id()); - self.send(&response).await - } - Err(e) => { - let response = CommunicationValue::new(CommunicationType::ErrorInternal) - .with_id(cv.get_id()) - .add_typed_default(DataType::ErrorType, DataValue::Str(e.to_string())); - self.send(&response).await - } - } - } - - async fn handle_get_notifications( - self: Arc, - cv: CommunicationValue, - ) -> OmikronResult<()> { - let user_id = cv.get_sender() as i64; - let response_array = match sql::get_notifications(user_id).await { - Ok(notifications) => notifications - .into_iter() - .map(|(sender, amount)| { - let tm = mtp::type_map::TypeMap::latest(); - DataValue::Container(vec![ - ( - DataType::SenderId.to_id(&tm), - DataValue::SignedNumber(sender.into()), - ), - ( - DataType::Amount.to_id(&tm), - DataValue::SignedNumber(amount.into()), - ), - ]) - }) - .collect(), - Err(e) => { - log!(PrintType::General, "SQL get_notifications error: {}", e); - vec![] - } - }; - - let response = CommunicationValue::new(CommunicationType::GetNotifications) - .with_id(cv.get_id()) - .add_typed_default(DataType::Notifications, DataValue::Array(response_array)); - self.send(&response).await - } - - async fn handle_read_notification( - self: Arc, - cv: CommunicationValue, - ) -> OmikronResult<()> { - let receiver_id = match cv.get_sender() { - s if s > 0 => s as i64, - _ => match cv.get_data(DataType::ReceiverId).as_number() { - Some(id) => id as i64, - None => return Ok(()), - }, - }; - - if let Some(other_id) = cv - .get_data(DataType::SenderId) - .as_number() - .map(|n| n as i64) - { - if let Err(e) = sql::read_notification(receiver_id, other_id).await { - log!(PrintType::General, "SQL read_notification error: {}", e); - } else { - let response = CommunicationValue::new(CommunicationType::ReadNotification) - .with_id(cv.get_id()); - let _ = self.send(&response).await; - - let sync_cv = CommunicationValue::new(CommunicationType::ReadNotification) - .with_receiver(receiver_id as u64) - .add_typed_default( - DataType::SenderId, - DataValue::SignedNumber(other_id.into()), - ); - crate::transport::omikron_manager::send_to_user(receiver_id, &sync_cv).await; - } - } - Ok(()) - } - - async fn handle_push_notification( - self: Arc, - cv: CommunicationValue, - ) -> OmikronResult<()> { - let receiver_id = match cv.get_receiver() { - r if r > 0 => r as i64, - _ => match cv.get_data(DataType::ReceiverId).as_number() { - Some(id) => id as i64, - None => return Ok(()), - }, - }; - - let sender_id = match cv.get_data(DataType::SenderId).as_number() { - Some(id) => id as i64, - None => cv.get_sender() as i64, - }; - - if let Err(e) = sql::add_notification(receiver_id, sender_id).await { - log!(PrintType::General, "SQL add_notification error: {}", e); - } else { - let response = - CommunicationValue::new(CommunicationType::PushNotification).with_id(cv.get_id()); - let _ = self.send(&response).await; - - let push_cv = CommunicationValue::new(CommunicationType::PushNotification) - .with_receiver(receiver_id as u64) - .add_typed_default( - DataType::SenderId, - DataValue::SignedNumber(sender_id.into()), - ); - crate::transport::omikron_manager::send_to_user(receiver_id, &push_cv).await; - } - Ok(()) - } - - async fn handle_get_states(self: Arc, cv: CommunicationValue) -> OmikronResult<()> { - let user_ids = match cv.get_data(DataType::UserIds) { - DataValue::Array(ids) => ids, - _ => return Ok(()), - }; - - let mut states = Vec::new(); - for id_val in user_ids { - if let DataValue::SignedNumber(user_id) = id_val { - let user_id = *user_id as i64; - let status = user_online_tracker::get_user_status(user_id); - let status_str = match status { - Some(ref us) => { - if us.connection_type == UserStatus::user_invisible { - "user_offline".to_string() - } else { - us.connection_type.to_string() - } - } - None => UserStatus::iota_offline.to_string(), - }; - let tm = mtp::type_map::TypeMap::latest(); - let mut map = Vec::new(); - map.push(( - DataType::UserId.to_id(&tm), - DataValue::SignedNumber(user_id.into()), - )); - map.push((DataType::UserState.to_id(&tm), DataValue::Str(status_str))); - states.push(DataValue::Container(map)); - } - } - - let response = CommunicationValue::new(CommunicationType::GetStates) - .with_id(cv.get_id()) - .add_typed_default(DataType::UserStates, DataValue::Array(states)); - self.send(&response).await - } - - async fn handle_ping(self: Arc, cv: CommunicationValue) -> OmikronResult<()> { - if let DataValue::SignedNumber(last_ping) = cv.get_data(DataType::LastPing) { + async fn ping(self: Arc, value: CommunicationValue) -> OmikronResult<()> { + if let DataValue::SignedNumber(last_ping) = value.get_data(DataType::LastPing) { *self.ping.write().await = *last_ping as i64; } - - let response = CommunicationValue::new(CommunicationType::Pong).with_id(cv.get_id()); + let response = CommunicationValue::new(CommunicationType::Pong).with_id(value.get_id()); self.send(&response).await } - - async fn send(self: Arc, cv: &CommunicationValue) -> OmikronResult<()> { - if !cv.is_type(CommunicationType::Pong) && !cv.is_type(CommunicationType::Ping) { - log_cv_out!(PrintType::Omikron, cv); + pub(crate) async fn send(self: Arc, value: &CommunicationValue) -> OmikronResult<()> { + if !value.is_type(CommunicationType::Pong) && !value.is_type(CommunicationType::Ping) { + log_cv_out!(PrintType::Omikron, value); } - let guard = self.sender.lock().await; - let sender = guard.as_ref().ok_or(OmikronError::NotConnected)?; - + let sender = guard + .as_ref() + .ok_or(crate::error::OmegaError::NotConnected)?; sender - .send(cv) + .send(value) .await - .map_err(|e| OmikronError::Send(e.to_string())) + .map_err(|error| crate::error::OmegaError::SendError(error.to_string())) } - - async fn send_error_response( + pub(crate) async fn send_error_response( self: Arc, message_id: u32, error_type: CommunicationType, ) -> OmikronResult<()> { - let error = CommunicationValue::new(error_type).with_id(message_id); - self.send(&error).await + self.send(&CommunicationValue::new(error_type).with_id(message_id)) + .await } - pub async fn close(self: Arc) { log_in!( self.get_omikron_id().await.unwrap_or(0), @@ -962,51 +228,38 @@ impl OmikronConnection { "Omikron connection Closed" ); } - async fn cleanup(self: Arc) { if self.id != 0 { log_in!(self.id as i64, PrintType::Omega, "Omikron disconnected"); omikron_manager::remove_omikron(self.id as i64).await; - user_online_tracker::untrack_omikron(self.id as i64).await; + crate::sql::user_online_tracker::untrack_omikron(self.id as i64).await; } - if let Some(handle) = self.cleanup_handle.lock().unwrap().take() { handle.abort(); } } - pub async fn get_omikron_id(self: Arc) -> Option { Some(self.id as i64) } - - pub async fn send_message(self: Arc, cv: &CommunicationValue) -> OmikronResult<()> { - self.send(cv).await + pub async fn send_message(self: Arc, value: &CommunicationValue) -> OmikronResult<()> { + self.send(value).await } } -// ============================================================================ -// Server Startup -// ============================================================================ - -pub async fn get_by_omikron_id( - omikron_id: u64, - _description: Option, -) -> Option { - sql::get_omikron_by_id(omikron_id as i64) +pub async fn get_by_omikron_id(omikron_id: u64, _: Option) -> Option { + crate::db::omikron_repo::get_omikron_by_id(OmikronId::from(omikron_id as i64)) .await .ok() - .map(|(bundle, _ip_address, _port)| bundle) + .map(|omikron| omikron.public_key) } -pub async fn complete_register(_pub_key: PublicKeyBundle, _description: Option) -> u64 { +pub async fn complete_register(_: PublicKeyBundle, _: Option) -> u64 { 0 } pub async fn start(port: u16) -> Result<(), Box> { - let cert_pem = load_file_vec("certs", "transport_cert.pem").expect("Error loading Pemfile"); - let key_pem = load_file_vec("certs", "transport_key.pem").expect("Error loading Keyfile"); - + let cert_pem = load_file_vec("certs", "transport_cert.pem")?; + let key_pem = load_file_vec("certs", "transport_key.pem")?; let web_config = server::server::build_web_config()?; - let host_config = HostConfig::new( IpAddr::from(Ipv4Addr::new(0, 0, 0, 0)), port, @@ -1034,30 +287,51 @@ pub async fn start(port: u16) -> Result<(), Box> { }) .with_authentication( load_keyring(), - Box::new(|user_id, description| Box::pin(get_by_omikron_id(user_id, description))), - Box::new(|pub_key, description| Box::pin(complete_register(pub_key, description))), + Box::new(|id, description| Box::pin(get_by_omikron_id(id, description))), + Box::new(|key, description| Box::pin(complete_register(key, description))), ) .with_authentication_policy(AuthenticationPolicy::ForceAuthentication); - let mut server = MTPWebServer::new(host_config, web_config).await?; log!("OmegaServer listening on port {}", port); - loop { let mut conn = match server.accept().await { Ok(Some(conn)) => conn, Ok(None) => break, - Err(e) => { - log_err!(0, PrintType::Omega, "Rejected omikron connection: {}", e); + Err(error) => { + log_err!( + 0, + PrintType::Omega, + "Rejected omikron connection: {}", + error + ); continue; } }; - - let omikron_connection = OmikronConnection::new(conn.sender, conn.client_id); + let config = crate::config::RateLimitConfig::from_env(); + let peer_ip = conn.remote_addr.map(|address| address.ip()); + let active = ACTIVE_CONNECTIONS.fetch_add(1, Ordering::AcqRel) + 1; + let peer_active = peer_ip.map(|ip| { + let mut count = ACTIVE_CONNECTIONS_BY_IP.entry(ip).or_insert(0); + *count += 1; + *count + }); + if active > config.transport_connections + || peer_active.is_some_and(|count| count > config.transport_connections_per_ip) + { + drop(ConnectionLimitGuard(peer_ip)); + log_err!( + 0, + PrintType::Omega, + "Rejected connection: active connection limit reached" + ); + continue; + } + let connection = OmikronConnection::new(conn.sender, conn.client_id); tokio::spawn(async move { - omikron_manager::add_omikron(omikron_connection.clone()).await; - omikron_connection.handle(&mut conn.receiver).await; + let _guard = ConnectionLimitGuard(peer_ip); + omikron_manager::add_omikron(connection.clone()).await; + connection.handle(&mut conn.receiver).await; }); } - Ok(()) } diff --git a/src/transport/omikron_manager.rs b/src/transport/omikron_manager.rs index f98a75c..2de4ea3 100644 --- a/src/transport/omikron_manager.rs +++ b/src/transport/omikron_manager.rs @@ -1,4 +1,4 @@ -use crate::transport::omikron_connection::OmikronConnection; +use crate::transport::connection::OmikronConnection; use dashmap::DashMap; use mtp::codec::CommunicationValue; use once_cell::sync::Lazy; @@ -27,11 +27,9 @@ pub async fn remove_omikron(omikron_id: i64) { } pub async fn get_random_omikron() -> Result, ()> { - let mut rng = rand::thread_rng(); - let keys: Vec<_> = OMIKRON_CONNECTIONS.iter().map(|e| *e.key()).collect(); - if let Some(key) = keys.into_iter().choose(&mut rng) { + if let Some(key) = keys.into_iter().choose(&mut rand::rng()) { if let Some(entry) = OMIKRON_CONNECTIONS.get(&key) { return Ok(entry.clone()); } diff --git a/src/util/file_util.rs b/src/util/file_util.rs index 593adbe..8630489 100644 --- a/src/util/file_util.rs +++ b/src/util/file_util.rs @@ -8,9 +8,8 @@ use zip::ZipArchive; use crate::log; -static WORKING_DIR: Lazy = Lazy::new(|| { - std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) -}); +static WORKING_DIR: Lazy = + Lazy::new(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))); pub fn delete_file(path: &str, name: &str) -> bool { let dir = Path::new(&get_directory()).join(path); diff --git a/src/util/logger.rs b/src/util/logger.rs index 31606b6..39eb933 100644 --- a/src/util/logger.rs +++ b/src/util/logger.rs @@ -269,8 +269,7 @@ fn format_data_container(data: Vec<(DataTypeId, DataValue)>, version: Version) - DataValue::Str(s) => format!("{}=\"{}\"", key_str, s), DataValue::Container(inner) => { - let inner_formatted = - format_data_container(inner, version.clone()); + let inner_formatted = format_data_container(inner, version.clone()); format!("{}={{ {} }}", key_str, inner_formatted) }