From ad8555bc6e91b833fbcc43393c14bda97a2c1270 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 20 Aug 2026 17:05:43 +0200 Subject: [PATCH] [Updt] Mtp 0.3.0 --- Cargo.lock | 619 +------------------ README.md | 8 +- client/Cargo.toml | 47 -- client/src/client_connection.rs | 106 +++- communities/Cargo.toml | 27 - communities/src/community_connection.rs | 65 +- communities/src/interactables/text_chat.rs | 29 +- communities/src/interactables/voice_chat.rs | 8 +- flake.nix | 9 + iota-auth/Cargo.toml | 50 -- iota-cli/Cargo.toml | 58 -- iota-cli/src/ipc_client.rs | 2 +- iota-connection/src/message_common.rs | 54 +- iota-connection/src/message_handlers.rs | 412 ++++++++----- iota-connection/src/relay.rs | 27 +- iota-core/Cargo.toml | 11 - iota-daemon-lib/Cargo.toml | 2 +- iota-daemon-lib/src/command_router.rs | 159 ++++- iota-daemon-lib/src/daemon_state.rs | 12 +- iota-daemon-lib/src/ipc_server.rs | 261 ++++++-- iota-daemon-lib/src/lib.rs | 2 +- iota-daemon-lib/tests/command_router.rs | 61 +- iota-daemon-lib/tests/ipc_server.rs | 252 ++++++++ iota-daemon/Cargo.toml | 2 - iota-ipc/src/lib.rs | 12 +- iota-ipc/src/protocol.rs | 62 +- iota-ipc/src/transport.rs | 40 +- iota-process-manager/Cargo.toml | 4 + iota-process-manager/src/lib.rs | 176 +++++- iota-state/Cargo.toml | 2 - iota-storage/Cargo.toml | 17 - iota-storage/src/util/config_util.rs | 15 + iota-terms/Cargo.toml | 1 - iota-updater/Cargo.toml | 19 - iota-util/src/crypto_helper.rs | 10 +- iota-util/src/mtp_compat.rs | 76 ++- iota/Cargo.toml | 1 - mtp-type-maps | 2 +- omikron-connector/Cargo.toml | 5 - omikron-connector/src/omikron_connection.rs | 631 +++++++++++++++----- omikron-connector/src/user_ops.rs | 4 +- other-iota/Cargo.toml | 54 -- systemd/iota-daemon.service | 1 + web-server/Cargo.toml | 1 - web-ui/Cargo.toml | 44 -- 45 files changed, 2019 insertions(+), 1441 deletions(-) create mode 100644 iota-daemon-lib/tests/ipc_server.rs diff --git a/Cargo.lock b/Cargo.lock index 7405bb8..925c024 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,29 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "actix" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" -dependencies = [ - "actix-rt", - "bitflags 2.13.1", - "bytes", - "crossbeam-channel", - "futures-core", - "futures-sink", - "futures-task", - "futures-util", - "log", - "once_cell", - "parking_lot", - "pin-project-lite", - "smallvec", - "tokio", - "tokio-util", -] - [[package]] name = "actix-codec" version = "0.5.2" @@ -217,24 +194,6 @@ dependencies = [ "url", ] -[[package]] -name = "actix-web-actors" -version = "4.3.1+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98c5300b38fd004fe7d2a964f9a90813fdbe8a81fed500587e78b1b71c6f980" -dependencies = [ - "actix", - "actix-codec", - "actix-http", - "actix-web", - "bytes", - "bytestring", - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - [[package]] name = "actix-web-codegen" version = "4.3.0" @@ -274,20 +233,6 @@ dependencies = [ "cpufeatures 0.2.17", ] -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - [[package]] name = "aho-corasick" version = "1.1.5" @@ -803,52 +748,14 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" name = "client" version = "0.1.0" dependencies = [ - "actix-web", - "actix-web-actors", - "aes-gcm", - "async-trait", - "base64 0.22.1", - "chrono", - "crossterm", "dashmap", - "futures", - "futures-util", - "hex", - "hkdf 0.12.4", - "hyper", - "hyper-util", - "iota-auth", "iota-connection", "iota-logger", - "iota-state", "iota-storage", "iota-util", - "json", - "lazy_static", "mtp", - "once_cell", - "open", - "pnet", - "rand 0.8.7", - "rand_core 0.6.4", - "ratatui", - "reqwest", - "rusqlite", - "rustls", - "rustls-pemfile", - "serde_json", - "sha2 0.11.0", - "strum", - "strum_macros", - "sysinfo", "tokio", - "tokio-tungstenite", - "tungstenite", "uuid", - "walkdir", - "warp", - "x448", - "zip", ] [[package]] @@ -1008,15 +915,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" -[[package]] -name = "crossbeam-channel" -version = "0.5.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.22" @@ -1082,15 +980,6 @@ dependencies = [ "phf", ] -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - [[package]] name = "ctutils" version = "0.4.2" @@ -1392,17 +1281,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.17.0" @@ -1483,12 +1361,6 @@ 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" @@ -1553,21 +1425,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1720,16 +1577,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - [[package]] name = "glob" version = "0.3.4" @@ -1867,30 +1714,6 @@ dependencies = [ "hashbrown 0.17.1", ] -[[package]] -name = "headers" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" -dependencies = [ - "base64 0.22.1", - "bytes", - "headers-core", - "http 1.5.0", - "httpdate", - "mime", - "sha1 0.10.7", -] - -[[package]] -name = "headers-core" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" -dependencies = [ - "http 1.5.0", -] - [[package]] name = "heck" version = "0.5.0" @@ -1903,15 +1726,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac 0.12.1", -] - [[package]] name = "hkdf" version = "0.13.0" @@ -2276,114 +2090,34 @@ dependencies = [ "serde_json", "serde_yaml", "tokio", - "tokio-util", ] [[package]] name = "iota-auth" version = "0.1.0" dependencies = [ - "aes-gcm", - "async-trait", - "base64 0.22.1", - "chrono", - "crossterm", - "dashmap", - "futures", - "futures-util", - "hex", - "hkdf 0.12.4", - "hyper", - "hyper-util", - "iota-logger", - "iota-state", - "iota-storage", - "iota-util", "json", - "lazy_static", - "mtp", - "once_cell", - "open", - "pnet", - "rand 0.8.7", - "rand_core 0.6.4", - "ratatui", - "reqwest", - "rusqlite", - "rustls", - "rustls-pemfile", - "serde_json", - "sha2 0.11.0", - "strum", - "strum_macros", - "sysinfo", - "tokio", - "tokio-tungstenite", - "tungstenite", - "uuid", - "walkdir", - "warp", - "x448", - "zip", ] [[package]] name = "iota-cli" version = "0.1.0" dependencies = [ - "actix-web", - "actix-web-actors", - "aes-gcm", - "async-trait", - "base64 0.22.1", "chrono", "crossterm", - "dashmap", - "futures", - "futures-util", - "hex", - "hkdf 0.12.4", - "hyper", - "hyper-util", "iota-ipc", - "iota-logger", "iota-paths", - "iota-process-manager", "iota-state", - "iota-storage", "iota-terms", - "iota-util", - "json", - "lazy_static", - "mtp", - "omikron-connector", "once_cell", "open", - "pnet", - "rand 0.8.7", - "rand_core 0.6.4", "ratatui", - "reqwest", - "rusqlite", - "rustls", - "rustls-pemfile", "serde", - "serde_json", "serde_yaml", - "sha2 0.11.0", - "strum", - "strum_macros", - "sysinfo", "tempfile", "tokio", - "tokio-tungstenite", "tokio-util", - "tungstenite", "unicode-width", - "walkdir", - "warp", - "x448", - "zip", ] [[package]] @@ -2400,7 +2134,6 @@ dependencies = [ name = "iota-core" version = "0.1.0" dependencies = [ - "dashmap", "iota-cli", "iota-logger", "iota-paths", @@ -2409,17 +2142,9 @@ dependencies = [ "iota-terms", "iota-updater", "iota-util", - "json", - "mtp", - "omikron-connector", - "once_cell", "pnet", - "ratatui", - "reqwest", "tokio", - "tokio-util", "web-server", - "web-ui", ] [[package]] @@ -2430,13 +2155,11 @@ dependencies = [ "iota-ipc", "iota-logger", "iota-paths", - "iota-state", "iota-storage", "iota-terms", "iota-util", "omikron-connector", "tokio", - "tokio-util", "web-server", ] @@ -2445,7 +2168,6 @@ name = "iota-daemon-lib" version = "0.1.0" dependencies = [ "async-trait", - "dashmap", "iota-ipc", "iota-logger", "iota-state", @@ -2455,6 +2177,7 @@ dependencies = [ "libc", "mtp", "omikron-connector", + "serde_json", "serde_yaml", "sysinfo", "tempfile", @@ -2506,6 +2229,8 @@ name = "iota-process-manager" version = "0.1.0" dependencies = [ "async-trait", + "libc", + "tempfile", "tokio", ] @@ -2515,9 +2240,7 @@ version = "0.1.0" dependencies = [ "dashmap", "json", - "mtp", "once_cell", - "serde", "sysinfo", "tokio", ] @@ -2526,42 +2249,26 @@ dependencies = [ name = "iota-storage" version = "0.1.0" dependencies = [ - "aes-gcm", "arc-swap", "base64 0.22.1", - "hex", - "hkdf 0.12.4", "iota-logger", "iota-paths", - "iota-state", "iota-util", "json", - "mtp", "once_cell", "r2d2", "rand 0.8.7", - "rand_core 0.6.4", - "ratatui", - "reqwest", "rusqlite", "serde", - "serde_json", "serde_yaml", - "sha2 0.11.0", - "sysinfo", "thiserror 2.0.20", "tokio", - "uuid", - "walkdir", - "x448", - "zip", ] [[package]] name = "iota-terms" version = "0.1.0" dependencies = [ - "iota-state", "iota-util", "json", "reqwest", @@ -2571,32 +2278,15 @@ dependencies = [ name = "iota-updater" version = "0.1.0" dependencies = [ - "aes-gcm", "anyhow", - "base64 0.22.1", "ed25519-dalek 2.2.0", "hex", - "hkdf 0.12.4", - "iota-logger", "iota-paths", - "json", - "mtp", - "once_cell", - "pnet", - "rand_core 0.6.4", - "ratatui", - "reqwest", - "semver", "serde", "serde_json", "sha2 0.11.0", - "sysinfo", "tempfile", "tokio", - "uuid", - "walkdir", - "x448", - "zip", ] [[package]] @@ -2943,16 +2633,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3037,7 +2717,7 @@ dependencies = [ [[package]] name = "mtp" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "mtp-client", "mtp-codec", @@ -3053,7 +2733,7 @@ dependencies = [ [[package]] name = "mtp-client" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "mtp-codec", "mtp-common", @@ -3066,7 +2746,7 @@ dependencies = [ [[package]] name = "mtp-codec" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "base64 0.23.1", "byteorder", @@ -3080,7 +2760,7 @@ dependencies = [ [[package]] name = "mtp-common" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "quinn", "rustls", @@ -3091,14 +2771,14 @@ dependencies = [ [[package]] name = "mtp-crypto" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "argon2", "base64 0.22.1", "chacha20poly1305", "ed25519-dalek 3.0.0", "getrandom 0.4.3", - "hkdf 0.13.0", + "hkdf", "ml-dsa", "mlkem-tls", "rand 0.10.2", @@ -3114,7 +2794,7 @@ dependencies = [ [[package]] name = "mtp-files" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "mtp-crypto", "rand 0.10.2", @@ -3125,7 +2805,7 @@ dependencies = [ [[package]] name = "mtp-host" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "mtp-codec", "mtp-common", @@ -3141,7 +2821,7 @@ dependencies = [ [[package]] name = "mtp-transport" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "async-trait", "mtp-codec", @@ -3161,7 +2841,7 @@ dependencies = [ [[package]] name = "mtp-type-map" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "serde", "serde_yaml", @@ -3170,7 +2850,7 @@ dependencies = [ [[package]] name = "mtp-webserver" version = "0.3.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#b331b9f6a3943d0331d8fdcb7c696d6c2bc5e8b8" +source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a" dependencies = [ "async-trait", "bytes", @@ -3195,23 +2875,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "native-tls" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nix" version = "0.29.0" @@ -3383,7 +3046,6 @@ dependencies = [ "async-trait", "base64 0.22.1", "dashmap", - "hex", "iota-connection", "iota-logger", "iota-state", @@ -3391,14 +3053,11 @@ dependencies = [ "iota-util", "json", "mtp", - "rand 0.8.7", "rand_core 0.6.4", "reqwest", - "sha2 0.11.0", "tokio", "tokio-util", "uuid", - "x448", ] [[package]] @@ -3429,49 +3088,12 @@ dependencies = [ "libc", ] -[[package]] -name = "openssl" -version = "0.10.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" -dependencies = [ - "bitflags 2.13.1", - "cfg-if", - "foreign-types", - "libc", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" -[[package]] -name = "openssl-sys" -version = "0.9.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "ordered-float" version = "4.6.0" @@ -3484,53 +3106,6 @@ dependencies = [ [[package]] name = "other-iota" version = "0.1.0" -dependencies = [ - "actix-web", - "actix-web-actors", - "aes-gcm", - "async-trait", - "base64 0.22.1", - "chrono", - "crossterm", - "dashmap", - "futures", - "futures-util", - "hex", - "hkdf 0.12.4", - "hyper", - "hyper-util", - "iota-auth", - "iota-logger", - "iota-state", - "iota-storage", - "iota-util", - "json", - "lazy_static", - "mtp", - "once_cell", - "open", - "pnet", - "rand 0.8.7", - "rand_core 0.6.4", - "ratatui", - "reqwest", - "rusqlite", - "rustls", - "rustls-pemfile", - "serde_json", - "sha2 0.11.0", - "strum", - "strum_macros", - "sysinfo", - "tokio", - "tokio-tungstenite", - "tungstenite", - "uuid", - "walkdir", - "warp", - "x448", - "zip", -] [[package]] name = "palette" @@ -3728,26 +3303,6 @@ dependencies = [ "siphasher", ] -[[package]] -name = "pin-project" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "pin-project-lite" version = "0.2.17" @@ -3882,18 +3437,6 @@ dependencies = [ "universal-hash", ] -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - [[package]] name = "portable-atomic" version = "1.15.0" @@ -4063,12 +3606,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" @@ -4492,12 +4029,6 @@ dependencies = [ "parking_lot", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -5158,16 +4689,6 @@ dependencies = [ "syn 3.0.3", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" @@ -5189,20 +4710,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-tungstenite" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17a073bfed563fa236697a068031408a93cd9522e08abf9933ead3e73411bd71" -dependencies = [ - "futures-util", - "log", - "native-tls", - "tokio", - "tokio-native-tls", - "tungstenite", -] - [[package]] name = "tokio-util" version = "0.7.19" @@ -5301,23 +4808,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tungstenite" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22" -dependencies = [ - "bytes", - "data-encoding", - "http 1.5.0", - "httparse", - "log", - "native-tls", - "rand 0.10.2", - "sha1 0.11.0", - "thiserror 2.0.20", -] - [[package]] name = "typenum" version = "1.20.1" @@ -5330,12 +4820,6 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - [[package]] name = "unicode-ident" version = "1.0.24" @@ -5475,33 +4959,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "warp" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0a808122a8a77eecdabaefd88ddb1913c4be5ea1465399f63ba64c7aa705fea" -dependencies = [ - "bytes", - "futures-util", - "headers", - "http 1.5.0", - "http-body", - "http-body-util", - "log", - "mime", - "mime_guess", - "percent-encoding", - "pin-project", - "scoped-tls", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-util", - "tower-service", - "tracing", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -5579,7 +5036,6 @@ dependencies = [ "bytes", "http 1.5.0", "iota-logger", - "iota-util", "mtp", "tokio", "tokio-util", @@ -5610,50 +5066,14 @@ name = "web-ui" version = "0.1.0" dependencies = [ "actix-web", - "actix-web-actors", - "aes-gcm", - "async-trait", - "base64 0.22.1", - "chrono", - "crossterm", - "dashmap", - "futures", - "futures-util", - "hex", - "hkdf 0.12.4", - "hyper", - "hyper-util", "iota-logger", "iota-state", "iota-storage", "iota-util", - "json", - "lazy_static", - "mtp", - "omikron-connector", - "once_cell", - "open", - "pnet", - "rand 0.8.7", - "rand_core 0.6.4", - "ratatui", - "reqwest", - "rusqlite", "rustls", "rustls-pemfile", "serde_json", - "sha2 0.11.0", - "strum", - "strum_macros", - "sysinfo", "tokio", - "tokio-tungstenite", - "tungstenite", - "uuid", - "walkdir", - "warp", - "x448", - "zip", ] [[package]] @@ -6031,17 +5451,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" diff --git a/README.md b/README.md index aee09ab..5a8c16a 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,12 @@ document without accepting it. # Linux daemon installation The system-managed daemon runs as the dedicated `iota` account and listens on -`/run/iota/iota.sock` through socket activation. Operator access is granted -through the `iota-operators` group. After installing, add an account with: +`/run/iota/iota.sock` through socket activation. The system IPC socket is the +privilege boundary. Operator access is granted through the `iota-operators` +group, and every account admitted through that socket is authorized for the +full operator-console role, including user management, identity rotation, +configuration, and daemon lifecycle commands. After installing, add an +account with: ```text usermod -aG iota-operators USER diff --git a/client/Cargo.toml b/client/Cargo.toml index d63431f..8759d48 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -9,53 +9,6 @@ iota-connection = { path = "../iota-connection" } iota-logger = { path = "../iota-logger" } iota-util = { path = "../iota-util" } iota-storage = { path = "../iota-storage" } -iota-state = { path = "../iota-state" } -iota-auth = { path = "../iota-auth" } - -actix-web = { version = "4", features = ["rustls-0_23"] } -actix-web-actors = "4" -aes-gcm = "0.10.3" -async-trait = "0.1.89" -base64 = "0.22.1" -chrono = "0.4.43" -crossterm = "*" dashmap = "6.1.0" -futures = "*" -futures-util = "*" -hex = "*" -hkdf = "0.12.4" -hyper = { version = "1.8.1", features = [ - "capi", - "client", - "full", - "http1", - "http2", - "nightly", - "server", -] } -hyper-util = { version = "*" } -json = "*" -lazy_static = "1.5.0" -once_cell = "1.21.3" -open = "5.3.3" -pnet = "0.35.0" -rand = "0.8" -rand_core = { version = "0.6", features = ["getrandom", "std"] } -ratatui = "0.30.0" -reqwest = "0.13.2" -rusqlite = "0.40.0" -rustls = { version = "0.23.37", features = ["aws-lc-rs"] } -rustls-pemfile = "2.2.0" -serde_json = "1.0.149" -sha2 = "0.11.0" -strum = "0.28.0" -strum_macros = "0.28.0" -sysinfo = "0.39.0" tokio = { version = "1.50.0", features = ["full"] } -tokio-tungstenite = { version = "*", features = ["native-tls"] } -tungstenite = "*" uuid = { version = "*", features = ["v4"] } -walkdir = "2.5.0" -warp = "*" -x448 = { version = "*" } -zip = "6.0.0" diff --git a/client/src/client_connection.rs b/client/src/client_connection.rs index a90df86..cfad489 100644 --- a/client/src/client_connection.rs +++ b/client/src/client_connection.rs @@ -140,7 +140,11 @@ impl ClientConnection { return; } - let _msg_id = cv.get_id(); + if cv.require_id().is_err() { + self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } if cv.is_type(CommunicationType::Challenge) { self.handle_challenge(&cv).await; @@ -154,7 +158,14 @@ impl ClientConnection { } if cv.is_type(CommunicationType::SaveAppData) { - let sender_id = cv.get_sender(); + let sender_id = match cv.require_sender() { + Ok(sender_id) => sender_id, + Err(_) => { + self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } + }; let _app_data = cv .get_data(DataType::AppData) .as_str() @@ -162,18 +173,25 @@ impl ClientConnection { .to_string(); let res = CommunicationValue::new(CommunicationType::SaveAppData) - .with_id(cv.get_id()) + .with_request_id(&cv) .with_receiver(sender_id); self.send_message(&res).await; return; } if cv.is_type(CommunicationType::LoadAppData) { - let sender_id = cv.get_sender(); + let sender_id = match cv.require_sender() { + Ok(sender_id) => sender_id, + Err(_) => { + self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } + }; let app_data = String::new(); let res = CommunicationValue::new(CommunicationType::LoadAppData) - .with_id(cv.get_id()) + .with_request_id(&cv) .with_receiver(sender_id) .add_typed_default(DataType::AppData, DataValue::Str(app_data)); self.send_message(&res).await; @@ -287,12 +305,32 @@ impl ClientConnection { } if cv.is_type(CommunicationType::SettingsSave) { - let my_id = cv.get_sender(); - let settings_name = cv.get_data(DataType::SettingsName).as_str().unwrap(); - let settings_value = cv.get_data(DataType::Payload).as_str().unwrap(); + let my_id = match cv.require_sender() { + Ok(my_id) => my_id, + Err(_) => { + self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } + }; + let Ok(my_id_i64) = i64::try_from(my_id) else { + self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + .await; + return; + }; + let Some(settings_name) = cv.get_data(DataType::SettingsName).as_str() else { + self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + .await; + return; + }; + let Some(settings_value) = cv.get_data(DataType::Payload).as_str() else { + self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + .await; + return; + }; let _ = iota_storage::util::settings::save( - my_id as i64, + my_id_i64, iota_storage::util::settings::GLOBAL_SESSION_ID, settings_name, settings_value, @@ -300,17 +338,33 @@ impl ClientConnection { let response = CommunicationValue::new(CommunicationType::SettingsSave) .with_receiver(my_id) - .with_id(cv.get_id()); + .with_request_id(&cv); self.send_message(&response).await; return; } if cv.is_type(CommunicationType::SettingsLoad) { - let my_id = cv.get_sender(); - let settings_name = cv.get_data(DataType::SettingsName).as_string().unwrap(); + let my_id = match cv.require_sender() { + Ok(my_id) => my_id, + Err(_) => { + self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } + }; + let Ok(my_id_i64) = i64::try_from(my_id) else { + self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + .await; + return; + }; + let Some(settings_name) = cv.get_data(DataType::SettingsName).as_string() else { + self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + .await; + return; + }; let settings_value_str = iota_storage::util::settings::load( - my_id as i64, + my_id_i64, iota_storage::util::settings::GLOBAL_SESSION_ID, &settings_name, ) @@ -318,7 +372,7 @@ impl ClientConnection { .flatten() .unwrap_or_default(); let response = CommunicationValue::new(CommunicationType::SettingsLoad) - .with_id(cv.get_id()) + .with_request_id(&cv) .with_receiver(my_id) .add_typed_default(DataType::Payload, DataValue::Str(settings_value_str)) .add_typed_default(DataType::SettingsName, DataValue::Str(settings_name)); @@ -328,15 +382,27 @@ impl ClientConnection { } if cv.is_type(CommunicationType::SettingsList) { - let my_id = cv.get_sender(); + let my_id = match cv.require_sender() { + Ok(my_id) => my_id, + Err(_) => { + self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } + }; + let Ok(my_id_i64) = i64::try_from(my_id) else { + self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + .await; + return; + }; let settings = iota_storage::util::settings::list( - my_id as i64, + my_id_i64, iota_storage::util::settings::GLOBAL_SESSION_ID, ) .unwrap_or_default(); let settings_json = settings.into_iter().map(DataValue::Str).collect(); let response = CommunicationValue::new(CommunicationType::SettingsList) - .with_id(cv.get_id()) + .with_request_id(&cv) .with_receiver(my_id) .add_typed_default(DataType::Settings, DataValue::Array(settings_json)); @@ -356,7 +422,7 @@ impl ClientConnection { if let Some(solved) = solved { let response = CommunicationValue::new(CommunicationType::ChallengeResponse) - .with_id(cv.get_id()) + .with_request_id(&cv) .add_typed_default(DataType::Challenge, DataValue::Str(solved)); self.send_message(&response).await; @@ -405,7 +471,9 @@ impl ClientConnection { timeout_duration: Option, ) -> Result { let (tx, mut rx) = mpsc::channel(1); - let msg_id = cv.get_id(); + let msg_id = cv + .require_id() + .map_err(|error| format!("cannot await response without a message id: {error}"))?; let task_tx = tx.clone(); self.waiting_tasks.insert( diff --git a/communities/Cargo.toml b/communities/Cargo.toml index 89c4f57..4279454 100644 --- a/communities/Cargo.toml +++ b/communities/Cargo.toml @@ -5,23 +5,11 @@ edition = "2024" [dependencies] mtp = { git = "https://git.methanium.net/Methanium/mtp.git" } -iota-logger = { path = "../iota-logger" } iota-util = { path = "../iota-util" } -iota-storage = { path = "../iota-storage" } -iota-state = { path = "../iota-state" } -iota-auth = { path = "../iota-auth" } - -actix-web = { version = "4", features = ["rustls-0_23"] } -actix-web-actors = "4" aes-gcm = "0.10.3" async-trait = "0.1.89" base64 = "0.22.1" -chrono = "0.4.43" -crossterm = "*" -dashmap = "6.1.0" futures = "*" -futures-util = "*" -hex = "*" hkdf = "0.12.4" hyper = { version = "1.8.1", features = [ "capi", @@ -34,27 +22,12 @@ hyper = { version = "1.8.1", features = [ ] } hyper-util = { version = "*" } json = "*" -lazy_static = "1.5.0" once_cell = "1.21.3" -open = "5.3.3" -pnet = "0.35.0" rand = "0.8" rand_core = { version = "0.6", features = ["getrandom", "std"] } -ratatui = "0.30.0" -reqwest = "0.13.2" -rusqlite = "0.40.0" -rustls = { version = "0.23.37", features = ["aws-lc-rs"] } -rustls-pemfile = "2.2.0" -serde_json = "1.0.149" sha2 = "0.11.0" -strum = "0.28.0" -strum_macros = "0.28.0" -sysinfo = "0.39.0" tokio = { version = "1.50.0", features = ["full"] } tokio-tungstenite = { version = "*", features = ["native-tls"] } tungstenite = "*" uuid = { version = "*", features = ["v4"] } -walkdir = "2.5.0" -warp = "*" x448 = { version = "*" } -zip = "6.0.0" diff --git a/communities/src/community_connection.rs b/communities/src/community_connection.rs index 685d9d8..68c0f48 100644 --- a/communities/src/community_connection.rs +++ b/communities/src/community_connection.rs @@ -2,7 +2,6 @@ use crate::auth::auth_user::AuthUser; use crate::communities::community::Community; use crate::communities::interactables::interactable::Interactable; use crate::users::user_manager::get_user; -use iota_util::mtp_compat::CommunicationValueCompat; use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead}; use base64::{Engine as _, engine::general_purpose::STANDARD}; use futures::SinkExt; @@ -22,6 +21,20 @@ use tungstenite::Message; use tungstenite::Utf8Bytes; use uuid::Uuid; use x448::PublicKey; + +trait CommunicationResponseExt { + fn with_request_id(self, request: &CommunicationValue) -> Self; +} + +impl CommunicationResponseExt for CommunicationValue { + fn with_request_id(mut self, request: &CommunicationValue) -> Self { + self = self.without_id(); + if let Some(id) = request.id() { + self = self.with_id(id); + } + self + } +} pub struct CommunityConnection { pub sender: Arc>, Message>>>, pub receiver: Arc>>>>, @@ -115,7 +128,7 @@ impl CommunityConnection { .unwrap_or(0); let Some(user) = get_user(user_id) else { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInvalidUserId) + self.send_error_response(&cv, CommunicationType::ErrorInvalidUserId) .await; return; }; @@ -145,7 +158,7 @@ impl CommunityConnection { let user_public_key_bytes = match STANDARD.decode(&user.public_key) { Ok(bytes) => bytes, Err(_) => { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInvalidUserId) + self.send_error_response(&cv, CommunicationType::ErrorInvalidUserId) .await; return; } @@ -154,14 +167,14 @@ impl CommunityConnection { let user_pub_key: PublicKey = match PublicKey::from_bytes(&user_public_key_bytes) { Some(key) => key, __ => { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInvalidUserId) + self.send_error_response(&cv, CommunicationType::ErrorInvalidUserId) .await; return; } }; let Some(community) = self.community.read().await.clone() else { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInternal) + self.send_error_response(&cv, CommunicationType::ErrorInternal) .await; return; }; @@ -172,7 +185,7 @@ impl CommunityConnection { let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) { Some(secret) => secret, _ => { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInternal) + self.send_error_response(&cv, CommunicationType::ErrorInternal) .await; return; } @@ -194,7 +207,7 @@ impl CommunityConnection { let encrypted_challenge = match cipher.encrypt(nonce, challenge_str.as_bytes()) { Ok(data) => data, Err(_) => { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInternal) + self.send_error_response(&cv, CommunicationType::ErrorInternal) .await; return; } @@ -209,7 +222,7 @@ impl CommunityConnection { STANDARD.encode(community_public_key.as_bytes()), ) .add_data_str(DataType::Challenge, STANDARD.encode(&encrypted_out)) - .with_id(cv.get_id()); + .with_request_id(&cv); self.send_message(&response).await; } @@ -217,7 +230,7 @@ impl CommunityConnection { let client_challenge_response_b64 = match cv.get_data(DataType::Challenge) { Some(data) => data.to_string(), _ => { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInvalidData) + self.send_error_response(&cv, CommunicationType::ErrorInvalidData) .await; return; } @@ -226,38 +239,38 @@ impl CommunityConnection { let challenge_response_bytes = match STANDARD.decode(&client_challenge_response_b64) { Ok(bytes) => bytes, Err(_) => { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInvalidData) + self.send_error_response(&cv, CommunicationType::ErrorInvalidData) .await; return; } }; if challenge_response_bytes.len() < 12 { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInvalidData) + self.send_error_response(&cv, CommunicationType::ErrorInvalidData) .await; return; } let Some(user) = self.auth.read().await.clone() else { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInternal) + self.send_error_response(&cv, CommunicationType::ErrorInternal) .await; return; }; let Some(user_pub_bytes) = STANDARD.decode(&user.public_key).ok() else { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInvalidData) + self.send_error_response(&cv, CommunicationType::ErrorInvalidData) .await; return; }; let Some(user_pub_key) = PublicKey::from_bytes(&user_pub_bytes) else { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInvalidPublicKey) + self.send_error_response(&cv, CommunicationType::ErrorInvalidPublicKey) .await; return; }; let Some(community) = self.community.read().await.clone() else { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInternal) + self.send_error_response(&cv, CommunicationType::ErrorInternal) .await; return; }; @@ -267,7 +280,7 @@ impl CommunityConnection { let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) { Some(secret) => secret, _ => { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInternal) + self.send_error_response(&cv, CommunicationType::ErrorInternal) .await; return; } @@ -291,7 +304,7 @@ impl CommunityConnection { let decrypted_bytes = match cipher.decrypt(nonce, ciphertext) { Ok(pt) => pt, Err(_) => { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInvalidChallenge) + self.send_error_response(&cv, CommunicationType::ErrorInvalidChallenge) .await; return; } @@ -300,7 +313,7 @@ impl CommunityConnection { let client_response = match String::from_utf8(decrypted_bytes) { Ok(str) => str, Err(_) => { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInvalidData) + self.send_error_response(&cv, CommunicationType::ErrorInvalidData) .await; return; } @@ -309,7 +322,7 @@ impl CommunityConnection { let expected_challenge = self.challenge.read().await.clone(); if client_response != expected_challenge { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInvalidChallenge) + self.send_error_response(&cv, CommunicationType::ErrorInvalidChallenge) .await; self.close().await; return; @@ -321,7 +334,7 @@ impl CommunityConnection { } let Some(community) = self.community.read().await.clone() else { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInternal) + self.send_error_response(&cv, CommunicationType::ErrorInternal) .await; return; }; @@ -329,7 +342,7 @@ impl CommunityConnection { let user_id = self.get_user_id().await; if user_id == 0 { - self.send_error_response(&cv.get_id(), CommunicationType::ErrorInvalidUserId) + self.send_error_response(&cv, CommunicationType::ErrorInvalidUserId) .await; return; } @@ -348,13 +361,17 @@ impl CommunityConnection { } c }) - .with_id(cv.get_id()); + .with_request_id(&cv); self.send_message(&response).await; } - async fn send_error_response(&self, message_id: &Uuid, error_type: CommunicationType) { - let error = CommunicationValue::new(error_type).with_id(*message_id); + async fn send_error_response( + &self, + request: &CommunicationValue, + error_type: CommunicationType, + ) { + let error = CommunicationValue::new(error_type).with_request_id(request); self.send_message(&error).await; } pub async fn close(&self) { diff --git a/communities/src/interactables/text_chat.rs b/communities/src/interactables/text_chat.rs index 6b69baa..780d8ed 100644 --- a/communities/src/interactables/text_chat.rs +++ b/communities/src/interactables/text_chat.rs @@ -8,7 +8,7 @@ use crate::{ }; use async_trait::async_trait; use json::{JsonValue, array, object}; -use iota_util::mtp_compat::{CommunicationValueCompat, OptionalDataValueExt}; +use iota_util::mtp_compat::{OptionalDataValueExt, RequiredCommunicationFields}; use std::fs; use std::path::Path; use std::sync::Arc; @@ -31,6 +31,10 @@ impl TextChat { } } pub fn add_message(&self, send_time: u128, sender: i64, message: &str) { + let Ok(send_time) = i64::try_from(send_time) else { + log!("Message timestamp exceeds local storage range"); + return; + }; let user_dir = &format!( "communities/{}/interactables/{}/{}", self.get_community().get_name(), @@ -74,7 +78,7 @@ impl TextChat { } let json_obj = object! { - "timestamp" => send_time as i64, + "timestamp" => send_time, "content" => message, "sender" => sender.to_string(), }; @@ -202,7 +206,7 @@ impl Interactable for TextChat { let mut payload = JsonValue::new_object(); payload["messages"] = messages; return CommunicationValue::new(CommunicationType::Function) - .with_id(cv.get_id()) + .with_request_id(&cv) .add_data_str(DataType::Name, self.name.clone()) .add_data_str(DataType::Path, self.path.clone()) .add_data_str(DataType::Result, "message_chunk".to_string()) @@ -210,19 +214,28 @@ impl Interactable for TextChat { } if cv.get_data(DataType::Function).unwrap().as_str().unwrap() == "send_message" { let message = payload["message"].as_str().unwrap(); + let sender = match cv.require_sender() { + Ok(sender) => sender, + Err(_) => return CommunicationValue::new(CommunicationType::ErrorInvalidData) + .with_request_id(&cv), + }; let milliseconds_timestamp: u128 = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis(); - self.add_message(milliseconds_timestamp, cv.get_sender(), message); + let Ok(sender) = i64::try_from(sender) else { + return CommunicationValue::new(CommunicationType::ErrorInvalidData) + .with_request_id(&cv); + }; + self.add_message(milliseconds_timestamp, sender, message); let mut distribution_payload = JsonValue::new_object(); distribution_payload["message"] = JsonValue::String(message.to_string()); - distribution_payload["sender_id"] = JsonValue::String(cv.get_sender().to_string()); + distribution_payload["sender_id"] = JsonValue::String(sender.to_string()); distribution_payload["send_time"] = JsonValue::String(milliseconds_timestamp.to_string()); let distribution = CommunicationValue::new(CommunicationType::Update) - .with_id(cv.get_id()) + .with_request_id(&cv) .add_data_str(DataType::Name, self.name.clone()) .add_data_str(DataType::Path, self.path.clone()) .add_data_str(DataType::Result, "message_live".to_string()) @@ -238,13 +251,13 @@ impl Interactable for TextChat { } } return CommunicationValue::new(CommunicationType::Function) - .with_id(cv.get_id()) + .with_request_id(&cv) .add_data_str(DataType::Name, self.name.clone()) .add_data_str(DataType::Path, self.path.clone()) .add_data_str(DataType::Result, "message_received".to_string()) .add_data(DataType::Payload, JsonValue::new_object()); } - CommunicationValue::new(CommunicationType::ErrorInternal).with_id(cv.get_id()) + CommunicationValue::new(CommunicationType::ErrorInternal).with_request_id(&cv) } fn to_json(&self) -> JsonValue { JsonValue::new_object() diff --git a/communities/src/interactables/voice_chat.rs b/communities/src/interactables/voice_chat.rs index 7e20022..7681765 100644 --- a/communities/src/interactables/voice_chat.rs +++ b/communities/src/interactables/voice_chat.rs @@ -1,7 +1,7 @@ use crate::communities::{community::Community, interactables::interactable::Interactable}; use async_trait::async_trait; use json::JsonValue; -use iota_util::mtp_compat::{CommunicationValueCompat, OptionalDataValueExt}; +use iota_util::mtp_compat::OptionalDataValueExt; use std::sync::Arc; use std::{any::Any, sync::RwLock}; use uuid::Uuid; @@ -131,7 +131,7 @@ impl Interactable for VoiceChat { response_payload["send_time"] = JsonValue::String(send_time.to_string()); return CommunicationValue::new(CommunicationType::Function) - .with_id(cv.get_id()) + .with_request_id(&cv) .add_data_str(DataType::Name, self.name.clone()) .add_data_str(DataType::Path, self.path.clone()) .add_data_str(DataType::Result, "getting_call".to_string()) @@ -159,13 +159,13 @@ impl Interactable for VoiceChat { response_payload["streaming"] = JsonValue::Boolean(streaming); return CommunicationValue::new(CommunicationType::Update) - .with_id(cv.get_id()) + .with_request_id(&cv) .add_data_str(DataType::Name, self.name.clone()) .add_data_str(DataType::Path, self.path.clone()) .add_data_str(DataType::Result, "user_changed".to_string()) .add_data(DataType::Payload, response_payload); } - CommunicationValue::new(CommunicationType::ErrorInternal).with_id(cv.get_id()) + CommunicationValue::new(CommunicationType::ErrorInternal).with_request_id(&cv) } fn to_json(&self) -> JsonValue { diff --git a/flake.nix b/flake.nix index e2ca8a1..5d64dc6 100644 --- a/flake.nix +++ b/flake.nix @@ -140,6 +140,12 @@ description = "Environment files to load for the Iota service."; }; + identitySecretFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Owner-readable file containing the passphrase for the protected Iota identity."; + }; + openFirewall = lib.mkOption { type = lib.types.bool; default = true; @@ -261,6 +267,9 @@ } // lib.optionalAttrs (cfg.environmentFiles != []) { EnvironmentFile = cfg.environmentFiles; + } + // lib.optionalAttrs (cfg.identitySecretFile != null) { + LoadCredential = "iota-identity:${cfg.identitySecretFile}"; }; }; diff --git a/iota-auth/Cargo.toml b/iota-auth/Cargo.toml index dc43347..f35935a 100644 --- a/iota-auth/Cargo.toml +++ b/iota-auth/Cargo.toml @@ -4,54 +4,4 @@ version = "0.1.0" edition = "2024" [dependencies] -mtp = { git = "https://git.methanium.net/Methanium/mtp.git" } -iota-logger = { path = "../iota-logger" } -iota-util = { path = "../iota-util" } -iota-storage = { path = "../iota-storage" } -iota-state = { path = "../iota-state" } - -aes-gcm = "0.10.3" -async-trait = "0.1.89" -base64 = "0.22.1" -chrono = "0.4.43" -crossterm = "*" -dashmap = "6.1.0" -futures = "*" -futures-util = "*" -hex = "*" -hkdf = "0.12.4" -hyper = { version = "1.8.1", features = [ - "capi", - "client", - "full", - "http1", - "http2", - "nightly", - "server", -] } -hyper-util = { version = "*" } json = "*" -lazy_static = "1.5.0" -once_cell = "1.21.3" -open = "5.3.3" -pnet = "0.35.0" -rand = "0.8" -rand_core = { version = "0.6", features = ["getrandom", "std"] } -ratatui = "0.30.0" -reqwest = "0.13.2" -rusqlite = "0.40.0" -rustls = { version = "0.23.37", features = ["aws-lc-rs"] } -rustls-pemfile = "2.2.0" -serde_json = "1.0.149" -sha2 = "0.11.0" -strum = "0.28.0" -strum_macros = "0.28.0" -sysinfo = "0.39.0" -tokio = { version = "1.50.0", features = ["full"] } -tokio-tungstenite = { version = "*", features = ["native-tls"] } -tungstenite = "*" -uuid = { version = "*", features = ["v4"] } -walkdir = "2.5.0" -warp = "*" -x448 = { version = "*" } -zip = "6.0.0" diff --git a/iota-cli/Cargo.toml b/iota-cli/Cargo.toml index 4489cdf..1536c5c 100644 --- a/iota-cli/Cargo.toml +++ b/iota-cli/Cargo.toml @@ -3,78 +3,20 @@ name = "iota-cli" version = "0.1.0" edition = "2024" -[features] -legacy-commands = [ - "dep:iota-logger", - "dep:iota-storage", - "dep:iota-util", - "dep:mtp", - "dep:omikron-connector", -] - [dependencies] -iota-logger = { path = "../iota-logger", optional = true } iota-state = { path = "../iota-state" } -iota-storage = { path = "../iota-storage", optional = true } iota-terms = { path = "../iota-terms" } -iota-util = { path = "../iota-util", optional = true } iota-ipc = { path = "../iota-ipc" } -iota-process-manager = { path = "../iota-process-manager" } iota-paths = { path = "../iota-paths" } -omikron-connector = { path = "../omikron-connector", optional = true } - -mtp = { git = "https://git.methanium.net/Methanium/mtp.git", optional = true } - - -actix-web = { version = "4", features = ["rustls-0_23"] } -actix-web-actors = "4" -aes-gcm = "0.10.3" -async-trait = "0.1.89" -base64 = "0.22.1" chrono = "0.4.43" crossterm = "*" -dashmap = "6.1.0" -futures = "*" -futures-util = "*" -hex = "*" -hkdf = "0.12.4" -hyper = { version = "1.8.1", features = [ - "capi", - "client", - "full", - "http1", - "http2", - "nightly", - "server", -] } -hyper-util = { version = "*" } -json = "*" -lazy_static = "1.5.0" once_cell = "1.21.3" open = "5.3.3" -pnet = "0.35.0" -rand = "0.8" -rand_core = { version = "0.6", features = ["getrandom", "std"] } ratatui = "0.30.0" -reqwest = "0.13.2" -rusqlite = "0.40.0" -rustls = { version = "0.23.37", features = ["aws-lc-rs"] } -rustls-pemfile = "2.2.0" -serde_json = "1.0.149" serde = { version = "1", features = ["derive"] } serde_yaml = "0.9" -sha2 = "0.11.0" -strum = "0.28.0" -strum_macros = "0.28.0" -sysinfo = "0.39.0" tokio = { version = "1.50.0", features = ["full"] } tokio-util = { version = "0.7", features = ["rt"] } -tokio-tungstenite = { version = "*", features = ["native-tls"] } -tungstenite = "*" -walkdir = "2.5.0" -warp = "*" -x448 = { version = "*" } -zip = "6.0.0" unicode-width = "0.2" [dev-dependencies] diff --git a/iota-cli/src/ipc_client.rs b/iota-cli/src/ipc_client.rs index ca912fc..45ca8fc 100644 --- a/iota-cli/src/ipc_client.rs +++ b/iota-cli/src/ipc_client.rs @@ -572,7 +572,7 @@ impl IpcClient { iota_ipc::IpcErrorCode::Timeout => "The daemon request timed out.", iota_ipc::IpcErrorCode::Cancelled => "The daemon request was cancelled.", iota_ipc::IpcErrorCode::Unauthorized => { - "The daemon rejected this operation as unauthorized." + "The daemon rejected this operation: the connected IPC account lacks the required role. Use the configured operator socket or ask an administrator to grant access." } iota_ipc::IpcErrorCode::InternalFailure => "The daemon reported an internal failure.", } diff --git a/iota-connection/src/message_common.rs b/iota-connection/src/message_common.rs index fff5bf6..742d9b6 100644 --- a/iota-connection/src/message_common.rs +++ b/iota-connection/src/message_common.rs @@ -2,7 +2,21 @@ use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use mtp::type_map::TypeMap; use std::time::{SystemTime, UNIX_EPOCH}; -pub use iota_util::mtp_compat::{CommunicationValueCompat, OptionalDataValueExt}; +pub use iota_util::mtp_compat::{MtpFieldError, OptionalDataValueExt, RequiredCommunicationFields}; + +pub trait CommunicationResponseExt { + fn with_request_id(self, request: &CommunicationValue) -> Self; +} + +impl CommunicationResponseExt for CommunicationValue { + fn with_request_id(mut self, request: &CommunicationValue) -> Self { + self = self.without_id(); + if let Some(id) = request.id() { + self = self.with_id(id); + } + self + } +} pub fn typed_container(items: Vec<(DataType, DataValue)>) -> DataValue { use mtp::type_map::{DataTypeId, TypeMap}; @@ -95,16 +109,48 @@ pub fn chat_secret_recipients(cv: &CommunicationValue) -> Option i64 { - SystemTime::now() + let millis = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() - .as_millis() as i64 + .as_millis(); + i64::try_from(millis).unwrap_or(i64::MAX) } pub fn error_response(request: &CommunicationValue, ty: CommunicationType) -> CommunicationValue { - let mut response = CommunicationValue::new(ty).with_id(request.id().unwrap_or_default()); + let mut response = CommunicationValue::new(ty).without_id(); + if let Some(id) = request.id() { + response = response.with_id(id); + } if let Some(sender) = request.sender() { response = response.with_receiver(sender); } response } + +#[cfg(test)] +mod tests { + use super::error_response; + use mtp::codec::{CommunicationType, CommunicationValue}; + + #[test] + fn error_response_preserves_an_absent_request_id() { + let request = CommunicationValue::new(CommunicationType::GetChats) + .without_id() + .with_sender(42); + let response = error_response(&request, CommunicationType::ErrorInvalidData); + + assert_eq!(response.id(), None); + assert_eq!(response.receiver(), Some(42)); + } + + #[test] + fn error_response_copies_an_existing_request_id() { + let request = CommunicationValue::new(CommunicationType::GetChats) + .with_id(7) + .with_sender(42); + let response = error_response(&request, CommunicationType::ErrorInvalidData); + + assert_eq!(response.id(), Some(7)); + assert_eq!(response.receiver(), Some(42)); + } +} diff --git a/iota-connection/src/message_handlers.rs b/iota-connection/src/message_handlers.rs index 4d7144f..4be17f6 100644 --- a/iota-connection/src/message_handlers.rs +++ b/iota-connection/src/message_handlers.rs @@ -10,15 +10,26 @@ use mtp::codec::{ use crate::relay::VerifiedRelayContext; +#[derive(Debug)] pub struct MessageMutation { pub sender_id: i64, pub partner_id: i64, pub send_time: i64, } -pub fn message_mutation(cv: &CommunicationValue) -> Result { - let sender_id = i64::try_from(cv.get_sender()) +fn required_sender_id(cv: &CommunicationValue) -> Result { + let sender = cv + .require_sender() .map_err(|_| error_response(cv, CommunicationType::ErrorInvalidData))?; + i64::try_from(sender).map_err(|_| error_response(cv, CommunicationType::ErrorInvalidData)) +} + +fn sender_wire_id(sender_id: i64) -> u64 { + u64::try_from(sender_id).expect("validated authenticated sender is non-negative") +} + +pub fn message_mutation(cv: &CommunicationValue) -> Result { + let sender_id = required_sender_id(cv)?; let partner_id = data_i64(cv, DataType::ChatPartnerId) .filter(|id| *id > 0) .ok_or_else(|| error_response(cv, CommunicationType::ErrorInvalidData))?; @@ -115,7 +126,7 @@ pub fn apply_verified_relay_content( match content.message_type.as_str() { "MessageSend" => { - let message = relay_string(&content.content, DataType::Content, &context.type_map) + let message = relay_string(&content.content, DataType::AppContent, &context.type_map) .ok_or_else(|| "Relay MessageSend is missing Content".to_string())?; let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map) .and_then(|value| i64::try_from(value).ok()) @@ -138,7 +149,7 @@ pub fn apply_verified_relay_content( Ok(()) } "MessageEdit" => { - let message = relay_string(&content.content, DataType::Content, &context.type_map) + let message = relay_string(&content.content, DataType::AppContent, &context.type_map) .ok_or_else(|| "Relay MessageEdit is missing Content".to_string())?; let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map) .and_then(|value| i64::try_from(value).ok()) @@ -206,7 +217,7 @@ pub fn handle_message_edit(cv: &CommunicationValue) -> CommunicationValue { Ok(mutation) => mutation, Err(response) => return response, }; - let Some(content) = cv.get_data(DataType::Content).as_str() else { + let Some(content) = cv.get_data(DataType::AppContent).as_str() else { return error_response(cv, CommunicationType::ErrorInvalidData); }; @@ -277,14 +288,17 @@ fn stored_message_fields( ) -> Vec<(DataType, DataValue)> { let mut fields = vec![ ( - DataType::MessageId, + DataType::AppMessageId, DataValue::SignedNumber(message.id as i128), ), ( DataType::SendTime, DataValue::SignedNumber(message.message_time as i128), ), - (DataType::Content, DataValue::Str(message.content.clone())), + ( + DataType::AppContent, + DataValue::Str(message.content.clone()), + ), ( DataType::MessageState, DataValue::Str(message.message_state.clone()), @@ -293,22 +307,22 @@ fn stored_message_fields( DataType::Height, DataValue::SignedNumber(message.height as i128), ), - ( - DataType::SenderId, - DataValue::UnsignedNumber(if message.sent_by_self { - storage_owner as u128 - } else { - partner_id as u128 - }), - ), ]; + let sender_id = if message.sent_by_self { + storage_owner + } else { + partner_id + }; + if let Ok(sender_id) = u128::try_from(sender_id) { + fields.push((DataType::SenderId, DataValue::UnsignedNumber(sender_id))); + } if message.edited { fields.push((DataType::Edited, DataValue::Bool(true))); } - if let Some(reply_to) = message.reply_to { + if let Some(reply_to) = message.reply_to.and_then(|id| u64::try_from(id).ok()) { fields.push(( DataType::ReplyId, - DataValue::UnsignedNumber(reply_to as u64 as u128), + DataValue::UnsignedNumber(u128::from(reply_to)), )); } if !message.reactions.is_empty() { @@ -345,7 +359,11 @@ pub fn handle_get_chat_secret(cv: &CommunicationValue) -> CommunicationValue { let Some(user_id) = data_string(cv, DataType::UserId) else { return error_response(cv, CommunicationType::ErrorInvalidData); }; - if user_id != cv.get_sender().to_string() { + let sender_id = match required_sender_id(cv) { + Ok(sender_id) => sender_id, + Err(response) => return response, + }; + if user_id != sender_id.to_string() { return error_response(cv, CommunicationType::ErrorNotFound); } let Some(chat_id) = data_string(cv, DataType::ChatId) else { @@ -358,8 +376,8 @@ pub fn handle_get_chat_secret(cv: &CommunicationValue) -> CommunicationValue { secret_id: data_string(cv, DataType::SecretId), }) { Ok(Some(record)) => CommunicationValue::new(CommunicationType::ChatSecretResponse) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()) + .with_request_id(cv) + .with_receiver(sender_wire_id(sender_id)) .add_typed_default(DataType::UserId, DataValue::Str(record.user_id)) .add_typed_default(DataType::ChatId, DataValue::Str(record.chat_id)) .add_typed_default(DataType::SecretId, DataValue::Str(record.secret_id)) @@ -380,7 +398,7 @@ pub fn handle_get_chat_secret(cv: &CommunicationValue) -> CommunicationValue { DataValue::Str(record.wrapping_scheme), ) .add_typed_default( - DataType::CreatedAt, + DataType::AppCreatedAt, DataValue::SignedNumber(record.created_at as i128), ) .add_typed_default( @@ -393,7 +411,10 @@ pub fn handle_get_chat_secret(cv: &CommunicationValue) -> CommunicationValue { } pub fn handle_create_app(cv: &CommunicationValue) -> CommunicationValue { - let sender_id = cv.get_sender() as i64; + let sender_id = match required_sender_id(cv) { + Ok(sender_id) => sender_id, + Err(response) => return response, + }; let app_identifier = cv .get_data(DataType::AppIdentifier) .as_str() @@ -415,12 +436,15 @@ pub fn handle_create_app(cv: &CommunicationValue) -> CommunicationValue { } CommunicationValue::new(CommunicationType::CreateApp) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64) + .with_request_id(cv) + .with_receiver(sender_wire_id(sender_id)) } pub fn handle_delete_app(cv: &CommunicationValue) -> CommunicationValue { - let sender_id = cv.get_sender() as i64; + let sender_id = match required_sender_id(cv) { + Ok(sender_id) => sender_id, + Err(response) => return response, + }; let app_identifier = cv .get_data(DataType::AppIdentifier) .as_str() @@ -437,8 +461,8 @@ pub fn handle_delete_app(cv: &CommunicationValue) -> CommunicationValue { } CommunicationValue::new(CommunicationType::DeleteApp) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64) + .with_request_id(cv) + .with_receiver(sender_wire_id(sender_id)) } fn contact_value( @@ -495,8 +519,8 @@ fn contact_ids_value(ids: impl IntoIterator) -> DataValue { #[cfg(test)] mod presence_tests { - use super::contact_ids_value; - use mtp::codec::DataValue; + use super::{contact_ids_value, handle_get_chats, message_mutation}; + use mtp::codec::{CommunicationType, CommunicationValue, DataValue}; #[test] fn contact_snapshot_is_sorted_and_deduplicated() { @@ -509,6 +533,26 @@ mod presence_tests { ]) ); } + + #[test] + fn message_mutation_rejects_a_missing_authenticated_sender() { + let request = CommunicationValue::new(CommunicationType::MessageEdit).with_id(11); + let response = message_mutation(&request).expect_err("missing sender must be rejected"); + + assert!(response.is_type(CommunicationType::ErrorInvalidData)); + assert_eq!(response.id(), Some(11)); + assert_eq!(response.receiver(), None); + } + + #[test] + fn read_handler_rejects_a_missing_authenticated_sender() { + let request = CommunicationValue::new(CommunicationType::GetChats).with_id(12); + let response = handle_get_chats(&request); + + assert!(response.is_type(CommunicationType::ErrorInvalidData)); + assert_eq!(response.id(), Some(12)); + assert_eq!(response.receiver(), None); + } } fn sync_error(cv: &CommunicationValue) -> CommunicationValue { @@ -523,7 +567,7 @@ fn sync_error(cv: &CommunicationValue) -> CommunicationValue { /// The sender is authenticated by MTP; a UserId embedded by a client is never trusted here. pub fn handle_client_connected(cv: &CommunicationValue) -> CommunicationValue { use iota_storage::util::sync::{self, CACHE_SCHEMA_VERSION}; - let user_id = match i64::try_from(cv.get_sender()) { + let user_id = match required_sender_id(cv) { Ok(id) if id > 0 => id, _ => return sync_error(cv), }; @@ -578,8 +622,8 @@ pub fn handle_client_connected(cv: &CommunicationValue) -> CommunicationValue { .map(|message| stored_message_value(message, user_id, message.external_user)) .collect(); CommunicationValue::new(CommunicationType::ClientStateSync) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()) + .with_request_id(cv) + .with_receiver(sender_wire_id(user_id)) .add_typed_default( DataType::SessionId, DataValue::SignedNumber(session_id as i128), @@ -603,6 +647,10 @@ pub fn handle_client_connected(cv: &CommunicationValue) -> CommunicationValue { ), ) .add_typed_default(DataType::Messages, DataValue::Array(message_values)) + .add_typed_default( + DataType::Communities, + DataValue::Array(community_values(user_id)), + ) .add_typed_default( DataType::DeletedMessageIds, DataValue::Array( @@ -627,7 +675,7 @@ pub fn handle_client_connected(cv: &CommunicationValue) -> CommunicationValue { pub fn handle_client_state_ack(cv: &CommunicationValue) -> CommunicationValue { use iota_storage::util::sync::{self, CACHE_SCHEMA_VERSION}; - let user_id = match i64::try_from(cv.get_sender()) { + let user_id = match required_sender_id(cv) { Ok(id) if id > 0 => id, _ => return sync_error(cv), }; @@ -654,46 +702,50 @@ pub fn handle_client_state_ack(cv: &CommunicationValue) -> CommunicationValue { } pub fn handle_message_state(cv: &CommunicationValue) { - let sender_id = &cv.get_sender(); - let receiver_id = match cv.get_data(DataType::ChatPartnerId).as_number() { - Some(id) => id, + let sender_id = match required_sender_id(cv) { + Ok(sender_id) => sender_id, + Err(_) => return, + }; + let receiver_id = match data_i64(cv, DataType::ChatPartnerId) { + Some(id) if id > 0 => id, _ => return, }; - let timestamp_i64 = if let Some(n) = cv.get_data(DataType::SendTime).as_number() { - n as i64 - } else if let Some(s) = cv.get_data(DataType::SendTime).as_str() { - s.parse::().unwrap_or_else(|_| now_millis_i64()) - } else { - now_millis_i64() - }; + let timestamp_i64 = data_i64(cv, DataType::SendTime).unwrap_or_else(now_millis_i64); let _ = chat_files::change_message_state( timestamp_i64, - receiver_id as i64, - *sender_id as i64, + receiver_id, + sender_id, MessageState::from_str(cv.get_data(DataType::MessageState).as_str().unwrap_or("")), ); } pub fn handle_messages_get(cv: &CommunicationValue) -> CommunicationValue { - let my_id = cv.get_sender(); - let partner_id = cv.get_data(DataType::UserId).as_number().unwrap_or(0); - let offset = cv.get_data(DataType::Offset).as_number().unwrap_or(0); - let amount = cv.get_data(DataType::Amount).as_number().unwrap_or(0); - let messages = chat_files::get_messages( - my_id as i64, - partner_id as i64, - offset as i64, - amount as i64, - ); + let my_id = match cv.require_sender() { + Ok(my_id) => my_id, + Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData), + }; + let Ok(my_id_i64) = i64::try_from(my_id) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; + let Some(partner_id) = data_i64(cv, DataType::UserId).filter(|id| *id > 0) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; + let Some(offset) = data_i64(cv, DataType::Offset).filter(|offset| *offset >= 0) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; + let Some(amount) = data_i64(cv, DataType::Amount).filter(|amount| *amount > 0) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; + let messages = chat_files::get_messages(my_id_i64, partner_id, offset, amount); let mut msg_array: Vec = Vec::new(); for m in &messages { - msg_array.push(stored_message_value(m, my_id as i64, partner_id as i64)); + msg_array.push(stored_message_value(m, my_id_i64, partner_id)); } CommunicationValue::new(CommunicationType::MessagesGet) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default(DataType::Messages, DataValue::Array(msg_array)) } @@ -703,7 +755,10 @@ pub fn handle_message_get(cv: &CommunicationValue) -> CommunicationValue { return error_response(cv, CommunicationType::ErrorInvalidData); }; let partner_id = data_i64(cv, DataType::ChatPartnerId); - let owner = cv.get_sender() as i64; + let owner = match required_sender_id(cv) { + Ok(owner) => owner, + Err(response) => return response, + }; let message = match chat_files::get_message(owner, send_time, partner_id) { Ok(Some(message)) => message, @@ -712,8 +767,8 @@ pub fn handle_message_get(cv: &CommunicationValue) -> CommunicationValue { }; let mut response = CommunicationValue::new(CommunicationType::MessageGet) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()); + .with_request_id(cv) + .with_receiver(u64::try_from(owner).expect("authenticated sender is non-negative")); for (data_type, value) in stored_message_fields(&message, owner, message.external_user) { response = response.add_typed_default(data_type, value); } @@ -721,8 +776,14 @@ pub fn handle_message_get(cv: &CommunicationValue) -> CommunicationValue { } pub fn handle_get_chats(cv: &CommunicationValue) -> CommunicationValue { - let user_id = cv.get_sender(); - let users = chats_util::get_users(user_id as i64); + let user_id = match cv.require_sender() { + Ok(user_id) => user_id, + Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData), + }; + let Ok(user_id_i64) = i64::try_from(user_id) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; + let users = chats_util::get_users(user_id_i64); let mut user_array = Vec::new(); for user in users { let mut container = Vec::new(); @@ -739,27 +800,28 @@ pub fn handle_get_chats(cv: &CommunicationValue) -> CommunicationValue { user_array.push(typed_container(container)); } CommunicationValue::new(CommunicationType::GetChats) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(user_id) .add_typed_default(DataType::UserIds, DataValue::Array(user_array)) } pub fn handle_add_conversation(cv: &CommunicationValue) -> CommunicationValue { - let user_id = cv.get_sender(); + let user_id = match cv.require_sender() { + Ok(user_id) => user_id, + Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData), + }; + let Ok(user_id_i64) = i64::try_from(user_id) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; let session_id = match data_i64(cv, DataType::SessionId) { Some(id) if id > 0 => id, _ => return sync_error(cv), }; - let other_id = match cv.get_data(DataType::ChatPartnerId).as_number() { - Some(n) => n as i64, - None => cv - .get_data(DataType::ChatPartnerId) - .as_str() - .unwrap_or("0") - .parse() - .unwrap_or(0), + let other_id = match data_i64(cv, DataType::ChatPartnerId) { + Some(id) if id > 0 => id, + _ => return error_response(cv, CommunicationType::ErrorInvalidData), }; - let mut contact = get_user(user_id as i64, other_id) + let mut contact = get_user(user_id_i64, other_id) .unwrap_or(iota_storage::users::contact::Contact::new(other_id)); if let Some(name) = cv.get_data(DataType::ChatPartnerName).as_str() { @@ -767,75 +829,97 @@ pub fn handle_add_conversation(cv: &CommunicationValue) -> CommunicationValue { } contact.set_last_message_at(now_millis_i64()); - mod_user(user_id as i64, &contact); + mod_user(user_id_i64, &contact); CommunicationValue::new(CommunicationType::AddConversation) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(user_id) .add_typed_default( DataType::SessionId, DataValue::SignedNumber(session_id as i128), ) - .add_typed_default(DataType::UserIds, current_contact_ids(user_id as i64)) + .add_typed_default(DataType::UserIds, current_contact_ids(user_id_i64)) } pub fn handle_add_community(cv: &CommunicationValue) -> CommunicationValue { + let sender_id = match required_sender_id(cv) { + Ok(sender_id) => sender_id, + Err(response) => return response, + }; + let Some(address) = cv.get_data(DataType::CommunityAddress).as_str() else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; + let Some(title) = cv.get_data(DataType::CommunityTitle).as_str() else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; + let Some(position) = cv.get_data(DataType::Position).as_str() else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; CommunitiesUtil::add_community( - cv.get_sender() as i64, - cv.get_data(DataType::CommunityAddress) - .as_str() - .unwrap() - .to_string(), - cv.get_data(DataType::CommunityTitle) - .as_str() - .unwrap() - .to_string(), - cv.get_data(DataType::Position) - .as_str() - .unwrap() - .to_string(), + sender_id, + address.to_string(), + title.to_string(), + position.to_string(), ); CommunicationValue::new(CommunicationType::AddCommunity) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()) + .with_request_id(cv) + .with_receiver(sender_wire_id(sender_id)) } pub fn handle_get_communities(cv: &CommunicationValue) -> CommunicationValue { - let mut comm_array = Vec::new(); - for c in CommunitiesUtil::get_communities(cv.get_sender() as i64) { - let mut container: Vec<(DataType, DataValue)> = Vec::new(); - container.push(( - DataType::CommunityAddress, - DataValue::Str(c.address.clone()), - )); - container.push((DataType::CommunityTitle, DataValue::Str(c.title.clone()))); - container.push((DataType::Position, DataValue::Str(c.position.clone()))); - comm_array.push(typed_container(container)); - } - + let sender_id = match required_sender_id(cv) { + Ok(sender_id) => sender_id, + Err(response) => return response, + }; CommunicationValue::new(CommunicationType::GetCommunities) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()) - .add_typed_default(DataType::Communities, DataValue::Array(comm_array)) + .with_request_id(cv) + .with_receiver(sender_wire_id(sender_id)) + .add_typed_default( + DataType::Communities, + DataValue::Array(community_values(sender_id)), + ) +} + +fn community_values(storage_owner: i64) -> Vec { + CommunitiesUtil::get_communities(storage_owner) + .into_iter() + .map(|community| { + typed_container(vec![ + ( + DataType::CommunityAddress, + DataValue::Str(community.address), + ), + (DataType::CommunityTitle, DataValue::Str(community.title)), + (DataType::Position, DataValue::Str(community.position)), + ]) + }) + .collect() } pub fn handle_remove_community(cv: &CommunicationValue) -> CommunicationValue { - CommunitiesUtil::remove_community( - cv.get_sender() as i64, - cv.get_data(DataType::CommunityAddress) - .as_str() - .unwrap() - .to_string(), - ); + let sender_id = match required_sender_id(cv) { + Ok(sender_id) => sender_id, + Err(response) => return response, + }; + let Some(address) = cv.get_data(DataType::CommunityAddress).as_str() else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; + CommunitiesUtil::remove_community(sender_id, address.to_string()); CommunicationValue::new(CommunicationType::RemoveCommunity) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()) + .with_request_id(cv) + .with_receiver(sender_wire_id(sender_id)) } pub fn handle_global_settings_save(cv: &CommunicationValue) -> CommunicationValue { - let my_id = cv.get_sender(); + let my_id = match cv.require_sender() { + Ok(my_id) => my_id, + Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData), + }; + let Ok(my_id_i64) = i64::try_from(my_id) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; let Some(settings_value) = cv.get_data(DataType::Payload).as_str() else { return CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default( DataType::Message, @@ -843,13 +927,13 @@ pub fn handle_global_settings_save(cv: &CommunicationValue) -> CommunicationValu ); }; - if settings::save_global(my_id as i64, settings_value).is_err() { + if settings::save_global(my_id_i64, settings_value).is_err() { return error_response(cv, CommunicationType::ErrorInvalidData); } let mut response = CommunicationValue::new(CommunicationType::GlobalSettingsSave) .with_receiver(my_id) - .with_id(cv.get_id()); + .with_request_id(cv); if let Some(session_id) = cv.get_data(DataType::SessionId).as_number() { response = response.add_typed_default( @@ -862,13 +946,19 @@ pub fn handle_global_settings_save(cv: &CommunicationValue) -> CommunicationValu } pub fn handle_global_settings_load(cv: &CommunicationValue) -> CommunicationValue { - let my_id = cv.get_sender(); - let Ok(settings_value) = settings::load_global(my_id as i64) else { + let my_id = match cv.require_sender() { + Ok(my_id) => my_id, + Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData), + }; + let Ok(my_id_i64) = i64::try_from(my_id) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; + let Ok(settings_value) = settings::load_global(my_id_i64) else { return error_response(cv, CommunicationType::ErrorInvalidData); }; let Some(settings_value_str) = settings_value else { let mut response = CommunicationValue::new(CommunicationType::ErrorNotFound) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default( DataType::Path, @@ -886,7 +976,7 @@ pub fn handle_global_settings_load(cv: &CommunicationValue) -> CommunicationValu }; let mut response = CommunicationValue::new(CommunicationType::GlobalSettingsLoad) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default(DataType::Payload, DataValue::Str(settings_value_str)); @@ -904,10 +994,16 @@ pub fn handle_settings_save( cv: &CommunicationValue, _expected_session_id: i128, ) -> CommunicationValue { - let my_id = cv.get_sender(); + let my_id = match cv.require_sender() { + Ok(my_id) => my_id, + Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData), + }; + let Ok(my_id_i64) = i64::try_from(my_id) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; let Some(session_id) = cv.get_data(DataType::SessionId).as_number() else { return CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default( DataType::Message, @@ -916,7 +1012,7 @@ pub fn handle_settings_save( }; if session_id == 0 || session_id > 1_000_000 { return CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default( DataType::Message, @@ -929,7 +1025,7 @@ pub fn handle_settings_save( }; let Some(settings_name) = cv.get_data(DataType::SettingsName).as_str() else { return CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default( DataType::Message, @@ -942,7 +1038,7 @@ pub fn handle_settings_save( }; let Some(settings_value) = cv.get_data(DataType::Payload).as_str() else { return CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default( DataType::Message, @@ -960,7 +1056,7 @@ pub fn handle_settings_save( || settings_name.contains("..") { return CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default( DataType::Message, @@ -975,21 +1071,17 @@ pub fn handle_settings_save( DataValue::SignedNumber(session_id as i128), ); } + let Ok(session_id_i64) = i64::try_from(session_id) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; - if settings::save( - my_id as i64, - session_id as i64, - settings_name, - settings_value, - ) - .is_err() - { + if settings::save(my_id_i64, session_id_i64, settings_name, settings_value).is_err() { return error_response(cv, CommunicationType::ErrorInvalidData); } CommunicationValue::new(CommunicationType::SettingsSave) .with_receiver(my_id) - .with_id(cv.get_id()) + .with_request_id(cv) .add_typed_default( DataType::SettingsName, DataValue::Str(settings_name.to_string()), @@ -1004,10 +1096,16 @@ pub fn handle_settings_load( cv: &CommunicationValue, _expected_session_id: i128, ) -> CommunicationValue { - let my_id = cv.get_sender(); + let my_id = match cv.require_sender() { + Ok(my_id) => my_id, + Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData), + }; + let Ok(my_id_i64) = i64::try_from(my_id) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; let Some(session_id) = cv.get_data(DataType::SessionId).as_number() else { return CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default( DataType::Message, @@ -1016,7 +1114,7 @@ pub fn handle_settings_load( }; if session_id == 0 || session_id > 1_000_000 { return CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default( DataType::Message, @@ -1027,9 +1125,12 @@ pub fn handle_settings_load( DataValue::SignedNumber(session_id as i128), ); } + let Ok(session_id_i64) = i64::try_from(session_id) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; let Some(settings_name) = cv.get_data(DataType::SettingsName).as_str() else { return CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default( DataType::Message, @@ -1047,7 +1148,7 @@ pub fn handle_settings_load( || settings_name.contains("..") { return CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default( DataType::Message, @@ -1063,12 +1164,12 @@ pub fn handle_settings_load( ); } - let Ok(settings_value) = settings::load(my_id as i64, session_id as i64, settings_name) else { + let Ok(settings_value) = settings::load(my_id_i64, session_id_i64, settings_name) else { return error_response(cv, CommunicationType::ErrorInvalidData); }; let Some(settings_value_str) = settings_value else { return CommunicationValue::new(CommunicationType::ErrorNotFound) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default( DataType::SettingsName, @@ -1081,7 +1182,7 @@ pub fn handle_settings_load( }; CommunicationValue::new(CommunicationType::SettingsLoad) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default(DataType::Payload, DataValue::Str(settings_value_str)) .add_typed_default( @@ -1098,10 +1199,16 @@ pub fn handle_settings_list( cv: &CommunicationValue, _expected_session_id: i128, ) -> CommunicationValue { - let my_id = cv.get_sender(); + let my_id = match cv.require_sender() { + Ok(my_id) => my_id, + Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData), + }; + let Ok(my_id_i64) = i64::try_from(my_id) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; let Some(session_id) = cv.get_data(DataType::SessionId).as_number() else { return CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default( DataType::Message, @@ -1110,7 +1217,7 @@ pub fn handle_settings_list( }; if session_id == 0 || session_id > 1_000_000 { return CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default( DataType::Message, @@ -1121,13 +1228,16 @@ pub fn handle_settings_list( DataValue::SignedNumber(session_id as i128), ); } + let Ok(session_id_i64) = i64::try_from(session_id) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; - let Ok(settings) = settings::list(my_id as i64, session_id as i64) else { + let Ok(settings) = settings::list(my_id_i64, session_id_i64) else { return error_response(cv, CommunicationType::ErrorInvalidData); }; let settings_json = settings.into_iter().map(DataValue::Str).collect(); CommunicationValue::new(CommunicationType::SettingsList) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(my_id) .add_typed_default(DataType::Settings, DataValue::Array(settings_json)) .add_typed_default( diff --git a/iota-connection/src/relay.rs b/iota-connection/src/relay.rs index 298a0df..13a52c5 100644 --- a/iota-connection/src/relay.rs +++ b/iota-connection/src/relay.rs @@ -1,8 +1,9 @@ use iota_util::route_target::RouteTarget; use mtp::codec::{ - CommunicationValue, ProtectionPolicy, RelayError, SignaturePolicy, TypeMap, + CommunicationValue, ProtectionPolicy, RelayError, RelayOpenOptions, SignaturePolicy, TypeMap, VerifiedRelayContent, VerifiedRelayMetadata, forward_relay_frame, - open_relay_content_with_keyrings, open_relay_metadata_with, relay_metadata_claimed_signer_id, + open_relay_content_with_limits_without_replay, open_relay_metadata_with_without_replay, + relay_metadata_claimed_signer_id_with_options, }; use mtp::crypto::{Keyring, PublicKeyBundle}; use std::fmt; @@ -146,7 +147,13 @@ where return Err(RelayValidationError::OuterSenderNotAllowed); } - let claimed_signer = relay_metadata_claimed_signer_id(frame, &[keyring])?; + let open_options = RelayOpenOptions::new(RELAY_PROTECTION_POLICY); + let claimed_signer = relay_metadata_claimed_signer_id_with_options( + frame, + &[keyring], + open_options.decode_limits, + open_options.protected_limits, + )?; let signing_keys = resolve_signing_keys(claimed_signer).await?; if signing_keys.is_empty() { return Err(RelayValidationError::MissingSigningKeys(claimed_signer)); @@ -157,13 +164,12 @@ where .type_map() .cloned() .ok_or(RelayValidationError::MissingTypeMap)?; - let metadata = open_relay_metadata_with( + let metadata = open_relay_metadata_with_without_replay( frame, &[keyring], Some(claimed_signer), move |signer_id| (signer_id == claimed_signer).then(|| resolver_keys.clone()), - RELAY_PROTECTION_POLICY, - None, + open_options, )?; let context = VerifiedRelayContext { @@ -186,12 +192,17 @@ pub fn open_verified_relay_content( keyrings: &[&Keyring], expected_recipient_id: u64, ) -> Result { - Ok(open_relay_content_with_keyrings( + Ok(open_relay_content_with_limits_without_replay( &relay.metadata, keyrings, &relay.signing_keys, Some(expected_recipient_id), - RELAY_PROTECTION_POLICY, + RelayOpenOptions { + policy: RELAY_PROTECTION_POLICY, + decode_limits: relay.metadata.decode_limits(), + encode_limits: relay.metadata.encode_limits(), + protected_limits: relay.metadata.protected_limits(), + }, )?) } diff --git a/iota-core/Cargo.toml b/iota-core/Cargo.toml index 03250d9..f28cc45 100644 --- a/iota-core/Cargo.toml +++ b/iota-core/Cargo.toml @@ -13,17 +13,6 @@ iota-terms = { path = "../iota-terms" } iota-updater = { path = "../iota-updater" } iota-util = { path = "../iota-util" } iota-paths = { path = "../iota-paths" } -omikron-connector = { path = "../omikron-connector" } web-server = { path = "../web-server" } -web-ui = { path = "../web-ui" } - -mtp = { git = "https://git.methanium.net/Methanium/mtp.git" } - -dashmap = "6.1.0" -json = "*" -once_cell = "1.21.3" pnet = "0.35.0" -ratatui = "0.30.0" -reqwest = "0.13.2" tokio = { version = "1.50.0", features = ["full"] } -tokio-util = { version = "0.7", features = ["rt"] } diff --git a/iota-daemon-lib/Cargo.toml b/iota-daemon-lib/Cargo.toml index 4eb0b7c..7512429 100644 --- a/iota-daemon-lib/Cargo.toml +++ b/iota-daemon-lib/Cargo.toml @@ -13,10 +13,10 @@ iota-updater = { path = "../iota-updater" } iota-util = { path = "../iota-util" } omikron-connector = { path = "../omikron-connector" } mtp = { git = "https://git.methanium.net/Methanium/mtp.git" } -dashmap = "6.1.0" libc = "0.2" sysinfo = "0.39.0" serde_yaml = "0.9" +serde_json = "1" tokio = { version = "1.50.0", features = ["full"] } tokio-util = { version = "0.7", features = ["rt"] } uuid = { version = "*", features = ["v4"] } diff --git a/iota-daemon-lib/src/command_router.rs b/iota-daemon-lib/src/command_router.rs index 7f9f1c7..a6a2718 100644 --- a/iota-daemon-lib/src/command_router.rs +++ b/iota-daemon-lib/src/command_router.rs @@ -1,10 +1,10 @@ use crate::log_buffer::LogBuffer; use crate::{DaemonRuntime, DaemonServices}; use iota_ipc::{ - CommunitySummary, ComponentStatusResponse, ConfigResponse, ExitIntent, IpcErrorCode, - LocalRequest, LogEntriesResponse, OmikronStatusResponse, ResponseEnvelope, ResponsePayload, - ResponseResult, StatusResponse, TaskSummary, UpdateStatusResponse, UserDetailResponse, - UserSummary, + CommunitySummary, ComponentStatusResponse, ConfigResponse, DaemonMessage, ExitIntent, + IpcErrorCode, LocalRequest, LogEntriesResponse, LogEntry, MAX_MESSAGE_SIZE, + OmikronStatusResponse, ResponseEnvelope, ResponsePayload, ResponseResult, StatusResponse, + TaskSummary, UpdateStatusResponse, UserDetailResponse, UserSummary, }; use iota_logger::{log, log_command}; use iota_storage::users::user_manager; @@ -15,6 +15,37 @@ use std::time::Duration; use crate::daemon_state::{ShutdownReason, StartupPhase}; +pub use iota_ipc::IpcRole; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PeerContext { + pub pid: i32, + pub uid: u32, + pub role: IpcRole, +} + +const MAX_LOG_ENTRIES_PER_RESPONSE: usize = 512; + +fn bounded_log_entries(mut entries: Vec) -> Vec { + entries.truncate(MAX_LOG_ENTRIES_PER_RESPONSE); + while !entries.is_empty() { + let response = DaemonMessage::Response(ResponseEnvelope { + request_id: u64::MAX, + result: ResponseResult::Ok(ResponsePayload::LogEntries(LogEntriesResponse { + entries: entries.clone(), + })), + }); + let fits = serde_json::to_vec(&response) + .map(|encoded| encoded.len() <= MAX_MESSAGE_SIZE) + .unwrap_or(false); + if fits { + return entries; + } + entries.remove(0); + } + entries +} + #[derive(Clone)] pub struct CommandRouter { runtime: Arc, @@ -35,8 +66,33 @@ impl CommandRouter { } } - pub async fn route(&self, request_id: u64, request: LocalRequest) -> ResponseEnvelope { - log_command!("{:?}", request); + pub async fn route( + &self, + peer: &PeerContext, + request_id: u64, + request: LocalRequest, + ) -> ResponseEnvelope { + if !peer.role.allows(request.required_role()) { + log!( + "IPC authorization denied: pid={}, uid={}, role={:?}, request={:?}", + peer.pid, + peer.uid, + peer.role, + request + ); + return ResponseEnvelope { + request_id, + result: ResponseResult::Error(IpcErrorCode::Unauthorized), + }; + } + + log_command!( + "pid={} uid={} role={:?} request={:?}", + peer.pid, + peer.uid, + peer.role, + request + ); let result = self.execute(request).await; ResponseEnvelope { request_id, result } } @@ -284,7 +340,7 @@ impl CommandRouter { { return ResponseResult::Error(IpcErrorCode::Conflict); } - self.runtime.shutdown(match intent { + self.runtime.request_shutdown(match intent { ExitIntent::Stop => ShutdownReason::Stop, ExitIntent::Restart => ShutdownReason::Restart, }); @@ -298,13 +354,13 @@ impl CommandRouter { }, )), LocalRequest::RestartDaemon => { - self.runtime.shutdown(ShutdownReason::Restart); + self.runtime.request_shutdown(ShutdownReason::Restart); ResponseResult::Ok(ResponsePayload::Acknowledged { message: "Daemon restart requested".into(), }) } LocalRequest::StopDaemon => { - self.runtime.shutdown(ShutdownReason::Stop); + self.runtime.request_shutdown(ShutdownReason::Stop); ResponseResult::Ok(ResponsePayload::Acknowledged { message: "Daemon shutdown requested".into(), }) @@ -378,7 +434,7 @@ impl CommandRouter { LocalRequest::ImportUser { .. } => ResponseResult::Error(IpcErrorCode::InvalidRequest), LocalRequest::GetLogs { limit } => { let entries = if let Ok(buf) = self.log_buffer.lock() { - buf.recent(limit) + bounded_log_entries(buf.recent(limit.min(MAX_LOG_ENTRIES_PER_RESPONSE))) } else { Vec::new() }; @@ -393,11 +449,10 @@ impl CommandRouter { Err(_e) => ResponseResult::Error(IpcErrorCode::InternalFailure), }, LocalRequest::ListCommunities => { - let iota_id = config_util::CONFIG - .load() - .iota_id - .map(|id| id as i64) - .unwrap_or(0); + let iota_id = config_util::CONFIG.load().iota_id; + let Ok(iota_id) = iota_id.map(i64::try_from).unwrap_or(Ok(0)) else { + return ResponseResult::Ok(ResponsePayload::Communities(Vec::new())); + }; let stored = iota_storage::util::communities_util::CommunitiesUtil::get_communities(iota_id); let summaries: Vec = stored @@ -412,3 +467,77 @@ impl CommandRouter { } } } + +#[cfg(test)] +mod tests { + use super::{IpcRole, LocalRequest, bounded_log_entries}; + use iota_ipc::{ExitIntent, LogEntry, SecretString}; + + #[test] + fn every_request_has_an_explicit_role_policy() { + let requests = [ + LocalRequest::GetStatus, + LocalRequest::ListTasks, + LocalRequest::ListUsers, + LocalRequest::CreateUser { + username: "alice".into(), + }, + LocalRequest::AttachUserFromTu { + credential: SecretString("credential".into()), + }, + LocalRequest::PurgeUserData { user_id: 1 }, + LocalRequest::ReleaseUser { user_id: 1 }, + LocalRequest::CompleteDeleteUser { + user_id: 1, + credential: None, + }, + LocalRequest::RemoveUser { user_id: 1 }, + LocalRequest::ReconnectOmikron, + LocalRequest::RotateIotaIdentity, + LocalRequest::RequestProcessExit { + intent: ExitIntent::Stop, + }, + LocalRequest::GetDaemonStatus, + LocalRequest::RestartDaemon, + LocalRequest::StopDaemon, + LocalRequest::GetConfig, + LocalRequest::SetConfig { + key: "port".into(), + value: "1984".into(), + }, + LocalRequest::ReloadConfig, + LocalRequest::GetOmikronStatus, + LocalRequest::ListComponents, + LocalRequest::GetUser { user_id: 1 }, + LocalRequest::ImportUser { + username: "alice".into(), + }, + LocalRequest::GetLogs { limit: 10 }, + LocalRequest::CheckUpdate, + LocalRequest::ListCommunities, + ]; + + assert_eq!(requests.len(), 25); + for request in requests { + let required = request.required_role(); + assert!(IpcRole::Admin.allows(required)); + assert_eq!( + IpcRole::Operate.allows(required), + required != IpcRole::Admin + ); + assert_eq!(IpcRole::Read.allows(required), required == IpcRole::Read); + } + } + + #[test] + fn log_responses_drop_entries_that_cannot_fit_one_ipc_frame() { + let entries = vec![LogEntry { + timestamp_ms: 0, + sender: "test".into(), + message: "x".repeat(2 * 1024 * 1024), + is_error: false, + }]; + + assert!(bounded_log_entries(entries).is_empty()); + } +} diff --git a/iota-daemon-lib/src/daemon_state.rs b/iota-daemon-lib/src/daemon_state.rs index 4c7347c..634cbe2 100644 --- a/iota-daemon-lib/src/daemon_state.rs +++ b/iota-daemon-lib/src/daemon_state.rs @@ -55,7 +55,7 @@ impl From for iota_ipc::StartupPhase { /* This wrapper exposes daemon state as IPC-safe snapshots while preserving a * single owned state instance for all daemon subsystems. The cancellation token - * is the single lifecycle signal — all subsystems check it instead of a + * is the single lifecycle signal, and all subsystems check it instead of a * separate boolean. */ pub struct DaemonRuntime { pub state: Arc, @@ -131,12 +131,20 @@ impl DaemonRuntime { } pub fn shutdown(&self, reason: ShutdownReason) { + self.request_shutdown(reason); + self.begin_shutdown(); + } + + pub fn request_shutdown(&self, reason: ShutdownReason) { if self.shutdown_tx.borrow().is_none() { let _ = self.shutdown_tx.send(Some(reason)); - self.cancellation.cancel(); } } + pub fn begin_shutdown(&self) { + self.cancellation.cancel(); + } + pub fn shutdown_reason(&self) -> Option { self.shutdown_tx.borrow().clone() } diff --git a/iota-daemon-lib/src/ipc_server.rs b/iota-daemon-lib/src/ipc_server.rs index 97b5dd6..a472348 100644 --- a/iota-daemon-lib/src/ipc_server.rs +++ b/iota-daemon-lib/src/ipc_server.rs @@ -1,23 +1,26 @@ use crate::deployment::from_environment; use crate::log_buffer::LogBuffer; -use crate::{CommandRouter, DaemonRuntime, DaemonServices}; +use crate::{CommandRouter, DaemonRuntime, DaemonServices, IpcRole, PeerContext}; use iota_ipc::{ ClientMessage, DaemonMessage, HelloAck, MIN_PROTOCOL_VERSION, PROTOCOL_VERSION, read_msg, write_msg, }; use iota_logger::log; +use iota_storage::util::config_util; use std::io::Result; -use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt}; +use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt, PermissionsExt}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::{env, fs::File, os::fd::FromRawFd, os::unix::net::UnixListener as StdUnixListener}; +use tokio::io::AsyncWriteExt; use tokio::net::{UnixListener, UnixStream}; -use tokio::sync::{broadcast, mpsc, watch}; +use tokio::sync::{Semaphore, broadcast, mpsc, watch}; use tokio::time::timeout; use uuid::Uuid; /// Per-client outbound queue capacity. const CLIENT_CHANNEL_SIZE: usize = 256; +const MAX_CONFIGURED_IPC_CLIENTS: usize = 4096; /// Maximum handshake retries before giving up. const MAX_HANDSHAKE_RETRIES: u32 = 1; @@ -36,6 +39,20 @@ struct ClientSubscription { metric_interval_ms: u64, } +enum WriterCommand { + Message(DaemonMessage), + Flush { + complete: tokio::sync::oneshot::Sender<()>, + }, +} + +fn configured_client_limit() -> usize { + config_util::CONFIG + .load() + .max_ipc_clients + .clamp(1, MAX_CONFIGURED_IPC_CLIENTS) +} + pub struct IpcServer { listener: UnixListener, runtime: Arc, @@ -45,6 +62,7 @@ pub struct IpcServer { state_rx: watch::Receiver, instance_id: String, _instance_lock: File, + client_limit: Arc, } impl IpcServer { @@ -96,11 +114,18 @@ impl IpcServer { } remove_stale_socket(&path).await?; let listener = UnixListener::bind(&path)?; - let _ = tokio::fs::set_permissions( - &path, - std::os::unix::fs::PermissionsExt::from_mode(0o600), - ) - .await; + if let Err(error) = + tokio::fs::set_permissions(&path, PermissionsExt::from_mode(0o600)).await + { + drop(listener); + let _ = tokio::fs::remove_file(&path).await; + return Err(error); + } + if let Err(error) = validate_manual_socket(&path).await { + drop(listener); + let _ = tokio::fs::remove_file(&path).await; + return Err(error); + } return Ok(Self { listener, runtime, @@ -110,6 +135,7 @@ impl IpcServer { state_rx, instance_id: Uuid::new_v4().to_string(), _instance_lock: lock, + client_limit: Arc::new(Semaphore::new(configured_client_limit())), }); } }; @@ -122,12 +148,21 @@ impl IpcServer { state_rx, instance_id: Uuid::new_v4().to_string(), _instance_lock: File::options().read(true).open("/dev/null")?, + client_limit: Arc::new(Semaphore::new(configured_client_limit())), }) } pub async fn serve(self) -> Result<()> { loop { let (stream, _addr) = self.listener.accept().await?; + let permit = match self.client_limit.clone().try_acquire_owned() { + Ok(permit) => permit, + Err(_) => { + eprintln!("IPC connection rejected: active client limit reached"); + drop(stream); + continue; + } + }; eprintln!("IPC client accepted"); let runtime = self.runtime.clone(); let services = self.services.clone(); @@ -136,6 +171,7 @@ impl IpcServer { let state_rx = self.state_rx.clone(); let instance_id = self.instance_id.clone(); tokio::spawn(async move { + let _permit = permit; if let Err(error) = handle_client( stream, runtime, @@ -232,6 +268,33 @@ async fn remove_stale_socket(path: &Path) -> Result<()> { } } +async fn validate_manual_socket(path: &Path) -> Result<()> { + let metadata = tokio::fs::symlink_metadata(path).await?; + if metadata.file_type().is_symlink() || !metadata.file_type().is_socket() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "bound IPC path is no longer a Unix socket", + )); + } + + let mode = metadata.permissions().mode() & 0o777; + if mode != 0o600 { + return Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + format!("IPC socket has unexpected mode {mode:o}"), + )); + } + + let expected_uid = unsafe { libc::geteuid() } as u32; + if metadata.uid() != expected_uid { + return Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "IPC socket ownership changed after bind", + )); + } + Ok(()) +} + #[derive(Clone, Debug)] struct PeerIdentity { pid: i32, @@ -274,6 +337,14 @@ fn peer_credentials(stream: &UnixStream) -> Result { } } +fn role_for_peer(_peer: &PeerIdentity) -> IpcRole { + // This deployment has one IPC listener. Its Unix socket permissions are + // the admission boundary: systemd grants access to root, the daemon, and + // members of iota-operators. Once a peer has passed that boundary, it is + // an administrator for the operator console protocol. + IpcRole::Admin +} + async fn handle_client( stream: UnixStream, runtime: Arc, @@ -283,16 +354,17 @@ async fn handle_client( mut state_rx: watch::Receiver, instance_id: String, ) -> Result<()> { - let peer = peer_credentials(&stream)?; - // Access control belongs to the Unix socket. The systemd socket grants - // iota-operators group access (0660); rejecting every UID other than the - // service account here would make that authorization ineffective. Manual - // sockets remain owner-only (0600) at bind time. + let peer_identity = peer_credentials(&stream)?; + let peer = PeerContext { + pid: peer_identity.pid, + uid: peer_identity.uid, + role: role_for_peer(&peer_identity), + }; let (mut reader, mut writer) = stream.into_split(); // A failed writer must stop the reader and any subsequent command work // for this client; otherwise the reader can remain parked forever. let session_cancellation = runtime.cancellation.child_token(); - let (directed_tx, directed_rx) = mpsc::channel::(CLIENT_CHANNEL_SIZE); + let (directed_tx, directed_rx) = mpsc::channel::(CLIENT_CHANNEL_SIZE); eprintln!("IPC handshake started (pid={}, uid={})", peer.pid, peer.uid); // --- Handshake --- @@ -343,7 +415,7 @@ async fn handle_client( break; } Ok(_) => { - // Unexpected first message — send error and close. + // Unexpected first message, send an error and close. return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Expected Hello as first message", @@ -353,7 +425,7 @@ async fn handle_client( }, } } - let _version = negotiated_version.ok_or_else(|| { + let negotiated_version = negotiated_version.ok_or_else(|| { std::io::Error::new(std::io::ErrorKind::Other, "Handshake failed after retries") })?; @@ -361,7 +433,7 @@ async fn handle_client( // --- Send initial state snapshot --- let initial = DaemonMessage::StateUpdate(runtime.snapshot()); - let _ = directed_tx.send(initial).await; + let _ = directed_tx.send(WriterCommand::Message(initial)).await; // --- Writer task: merge directed responses + shared log events --- let mut log_rx = log_tx.subscribe(); @@ -375,19 +447,29 @@ async fn handle_client( tokio::spawn(async move { let mut directed_rx = directed_rx; let mut last_metric_sent = tokio::time::Instant::now(); + let mut state_updates_open = true; loop { let metric_interval = sub_rx.borrow().metric_interval_ms; tokio::select! { + _ = session_cancellation.cancelled() => break, // Directed messages (responses to this client's requests) - msg = directed_rx.recv() => { - match msg { - Some(message) => { + command = directed_rx.recv() => { + match command { + Some(WriterCommand::Message(message)) => { if let Err(error) = write_client_message(&mut writer, &message).await { eprintln!("IPC client writer stopped while sending directed message: {error}"); session_cancellation.cancel(); break; } } + Some(WriterCommand::Flush { complete }) => { + if let Err(error) = writer.flush().await { + eprintln!("IPC client writer stopped while flushing: {error}"); + session_cancellation.cancel(); + break; + } + let _ = complete.send(()); + } None => break, } } @@ -435,12 +517,16 @@ async fn handle_client( break; } } - Err(broadcast::error::RecvError::Closed) => break, + Err(broadcast::error::RecvError::Closed) => { + session_cancellation.cancel(); + break; + } } } - changed = state_rx.changed() => { + changed = state_rx.changed(), if state_updates_open => { if changed.is_err() { - break; + state_updates_open = false; + continue; } let snapshot = state_rx.borrow().clone(); if let Err(error) = write_client_message(&mut writer, &DaemonMessage::StateUpdate(snapshot)).await { @@ -475,9 +561,14 @@ async fn handle_client( iota_ipc::LocalRequest::StopDaemon => Some("shutdown requested"), _ => None, }; - let response = if envelope.protocol_version < MIN_PROTOCOL_VERSION - || envelope.protocol_version > PROTOCOL_VERSION - { + let response = if envelope.protocol_version != negotiated_version { + log!( + "IPC protocol mismatch: pid={}, uid={}, negotiated={}, request={}", + peer.pid, + peer.uid, + negotiated_version, + envelope.protocol_version + ); iota_ipc::ResponseEnvelope { request_id: envelope.request_id, result: iota_ipc::ResponseResult::Error( @@ -485,21 +576,42 @@ async fn handle_client( ), } } else { - router.route(envelope.request_id, envelope.request).await + router + .route(&peer, envelope.request_id, envelope.request) + .await }; - let _ = directed_tx.send(DaemonMessage::Response(response)).await; - if let Some(reason) = shutdown_reason { + let should_shutdown = shutdown_reason.is_some() + && matches!(&response.result, iota_ipc::ResponseResult::Ok(_)); + let _ = directed_tx + .send(WriterCommand::Message(DaemonMessage::Response(response))) + .await; + if let Some(reason) = shutdown_reason.filter(|_| should_shutdown) { let _ = directed_tx - .send(DaemonMessage::LifecycleEvent( + .send(WriterCommand::Message(DaemonMessage::LifecycleEvent( iota_ipc::LifecycleEvent::Shutdown { reason: reason.into(), }, - )) + ))) .await; - // The request itself initiates daemon cancellation. Give - // the dedicated writer a chance to flush the response - // and lifecycle event before this session is torn down. - tokio::time::sleep(std::time::Duration::from_millis(100)).await; + let (flush_tx, flush_rx) = tokio::sync::oneshot::channel(); + let _ = directed_tx + .send(WriterCommand::Flush { complete: flush_tx }) + .await; + timeout(CLIENT_IO_TIMEOUT, flush_rx) + .await + .map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::TimedOut, + "IPC shutdown response flush timed out", + ) + })? + .map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "IPC writer stopped before shutdown flush", + ) + })?; + runtime.begin_shutdown(); break; } } @@ -515,26 +627,51 @@ async fn handle_client( metric_interval_ms: interval, }); let snapshot = DaemonMessage::StateUpdate(runtime.snapshot()); - let _ = directed_tx.send(snapshot).await; - let _ = directed_tx.send(DaemonMessage::Subscribed).await; + let _ = directed_tx.send(WriterCommand::Message(snapshot)).await; + let _ = directed_tx + .send(WriterCommand::Message(DaemonMessage::Subscribed)) + .await; } Ok(ClientMessage::Ping { seq }) => { - let _ = directed_tx.send(DaemonMessage::Pong { seq }).await; + let _ = directed_tx + .send(WriterCommand::Message(DaemonMessage::Pong { seq })) + .await; } Ok(ClientMessage::Hello { .. }) => { // Re-handshake on existing connection: treat as resubscribe let snapshot = DaemonMessage::StateUpdate(runtime.snapshot()); - let _ = directed_tx.send(snapshot).await; + let _ = directed_tx.send(WriterCommand::Message(snapshot)).await; } Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => break, Err(error) => { - writer_task.abort(); + session_cancellation.cancel(); + drop(directed_tx); + let mut writer_task = writer_task; + match timeout(CLIENT_IO_TIMEOUT, &mut writer_task).await { + Ok(_) => {} + Err(_) => { + writer_task.abort(); + let _ = writer_task.await; + } + } return Err(error); } } } + drop(directed_tx); session_cancellation.cancel(); - writer_task.abort(); + let mut writer_task = writer_task; + match timeout(CLIENT_IO_TIMEOUT, &mut writer_task).await { + Ok(Ok(())) => {} + Ok(Err(error)) => { + eprintln!("IPC client writer task failed: {error}"); + } + Err(_) => { + eprintln!("IPC client writer did not stop before timeout"); + writer_task.abort(); + let _ = writer_task.await; + } + } log!( "IPC client disconnected (pid={}, uid={})", peer.pid, @@ -563,4 +700,46 @@ mod tests { .is_err() ); } + + #[tokio::test] + async fn manual_socket_validation_requires_owner_only_mode() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("ipc.sock"); + let listener = StdUnixListener::bind(&path).expect("test socket binds"); + tokio::fs::set_permissions(&path, PermissionsExt::from_mode(0o600)) + .await + .expect("test socket permissions apply"); + + validate_manual_socket(&path) + .await + .expect("manual socket validation succeeds"); + drop(listener); + } + + #[tokio::test] + async fn manual_socket_validation_rejects_unexpected_mode() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("ipc.sock"); + let listener = StdUnixListener::bind(&path).expect("test socket binds"); + tokio::fs::set_permissions(&path, PermissionsExt::from_mode(0o660)) + .await + .expect("test socket permissions apply"); + + let error = validate_manual_socket(&path) + .await + .expect_err("group-accessible manual socket must be rejected"); + assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied); + drop(listener); + } + + #[test] + fn an_admitted_operator_peer_receives_administrator_role() { + let peer = PeerIdentity { + pid: 123, + uid: 1000, + _gid: 1000, + }; + + assert_eq!(role_for_peer(&peer), IpcRole::Admin); + } } diff --git a/iota-daemon-lib/src/lib.rs b/iota-daemon-lib/src/lib.rs index 6ccd977..3afeff8 100644 --- a/iota-daemon-lib/src/lib.rs +++ b/iota-daemon-lib/src/lib.rs @@ -7,7 +7,7 @@ pub mod log_buffer; pub mod services; pub mod task_registry; -pub use command_router::CommandRouter; +pub use command_router::{CommandRouter, IpcRole, PeerContext}; pub use daemon_state::{DaemonRuntime, ShutdownReason, StartupPhase}; pub use ipc_server::IpcServer; pub use services::DaemonServices; diff --git a/iota-daemon-lib/tests/command_router.rs b/iota-daemon-lib/tests/command_router.rs index 2bf8bd7..99a2aaa 100644 --- a/iota-daemon-lib/tests/command_router.rs +++ b/iota-daemon-lib/tests/command_router.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use iota_daemon_lib::log_buffer::LogBuffer; -use iota_daemon_lib::{CommandRouter, DaemonRuntime, DaemonServices}; -use iota_ipc::{LocalRequest, ResponseResult}; +use iota_daemon_lib::{CommandRouter, DaemonRuntime, DaemonServices, IpcRole, PeerContext}; +use iota_ipc::{IpcErrorCode, LocalRequest, ResponseResult}; use mtp::codec::CommunicationValue; use omikron_connector::{OmikronClient, OmikronError}; use std::sync::{ @@ -13,6 +13,22 @@ use std::time::Duration; struct FakeOmikron { reconnects: AtomicUsize, } + +fn admin_peer() -> PeerContext { + PeerContext { + pid: 1, + uid: 0, + role: IpcRole::Admin, + } +} + +fn read_peer() -> PeerContext { + PeerContext { + pid: 2, + uid: 1000, + role: IpcRole::Read, + } +} #[async_trait] impl OmikronClient for FakeOmikron { async fn send_message(&self, _: &CommunicationValue) -> Result<(), OmikronError> { @@ -55,7 +71,10 @@ async fn reconnect_uses_the_injected_client() { Arc::new(Mutex::new(LogBuffer::new(100))), ); assert!(matches!( - router.route(1, LocalRequest::ReconnectOmikron).await.result, + router + .route(&admin_peer(), 1, LocalRequest::ReconnectOmikron) + .await + .result, ResponseResult::Ok(_) )); assert_eq!(fake.reconnects.load(Ordering::SeqCst), 1); @@ -79,10 +98,44 @@ async fn identity_rotation_is_available_while_omikron_is_offline() { ); assert!(matches!( router - .route(1, LocalRequest::RotateIotaIdentity) + .route(&admin_peer(), 1, LocalRequest::RotateIotaIdentity) .await .result, ResponseResult::Ok(_) )); assert_eq!(fake.reconnects.load(Ordering::SeqCst), 1); } + +#[tokio::test] +async fn read_role_cannot_execute_an_administrative_request() { + let fake = Arc::new(FakeOmikron { + reconnects: AtomicUsize::new(0), + }); + let services = Arc::new(DaemonServices { + omikron: fake.clone(), + users: Default::default(), + config: Default::default(), + active: true, + }); + let router = CommandRouter::new( + Arc::new(DaemonRuntime::new()), + services, + Arc::new(Mutex::new(LogBuffer::new(100))), + ); + + assert!(matches!( + router + .route( + &read_peer(), + 9, + LocalRequest::SetConfig { + key: "port".into(), + value: "1984".into(), + }, + ) + .await + .result, + ResponseResult::Error(IpcErrorCode::Unauthorized) + )); + assert_eq!(fake.reconnects.load(Ordering::SeqCst), 0); +} diff --git a/iota-daemon-lib/tests/ipc_server.rs b/iota-daemon-lib/tests/ipc_server.rs new file mode 100644 index 0000000..7540ea4 --- /dev/null +++ b/iota-daemon-lib/tests/ipc_server.rs @@ -0,0 +1,252 @@ +use async_trait::async_trait; +use iota_daemon_lib::{DaemonRuntime, DaemonServices, IpcServer}; +use iota_ipc::{ + ClientMessage, DaemonMessage, ExitIntent, IpcErrorCode, LocalRequest, PROTOCOL_VERSION, + RequestEnvelope, ResponseResult, read_msg, write_msg, +}; +use iota_storage::util::config_util::{self, IotaConfig}; +use mtp::codec::CommunicationValue; +use omikron_connector::{OmikronClient, OmikronError}; +use std::path::Path; +use std::sync::Arc; +use std::time::Duration; +use tokio::net::UnixStream; +use tokio::sync::{broadcast, watch}; + +struct ConfigRestore(Arc); + +impl Drop for ConfigRestore { + fn drop(&mut self) { + config_util::CONFIG.store(self.0.clone()); + } +} + +fn set_client_limit(limit: usize) -> ConfigRestore { + let previous = config_util::CONFIG.load_full(); + let mut config = (*previous).clone(); + config.max_ipc_clients = limit; + config_util::CONFIG.store(Arc::new(config)); + ConfigRestore(previous) +} + +async fn start_server( + path: &Path, + services: Arc, +) -> (Arc, tokio::task::JoinHandle<()>) { + let runtime = Arc::new(DaemonRuntime::new()); + let (log_tx, _) = broadcast::channel(32); + let log_buffer = Arc::new(std::sync::Mutex::new( + iota_daemon_lib::log_buffer::LogBuffer::new(32), + )); + let (_, state_rx) = watch::channel(runtime.snapshot()); + let server = IpcServer::bind( + path.to_owned(), + runtime.clone(), + services, + log_tx, + log_buffer, + state_rx, + ) + .await + .expect("IPC server binds"); + let task = tokio::spawn(async move { + let _ = server.serve().await; + }); + (runtime, task) +} + +async fn try_connect_and_await_hello(path: &Path) -> std::io::Result { + let mut stream = UnixStream::connect(path).await?; + write_msg( + &mut stream, + &ClientMessage::Hello { + supported_versions: vec![PROTOCOL_VERSION], + }, + ) + .await?; + let message: DaemonMessage = read_msg(&mut stream).await?; + if !matches!(message, DaemonMessage::HelloAck(_)) { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "expected HelloAck", + )); + } + Ok(stream) +} + +async fn connect_and_await_hello(path: &Path) -> UnixStream { + try_connect_and_await_hello(path) + .await + .expect("IPC connection completes the Hello exchange") +} + +#[tokio::test] +async fn active_client_limit_rejects_excess_clients_and_releases_permits() { + let _config = set_client_limit(1); + let directory = tempfile::tempdir().unwrap(); + let socket = directory.path().join("ipc.sock"); + let (_runtime, server_task) = start_server(&socket, DaemonServices::inactive()).await; + + let first = connect_and_await_hello(&socket).await; + let mut rejected = UnixStream::connect(&socket) + .await + .expect("second connection reaches the Unix listener"); + let rejected_result = tokio::time::timeout( + Duration::from_secs(2), + read_msg::<_, DaemonMessage>(&mut rejected), + ) + .await + .expect("rejected client is closed promptly"); + assert!(rejected_result.is_err()); + + drop(first); + let deadline = tokio::time::Instant::now() + Duration::from_secs(2); + let _released = loop { + match try_connect_and_await_hello(&socket).await { + Ok(stream) => break stream, + Err(_error) if tokio::time::Instant::now() < deadline => { + tokio::time::sleep(Duration::from_millis(10)).await; + } + Err(error) => panic!("client permit was not released: {error}"), + } + }; + server_task.abort(); + let _ = server_task.await; +} + +#[tokio::test] +async fn request_with_version_different_from_hello_is_rejected() { + let directory = tempfile::tempdir().unwrap(); + let socket = directory.path().join("ipc.sock"); + let (_runtime, server_task) = start_server(&socket, DaemonServices::inactive()).await; + let mut stream = connect_and_await_hello(&socket).await; + + write_msg( + &mut stream, + &ClientMessage::Request(RequestEnvelope { + request_id: 7, + protocol_version: PROTOCOL_VERSION + 1, + request: LocalRequest::GetStatus, + }), + ) + .await + .expect("request sends"); + + let response = loop { + match read_msg::<_, DaemonMessage>(&mut stream) + .await + .expect("daemon response arrives") + { + DaemonMessage::Response(response) => break response, + _ => continue, + } + }; + assert_eq!(response.request_id, 7); + assert!(matches!( + response.result, + ResponseResult::Error(IpcErrorCode::UnsupportedVersion) + )); + + drop(stream); + server_task.abort(); + let _ = server_task.await; +} + +struct TestOmikron; + +#[async_trait] +impl OmikronClient for TestOmikron { + async fn send_message(&self, _: &CommunicationValue) -> Result<(), OmikronError> { + Ok(()) + } + + async fn await_response( + &self, + _: &CommunicationValue, + _: Duration, + ) -> Result { + Err(OmikronError::Disconnected("test client".into())) + } + + async fn reconnect(&self) -> Result<(), OmikronError> { + Ok(()) + } + + async fn rotate_identity(&self) -> Result<(), OmikronError> { + Ok(()) + } + + async fn is_connected(&self) -> bool { + true + } +} + +fn active_services() -> Arc { + Arc::new(DaemonServices { + omikron: Arc::new(TestOmikron), + users: Default::default(), + config: Default::default(), + active: true, + }) +} + +#[tokio::test] +async fn shutdown_delivers_response_and_lifecycle_event_before_eof() { + let directory = tempfile::tempdir().unwrap(); + let socket = directory.path().join("ipc.sock"); + let (runtime, server_task) = start_server(&socket, active_services()).await; + let mut stream = connect_and_await_hello(&socket).await; + + write_msg( + &mut stream, + &ClientMessage::Request(RequestEnvelope { + request_id: 8, + protocol_version: PROTOCOL_VERSION, + request: LocalRequest::RequestProcessExit { + intent: ExitIntent::Stop, + }, + }), + ) + .await + .expect("shutdown request sends"); + + let mut response_seen = false; + let mut lifecycle_seen = false; + for _ in 0..4 { + match tokio::time::timeout( + Duration::from_secs(2), + read_msg::<_, DaemonMessage>(&mut stream), + ) + .await + .expect("shutdown message arrives") + .expect("shutdown stream remains readable") + { + DaemonMessage::Response(response) => { + assert_eq!(response.request_id, 8); + assert!(matches!(response.result, ResponseResult::Ok(_))); + response_seen = true; + } + DaemonMessage::LifecycleEvent(iota_ipc::LifecycleEvent::Shutdown { .. }) => { + lifecycle_seen = true; + } + _ => {} + } + if response_seen && lifecycle_seen { + break; + } + } + + assert!(response_seen); + assert!(lifecycle_seen); + let eof = tokio::time::timeout( + Duration::from_secs(2), + read_msg::<_, DaemonMessage>(&mut stream), + ) + .await + .expect("shutdown connection closes after flush"); + assert!(eof.is_err()); + assert!(runtime.is_shutting_down()); + + server_task.abort(); + let _ = server_task.await; +} diff --git a/iota-daemon/Cargo.toml b/iota-daemon/Cargo.toml index 29de9e1..87a5636 100644 --- a/iota-daemon/Cargo.toml +++ b/iota-daemon/Cargo.toml @@ -7,7 +7,6 @@ edition = "2024" iota-daemon-lib = { path = "../iota-daemon-lib" } iota-ipc = { path = "../iota-ipc" } iota-logger = { path = "../iota-logger" } -iota-state = { path = "../iota-state" } iota-paths = { path = "../iota-paths" } iota-storage = { path = "../iota-storage" } iota-util = { path = "../iota-util" } @@ -15,4 +14,3 @@ iota-terms = { path = "../iota-terms" } omikron-connector = { path = "../omikron-connector" } web-server = { path = "../web-server" } tokio = { version = "1.50.0", features = ["full"] } -tokio-util = { version = "0.7", features = ["rt"] } diff --git a/iota-ipc/src/lib.rs b/iota-ipc/src/lib.rs index af695f2..acf72b1 100644 --- a/iota-ipc/src/lib.rs +++ b/iota-ipc/src/lib.rs @@ -5,13 +5,13 @@ pub mod transport; pub use protocol::{ ClientMessage, CommunitySummary, ComponentHealth, ComponentId, ComponentStatusResponse, ConfigResponse, ConnectionStatus, DaemonMessage, DaemonStatusResponse, DeploymentMode, - ExitIntent, HealthStatus, HelloAck, IpcErrorCode, LifecycleEvent, LifecyclePhase, LocalRequest, - LocalUserState, LogEntriesResponse, LogEntry, MetricSample, OmikronStatusResponse, - RequestEnvelope, ResponseEnvelope, ResponsePayload, ResponseResult, SecretString, StartupPhase, - StateSnapshot, StatusResponse, SupervisorKind, TaskSummary, UpdateStatusResponse, - UserDetailResponse, UserSummary, + ExitIntent, HealthStatus, HelloAck, IpcErrorCode, IpcRole, LifecycleEvent, LifecyclePhase, + LocalRequest, LocalUserState, LogEntriesResponse, LogEntry, MetricSample, + OmikronStatusResponse, RequestEnvelope, ResponseEnvelope, ResponsePayload, ResponseResult, + SecretString, StartupPhase, StateSnapshot, StatusResponse, SupervisorKind, TaskSummary, + UpdateStatusResponse, UserDetailResponse, UserSummary, }; -pub use transport::{read_msg, write_msg}; +pub use transport::{MAX_MESSAGE_SIZE, read_msg, write_msg}; /// Current IPC protocol version. pub const PROTOCOL_VERSION: u16 = 2; diff --git a/iota-ipc/src/protocol.rs b/iota-ipc/src/protocol.rs index 01cd14e..4006583 100644 --- a/iota-ipc/src/protocol.rs +++ b/iota-ipc/src/protocol.rs @@ -97,6 +97,59 @@ pub enum LocalRequest { ListCommunities, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum IpcRole { + Read, + Operate, + Admin, +} + +impl IpcRole { + pub fn allows(self, required: IpcRole) -> bool { + matches!( + (self, required), + (IpcRole::Admin, _) + | (IpcRole::Operate, IpcRole::Operate | IpcRole::Read) + | (IpcRole::Read, IpcRole::Read) + ) + } +} + +impl LocalRequest { + /// Return the minimum authenticated local role required to execute a + /// request. New request variants must be assigned explicitly here. + pub fn required_role(&self) -> IpcRole { + match self { + Self::GetStatus + | Self::ListTasks + | Self::ListUsers + | Self::GetDaemonStatus + | Self::GetOmikronStatus + | Self::ListComponents + | Self::GetUser { .. } + | Self::GetLogs { .. } + | Self::CheckUpdate + | Self::ListCommunities => IpcRole::Read, + + Self::ReconnectOmikron | Self::ReloadConfig => IpcRole::Operate, + + Self::CreateUser { .. } + | Self::AttachUserFromTu { .. } + | Self::PurgeUserData { .. } + | Self::ReleaseUser { .. } + | Self::CompleteDeleteUser { .. } + | Self::RemoveUser { .. } + | Self::RotateIotaIdentity + | Self::RequestProcessExit { .. } + | Self::RestartDaemon + | Self::StopDaemon + | Self::GetConfig + | Self::SetConfig { .. } + | Self::ImportUser { .. } => IpcRole::Admin, + } + } +} + #[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum ExitIntent { @@ -296,7 +349,9 @@ impl std::fmt::Display for IpcErrorCode { Self::Disconnected => "the daemon connection was lost", Self::Timeout => "the daemon did not respond in time", Self::Cancelled => "the daemon cancelled the request", - Self::Unauthorized => "the daemon denied this operation", + Self::Unauthorized => { + "the daemon denied this operation because the IPC account lacks the required role" + } Self::InternalFailure => "the daemon encountered an internal failure", }) } @@ -317,6 +372,11 @@ mod error_tests { .to_string() .contains("InternalFailure") ); + assert!( + IpcErrorCode::Unauthorized + .to_string() + .contains("required role") + ); } } diff --git a/iota-ipc/src/transport.rs b/iota-ipc/src/transport.rs index 2793451..6a4382f 100644 --- a/iota-ipc/src/transport.rs +++ b/iota-ipc/src/transport.rs @@ -3,7 +3,10 @@ use serde::de::DeserializeOwned; use std::io::{Error, ErrorKind, Result}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -const MAX_MESSAGE_SIZE: usize = 1024 * 1024; +/// Maximum encoded payload size for a single IPC frame. +/// +/// This is a wire-level contract shared by both sides of the connection. +pub const MAX_MESSAGE_SIZE: usize = 1024 * 1024; /* Length-prefixing preserves message boundaries on a byte stream and bounds * allocations before JSON is deserialized. */ @@ -14,6 +17,12 @@ where { let payload = serde_json::to_vec(message).map_err(|error| Error::new(ErrorKind::InvalidData, error))?; + if payload.len() > MAX_MESSAGE_SIZE { + return Err(Error::new( + ErrorKind::InvalidData, + "IPC message exceeds limit", + )); + } let len = u32::try_from(payload.len()) .map_err(|_| Error::new(ErrorKind::InvalidData, "IPC message is too large"))?; writer.write_u32(len).await?; @@ -40,7 +49,7 @@ where #[cfg(test)] mod tests { - use super::{read_msg, write_msg}; + use super::{MAX_MESSAGE_SIZE, read_msg, write_msg}; use crate::protocol::{ClientMessage, LocalRequest, RequestEnvelope}; #[tokio::test] @@ -57,4 +66,31 @@ mod tests { let received: ClientMessage = read_msg(&mut reader).await.expect("read succeeds"); assert!(matches!(received, ClientMessage::Request(req) if req.request_id == 4)); } + + #[tokio::test] + async fn write_rejects_message_above_frame_limit() { + let (mut writer, _reader) = tokio::io::duplex(MAX_MESSAGE_SIZE + 16); + let message = "x".repeat(MAX_MESSAGE_SIZE + 1); + + let error = write_msg(&mut writer, &message) + .await + .expect_err("oversized payload must be rejected before framing"); + + assert_eq!(error.kind(), std::io::ErrorKind::InvalidData); + assert!(error.to_string().contains("exceeds limit")); + } + + #[tokio::test] + async fn read_rejects_frame_above_limit_before_allocating_payload() { + let (mut writer, mut reader) = tokio::io::duplex(16); + tokio::io::AsyncWriteExt::write_u32(&mut writer, (MAX_MESSAGE_SIZE + 1) as u32) + .await + .expect("length prefix write succeeds"); + + let error = read_msg::<_, ClientMessage>(&mut reader) + .await + .expect_err("oversized frame must be rejected"); + + assert_eq!(error.kind(), std::io::ErrorKind::InvalidData); + } } diff --git a/iota-process-manager/Cargo.toml b/iota-process-manager/Cargo.toml index c159ef8..6977698 100644 --- a/iota-process-manager/Cargo.toml +++ b/iota-process-manager/Cargo.toml @@ -6,3 +6,7 @@ edition = "2024" [dependencies] async-trait = "0.1" tokio = { version = "1.50", features = ["process", "time", "io-util", "macros", "rt"] } + +[dev-dependencies] +libc = "0.2" +tempfile = "3" diff --git a/iota-process-manager/src/lib.rs b/iota-process-manager/src/lib.rs index c0ebb0d..3512a90 100644 --- a/iota-process-manager/src/lib.rs +++ b/iota-process-manager/src/lib.rs @@ -167,26 +167,66 @@ pub async fn detect() -> Option> { mod systemd { use super::*; use std::{path::Path, process::Stdio}; - use tokio::{process::Command, time::timeout}; + use tokio::{io::AsyncRead, process::Command, time::timeout}; const SERVICE: &str = "iota-daemon.service"; const SOCKET: &str = "iota-daemon.socket"; const COMMON: [&str; 2] = ["--no-pager", "--no-ask-password"]; + const MAX_COMMAND_OUTPUT_BYTES: usize = 1024 * 1024; pub struct RealExecutor; - #[async_trait] - impl CommandExecutor for RealExecutor { - async fn output( + + async fn read_bounded(reader: R) -> std::io::Result> + where + R: AsyncRead + Unpin, + { + use tokio::io::AsyncReadExt; + + let mut output = Vec::new(); + reader + .take((MAX_COMMAND_OUTPUT_BYTES + 1) as u64) + .read_to_end(&mut output) + .await?; + if output.len() > MAX_COMMAND_OUTPUT_BYTES { + output.truncate(MAX_COMMAND_OUTPUT_BYTES); + } + Ok(output) + } + + async fn collect_output( + stdout: tokio::process::ChildStdout, + stderr: tokio::process::ChildStderr, + ) -> Result<(Vec, Vec), ProcessManagerError> { + let (stdout_result, stderr_result) = + tokio::join!(read_bounded(stdout), read_bounded(stderr)); + let stdout = stdout_result.map_err(|error| { + ProcessManagerError::new( + ProcessManagerErrorKind::CommandFailed, + format!("stdout read failed: {error}"), + ) + })?; + let stderr = stderr_result.map_err(|error| { + ProcessManagerError::new( + ProcessManagerErrorKind::CommandFailed, + format!("stderr read failed: {error}"), + ) + })?; + Ok((stdout, stderr)) + } + + impl RealExecutor { + async fn output_with_timeout( &self, program: &str, args: &[&str], + process_timeout: std::time::Duration, ) -> Result { - let child = Command::new(program) + let mut child = Command::new(program) .args(args) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .kill_on_drop(false) + .kill_on_drop(true) .spawn() .map_err(|e| { ProcessManagerError::new( @@ -194,28 +234,79 @@ mod systemd { format!("Could not run {program}: {e}"), ) })?; - let output = timeout(PROCESS_MANAGER_TIMEOUT, child.wait_with_output()) - .await - .map_err(|_| { - ProcessManagerError::new( + + let stdout = child.stdout.take().ok_or_else(|| { + ProcessManagerError::new( + ProcessManagerErrorKind::CommandFailed, + "command stdout pipe was not created", + ) + })?; + let stderr = child.stderr.take().ok_or_else(|| { + ProcessManagerError::new( + ProcessManagerErrorKind::CommandFailed, + "command stderr pipe was not created", + ) + })?; + let output_task = tokio::spawn(collect_output(stdout, stderr)); + + let status = match timeout(process_timeout, child.wait()).await { + Ok(result) => result.map_err(|e| { + ProcessManagerError::new(ProcessManagerErrorKind::CommandFailed, e.to_string()) + })?, + Err(_) => { + // Keep the Child alive across the timeout. Explicitly + // terminate it and await wait() so the OS child is + // reaped before reporting the timeout. + let kill_error = child.start_kill().err(); + let wait_error = child.wait().await.err(); + output_task.abort(); + let _ = output_task.await; + + if let Some(error) = wait_error { + return Err(ProcessManagerError::new( + ProcessManagerErrorKind::CommandFailed, + format!("{program} timed out and could not be reaped: {error}"), + )); + } + let termination_detail = kill_error + .map(|error| format!("; termination request reported: {error}")) + .unwrap_or_default(); + return Err(ProcessManagerError::new( ProcessManagerErrorKind::TimedOut, format!( - "{program} timed out after {} seconds", - PROCESS_MANAGER_TIMEOUT.as_secs() + "{program} timed out after {} seconds{termination_detail}", + process_timeout.as_secs(), ), - ) - })? - .map_err(|e| { - ProcessManagerError::new(ProcessManagerErrorKind::CommandFailed, e.to_string()) - })?; + )); + } + }; + + let (stdout, stderr) = output_task.await.map_err(|error| { + ProcessManagerError::new( + ProcessManagerErrorKind::CommandFailed, + format!("command output task failed: {error}"), + ) + })??; Ok(CommandOutput { - success: output.status.success(), - stdout: String::from_utf8_lossy(&output.stdout).into_owned(), - stderr: String::from_utf8_lossy(&output.stderr).into_owned(), + success: status.success(), + stdout: String::from_utf8_lossy(&stdout).into_owned(), + stderr: String::from_utf8_lossy(&stderr).into_owned(), }) } } + #[async_trait] + impl CommandExecutor for RealExecutor { + async fn output( + &self, + program: &str, + args: &[&str], + ) -> Result { + self.output_with_timeout(program, args, PROCESS_MANAGER_TIMEOUT) + .await + } + } + pub struct SystemdManager { executor: Arc, service: &'static str, @@ -461,6 +552,51 @@ mod systemd { assert!(call.contains(&"--no-pager".into())); assert!(call.contains(&"--no-ask-password".into())); } + + #[tokio::test] + async fn timed_out_real_child_is_terminated_and_reaped() { + use std::fs; + use std::time::Duration; + + let directory = tempfile::tempdir().unwrap(); + let pid_file = directory.path().join("child.pid"); + let script = format!( + "printf '%s' \"$$\" > '{}'; exec sleep 60", + pid_file.display() + ); + let executor = RealExecutor; + let task = tokio::spawn(async move { + executor + .output_with_timeout("sh", &["-c", &script], Duration::from_millis(50)) + .await + }); + + let deadline = tokio::time::Instant::now() + Duration::from_secs(2); + let pid = loop { + if let Ok(contents) = fs::read_to_string(&pid_file) { + if let Ok(pid) = contents.parse::() { + break pid; + } + } + assert!(tokio::time::Instant::now() < deadline); + tokio::task::yield_now().await; + }; + + let result = task.await.unwrap(); + assert_eq!( + result.unwrap_err().kind(), + ProcessManagerErrorKind::TimedOut + ); + assert!(!std::path::Path::new(&format!("/proc/{pid}")).exists()); + + let mut status = 0; + let wait_result = unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) }; + assert_eq!(wait_result, -1); + assert_eq!( + std::io::Error::last_os_error().raw_os_error(), + Some(libc::ECHILD) + ); + } } } diff --git a/iota-state/Cargo.toml b/iota-state/Cargo.toml index 8c9ab8a..2192830 100644 --- a/iota-state/Cargo.toml +++ b/iota-state/Cargo.toml @@ -13,5 +13,3 @@ once_cell = "1.21.3" tokio = { version = "1.50.0", features = ["full"] } json = "*" sysinfo = "0.39.0" -mtp = { git = "https://git.methanium.net/Methanium/mtp.git" } -serde = { version = "1", features = ["derive"] } diff --git a/iota-storage/Cargo.toml b/iota-storage/Cargo.toml index caf6417..9f91c52 100644 --- a/iota-storage/Cargo.toml +++ b/iota-storage/Cargo.toml @@ -5,33 +5,16 @@ edition = "2024" [dependencies] iota-logger = { path = "../iota-logger" } -iota-state = { path = "../iota-state" } iota-util = { path = "../iota-util" } iota-paths = { path = "../iota-paths" } - -mtp = { git = "https://git.methanium.net/Methanium/mtp.git" } - -aes-gcm = "0.10.3" base64 = "0.22.1" -hex = "*" -hkdf = "0.12.4" json = "*" arc-swap = "1" once_cell = "1.21.3" r2d2 = "0.8" serde = { version = "1", features = ["derive"] } -serde_json = "1" serde_yaml = "0.9" thiserror = "2" rand = "0.8" -rand_core = { version = "0.6", features = ["getrandom", "std"] } -ratatui = "0.30.0" -reqwest = "0.13.2" rusqlite = "0.40.0" -sha2 = "0.11.0" -sysinfo = "0.39.0" tokio = { version = "1.50.0", features = ["full"] } -uuid = { version = "*", features = ["v4"] } -walkdir = "2.5.0" -x448 = { version = "*" } -zip = "6.0.0" diff --git a/iota-storage/src/util/config_util.rs b/iota-storage/src/util/config_util.rs index 60b1cda..371cb1c 100644 --- a/iota-storage/src/util/config_util.rs +++ b/iota-storage/src/util/config_util.rs @@ -29,6 +29,8 @@ pub struct IotaConfig { pub private_key: Option, #[serde(default = "default_read_receipts_enabled")] pub read_receipts_enabled: bool, + #[serde(default = "default_max_ipc_clients")] + pub max_ipc_clients: usize, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -87,6 +89,10 @@ const fn default_read_receipts_enabled() -> bool { true } +const fn default_max_ipc_clients() -> usize { + 64 +} + impl Default for IotaConfig { fn default() -> Self { Self { @@ -99,6 +105,7 @@ impl Default for IotaConfig { public_key: None, private_key: None, read_receipts_enabled: default_read_receipts_enabled(), + max_ipc_clients: default_max_ipc_clients(), } } } @@ -200,6 +207,14 @@ pub fn modify_config_value(key: &str, value: &str) -> Result<(), &'static str> { modify_config(|cfg| cfg.read_receipts_enabled = parsed); Ok(()) } + "max_ipc_clients" => { + let parsed: usize = value.parse().map_err(|_| "invalid max_ipc_clients")?; + if parsed == 0 { + return Err("max_ipc_clients must be greater than zero"); + } + modify_config(|cfg| cfg.max_ipc_clients = parsed); + Ok(()) + } "web.mode" => { let mode = match value { "disabled" => WebMode::Disabled, diff --git a/iota-terms/Cargo.toml b/iota-terms/Cargo.toml index 607dce1..fe051de 100644 --- a/iota-terms/Cargo.toml +++ b/iota-terms/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.0" edition = "2024" [dependencies] -iota-state = { path = "../iota-state" } iota-util = { path = "../iota-util" } json = "*" diff --git a/iota-updater/Cargo.toml b/iota-updater/Cargo.toml index 2f6bf04..1fbbef9 100644 --- a/iota-updater/Cargo.toml +++ b/iota-updater/Cargo.toml @@ -4,31 +4,12 @@ version = "0.1.0" edition = "2024" [dependencies] -iota-logger = { path = "../iota-logger" } iota-paths = { path = "../iota-paths" } - -mtp = { git = "https://git.methanium.net/Methanium/mtp.git" } - -json = "*" -pnet = "0.35.0" -ratatui = "0.30.0" -reqwest = "0.13.2" tokio = { version = "1.50.0", features = ["full"] } -sysinfo = "0.39.0" -uuid = { version = "*", features = ["v4"] } -walkdir = "2.5.0" -zip = "6.0.0" -aes-gcm = "0.10.3" -base64 = "0.22.1" -rand_core = { version = "0.6", features = ["getrandom", "std"] } sha2 = "0.11.0" -x448 = { version = "*" } -hkdf = "0.12.4" -once_cell = "1.21.3" hex = "*" serde = "1.0.228" tempfile = "3.27.0" anyhow = "1.0.102" -semver = "1.0.28" ed25519-dalek = "2.2.0" serde_json = "1.0" diff --git a/iota-util/src/crypto_helper.rs b/iota-util/src/crypto_helper.rs index 9375eb4..a258d17 100644 --- a/iota-util/src/crypto_helper.rs +++ b/iota-util/src/crypto_helper.rs @@ -6,7 +6,10 @@ pub fn generate_keyring() -> Keyring { } pub fn keyring_to_base64(keyring: &Keyring) -> String { - STANDARD.encode(keyring.to_bytes()) + let bytes = keyring + .try_to_bytes() + .expect("keyring fields must fit the wire format"); + STANDARD.encode(bytes) } pub fn keyring_from_base64(s: &str) -> Option { @@ -15,7 +18,10 @@ pub fn keyring_from_base64(s: &str) -> Option { } pub fn public_key_bundle_to_base64(bundle: &PublicKeyBundle) -> String { - STANDARD.encode(bundle.as_bytes()) + let bytes = bundle + .try_as_bytes() + .expect("public key bundle fields must fit the wire format"); + STANDARD.encode(bytes) } pub fn public_key_bundle_from_base64(s: &str) -> Option { diff --git a/iota-util/src/mtp_compat.rs b/iota-util/src/mtp_compat.rs index 02aced4..adc47d3 100644 --- a/iota-util/src/mtp_compat.rs +++ b/iota-util/src/mtp_compat.rs @@ -1,28 +1,42 @@ use mtp::codec::{CommunicationValue, DataValue}; use mtp::type_map::DataTypeId; -/* - * Keep legacy control-plane handlers source-compatible while they migrate to - * MTP's explicit optional routing fields. Relay handlers must use sender() and - * receiver() directly so an absent outer sender cannot become an identity. - */ -pub trait CommunicationValueCompat { - fn get_id(&self) -> u32; - fn get_sender(&self) -> u64; - fn get_receiver(&self) -> u64; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MtpFieldError { + MissingId, + MissingSender, + MissingReceiver, } -impl CommunicationValueCompat for CommunicationValue { - fn get_id(&self) -> u32 { - self.id().unwrap_or_default() +impl std::fmt::Display for MtpFieldError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::MissingId => "missing message id", + Self::MissingSender => "missing sender", + Self::MissingReceiver => "missing receiver", + }) + } +} + +impl std::error::Error for MtpFieldError {} + +pub trait RequiredCommunicationFields { + fn require_id(&self) -> Result; + fn require_sender(&self) -> Result; + fn require_receiver(&self) -> Result; +} + +impl RequiredCommunicationFields for CommunicationValue { + fn require_id(&self) -> Result { + self.id().ok_or(MtpFieldError::MissingId) } - fn get_sender(&self) -> u64 { - self.sender().unwrap_or_default() + fn require_sender(&self) -> Result { + self.sender().ok_or(MtpFieldError::MissingSender) } - fn get_receiver(&self) -> u64 { - self.receiver().unwrap_or_default() + fn require_receiver(&self) -> Result { + self.receiver().ok_or(MtpFieldError::MissingReceiver) } } @@ -65,3 +79,33 @@ impl<'a> OptionalDataValueExt<'a> for Option<&'a DataValue> { self.and_then(DataValue::as_container) } } + +#[cfg(test)] +mod tests { + use super::{MtpFieldError, RequiredCommunicationFields}; + use mtp::codec::{CommunicationType, CommunicationValue}; + + #[test] + fn missing_routing_fields_are_reported_instead_of_defaulted() { + let message = CommunicationValue::new(CommunicationType::Success).without_id(); + + assert_eq!(message.require_id(), Err(MtpFieldError::MissingId)); + assert_eq!(message.require_sender(), Err(MtpFieldError::MissingSender)); + assert_eq!( + message.require_receiver(), + Err(MtpFieldError::MissingReceiver) + ); + } + + #[test] + fn present_routing_fields_are_returned_unchanged() { + let message = CommunicationValue::new(CommunicationType::Success) + .with_id(7) + .with_sender(8) + .with_receiver(9); + + assert_eq!(message.require_id(), Ok(7)); + assert_eq!(message.require_sender(), Ok(8)); + assert_eq!(message.require_receiver(), Ok(9)); + } +} diff --git a/iota/Cargo.toml b/iota/Cargo.toml index b05cfe8..1fc0858 100644 --- a/iota/Cargo.toml +++ b/iota/Cargo.toml @@ -13,7 +13,6 @@ iota-paths = { path = "../iota-paths" } iota-terms = { path = "../iota-terms" } iota-util = { path = "../iota-util" } tokio = { version = "1.50.0", features = ["full"] } -tokio-util = { version = "0.7", features = ["rt"] } serde_json = "1" serde_yaml = "0.9" clap = { version = "4.5", features = ["derive"] } diff --git a/mtp-type-maps b/mtp-type-maps index 486541b..a297dcc 160000 --- a/mtp-type-maps +++ b/mtp-type-maps @@ -1 +1 @@ -Subproject commit 486541b9483356ff49ff3ec7016f87d3ecbeaa0e +Subproject commit a297dcce60bc6e84696c6a16f5fd510beb2ca643 diff --git a/omikron-connector/Cargo.toml b/omikron-connector/Cargo.toml index 31796b0..c4773f7 100644 --- a/omikron-connector/Cargo.toml +++ b/omikron-connector/Cargo.toml @@ -14,7 +14,6 @@ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [ "client", "crypto", "files", - "raw", ] } dashmap = "6.2.1" @@ -24,8 +23,4 @@ tokio = { version = "1.50.0", features = ["full"] } tokio-util = { version = "0.7", features = ["rt"] } uuid = { version = "*", features = ["v4"] } base64 = "0.22.1" -hex = "*" -rand = "0.8" rand_core = { version = "0.6", features = ["getrandom", "std"] } -sha2 = "0.11.0" -x448 = { version = "*" } diff --git a/omikron-connector/src/omikron_connection.rs b/omikron-connector/src/omikron_connection.rs index 1e1f5c7..5a63c5a 100755 --- a/omikron-connector/src/omikron_connection.rs +++ b/omikron-connector/src/omikron_connection.rs @@ -9,12 +9,12 @@ use iota_util::crypto_util::{self}; use mtp::client::{Client, ClientConfig, MTPConnection, Policy, SendMode, Sender}; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use mtp::crypto::{Keyring, PublicKeyBundle}; +use rand_core::RngCore; use std::env; +use std::fs; +use std::io::ErrorKind; use std::path::{Path, PathBuf}; -use std::sync::{ - Arc, LazyLock, - atomic::{AtomicU32, Ordering}, -}; +use std::sync::{Arc, LazyLock}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use tokio::sync::{Mutex, RwLock, Semaphore, oneshot, watch}; use tokio::task::JoinHandle; @@ -38,6 +38,9 @@ use iota_util::route_target::RouteTarget; // ============================================================================ const IOTA_KEYRING_PATH: &str = "iota.mk"; +const IDENTITY_SECRET_ENV: &str = "IOTA_IDENTITY_SECRET"; +const IDENTITY_SECRET_FILE_ENV: &str = "IOTA_IDENTITY_SECRET_FILE"; +const SYSTEMD_IDENTITY_CREDENTIAL: &str = "iota-identity"; static IDENTITY_PATH: std::sync::OnceLock = std::sync::OnceLock::new(); /// Must be called by the daemon before any Omikron connection is attempted. @@ -60,7 +63,173 @@ const TASK_CLEANUP_INTERVAL: Duration = Duration::from_secs(60); const TASK_MAX_AGE: Duration = Duration::from_secs(60); const MAX_CONCURRENT_HANDLERS: usize = 20; const RELAY_RETENTION_MILLIS: i64 = 30 * 24 * 60 * 60 * 1000; -static NEXT_RELAY_FRAME_ID: AtomicU32 = AtomicU32::new(1); + +#[derive(Debug)] +pub enum IdentityError { + Storage(mtp::files::FileError), + Directory(std::io::Error), + Secret(String), + InvalidLegacyIdentity, + Verification(String), +} + +impl std::fmt::Display for IdentityError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Storage(error) => write!(f, "identity storage error: {error}"), + Self::Directory(error) => write!(f, "unable to create identity directory: {error}"), + Self::Secret(error) => write!(f, "unable to load identity secret: {error}"), + Self::InvalidLegacyIdentity => f.write_str("legacy identity is invalid"), + Self::Verification(error) => { + write!(f, "persisted identity could not be verified: {error}") + } + } + } +} + +impl std::error::Error for IdentityError {} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct ConnectionAttemptResult { + became_healthy: bool, +} + +fn jittered_reconnect_delay(delay: Duration) -> Duration { + let ceiling_ms = u64::try_from(MAX_RECONNECT_DELAY.as_millis()) + .expect("reconnect ceiling must fit in milliseconds"); + let base_ms = u64::try_from(delay.as_millis().min(u128::from(ceiling_ms))) + .expect("bounded reconnect delay must fit in milliseconds"); + let jitter_span = base_ms / 5; + if jitter_span == 0 { + return Duration::from_millis(base_ms); + } + + let mut rng = rand_core::OsRng; + let range = jitter_span.saturating_mul(2).saturating_add(1); + let offset = (rng.next_u64() % range) as i128 - jitter_span as i128; + let jittered = (base_ms as i128 + offset).clamp(0, i128::from(ceiling_ms)); + Duration::from_millis(u64::try_from(jittered).expect("bounded jitter must be non-negative")) +} + +fn wire_user_id(user_id: i64) -> u64 { + u64::try_from(user_id).expect("validated user ID is non-negative") +} + +fn load_identity_secret() -> Result, IdentityError> { + if let Some(path) = env::var_os(IDENTITY_SECRET_FILE_ENV) { + let path = PathBuf::from(path); + let mut secret = fs::read(&path) + .map_err(|error| IdentityError::Secret(format!("{}: {error}", path.display())))?; + while matches!(secret.last(), Some(b'\n' | b'\r')) { + secret.pop(); + } + if secret.is_empty() { + return Err(IdentityError::Secret(format!( + "{} is empty", + path.display() + ))); + } + return Ok(secret); + } + + if let Ok(secret) = env::var(IDENTITY_SECRET_ENV) { + if secret.is_empty() { + return Err(IdentityError::Secret(format!( + "{IDENTITY_SECRET_ENV} is empty" + ))); + } + return Ok(secret.into_bytes()); + } + + if let Ok(credentials_dir) = env::var("CREDENTIALS_DIRECTORY") { + let path = Path::new(&credentials_dir).join(SYSTEMD_IDENTITY_CREDENTIAL); + let mut secret = fs::read(&path) + .map_err(|error| IdentityError::Secret(format!("{}: {error}", path.display())))?; + while matches!(secret.last(), Some(b'\n' | b'\r')) { + secret.pop(); + } + if secret.is_empty() { + return Err(IdentityError::Secret(format!( + "{} is empty", + path.display() + ))); + } + return Ok(secret); + } + + Err(IdentityError::Secret(format!( + "set {IDENTITY_SECRET_FILE_ENV}, {IDENTITY_SECRET_ENV}, or a systemd identity credential" + ))) +} + +fn load_legacy_raw_keyring(path: &Path) -> Result { + let bytes = fs::read(path).map_err(|error| IdentityError::Storage(error.into()))?; + if bytes.len() < 5 || bytes[..4] != *b"MTMK" || bytes[4] != 1 { + return Err(IdentityError::InvalidLegacyIdentity); + } + Keyring::from_bytes(&bytes[5..]).map_err(|_| IdentityError::InvalidLegacyIdentity) +} + +fn save_protected_keyring_verified( + keyring: &Keyring, + path: &Path, + passphrase: &[u8], +) -> Result<(), IdentityError> { + mtp::files::save_keyring(keyring, path, passphrase).map_err(IdentityError::Storage)?; + let persisted = mtp::files::load_keyring(path, passphrase).map_err(IdentityError::Storage)?; + let expected = keyring + .try_to_bytes() + .map_err(|error| IdentityError::Verification(error.to_string()))?; + let actual = persisted + .try_to_bytes() + .map_err(|error| IdentityError::Verification(error.to_string()))?; + if expected != actual { + return Err(IdentityError::Verification( + "persisted keyring differs from the requested identity".into(), + )); + } + Ok(()) +} + +fn load_or_migrate_keyring_at( + path: &Path, + legacy: Option, + passphrase: &[u8], +) -> Result { + if let Some(parent) = path + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + { + fs::create_dir_all(parent).map_err(IdentityError::Directory)?; + } + + match mtp::files::load_keyring(path, passphrase) { + Ok(keyring) => return Ok(keyring), + Err(mtp::files::FileError::Io(error)) if error.kind() == ErrorKind::NotFound => {} + Err(mtp::files::FileError::UnprotectedKeyring) => { + let keyring = load_legacy_raw_keyring(path)?; + save_protected_keyring_verified(&keyring, path, passphrase)?; + return Ok(keyring); + } + Err(error) => return Err(IdentityError::Storage(error)), + } + + let keyring = match legacy { + Some(encoded) => { + keyring_from_base64(&encoded).ok_or(IdentityError::InvalidLegacyIdentity)? + } + None => { + log!( + "No existing Iota identity found at {}; generating a new identity", + path.display() + ); + crypto_helper::generate_keyring() + } + }; + + save_protected_keyring_verified(&keyring, path, passphrase)?; + Ok(keyring) +} // ============================================================================ // Waiting Task System @@ -128,14 +297,6 @@ pub struct OmikronConnection { pub(crate) app: Arc>, } -fn ensure_relay_frame_id(frame: CommunicationValue) -> CommunicationValue { - if frame.id().is_some_and(|id| id != 0) { - return frame; - } - let id = NEXT_RELAY_FRAME_ID.fetch_add(1, Ordering::Relaxed).max(1); - frame.with_id(id) -} - impl OmikronConnection { pub fn new(active_tasks: Arc>, app: Arc>) -> Self { Self::with_cancellation(CancellationToken::new(), active_tasks, app) @@ -248,29 +409,29 @@ impl OmikronConnection { break; } - match self.clone().connect_once().await { - Ok(()) => { - if *self.reconnect_on_close.read().await { - log!("Connection lost, reconnecting in {:?}...", reconnect_delay); - } else { + let retry_reason = match self.clone().connect_once().await { + Ok(result) => { + if result.became_healthy { + reconnect_delay = RECONNECT_DELAY; + } + if !*self.reconnect_on_close.read().await { break; } + "Connection lost".to_string() } Err(e) => { if self.auth_failure.read().await.is_some() { log!("Authentication failed, stopping reconnection: {}", e); break; } - log!( - "Connection failed: {}, retrying in {:?}...", - e, - reconnect_delay - ); + format!("Connection failed: {e}") } - } + }; + let delay = jittered_reconnect_delay(reconnect_delay); + log!("{}, retrying in {:?}...", retry_reason, delay); tokio::select! { - _ = sleep(reconnect_delay) => {} + _ = sleep(delay) => {} _ = shutdown_rx.changed() => { if *shutdown_rx.borrow() { break; @@ -282,11 +443,16 @@ impl OmikronConnection { } } - async fn connect_once(self: Arc) -> Result<(), String> { + async fn connect_once(self: Arc) -> Result { self.set_state(ConnectionState::Connecting).await; log_t!("omikron_connecting"); - let keyring = Arc::new(self.load_or_migrate_keyring().await); + let identity_secret = load_identity_secret().map_err(|error| error.to_string())?; + let keyring = Arc::new( + self.load_or_migrate_keyring(&identity_secret) + .await + .map_err(|error| format!("Iota identity initialization failed: {error}"))?, + ); *self.keyring.write().await = Some(keyring.clone()); let existing_iota_id = CONFIG.load().iota_id; @@ -298,29 +464,21 @@ impl OmikronConnection { log!("Connecting to Omikron at {}", addr_str); + let policy = Policy::default() + .with_send_mode(SendMode::SingleStreamPerMessage) + .with_timeouts( + Duration::from_millis(2_000), + Duration::from_millis(2_000), + Duration::from_millis(30_000), + ) + .with_keep_alive(Some(Duration::from_secs(6))) + .with_receiver_queue_capacity(1000) + .with_max_concurrent_stream_tasks(10) + .with_persistent_stream_retries(5, Duration::from_secs(5)); let client_config = ClientConfig::new(&addr_str) .with_description("iota") - .with_policy(Policy { - send_mode: SendMode::SingleStreamPerMessage, - max_message_size: 1_000_000_000, - handshake_max_message_size: 1_000_000_000, - close_frame_len: u32::MAX, - application_close_code: 0, - open_stream_timeout: Duration::from_millis(2_000), - write_timeout: Duration::from_millis(2_000), - accept_stream_timeout: Duration::from_millis(10_000), - read_timeout: Duration::from_millis(30_000), - keep_alive_interval: Some(Duration::from_secs(6)), - max_idle_timeout: None, - force_close_delay: Duration::from_millis(300), - receiver_queue_capacity: 1000, - max_concurrent_stream_tasks: 10, - persistent_stream_max_retries: 5, - persistent_stream_retry_backoff: Duration::from_secs(5), - max_frames_per_stream: None, - }) - .with_ping_interval(MAINTENANCE_INTERVAL) - .with_max_missed_pings(0); + .with_policy(policy) + .with_ping_interval(MAINTENANCE_INTERVAL); let connection = match Client::auth_connect_or_register( client_config, @@ -389,13 +547,9 @@ impl OmikronConnection { } match result { - Ok(()) => { - if *self.reconnect_on_close.read().await { - Err("Connection closed, will reconnect".to_string()) - } else { - Ok(()) - } - } + Ok(()) => Ok(ConnectionAttemptResult { + became_healthy: true, + }), Err(e) => Err(format!("Read loop error: {}", e)), } } @@ -404,41 +558,8 @@ impl OmikronConnection { // Identity (own Keyring, migrated from the legacy base64-in-config format) // ------------------------------------------------------------------------- - /* - * `iota.mk` is now the source of truth for this Iota's identity. A - * pre-existing base64 keyring in config.json (from before the MTP auth - * migration) is imported once so already-registered Iotas keep their - * identity, and mirrored back into config.json for older code paths - * that still read it directly. - */ - async fn load_or_migrate_keyring(&self) -> Keyring { - let path = identity_path(); - if let Ok(kr) = mtp::files::load_keyring_raw(path) { - return kr; - } - - let legacy = CONFIG.load().keyring.clone(); - let keyring = legacy - .and_then(|b64| keyring_from_base64(&b64)) - .unwrap_or_else(|| { - log!( - "WARNING: No existing keyring found. Neither {} nor config.json \ - contain a keyring; generating a new identity. If you already had \ - an Iota identity, restore {} from a backup to avoid losing access.", - path.display(), - path.display() - ); - crypto_helper::generate_keyring() - }); - - if let Some(parent) = path.parent() { - let _ = std::fs::create_dir_all(parent); - } - if let Err(e) = mtp::files::save_keyring_raw(&keyring, path) { - log!("Failed to persist {}: {}", path.display(), e); - } - - keyring + async fn load_or_migrate_keyring(&self, passphrase: &[u8]) -> Result { + load_or_migrate_keyring_at(identity_path(), CONFIG.load().keyring.clone(), passphrase) } // ------------------------------------------------------------------------- @@ -503,17 +624,24 @@ impl OmikronConnection { }; let (host, port, public_key) = if let Some(endpoint) = discovered { + let discovered_key_bytes = endpoint.public_key.try_as_bytes().map_err(|error| { + format!("Failed to serialize discovered Omikron public key: {error}") + })?; match &cached_key { - Some(cached) if cached.as_bytes() != endpoint.public_key.as_bytes() => { - log!( - "Fetched Omikron public key differs from the cached {} - keeping the \ - cached key. Delete {} manually if this is an expected key rotation.", - OMIKRON_PUBLIC_KEY_PATH, - OMIKRON_PUBLIC_KEY_PATH - ); + Some(cached) => { + let cached_key_bytes = cached.try_as_bytes().map_err(|error| { + format!("Failed to serialize cached Omikron public key: {error}") + })?; + if cached_key_bytes != discovered_key_bytes { + log!( + "Fetched Omikron public key differs from the cached {} - keeping the \ + cached key. Delete {} manually if this is an expected key rotation.", + OMIKRON_PUBLIC_KEY_PATH, + OMIKRON_PUBLIC_KEY_PATH + ); + } (endpoint.host, endpoint.port, cached.clone()) } - Some(cached) => (endpoint.host, endpoint.port, cached.clone()), None => { if let Err(e) = mtp::files::save_public_key_bundle( &endpoint.public_key, @@ -563,7 +691,13 @@ impl OmikronConnection { }); continue; } - let msg_id = cv.get_id(); + let Some(msg_id) = cv.id() else { + let self_clone = self.clone(); + tokio::spawn(async move { + self_clone.handle_message_impl(cv).await; + }); + continue; + }; if let Some((_, task)) = WAITING_TASKS.remove(&msg_id) { if (task.task)(cv.clone()) { continue; @@ -613,7 +747,7 @@ impl OmikronConnection { } if let Some(ping) = connection.get_ping() { - let ping_ms = ping.as_millis() as i64; + let ping_ms = i64::try_from(ping.as_millis()).unwrap_or(i64::MAX); *self.last_ping.lock().await = ping_ms; self.app.lock().unwrap().push_ping_val(ping_ms as f64); } @@ -631,7 +765,10 @@ impl OmikronConnection { &self, signer_id: u64, ) -> Result, RelayValidationError> { - if let Some(user) = iota_storage::users::user_manager::get_user(signer_id as i64) { + let signer_id_i64 = i64::try_from(signer_id).map_err(|_| { + RelayValidationError::KeyLookup("signer ID exceeds local storage range".into()) + })?; + if let Some(user) = iota_storage::users::user_manager::get_user(signer_id_i64) { let key = iota_util::crypto_helper::public_key_bundle_from_base64(&user.public_key) .ok_or_else(|| { RelayValidationError::KeyLookup("stored user key is invalid".into()) @@ -641,7 +778,7 @@ impl OmikronConnection { let request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default( DataType::UserId, - DataValue::UnsignedNumber(signer_id as u128), + DataValue::UnsignedNumber(u128::from(signer_id)), ); let response = self .await_response(&request, Some(Duration::from_secs(10))) @@ -663,15 +800,19 @@ impl OmikronConnection { } pub async fn hosting_iota_for_user(&self, user_id: u64) -> Result { - if iota_storage::users::user_manager::get_user(user_id as i64).is_some() { + let user_id_i64 = i64::try_from(user_id) + .map_err(|_| "user ID exceeds local storage range".to_string())?; + if iota_storage::users::user_manager::get_user(user_id_i64).is_some() { return CONFIG .load() .iota_id .ok_or_else(|| "Iota identity is not configured".into()); } - let request = CommunicationValue::new(CommunicationType::GetUserData) - .add_typed_default(DataType::UserId, DataValue::UnsignedNumber(user_id as u128)); + let request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default( + DataType::UserId, + DataValue::UnsignedNumber(u128::from(user_id)), + ); let response = self .await_response(&request, Some(Duration::from_secs(10))) .await?; @@ -693,17 +834,19 @@ impl OmikronConnection { } async fn handle_relay(self: Arc, frame: CommunicationValue) { - let frame = ensure_relay_frame_id(frame); - let incoming_frame_id = frame.id(); + let Some(incoming_frame_id) = frame.id() else { + log!("Rejecting Relay without a message id"); + return; + }; let Some(local_iota_id) = CONFIG.load().iota_id else { log!("Rejecting Relay because this Iota has no registered identity"); - self.send_relay_response(incoming_frame_id, CommunicationType::ErrorInvalidData) + self.send_relay_response(Some(incoming_frame_id), CommunicationType::ErrorInvalidData) .await; return; }; let Some(keyring) = self.keyring.read().await.as_ref().cloned() else { log!("Rejecting Relay because the Iota keyring is unavailable"); - self.send_relay_response(incoming_frame_id, CommunicationType::ErrorInternal) + self.send_relay_response(Some(incoming_frame_id), CommunicationType::ErrorInternal) .await; return; }; @@ -725,24 +868,29 @@ impl OmikronConnection { Ok(value) => value, Err(error) => { log!("Relay metadata verification failed: {}", error); - self.send_relay_response(incoming_frame_id, CommunicationType::ErrorInvalidData) - .await; + self.send_relay_response( + Some(incoming_frame_id), + CommunicationType::ErrorInvalidData, + ) + .await; return; } }; - let signer_is_local = - iota_storage::users::user_manager::get_user(verified.context.signer_id as i64) - .is_some(); - let recipient_is_local = - iota_storage::users::user_manager::get_user(verified.context.final_recipient_id as i64) - .is_some(); + let signer_is_local = i64::try_from(verified.context.signer_id) + .ok() + .and_then(iota_storage::users::user_manager::get_user) + .is_some(); + let recipient_is_local = i64::try_from(verified.context.final_recipient_id) + .ok() + .and_then(iota_storage::users::user_manager::get_user) + .is_some(); if !signer_is_local && !recipient_is_local { log!( "Rejecting Relay with no local origin or destination: signer {}, recipient {}", verified.context.signer_id, verified.context.final_recipient_id, ); - self.send_relay_response(frame.id(), CommunicationType::ErrorInvalidData) + self.send_relay_response(Some(incoming_frame_id), CommunicationType::ErrorInvalidData) .await; return; } @@ -760,7 +908,7 @@ impl OmikronConnection { } }; let type_map_version = verified.context.type_map.version.to_string(); - let frame_id = frame.id().unwrap_or_default(); + let frame_id = incoming_frame_id; let reservation = match relay_replay::reserve( verified.context.signer_id, &verified.context.message_id, @@ -927,7 +1075,7 @@ impl OmikronConnection { RouteTarget::User(destination), &bytes, now_millis_i64(), - forwarded.id().unwrap_or_default(), + frame_id, &type_map_version, ) { log!("Relay could not be queued for client delivery: {}", error); @@ -1069,7 +1217,7 @@ impl OmikronConnection { } // ------------------------------------------------------------------------- - // Message Handling — Dispatch + // Message Handling - Dispatch // ------------------------------------------------------------------------- pub async fn handle_message(self: Arc, cv: CommunicationValue) { @@ -1096,7 +1244,10 @@ impl OmikronConnection { } } - let msg_id = cv.get_id(); + let Some(msg_id) = cv.id() else { + self.handle_message_impl(cv).await; + return; + }; if let Some((_, task)) = WAITING_TASKS.remove(&msg_id) { if (task.task)(cv.clone()) { @@ -1112,6 +1263,12 @@ impl OmikronConnection { self.handle_relay(cv).await; return; } + if cv.require_id().is_err() { + let _ = self + .send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } if matches!( iota_connection::relay::message_security_class(&cv), @@ -1191,7 +1348,7 @@ impl OmikronConnection { } let acknowledgement = CommunicationValue::new(CommunicationType::EraseHostedUserDataAck) - .with_id(cv.get_id()) + .with_request_id(cv) .add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into())); let _ = self.send_message(&acknowledgement).await; } @@ -1203,7 +1360,15 @@ impl OmikronConnection { } async fn handle_app_identification(self: Arc, cv: &CommunicationValue) { - let sender_id = cv.get_sender(); + let sender_id = match cv.require_sender() { + Ok(sender_id) => sender_id, + Err(_) => { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } + }; let app_identifier = cv .get_data(DataType::AppIdentifier) .as_str() @@ -1214,7 +1379,12 @@ impl OmikronConnection { .as_str() .unwrap_or("") .to_string(); - let user_id = cv.get_data(DataType::UserId).as_number().unwrap_or(0) as i64; + let Some(user_id) = data_i64(cv, DataType::UserId).filter(|id| *id > 0) else { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; + return; + }; let mut trusted = false; if let Some(user) = iota_storage::users::user_manager::get_user(user_id) { @@ -1235,7 +1405,8 @@ impl OmikronConnection { if let Some(app_pub_bundle) = iota_util::crypto_helper::public_key_bundle_from_base64(&app_public_key) { - if let Ok(keyring) = mtp::files::load_keyring_raw(identity_path()) { + let keyring = self.keyring.read().await.as_ref().cloned(); + if let Some(keyring) = keyring { if let Ok(encrypted_challenge) = crypto_util::encrypt_challenge(&challenge, &app_pub_bundle) { @@ -1243,7 +1414,7 @@ impl OmikronConnection { let pub_k_b64 = crypto_helper::public_key_bundle_to_base64(&bundle); let res = CommunicationValue::new(CommunicationType::AppChallenge) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(sender_id) .add_typed_default(DataType::PublicKey, DataValue::Str(pub_k_b64)) .add_typed_default( @@ -1259,18 +1430,26 @@ impl OmikronConnection { } let res = CommunicationValue::new(CommunicationType::ErrorInvalidChallenge) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(sender_id); let _ = self.send_message(&res).await; } async fn handle_app_challenge_response(self: Arc, cv: &CommunicationValue) { - let sender_id = cv.get_sender(); + let sender_id = match cv.require_sender() { + Ok(sender_id) => sender_id, + Err(_) => { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } + }; if let Some((_, expected_challenge)) = self.app_challenges.remove(&sender_id) { if let Some(DataValue::Str(response)) = cv.get_data(DataType::Challenge) { if expected_challenge == *response { let res = CommunicationValue::new(CommunicationType::AppIdentificationResponse) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(sender_id); let _ = self.send_message(&res).await; return; @@ -1278,13 +1457,21 @@ impl OmikronConnection { } } let res = CommunicationValue::new(CommunicationType::ErrorInvalidChallenge) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(sender_id); let _ = self.send_message(&res).await; } async fn handle_save_app_data(self: Arc, cv: &CommunicationValue) { - let sender_id = cv.get_sender(); + let sender_id = match cv.require_sender() { + Ok(sender_id) => sender_id, + Err(_) => { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } + }; let app_data = cv .get_data(DataType::AppData) .as_str() @@ -1297,13 +1484,21 @@ impl OmikronConnection { } let res = CommunicationValue::new(CommunicationType::SaveAppData) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(sender_id); let _ = self.send_message(&res).await; } async fn handle_load_app_data(self: Arc, cv: &CommunicationValue) { - let sender_id = cv.get_sender(); + let sender_id = match cv.require_sender() { + Ok(sender_id) => sender_id, + Err(_) => { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; + return; + } + }; let mut app_data = String::new(); if let Some(session) = self.app_sessions.get(&sender_id) { @@ -1312,7 +1507,7 @@ impl OmikronConnection { } let res = CommunicationValue::new(CommunicationType::LoadAppData) - .with_id(cv.get_id()) + .with_request_id(cv) .with_receiver(sender_id) .add_typed_default(DataType::AppData, DataValue::Str(app_data)); let _ = self.send_message(&res).await; @@ -1353,9 +1548,9 @@ impl OmikronConnection { extra: Vec<(DataType, DataValue)>, ) -> CommunicationValue { let mut message = CommunicationValue::new(ty) - .with_id(request.get_id()) - .with_sender(mutation.sender_id as u64) - .with_receiver(mutation.partner_id as u64) + .with_request_id(request) + .with_sender(wire_user_id(mutation.sender_id)) + .with_receiver(wire_user_id(mutation.partner_id)) .add_typed_default( DataType::ChatPartnerId, DataValue::SignedNumber(mutation.sender_id as i128), @@ -1371,18 +1566,26 @@ impl OmikronConnection { } async fn persist_and_deliver_remote_edit(&self, cv: &CommunicationValue) { - let sender_id = match i64::try_from(cv.get_sender()) { - Ok(sender_id) => sender_id, - Err(_) => return, + let sender_id = match cv + .require_sender() + .ok() + .and_then(|id| i64::try_from(id).ok()) + { + Some(sender_id) => sender_id, + None => return, }; - let receiver_id = match i64::try_from(cv.get_receiver()) { - Ok(receiver_id) if receiver_id > 0 => receiver_id, + let receiver_id = match cv + .require_receiver() + .ok() + .and_then(|id| i64::try_from(id).ok()) + { + Some(receiver_id) if receiver_id > 0 => receiver_id, _ => return, }; let Some(send_time) = data_i64(cv, DataType::SendTime).filter(|time| *time > 0) else { return; }; - let Some(content) = cv.get_data(DataType::Content).as_str() else { + let Some(content) = cv.get_data(DataType::AppContent).as_str() else { return; }; if chat_files::apply_remote_edit(receiver_id, sender_id, send_time, sender_id, content) @@ -1393,12 +1596,20 @@ impl OmikronConnection { } async fn persist_and_deliver_remote_reaction(&self, cv: &CommunicationValue, add: bool) { - let sender_id = match i64::try_from(cv.get_sender()) { - Ok(sender_id) => sender_id, - Err(_) => return, + let sender_id = match cv + .require_sender() + .ok() + .and_then(|id| i64::try_from(id).ok()) + { + Some(sender_id) => sender_id, + None => return, }; - let receiver_id = match i64::try_from(cv.get_receiver()) { - Ok(receiver_id) if receiver_id > 0 => receiver_id, + let receiver_id = match cv + .require_receiver() + .ok() + .and_then(|id| i64::try_from(id).ok()) + { + Some(receiver_id) if receiver_id > 0 => receiver_id, _ => return, }; let Some(send_time) = data_i64(cv, DataType::SendTime).filter(|time| *time > 0) else { @@ -1421,12 +1632,20 @@ impl OmikronConnection { } async fn persist_and_deliver_remote_delete(&self, cv: &CommunicationValue) { - let sender_id = match i64::try_from(cv.get_sender()) { - Ok(sender_id) => sender_id, - Err(_) => return, + let sender_id = match cv + .require_sender() + .ok() + .and_then(|id| i64::try_from(id).ok()) + { + Some(sender_id) => sender_id, + None => return, }; - let receiver_id = match i64::try_from(cv.get_receiver()) { - Ok(receiver_id) if receiver_id > 0 => receiver_id, + let receiver_id = match cv + .require_receiver() + .ok() + .and_then(|id| i64::try_from(id).ok()) + { + Some(receiver_id) if receiver_id > 0 => receiver_id, _ => return, }; let Some(send_time) = data_i64(cv, DataType::SendTime).filter(|time| *time > 0) else { @@ -1449,7 +1668,7 @@ impl OmikronConnection { .await; return; }; - let Some(content) = cv.get_data(DataType::Content).as_str() else { + let Some(content) = cv.get_data(DataType::AppContent).as_str() else { let _ = self .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) .await; @@ -1459,7 +1678,7 @@ impl OmikronConnection { CommunicationType::MessageEditLive, cv, &mutation, - vec![(DataType::Content, DataValue::Str(content.to_string()))], + vec![(DataType::AppContent, DataValue::Str(content.to_string()))], ); if iota_storage::users::user_manager::get_user(mutation.partner_id).is_some() && chat_files::apply_remote_edit( @@ -1558,9 +1777,13 @@ impl OmikronConnection { } async fn handle_message_delete_live(self: Arc, cv: &CommunicationValue) { - let sender_id = match i64::try_from(cv.get_sender()) { - Ok(sender_id) => sender_id, - Err(_) => return, + let sender_id = match cv + .require_sender() + .ok() + .and_then(|id| i64::try_from(id).ok()) + { + Some(sender_id) => sender_id, + None => return, }; if iota_storage::users::user_manager::get_user(sender_id).is_none() { self.persist_and_deliver_remote_delete(cv).await; @@ -1741,7 +1964,9 @@ impl OmikronConnection { timeout_duration: Option, ) -> Result { let (tx, rx) = oneshot::channel(); - let msg_id = cv.get_id(); + let msg_id = cv + .require_id() + .map_err(|error| format!("cannot await response without a message id: {error}"))?; WAITING_TASKS.insert( msg_id, @@ -1858,6 +2083,8 @@ impl OmikronConnection { /// recovery does not silently destroy the user's previous identity. pub async fn rotate_identity(self: &Arc) -> Result<(), OmikronError> { log!("Iota identity rotation requested"); + let identity_secret = + load_identity_secret().map_err(|error| OmikronError::Internal(error.to_string()))?; self.stop().await; let path = identity_path(); @@ -1877,7 +2104,10 @@ impl OmikronConnection { } let keyring = crypto_helper::generate_keyring(); - if let Some(parent) = path.parent() { + if let Some(parent) = path + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + { std::fs::create_dir_all(parent).map_err(|error| { OmikronError::Internal(format!( "could not create identity directory {}: {error}", @@ -1885,7 +2115,7 @@ impl OmikronConnection { )) })?; } - mtp::files::save_keyring_raw(&keyring, path).map_err(|error| { + save_protected_keyring_verified(&keyring, path, &identity_secret).map_err(|error| { OmikronError::Internal(format!( "could not save new identity {}: {error}", path.display() @@ -2052,3 +2282,80 @@ impl OmikronClient for OmikronConnection { Self::is_connected(self).await } } + +#[cfg(test)] +mod identity_tests { + use super::*; + + fn test_path(name: &str) -> PathBuf { + std::env::temp_dir().join(format!( + "iota-identity-{name}-{}-{}", + std::process::id(), + Uuid::new_v4() + )) + } + + #[test] + fn generated_identity_is_protected_and_survives_reload() { + let path = test_path("reload"); + let passphrase = b"test identity secret"; + let keyring = load_or_migrate_keyring_at(&path, None, passphrase).expect("identity saves"); + let reloaded = load_or_migrate_keyring_at(&path, None, passphrase).expect("identity loads"); + assert_eq!( + keyring.try_to_bytes().expect("keyring serializes"), + reloaded.try_to_bytes().expect("keyring serializes") + ); + assert!(mtp::files::load_keyring(&path, b"wrong secret").is_err()); + let _ = fs::remove_file(path); + } + + #[test] + fn corrupt_existing_identity_does_not_generate_a_replacement() { + let path = test_path("corrupt"); + fs::write(&path, b"not a keyring").expect("corrupt fixture writes"); + let error = load_or_migrate_keyring_at(&path, None, b"test identity secret") + .expect_err("corrupt identity must fail"); + assert!(matches!(error, IdentityError::Storage(_))); + let _ = fs::remove_file(path); + } + + #[test] + fn legacy_raw_identity_is_migrated_only_when_the_raw_format_is_valid() { + let path = test_path("legacy"); + let keyring = crypto_helper::generate_keyring(); + let mut raw = b"MTMK".to_vec(); + raw.push(1); + raw.extend_from_slice(&keyring.try_to_bytes().expect("keyring serializes")); + fs::write(&path, raw).expect("legacy fixture writes"); + + let migrated = load_or_migrate_keyring_at(&path, None, b"test identity secret") + .expect("legacy identity migrates"); + assert_eq!( + migrated.try_to_bytes().expect("keyring serializes"), + keyring.try_to_bytes().expect("keyring serializes") + ); + let _ = fs::remove_file(path); + } + + #[test] + fn identity_directory_failure_is_returned() { + let parent = test_path("parent-file"); + fs::write(&parent, b"not a directory").expect("parent fixture writes"); + let path = parent.join("iota.mk"); + let error = load_or_migrate_keyring_at(&path, None, b"test identity secret") + .expect_err("directory failure must be returned"); + assert!(matches!(error, IdentityError::Directory(_))); + let _ = fs::remove_file(parent); + } + + #[test] + fn reconnect_jitter_stays_bounded_by_the_exponential_delay_ceiling() { + for _ in 0..32 { + let delay = jittered_reconnect_delay(Duration::from_secs(5)); + assert!(delay >= Duration::from_secs(4)); + assert!(delay <= Duration::from_secs(6)); + } + + assert!(jittered_reconnect_delay(Duration::from_secs(600)) <= MAX_RECONNECT_DELAY); + } +} diff --git a/omikron-connector/src/user_ops.rs b/omikron-connector/src/user_ops.rs index 3008120..60f8429 100644 --- a/omikron-connector/src/user_ops.rs +++ b/omikron-connector/src/user_ops.rs @@ -377,7 +377,7 @@ mod tests { use super::{CreateUserError, request_user_id, valid_username}; use crate::{OmikronClient, OmikronError}; use async_trait::async_trait; - use iota_util::mtp_compat::CommunicationValueCompat; + use iota_connection::message_common::CommunicationResponseExt; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use std::time::Duration; @@ -397,7 +397,7 @@ mod tests { _: Duration, ) -> Result { assert!(request.is_type(CommunicationType::GetRegister)); - Ok(self.response.clone().with_id(request.get_id())) + Ok(self.response.clone().with_request_id(request)) } async fn reconnect(&self) -> Result<(), OmikronError> { diff --git a/other-iota/Cargo.toml b/other-iota/Cargo.toml index a6933c3..1fb5337 100644 --- a/other-iota/Cargo.toml +++ b/other-iota/Cargo.toml @@ -4,57 +4,3 @@ version = "0.1.0" edition = "2024" [dependencies] -mtp = { git = "https://git.methanium.net/Methanium/mtp.git" } -iota-logger = { path = "../iota-logger" } -iota-util = { path = "../iota-util" } -iota-storage = { path = "../iota-storage" } -iota-state = { path = "../iota-state" } -iota-auth = { path = "../iota-auth" } - -actix-web = { version = "4", features = ["rustls-0_23"] } -actix-web-actors = "4" -aes-gcm = "0.10.3" -async-trait = "0.1.89" -base64 = "0.22.1" -chrono = "0.4.43" -crossterm = "*" -dashmap = "6.1.0" -futures = "*" -futures-util = "*" -hex = "*" -hkdf = "0.12.4" -hyper = { version = "1.8.1", features = [ - "capi", - "client", - "full", - "http1", - "http2", - "nightly", - "server", -] } -hyper-util = { version = "*" } -json = "*" -lazy_static = "1.5.0" -once_cell = "1.21.3" -open = "5.3.3" -pnet = "0.35.0" -rand = "0.8" -rand_core = { version = "0.6", features = ["getrandom", "std"] } -ratatui = "0.30.0" -reqwest = "0.13.2" -rusqlite = "0.40.0" -rustls = { version = "0.23.37", features = ["aws-lc-rs"] } -rustls-pemfile = "2.2.0" -serde_json = "1.0.149" -sha2 = "0.11.0" -strum = "0.28.0" -strum_macros = "0.28.0" -sysinfo = "0.39.0" -tokio = { version = "1.50.0", features = ["full"] } -tokio-tungstenite = { version = "*", features = ["native-tls"] } -tungstenite = "*" -uuid = { version = "*", features = ["v4"] } -walkdir = "2.5.0" -warp = "*" -x448 = { version = "*" } -zip = "6.0.0" diff --git a/systemd/iota-daemon.service b/systemd/iota-daemon.service index a5a2903..2c6340b 100644 --- a/systemd/iota-daemon.service +++ b/systemd/iota-daemon.service @@ -17,6 +17,7 @@ Environment=IOTA_SOCKET=/run/iota/iota.sock Environment=IOTA_DATA_DIR=/var/lib/iota Environment=IOTA_DEPLOYMENT_MODE=system_always_on Environment=IOTA_SUPERVISOR=systemd +LoadCredential=iota-identity:/etc/iota/iota-identity.secret # Exit code 75 = restart requested (daemon-specific convention) RestartPreventExitStatus=0 diff --git a/web-server/Cargo.toml b/web-server/Cargo.toml index ad49fba..c07501f 100644 --- a/web-server/Cargo.toml +++ b/web-server/Cargo.toml @@ -9,6 +9,5 @@ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["web-s bytes = "1" http = "1" iota-logger = { path = "../iota-logger" } -iota-util = { path = "../iota-util" } tokio = { version = "1.50.0", features = ["full"] } tokio-util = { version = "0.7", features = ["rt"] } diff --git a/web-ui/Cargo.toml b/web-ui/Cargo.toml index 988aac1..f6f6ff8 100644 --- a/web-ui/Cargo.toml +++ b/web-ui/Cargo.toml @@ -4,57 +4,13 @@ version = "0.1.0" edition = "2024" [dependencies] -omikron-connector = { path = "../omikron-connector" } iota-storage = { path = "../iota-storage" } iota-state = { path = "../iota-state" } iota-util = { path = "../iota-util" } iota-logger = { path = "../iota-logger" } -mtp = { git = "https://git.methanium.net/Methanium/mtp.git" } actix-web = { version = "4", features = ["rustls-0_23"] } -actix-web-actors = "4" -aes-gcm = "0.10.3" -async-trait = "0.1.89" -base64 = "0.22.1" -chrono = "0.4.43" -crossterm = "*" -dashmap = "6.1.0" -futures = "*" -futures-util = "*" -hex = "*" -hkdf = "0.12.4" -hyper = { version = "1.8.1", features = [ - "capi", - "client", - "full", - "http1", - "http2", - "nightly", - "server", -] } -hyper-util = { version = "*" } -json = "*" -lazy_static = "1.5.0" -once_cell = "1.21.3" -open = "5.3.3" -pnet = "0.35.0" -rand = "0.8" -rand_core = { version = "0.6", features = ["getrandom", "std"] } -ratatui = "0.30.0" -reqwest = "0.13.2" -rusqlite = "0.40.0" rustls = { version = "0.23.37", features = ["aws-lc-rs"] } rustls-pemfile = "2.2.0" serde_json = "1.0.149" -sha2 = "0.11.0" -strum = "0.28.0" -strum_macros = "0.28.0" -sysinfo = "0.39.0" tokio = { version = "1.50.0", features = ["full"] } -tokio-tungstenite = { version = "*", features = ["native-tls"] } -tungstenite = "*" -uuid = { version = "*", features = ["v4"] } -walkdir = "2.5.0" -warp = "*" -x448 = { version = "*" } -zip = "6.0.0"