Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9af194451d | |||
|
|
4caa6bb3e9 |
||
|
|
8160f8d0cb |
36 changed files with 860 additions and 355 deletions
147
Cargo.lock
generated
147
Cargo.lock
generated
|
|
@ -86,9 +86,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "actix-rt"
|
||||
version = "2.11.0"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63"
|
||||
checksum = "c25da0441692de4ad67950cb7ed6c9ce4b669a6609525e547566c2e1ab4d695c"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"tokio",
|
||||
|
|
@ -96,9 +96,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "actix-server"
|
||||
version = "2.7.0"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3716aae056e2f869b7b5cfd8a08fcf98890f8455bec61d69c7dff5d8576f9d2b"
|
||||
checksum = "cc8dcb6fa613d47c3b764a7dc672b4b31f25751dcbcf542f7b16f3e1f700044c"
|
||||
dependencies = [
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
|
|
@ -152,9 +152,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "actix-web"
|
||||
version = "4.14.1"
|
||||
version = "4.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58356675d8c86d2e720480645a0316808471a62d0073f6a3b98810a5e0ca0e73"
|
||||
checksum = "bbacab3593b6b4f7be815076fc52d60a83c873426824675417e2abdd229e2e36"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-http",
|
||||
|
|
@ -623,9 +623,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.4.3"
|
||||
version = "1.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
|
||||
checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
|
|
@ -1034,9 +1034,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.24.0"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88490bf1b990d87eaaa7ac8aa887f629a08e7359765b4911faf63c3763347d23"
|
||||
checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
|
|
@ -1044,9 +1044,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.24.0"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "084e274f91c482280130e1e34e0b8d6e66776a060d7b6de7b84289ca778868c4"
|
||||
checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
|
||||
dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
|
|
@ -1057,9 +1057,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.24.0"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68f5792fa0d41cd2325ce0ffa64f0a340eaebd4971a3a0c5e1ffd2cc488a355e"
|
||||
checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
|
|
@ -1196,16 +1196,6 @@ dependencies = [
|
|||
"ctutils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.7"
|
||||
|
|
@ -1283,9 +1273,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.17.0"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
|
||||
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
|
|
@ -1604,9 +1594,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.16"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27"
|
||||
checksum = "839c0e8a181239723652be9062bb56ca5bf5f64011f73b623f6f4fc59086a228"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
|
|
@ -1835,7 +1825,7 @@ dependencies = [
|
|||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"h2 0.4.16",
|
||||
"h2 0.4.18",
|
||||
"http 1.5.0",
|
||||
"http-body",
|
||||
"httparse",
|
||||
|
|
@ -1981,9 +1971,9 @@ checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
|
|||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428"
|
||||
checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
|
|
@ -2250,7 +2240,7 @@ name = "iota-storage"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"base64 0.23.1",
|
||||
"base64 0.22.1",
|
||||
"iota-logger",
|
||||
"iota-paths",
|
||||
"iota-util",
|
||||
|
|
@ -2270,8 +2260,8 @@ name = "iota-terms"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"iota-util",
|
||||
"json",
|
||||
"reqwest",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2293,12 +2283,13 @@ dependencies = [
|
|||
name = "iota-util"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"base64 0.22.1",
|
||||
"hex",
|
||||
"iota-paths",
|
||||
"mtp",
|
||||
"reqwest",
|
||||
"sysinfo",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
|
|
@ -2458,9 +2449,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "keccak"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffd9697dc4a9a62e2da93389f34400b77a28f0287711263cabb203b3ccb9c0e4"
|
||||
checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
|
|
@ -2717,7 +2708,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "mtp"
|
||||
version = "0.3.0"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#c30315af944bda05ecdb9c4e9cc350cb99cf7b2e"
|
||||
dependencies = [
|
||||
"mtp-client",
|
||||
"mtp-codec",
|
||||
|
|
@ -2733,7 +2724,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "mtp-client"
|
||||
version = "0.3.0"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#c30315af944bda05ecdb9c4e9cc350cb99cf7b2e"
|
||||
dependencies = [
|
||||
"mtp-codec",
|
||||
"mtp-common",
|
||||
|
|
@ -2746,7 +2737,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "mtp-codec"
|
||||
version = "0.3.0"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#c30315af944bda05ecdb9c4e9cc350cb99cf7b2e"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"byteorder",
|
||||
|
|
@ -2760,10 +2751,9 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "mtp-common"
|
||||
version = "0.3.0"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#c30315af944bda05ecdb9c4e9cc350cb99cf7b2e"
|
||||
dependencies = [
|
||||
"quinn",
|
||||
"rustls",
|
||||
"thiserror 2.0.20",
|
||||
"wtransport",
|
||||
]
|
||||
|
|
@ -2771,7 +2761,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "mtp-crypto"
|
||||
version = "0.3.0"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#c30315af944bda05ecdb9c4e9cc350cb99cf7b2e"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"base64 0.22.1",
|
||||
|
|
@ -2794,7 +2784,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "mtp-files"
|
||||
version = "0.3.0"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#c30315af944bda05ecdb9c4e9cc350cb99cf7b2e"
|
||||
dependencies = [
|
||||
"mtp-crypto",
|
||||
"rand 0.10.2",
|
||||
|
|
@ -2805,7 +2795,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "mtp-host"
|
||||
version = "0.3.0"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#c30315af944bda05ecdb9c4e9cc350cb99cf7b2e"
|
||||
dependencies = [
|
||||
"mtp-codec",
|
||||
"mtp-common",
|
||||
|
|
@ -2821,7 +2811,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "mtp-transport"
|
||||
version = "0.3.0"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#c30315af944bda05ecdb9c4e9cc350cb99cf7b2e"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"mtp-codec",
|
||||
|
|
@ -2841,7 +2831,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "mtp-type-map"
|
||||
version = "0.3.0"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#c30315af944bda05ecdb9c4e9cc350cb99cf7b2e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
|
|
@ -2850,7 +2840,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "mtp-webserver"
|
||||
version = "0.3.0"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#a5c8d4f0c898c78351e9d54124886c86e789a22a"
|
||||
source = "git+https://git.methanium.net/Methanium/mtp.git#c30315af944bda05ecdb9c4e9cc350cb99cf7b2e"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
|
|
@ -2967,15 +2957,6 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
|
||||
dependencies = [
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.2"
|
||||
|
|
@ -2983,24 +2964,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||
|
||||
[[package]]
|
||||
name = "objc2-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3013,17 +2976,6 @@ dependencies = [
|
|||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-open-directory"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "octets"
|
||||
version = "0.3.6"
|
||||
|
|
@ -3044,7 +2996,7 @@ name = "omikron-connector"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.23.1",
|
||||
"base64 0.22.1",
|
||||
"dashmap",
|
||||
"iota-connection",
|
||||
"iota-logger",
|
||||
|
|
@ -3800,7 +3752,7 @@ dependencies = [
|
|||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"h2 0.4.16",
|
||||
"h2 0.4.18",
|
||||
"http 1.5.0",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
|
|
@ -3980,9 +3932,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
|||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.14"
|
||||
version = "0.103.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a"
|
||||
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
|
|
@ -4193,7 +4145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a"
|
||||
dependencies = [
|
||||
"digest 0.11.3",
|
||||
"keccak 0.2.1",
|
||||
"keccak 0.2.2",
|
||||
"sponge-cursor",
|
||||
]
|
||||
|
||||
|
|
@ -4441,16 +4393,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.39.6"
|
||||
version = "0.38.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6"
|
||||
checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"memchr",
|
||||
"ntapi",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-kit",
|
||||
"objc2-open-directory",
|
||||
"windows",
|
||||
]
|
||||
|
||||
|
|
@ -4482,7 +4433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.3",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
|
|
@ -5577,9 +5528,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.7"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8"
|
||||
checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
|
|
@ -5588,9 +5539,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.5"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9"
|
||||
checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ impl ClientConnection {
|
|||
return;
|
||||
};
|
||||
|
||||
let encrypted_challenge = cv.get_data(DataType::Challenge).as_str().unwrap();
|
||||
let Some(encrypted_challenge) = cv.get_data(DataType::Challenge).as_str() else { return };
|
||||
|
||||
let solved = crypto_util::decrypt_challenge(encrypted_challenge, &keyring).ok();
|
||||
|
||||
|
|
|
|||
|
|
@ -6,28 +6,11 @@ edition = "2024"
|
|||
[dependencies]
|
||||
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
|
||||
iota-util = { path = "../iota-util" }
|
||||
aes-gcm = "0.10.3"
|
||||
async-trait = "0.1.89"
|
||||
base64 = "0.23.0"
|
||||
|
||||
futures = "*"
|
||||
hkdf = "0.12.4"
|
||||
hyper = { version = "1.8.1", features = [
|
||||
"capi",
|
||||
"client",
|
||||
"full",
|
||||
"http1",
|
||||
"http2",
|
||||
"nightly",
|
||||
"server",
|
||||
] }
|
||||
hyper-util = { version = "*" }
|
||||
json = "*"
|
||||
once_cell = "1.21.3"
|
||||
rand = "0.8"
|
||||
rand_core = { version = "0.6", features = ["getrandom", "std"] }
|
||||
sha2 = "0.11.0"
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
tokio-tungstenite = { version = "*", features = ["native-tls"] }
|
||||
tungstenite = "*"
|
||||
uuid = { version = "*", features = ["v4"] }
|
||||
x448 = { version = "*" }
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ impl Community {
|
|||
let mut buf = [0u8; 56];
|
||||
let mut rng = OsRng;
|
||||
rng.fill_bytes(&mut buf);
|
||||
let private_key = Secret::from_bytes(&buf).unwrap();
|
||||
let private_key = Secret::from(buf);
|
||||
let public_key = PublicKey::from(&private_key);
|
||||
Community {
|
||||
name: String::new(),
|
||||
|
|
@ -58,7 +58,7 @@ impl Community {
|
|||
let mut buf = [0u8; 56];
|
||||
let mut rng = OsRng;
|
||||
rng.fill_bytes(&mut buf);
|
||||
let private_key = Secret::from_bytes(&buf).unwrap();
|
||||
let private_key = Secret::from(buf);
|
||||
let public_key = PublicKey::from(&private_key);
|
||||
let c = Community {
|
||||
name,
|
||||
|
|
@ -125,7 +125,7 @@ impl Community {
|
|||
self.members.clone()
|
||||
}
|
||||
pub fn get_private_key(&self) -> Secret {
|
||||
Secret::from_bytes(self.private_key.as_bytes()).unwrap()
|
||||
Secret::from(*self.private_key.as_bytes())
|
||||
}
|
||||
pub fn get_public_key(&self) -> &PublicKey {
|
||||
&self.public_key
|
||||
|
|
@ -222,12 +222,14 @@ impl Community {
|
|||
for interactable in target_interactables.iter() {
|
||||
if interactable.get_name() == name {
|
||||
if interactable.get_codec() == "category" {
|
||||
let category: &Category =
|
||||
interactable.as_any().downcast_ref::<Category>().unwrap();
|
||||
let Some(category) = interactable.as_any().downcast_ref::<Category>() else {
|
||||
return CommunicationValue::new(CommunicationType::ErrorInternal);
|
||||
};
|
||||
// cannot move a value of type dyn Interactable the size of dyn Interactable cannot be statically determined (rustc E0161)
|
||||
return category
|
||||
.get_child(path.to_string(), name.to_string())
|
||||
.unwrap()
|
||||
.ok_or(CommunicationValue::new(CommunicationType::ErrorInternal))
|
||||
.unwrap_or_else(|error| return error)
|
||||
.run_function(cv.clone())
|
||||
.await;
|
||||
} else {
|
||||
|
|
@ -267,7 +269,7 @@ impl Community {
|
|||
let mut data = JsonValue::new_object();
|
||||
|
||||
let mut permissions = JsonValue::new_array();
|
||||
for perm in self.permissions.get(user).unwrap() {
|
||||
for perm in self.permissions.get(user).into_iter().flatten() {
|
||||
if let Ok(_) = permissions.push(perm.to_string()) {}
|
||||
}
|
||||
|
||||
|
|
@ -287,10 +289,10 @@ impl Community {
|
|||
}
|
||||
pub async fn load(name: &String) -> Option<Arc<Community>> {
|
||||
let file_contents = file_util::load_file(&format!("communities/{}/", name), "config.json");
|
||||
let json_content = json::parse(&file_contents).unwrap();
|
||||
let json_content = json::parse(&file_contents).ok()?;
|
||||
|
||||
let user_data = file_util::load_file(&format!("communities/{}/", name), "users.json");
|
||||
let user_json: JsonValue = json::parse(&user_data).unwrap();
|
||||
let user_json: JsonValue = json::parse(&user_data).ok()?;
|
||||
let mut users = Vec::new();
|
||||
let mut permissions: HashMap<i64, Vec<Permission>> = HashMap::new();
|
||||
|
||||
|
|
@ -318,25 +320,17 @@ pub async fn load(name: &String) -> Option<Arc<Community>> {
|
|||
};
|
||||
|
||||
let community = Community {
|
||||
name: json_content["name"].as_str().unwrap().to_string(),
|
||||
name: json_content["name"].as_str()?.to_string(),
|
||||
owner_id: Arc::new(RwLock::new(json_content["owner_id"].as_i64().unwrap_or(0))),
|
||||
members: users,
|
||||
roles,
|
||||
permissions,
|
||||
private_key: Secret::from_bytes(
|
||||
&STANDARD
|
||||
.decode(json_content["private_key"].as_str().unwrap())
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
public_key: PublicKey::from(
|
||||
&Secret::from_bytes(
|
||||
&STANDARD
|
||||
.decode(json_content["private_key"].as_str().unwrap())
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
&STANDARD.decode(json_content["private_key"].as_str()?).ok()?,
|
||||
)?,
|
||||
public_key: PublicKey::from(&Secret::from_bytes(
|
||||
&STANDARD.decode(json_content["private_key"].as_str()?).ok()?,
|
||||
)?),
|
||||
interactables: Arc::new(RwLock::new(Vec::new())),
|
||||
connections: Arc::new(RwLock::new(HashMap::new())),
|
||||
};
|
||||
|
|
@ -346,7 +340,7 @@ pub async fn load(name: &String) -> Option<Arc<Community>> {
|
|||
file_util::get_children(&format!("communities/{}/interactables/", name));
|
||||
for file in interactable_files {
|
||||
if file.contains(".json") {
|
||||
let name = file.split('.').next().unwrap().to_string();
|
||||
let Some(name) = file.split('.').next().map(str::to_string) else { continue };
|
||||
let interactable: Box<dyn Interactable> =
|
||||
registry::load(comarc.clone(), String::new(), name).await;
|
||||
comarc.add_interactable(Arc::new(interactable)).await;
|
||||
|
|
|
|||
|
|
@ -65,7 +65,9 @@ impl CommunityConnection {
|
|||
pub async fn send_message(&self, message: &CommunicationValue) {
|
||||
let mut sender = self.sender.write().await; // Access the SplitSink
|
||||
let message_text = Message::Text(Utf8Bytes::from(message.to_json().to_string()));
|
||||
sender.send(message_text).await.unwrap(); // Send the message via the SplitSink
|
||||
if let Err(error) = sender.send(message_text).await {
|
||||
log::error!("failed to send community message: {error}");
|
||||
}
|
||||
}
|
||||
pub async fn get_community(&self) -> Option<Arc<Community>> {
|
||||
self.community.read().await.clone()
|
||||
|
|
@ -107,14 +109,15 @@ impl CommunityConnection {
|
|||
}
|
||||
}
|
||||
async fn handle_function(&self, cv: CommunicationValue) {
|
||||
let name = cv.get_data(DataType::Name).unwrap().as_str().unwrap();
|
||||
let path = cv.get_data(DataType::Path).unwrap().as_str().unwrap();
|
||||
let function = cv.get_data(DataType::Function).unwrap().as_str().unwrap();
|
||||
let Some(name) = cv.get_data(DataType::Name).as_str() else { return };
|
||||
let Some(path) = cv.get_data(DataType::Path).as_str() else { return };
|
||||
let Some(function) = cv.get_data(DataType::Function).as_str() else { return };
|
||||
|
||||
let result = self
|
||||
.get_community()
|
||||
.await
|
||||
.unwrap()
|
||||
.ok_or(())
|
||||
.unwrap_or_else(|_| return)
|
||||
.run_function(self.get_user_id().await, name, path, function, &cv)
|
||||
.await;
|
||||
|
||||
|
|
@ -381,13 +384,9 @@ impl CommunityConnection {
|
|||
pub async fn handle_close(self: Arc<Self>) {
|
||||
if self.is_identified().await {
|
||||
if self.get_user_id().await != 0 {
|
||||
self.community
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.remove_connection(self.clone())
|
||||
.await;
|
||||
if let Some(community) = self.community.read().await.as_ref() {
|
||||
community.remove_connection(self.clone()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,14 +30,13 @@ impl Category {
|
|||
.find(|child| child.get_name() == &name)
|
||||
.cloned()
|
||||
} else {
|
||||
let sub_module = path.split("/").next().unwrap();
|
||||
let sub_module = path.split('/').next()?;
|
||||
let next = self
|
||||
.children
|
||||
.iter()
|
||||
.find(|child| child.get_name() == sub_module)
|
||||
.unwrap();
|
||||
.find(|child| child.get_name() == sub_module)?;
|
||||
if next.get_codec() == "category" {
|
||||
let next_cat = next.as_any().downcast_ref::<Category>().unwrap();
|
||||
let next_cat = next.as_any().downcast_ref::<Category>()?;
|
||||
next_cat.get_child(path, name)
|
||||
} else {
|
||||
Some(next.clone())
|
||||
|
|
|
|||
|
|
@ -3,11 +3,17 @@ name = "iota-cli"
|
|||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
legacy-commands = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
iota-state = { path = "../iota-state" }
|
||||
iota-terms = { path = "../iota-terms" }
|
||||
iota-ipc = { path = "../iota-ipc" }
|
||||
iota-paths = { path = "../iota-paths" }
|
||||
|
||||
|
||||
chrono = "0.4.43"
|
||||
crossterm = "*"
|
||||
once_cell = "1.21.3"
|
||||
|
|
|
|||
|
|
@ -94,19 +94,19 @@ impl Screen for TermsCheckerScreen {
|
|||
height: content_height,
|
||||
});
|
||||
let eula_text = if size.width < 70 {
|
||||
"EULA ¹ (https://legal.tensamin.net/eula/)"
|
||||
"EULA ¹ (https://legal.methanium.net/tensamin/eula)"
|
||||
} else {
|
||||
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/)"
|
||||
"End User Licence Agreement ¹ (https://legal.methanium.net/tensamin/eula)"
|
||||
};
|
||||
let tos_text = if size.width < 72 {
|
||||
"ToS ² (https://legal.tensamin.net/terms-of-service/)"
|
||||
"ToS ² (https://legal.methanium.net/tensamin/terms-of-service)"
|
||||
} else {
|
||||
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/)"
|
||||
"Terms of Service ² (https://legal.methanium.net/tensamin/terms-of-service)"
|
||||
};
|
||||
let pp_text = if size.width < 68 {
|
||||
"PP ² (https://legal.tensamin.net/privacy-policy/)"
|
||||
"PP ² (https://legal.methanium.net/tensamin/privacy-policy)"
|
||||
} else {
|
||||
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/)"
|
||||
"Privacy Policy ² (https://legal.methanium.net/tensamin/privacy-policy)"
|
||||
};
|
||||
|
||||
let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) = if size.width > 143 {
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ impl Screen for TermsUpdaterScreen {
|
|||
if self.eula_future {
|
||||
if size.width < 80 {
|
||||
text_lines.push(checkbox(
|
||||
"EULA ¹³ (https://legal.tensamin.net/eula/newest/)",
|
||||
"EULA ¹³ (https://legal.methanium.net/tensamin/eula)",
|
||||
self.eula,
|
||||
self.focus == Focus::Eula,
|
||||
true,
|
||||
|
|
@ -214,7 +214,7 @@ impl Screen for TermsUpdaterScreen {
|
|||
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
||||
} else {
|
||||
text_lines.push(checkbox(
|
||||
"End User Licence Agreement ¹³ (https://legal.tensamin.net/eula/newest/)",
|
||||
"End User Licence Agreement ¹³ (https://legal.methanium.net/tensamin/eula)",
|
||||
self.eula,
|
||||
self.focus == Focus::Eula,
|
||||
true,
|
||||
|
|
@ -229,14 +229,14 @@ impl Screen for TermsUpdaterScreen {
|
|||
} else {
|
||||
if size.width < 80 {
|
||||
text_lines.push(checkbox(
|
||||
"EULA ¹ (https://legal.tensamin.net/eula/newest/)",
|
||||
"EULA ¹ (https://legal.methanium.net/tensamin/eula)",
|
||||
self.eula,
|
||||
self.focus == Focus::Eula,
|
||||
true,
|
||||
));
|
||||
} else {
|
||||
text_lines.push(checkbox(
|
||||
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)",
|
||||
"End User Licence Agreement ¹ (https://legal.methanium.net/tensamin/eula)",
|
||||
self.eula,
|
||||
self.focus == Focus::Eula,
|
||||
true,
|
||||
|
|
@ -250,7 +250,7 @@ impl Screen for TermsUpdaterScreen {
|
|||
if self.tos_future {
|
||||
if size.width < 80 {
|
||||
text_lines.push(checkbox(
|
||||
"ToS ²³ (https://legal.tensamin.net/tos/newest/)",
|
||||
"ToS ²³ (https://legal.methanium.net/tensamin/terms-of-service)",
|
||||
self.tos,
|
||||
self.focus == Focus::Tos,
|
||||
self.eula,
|
||||
|
|
@ -263,7 +263,7 @@ impl Screen for TermsUpdaterScreen {
|
|||
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
||||
} else {
|
||||
text_lines.push(checkbox(
|
||||
"Terms of Service ²³ (https://legal.tensamin.net/terms-of-service/newest/)",
|
||||
"Terms of Service ²³ (https://legal.methanium.net/tensamin/terms-of-service)",
|
||||
self.tos,
|
||||
self.focus == Focus::Tos,
|
||||
self.eula,
|
||||
|
|
@ -278,14 +278,14 @@ impl Screen for TermsUpdaterScreen {
|
|||
} else {
|
||||
if size.width < 80 {
|
||||
text_lines.push(checkbox(
|
||||
"ToS ² (https://legal.tensamin.net/tos/newest/)",
|
||||
"ToS ² (https://legal.methanium.net/tensamin/terms-of-service)",
|
||||
self.tos,
|
||||
self.focus == Focus::Tos,
|
||||
self.eula,
|
||||
));
|
||||
} else {
|
||||
text_lines.push(checkbox(
|
||||
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)",
|
||||
"Terms of Service ² (https://legal.methanium.net/tensamin/terms-of-service)",
|
||||
self.tos,
|
||||
self.focus == Focus::Tos,
|
||||
self.eula,
|
||||
|
|
@ -299,7 +299,7 @@ impl Screen for TermsUpdaterScreen {
|
|||
if self.pp_future {
|
||||
if size.width < 80 {
|
||||
text_lines.push(checkbox(
|
||||
"PP ²³ (https://legal.tensamin.net/privacy-policy/newest/)",
|
||||
"PP ²³ (https://legal.methanium.net/tensamin/privacy-policy)",
|
||||
self.pp,
|
||||
self.focus == Focus::Pp,
|
||||
self.eula,
|
||||
|
|
@ -312,7 +312,7 @@ impl Screen for TermsUpdaterScreen {
|
|||
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
|
||||
} else {
|
||||
text_lines.push(checkbox(
|
||||
"Privacy Policy ²³ (https://legal.tensamin.net/privacy-policy/newest/)",
|
||||
"Privacy Policy ²³ (https://legal.methanium.net/tensamin/privacy-policy)",
|
||||
self.pp,
|
||||
self.focus == Focus::Pp,
|
||||
self.eula,
|
||||
|
|
@ -327,14 +327,14 @@ impl Screen for TermsUpdaterScreen {
|
|||
} else {
|
||||
if size.width < 80 {
|
||||
text_lines.push(checkbox(
|
||||
"PP ² (https://legal.tensamin.net/privacy-policy/newest/)",
|
||||
"PP ² (https://legal.methanium.net/tensamin/privacy-policy)",
|
||||
self.pp,
|
||||
self.focus == Focus::Pp,
|
||||
self.eula,
|
||||
));
|
||||
} else {
|
||||
text_lines.push(checkbox(
|
||||
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)",
|
||||
"Privacy Policy ² (https://legal.methanium.net/tensamin/privacy-policy)",
|
||||
self.pp,
|
||||
self.focus == Focus::Pp,
|
||||
self.eula,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ use iota_util::route_target::RouteTarget;
|
|||
use mtp::codec::{
|
||||
CommunicationValue, ProtectionPolicy, RelayError, RelayOpenOptions, SignaturePolicy, TypeMap,
|
||||
VerifiedRelayContent, VerifiedRelayMetadata, forward_relay_frame,
|
||||
open_relay_content_with_keyrings, open_relay_metadata_with_without_replay,
|
||||
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,
|
||||
};
|
||||
|
|
@ -169,7 +171,7 @@ where
|
|||
&[keyring],
|
||||
Some(claimed_signer),
|
||||
move |signer_id| (signer_id == claimed_signer).then(|| resolver_keys.clone()),
|
||||
open_options,
|
||||
RelayOpenOptions::new(RELAY_PROTECTION_POLICY),
|
||||
)?;
|
||||
|
||||
let context = VerifiedRelayContext {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ use std::sync::Arc;
|
|||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use iota_cli::screens::terms_checker::{TermsCheckerScreen, UserChoice};
|
||||
use iota_cli::screens::terms_updater::{TermsUpdaterScreen, UpdateDecision};
|
||||
use iota_cli::ui::UI;
|
||||
use iota_terms::{Doc, TermsType as Type, get_current_docs, get_newest_docs};
|
||||
use iota_terms::{Doc, TermsType as Type, get_current_docs};
|
||||
use iota_util::file_util::{load_file, save_file};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
|
|
@ -12,9 +11,11 @@ pub async fn check(ui: Arc<UI>) -> Result<(bool, bool), String> {
|
|||
let mut state = ConsentState::load_state();
|
||||
|
||||
ensure_initial_consent(ui.clone(), &mut state).await?;
|
||||
// A mandatory document update is a hard bootstrap gate. In particular,
|
||||
// refusing it must not allow service setup or daemon access to continue.
|
||||
ensure_updates(ui, &mut state).await?;
|
||||
/*
|
||||
* The raw legal endpoint exposes only the current document. Restore this
|
||||
* flow when it provides future versions that users can accept early.
|
||||
*/
|
||||
// ensure_updates(ui, &mut state).await?;
|
||||
|
||||
state = state.sanitize();
|
||||
state.save_state();
|
||||
|
|
@ -42,11 +43,9 @@ async fn ensure_initial_consent(ui: Arc<UI>, state: &mut ConsentState) -> Result
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let docs = get_current_docs().await;
|
||||
if docs.is_none() {
|
||||
return Err("Could not connect to the legal endpoint to fetch the current agreements. Please check your internet connection.".to_string());
|
||||
}
|
||||
let (current_eula, current_tos, current_privacy) = docs.unwrap();
|
||||
let (current_eula, current_tos, current_privacy) = get_current_docs().await.ok_or_else(|| {
|
||||
"Could not connect to the legal endpoint to fetch the current agreements. Please check your internet connection.".to_string()
|
||||
})?;
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
|
|
@ -73,6 +72,7 @@ async fn ensure_initial_consent(ui: Arc<UI>, state: &mut ConsentState) -> Result
|
|||
UserChoice::Deny => Ok(()),
|
||||
}
|
||||
}
|
||||
/*
|
||||
async fn ensure_updates(ui: Arc<UI>, state: &mut ConsentState) -> Result<(), String> {
|
||||
let Some((eula_update, tos_update, privacy_update)) = get_updates().await else {
|
||||
return Ok(());
|
||||
|
|
@ -115,6 +115,8 @@ async fn ensure_updates(ui: Arc<UI>, state: &mut ConsentState) -> Result<(), Str
|
|||
state.save_state();
|
||||
Ok(())
|
||||
}
|
||||
*/
|
||||
/*
|
||||
fn apply_future_updates(
|
||||
state: &mut ConsentState,
|
||||
result: UserChoice,
|
||||
|
|
@ -225,6 +227,7 @@ async fn get_updates() -> Option<(
|
|||
None
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConsentState {
|
||||
|
|
@ -299,7 +302,7 @@ impl ConsentState {
|
|||
|
||||
if let Some(eula) = &self.eula {
|
||||
file_out.push_str(&format!("\
|
||||
\n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence agreement. You can find our EULA at https://legal.tensamin.net/eula/\
|
||||
\n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence agreement. You can find our EULA at https://legal.methanium.net/tensamin/eula\
|
||||
\nEULA={}\
|
||||
\nEULA-VERSION={}\
|
||||
\nEULA-HASH={}\
|
||||
|
|
@ -309,7 +312,7 @@ impl ConsentState {
|
|||
&& let Some(tos) = &self.tos
|
||||
{
|
||||
file_out.push_str(&format!("\
|
||||
\n\"Terms-of-Service=true\" indicates that you read, understood and accepted Tensamin's Terms of Service. You can find our Terms of Serivce at https://legal.tensamin.net/tos/\
|
||||
\n\"Terms-of-Service=true\" indicates that you read, understood and accepted Tensamin's Terms of Service. You can find our Terms of Serivce at https://legal.methanium.net/tensamin/terms-of-service\
|
||||
\nTerms-of-Service={}\
|
||||
\nTerms-of-Service-VERSION={}\
|
||||
\nTerms-of-Service-HASH={}\
|
||||
|
|
@ -319,7 +322,7 @@ impl ConsentState {
|
|||
&& let Some(pp) = &self.privacy
|
||||
{
|
||||
file_out.push_str(&format!("\
|
||||
\n\"Privacy-Policy=true\" indicates that you read, understood and accepted Tensamin's Privacy Policy. You can find our Privacy Policy at https://legal.tensamin.net/privacy/\
|
||||
\n\"Privacy-Policy=true\" indicates that you read, understood and accepted Tensamin's Privacy Policy. You can find our Privacy Policy at https://legal.methanium.net/tensamin/privacy-policy\
|
||||
\nPrivacy-Policy={}\
|
||||
\nPrivacy-Policy-VERSION={}\
|
||||
\nPrivacy-Policy-HASH={}\
|
||||
|
|
@ -327,7 +330,7 @@ impl ConsentState {
|
|||
}
|
||||
} else {
|
||||
file_out.push_str("\
|
||||
\n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence Agreement. You can find Tensamin's EULA at https://legal.tensamin.net/eula/\
|
||||
\n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence Agreement. You can find Tensamin's EULA at https://legal.methanium.net/tensamin/eula\
|
||||
\nEULA=false\
|
||||
");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ iota-util = { path = "../iota-util" }
|
|||
omikron-connector = { path = "../omikron-connector" }
|
||||
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
|
||||
libc = "0.2"
|
||||
sysinfo = "0.39.0"
|
||||
sysinfo = "0.38.0"
|
||||
serde_yaml = "0.9"
|
||||
serde_json = "1"
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
|
|
|
|||
|
|
@ -102,15 +102,18 @@ async fn main() -> ExitCode {
|
|||
let (state_tx, state_rx) = watch::channel(runtime.snapshot());
|
||||
|
||||
runtime.set_startup_phase(StartupPhase::LoadingUsers);
|
||||
if tokio::task::spawn_blocking(user_manager::load_users_sync)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(Result::ok)
|
||||
.is_none()
|
||||
{
|
||||
let storage_error = match iota_storage::util::db::verify_and_backup_database() {
|
||||
Ok(()) => tokio::task::spawn_blocking(user_manager::load_users_sync)
|
||||
.await
|
||||
.map_err(|error| format!("user storage task failed: {error}"))
|
||||
.and_then(|result| result.map_err(|error| error.to_string()))
|
||||
.err(),
|
||||
Err(error) => Some(error.to_string()),
|
||||
};
|
||||
if let Some(error) = storage_error {
|
||||
runtime.set_component_failed(
|
||||
iota_ipc::ComponentId::Storage,
|
||||
"user storage failed to load".into(),
|
||||
format!("user storage failed to load: {error}"),
|
||||
);
|
||||
} else {
|
||||
runtime.set_component_healthy(iota_ipc::ComponentId::Storage, None);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ pub fn install_linux_bundle(bundle: &Path) -> Result<()> {
|
|||
install_linux_bundle_with_operator(bundle, None)
|
||||
}
|
||||
|
||||
pub fn bootstrap_linux_bundle(bundle: &Path, operator: Option<&str>) -> Result<()> {
|
||||
install_linux_bundle_with_operator(bundle, operator)
|
||||
}
|
||||
|
||||
pub fn install_linux_bundle_with_operator(bundle: &Path, operator: Option<&str>) -> Result<()> {
|
||||
if std::env::consts::OS != "linux" {
|
||||
bail!("Linux systemd bundles are not supported on this platform");
|
||||
|
|
@ -118,6 +122,12 @@ pub fn install_linux_bundle_with_operator(bundle: &Path, operator: Option<&str>)
|
|||
],
|
||||
)?;
|
||||
run("systemd-sysusers", &[])?;
|
||||
for directory in ["/var/lib/iota", "/var/cache/iota", "/var/log/iota"] {
|
||||
run(
|
||||
"install",
|
||||
&["-d", "-m", "0750", "-o", "iota", "-g", "iota", directory],
|
||||
)?;
|
||||
}
|
||||
if let Some(operator) = operator {
|
||||
run("usermod", &["-aG", "iota-operators", operator])?;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ pub const COMMANDS: &[&str] = &[
|
|||
"config get",
|
||||
"config set ",
|
||||
"config reload",
|
||||
"health",
|
||||
"components",
|
||||
"logs",
|
||||
"update check",
|
||||
|
|
@ -84,6 +85,7 @@ pub fn parse(line: &str) -> Option<LocalRequest> {
|
|||
}),
|
||||
["config", "reload"] => Some(LocalRequest::ReloadConfig),
|
||||
["omikron", "status"] => Some(LocalRequest::GetOmikronStatus),
|
||||
["health"] => Some(LocalRequest::ListComponents),
|
||||
["components"] => Some(LocalRequest::ListComponents),
|
||||
["logs"] => Some(LocalRequest::GetLogs { limit: 100 }),
|
||||
["update", "check"] => Some(LocalRequest::CheckUpdate),
|
||||
|
|
@ -229,6 +231,14 @@ mod tests {
|
|||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_health() {
|
||||
assert!(matches!(
|
||||
parse("health"),
|
||||
Some(LocalRequest::ListComponents)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_users_show() {
|
||||
let req = parse("users show 42").unwrap();
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ fn format_data_container(data: Vec<(DataTypeId, DataValue)>, version: Version) -
|
|||
let key_str = key.to_string();
|
||||
|
||||
match value {
|
||||
DataValue::Str(s) => format!("{}=\"{}\"", key_str, s),
|
||||
DataValue::Str(s) => format!("{}=\"{}\"", key_str, abbreviate_string(&s)),
|
||||
|
||||
DataValue::Container(inner) => {
|
||||
let inner_formatted = format_data_container(inner, version.clone());
|
||||
|
|
@ -386,7 +386,7 @@ fn format_array(arr: Vec<DataValue>, version: Version) -> String {
|
|||
let parts: Vec<String> = arr
|
||||
.into_iter()
|
||||
.map(|value| match value {
|
||||
DataValue::Str(s) => format!("\"{}\"", s),
|
||||
DataValue::Str(s) => format!("\"{}\"", abbreviate_string(&s)),
|
||||
|
||||
DataValue::Container(inner) => {
|
||||
let inner_formatted = format_data_container(inner, version.clone());
|
||||
|
|
@ -412,6 +412,31 @@ fn format_array(arr: Vec<DataValue>, version: Version) -> String {
|
|||
parts.join(", ")
|
||||
}
|
||||
|
||||
fn abbreviate_string(value: &str) -> String {
|
||||
const EDGE_LENGTH: usize = 4;
|
||||
|
||||
let chars: Vec<char> = value.chars().collect();
|
||||
if chars.len() <= EDGE_LENGTH * 2 {
|
||||
return value.to_string();
|
||||
}
|
||||
|
||||
let prefix: String = chars.iter().take(EDGE_LENGTH).collect();
|
||||
let suffix: String = chars.iter().rev().take(EDGE_LENGTH).rev().collect();
|
||||
format!("{prefix}...{suffix}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::abbreviate_string;
|
||||
|
||||
#[test]
|
||||
fn abbreviates_only_strings_longer_than_eight_characters() {
|
||||
assert_eq!(abbreviate_string("12345678"), "12345678");
|
||||
assert_eq!(abbreviate_string("123456789"), "1234...6789");
|
||||
assert_eq!(abbreviate_string("YWJjZGVmZ2hpag=="), "YWJj...ag==");
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_cv {
|
||||
($kind:expr, $cv:expr) => {
|
||||
|
|
|
|||
|
|
@ -70,20 +70,37 @@ pub struct IotaPaths {
|
|||
impl IotaPaths {
|
||||
pub fn resolve(scope: Scope) -> Result<Self, PathError> {
|
||||
let defaults = Defaults::for_scope(scope)?;
|
||||
let config_dir = override_first(&["IOTA_CONFIG_DIR"])?.unwrap_or(defaults.config_dir);
|
||||
let data_root = if scope == Scope::User {
|
||||
absolute_env("IOTA_DATA_ROOT")?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let config_dir = override_first(&["IOTA_CONFIG_DIR"])?
|
||||
.or_else(|| data_root.as_ref().map(|root| root.join("config")))
|
||||
.unwrap_or(defaults.config_dir);
|
||||
// IOTA_DATA_DIR is intentionally only a compatibility alias. Parse it
|
||||
// exactly like every other override; do not hide an invalid value.
|
||||
let state_dir =
|
||||
override_first(&["IOTA_STATE_DIR", "IOTA_DATA_DIR"])?.unwrap_or(defaults.state_dir);
|
||||
let cache_dir = override_first(&["IOTA_CACHE_DIR"])?.unwrap_or(defaults.cache_dir);
|
||||
let runtime_dir = override_first(&["IOTA_RUNTIME_DIR"])?.or(defaults.runtime_dir);
|
||||
let log_dir = override_first(&["IOTA_LOG_DIR"])?.unwrap_or(defaults.log_dir);
|
||||
let state_dir = override_first(&["IOTA_STATE_DIR", "IOTA_DATA_DIR"])?
|
||||
.or_else(|| data_root.as_ref().map(|root| root.join("state")))
|
||||
.unwrap_or(defaults.state_dir);
|
||||
let cache_dir = override_first(&["IOTA_CACHE_DIR"])?
|
||||
.or_else(|| data_root.as_ref().map(|root| root.join("cache")))
|
||||
.unwrap_or(defaults.cache_dir);
|
||||
let runtime_dir = override_first(&["IOTA_RUNTIME_DIR"])?
|
||||
.or_else(|| data_root.as_ref().map(|root| root.join("runtime")))
|
||||
.or(defaults.runtime_dir);
|
||||
let log_dir = override_first(&["IOTA_LOG_DIR"])?
|
||||
.or_else(|| data_root.as_ref().map(|root| root.join("logs")))
|
||||
.unwrap_or(defaults.log_dir);
|
||||
let asset_dir = override_first(&["IOTA_ASSET_DIR", "IOTA_WEB_ASSET_DIR"])?
|
||||
.or_else(|| data_root.as_ref().map(|root| root.join("web")))
|
||||
.unwrap_or(defaults.asset_dir);
|
||||
let install_root = override_first(&["IOTA_INSTALL_ROOT"])?.unwrap_or(defaults.install_root);
|
||||
let install_root = override_first(&["IOTA_INSTALL_ROOT"])?
|
||||
.or_else(|| data_root.as_ref().map(|root| root.join("bin")))
|
||||
.unwrap_or(defaults.install_root);
|
||||
let config_file = override_first(&["IOTA_CONFIG_FILE"])?
|
||||
.unwrap_or_else(|| config_dir.join("config.yaml"));
|
||||
let ipc_endpoint = resolve_ipc(scope, defaults.ipc_endpoint)?;
|
||||
let ipc_endpoint = resolve_ipc(scope, defaults.ipc_endpoint, data_root.as_deref())?;
|
||||
let runtime_dir = runtime_dir.or_else(|| match &ipc_endpoint {
|
||||
IpcEndpoint::UnixSocket(path) => path.parent().map(Path::to_path_buf),
|
||||
IpcEndpoint::WindowsPipe(_) => None,
|
||||
|
|
@ -136,10 +153,20 @@ impl IotaPaths {
|
|||
&self.cache_dir,
|
||||
&self.log_dir,
|
||||
] {
|
||||
create_directory(directory, self.scope == Scope::User)?;
|
||||
create_directory(directory, self.scope == Scope::User).map_err(|error| {
|
||||
std::io::Error::new(
|
||||
error.kind(),
|
||||
format!("cannot prepare {}: {error}", directory.display()),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
if let Some(runtime) = &self.runtime_dir {
|
||||
create_directory(runtime, self.scope == Scope::User)?;
|
||||
create_directory(runtime, self.scope == Scope::User).map_err(|error| {
|
||||
std::io::Error::new(
|
||||
error.kind(),
|
||||
format!("cannot prepare {}: {error}", runtime.display()),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -337,12 +364,19 @@ fn override_first(names: &[&'static str]) -> Result<Option<PathBuf>, PathError>
|
|||
}
|
||||
Ok(None)
|
||||
}
|
||||
fn resolve_ipc(scope: Scope, default: Option<IpcEndpoint>) -> Result<IpcEndpoint, PathError> {
|
||||
fn resolve_ipc(
|
||||
scope: Scope,
|
||||
default: Option<IpcEndpoint>,
|
||||
data_root: Option<&Path>,
|
||||
) -> Result<IpcEndpoint, PathError> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if let Some(path) = absolute_env("IOTA_SOCKET")? {
|
||||
return Ok(IpcEndpoint::UnixSocket(path));
|
||||
}
|
||||
if let Some(root) = data_root {
|
||||
return Ok(IpcEndpoint::UnixSocket(root.join("runtime/iota.sock")));
|
||||
}
|
||||
if scope == Scope::User {
|
||||
return Err(PathError::MissingRequiredOverride("IOTA_SOCKET"));
|
||||
}
|
||||
|
|
@ -451,9 +485,13 @@ pub fn daemon_endpoints() -> Vec<PathBuf> {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{
|
||||
Mutex,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
static TEST_ID: AtomicU64 = AtomicU64::new(0);
|
||||
static ENVIRONMENT: Mutex<()> = Mutex::new(());
|
||||
#[test]
|
||||
fn system_layout_is_fhs() {
|
||||
let p = IotaPaths::resolve(Scope::System).unwrap();
|
||||
|
|
@ -501,4 +539,31 @@ mod tests {
|
|||
assert!(state.join("path-layout-v2.json").is_file());
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_root_keeps_unmanaged_user_paths_together() {
|
||||
let _guard = ENVIRONMENT.lock().unwrap();
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"iota-data-root-test-{}-{}",
|
||||
std::process::id(),
|
||||
TEST_ID.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
unsafe {
|
||||
std::env::set_var("IOTA_DATA_ROOT", &root);
|
||||
}
|
||||
let paths = IotaPaths::resolve(Scope::User).unwrap();
|
||||
unsafe {
|
||||
std::env::remove_var("IOTA_DATA_ROOT");
|
||||
}
|
||||
|
||||
assert_eq!(paths.config_file, root.join("config/config.yaml"));
|
||||
assert_eq!(paths.state_dir, root.join("state"));
|
||||
assert_eq!(paths.cache_dir, root.join("cache"));
|
||||
assert_eq!(paths.log_dir, root.join("logs"));
|
||||
assert_eq!(paths.runtime_dir, Some(root.join("runtime")));
|
||||
assert_eq!(
|
||||
paths.ipc_endpoint,
|
||||
IpcEndpoint::UnixSocket(root.join("runtime/iota.sock"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,4 +12,8 @@ dashmap = "6.1.0"
|
|||
once_cell = "1.21.3"
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
json = "*"
|
||||
<<<<<<< HEAD
|
||||
sysinfo = "0.38.0"
|
||||
=======
|
||||
sysinfo = "0.39.0"
|
||||
>>>>>>> refs/remotes/origin/main
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ edition = "2024"
|
|||
iota-logger = { path = "../iota-logger" }
|
||||
iota-util = { path = "../iota-util" }
|
||||
iota-paths = { path = "../iota-paths" }
|
||||
base64 = "0.23.0"
|
||||
<<<<<<< HEAD
|
||||
|
||||
=======
|
||||
>>>>>>> refs/remotes/origin/main
|
||||
base64 = "0.22.1"
|
||||
json = "*"
|
||||
arc-swap = "1"
|
||||
once_cell = "1.21.3"
|
||||
|
|
|
|||
|
|
@ -156,10 +156,8 @@ pub fn save_config_to(path: &Path) {
|
|||
return;
|
||||
}
|
||||
}
|
||||
let temporary = path.with_extension("yaml.tmp");
|
||||
if let Err(error) = fs::write(&temporary, yaml).and_then(|_| fs::rename(&temporary, path)) {
|
||||
if let Err(error) = iota_util::atomic_file::replace(path, yaml.as_bytes(), 3) {
|
||||
eprintln!("Cannot save {}: {error}", path.display());
|
||||
let _ = fs::remove_file(temporary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ impl ManageConnection for SqliteManager {
|
|||
fn connect(&self) -> Result<Connection, rusqlite::Error> {
|
||||
let path = db_file_path(DB_NAME);
|
||||
let conn = Connection::open(path)?;
|
||||
conn.execute_batch("PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;")?;
|
||||
conn.execute_batch("PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;")?;
|
||||
conn.busy_timeout(Duration::from_millis(250))?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
|
@ -55,6 +55,52 @@ where
|
|||
f(&conn)
|
||||
}
|
||||
|
||||
/* Verify the persistent database before the pool is initialized. A corrupt
|
||||
* database is moved aside rather than opened again, preserving material for
|
||||
* operator recovery while allowing the daemon to report the failed storage. */
|
||||
pub fn verify_and_backup_database() -> Result<(), StorageError> {
|
||||
let storage_dir = iota_util::file_util::storage_directory();
|
||||
std::fs::create_dir_all(&storage_dir)?;
|
||||
let path = storage_dir.join(format!("{DB_NAME}.sqlite3"));
|
||||
if !path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let connection = Connection::open(&path)?;
|
||||
connection.execute_batch("PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;")?;
|
||||
connection.execute_batch("PRAGMA wal_checkpoint(FULL);")?;
|
||||
let integrity: String = connection.query_row("PRAGMA integrity_check", [], |row| row.get(0))?;
|
||||
drop(connection);
|
||||
if integrity != "ok" {
|
||||
let recovery = storage_dir.join("recovery");
|
||||
std::fs::create_dir_all(&recovery)?;
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
for suffix in ["", "-wal", "-shm"] {
|
||||
let source = PathBuf::from(format!("{}{}", path.display(), suffix));
|
||||
if source.exists() {
|
||||
let destination = recovery.join(format!("{DB_NAME}.sqlite3.{timestamp}{suffix}"));
|
||||
std::fs::rename(source, destination)?;
|
||||
}
|
||||
}
|
||||
return Err(StorageError::Other(format!(
|
||||
"database integrity check failed ({integrity}); moved database files to {}",
|
||||
recovery.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let backup_dir = storage_dir.join("backups");
|
||||
std::fs::create_dir_all(&backup_dir)?;
|
||||
let backup = backup_dir.join(format!("{DB_NAME}.sqlite3"));
|
||||
let temporary = backup_dir.join(format!(".{DB_NAME}.sqlite3.tmp"));
|
||||
std::fs::copy(&path, &temporary)?;
|
||||
std::fs::File::open(&temporary)?.sync_all()?;
|
||||
std::fs::rename(temporary, backup)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn db_file_path(db_name: &str) -> PathBuf {
|
||||
let storage_dir = iota_util::file_util::storage_directory();
|
||||
// Creating storage belongs to initialization/connection setup, never to a
|
||||
|
|
|
|||
|
|
@ -6,5 +6,5 @@ edition = "2024"
|
|||
[dependencies]
|
||||
iota-util = { path = "../iota-util" }
|
||||
|
||||
json = "*"
|
||||
reqwest = "0.13.2"
|
||||
tokio = { version = "1.50.0", features = ["macros"] }
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ impl ConsentRecord {
|
|||
}
|
||||
|
||||
fn matches_doc(value: &Option<(String, String)>, doc: &Doc) -> bool {
|
||||
matches!(value, Some((version, hash)) if version == &doc.get_version() && hash == &doc.get_hash())
|
||||
matches!(value, Some((_, hash)) if hash == &doc.get_hash())
|
||||
}
|
||||
|
||||
pub fn load(state_dir: &Path) -> ConsentRecord {
|
||||
|
|
@ -82,8 +82,5 @@ pub fn save(state_dir: &Path, record: &ConsentRecord) -> io::Result<()> {
|
|||
text.push_str(&format!("{name}={version}:{hash}\n"));
|
||||
}
|
||||
}
|
||||
let path = state_dir.join(FILE_NAME);
|
||||
let temporary = state_dir.join(format!(".{FILE_NAME}.{}.tmp", std::process::id()));
|
||||
fs::write(&temporary, text)?;
|
||||
fs::rename(temporary, path)
|
||||
iota_util::atomic_file::replace(&state_dir.join(FILE_NAME), text.as_bytes(), 3)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
use json::{JsonValue, object::Object};
|
||||
|
||||
use crate::terms_getter::Type;
|
||||
use iota_util::file_util::load_file;
|
||||
use iota_util::crypto_helper::hex_hash;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(unused)]
|
||||
|
|
@ -23,15 +21,24 @@ impl Doc {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn from_raw(doc_type: Type, content: String, timestamp: u64) -> Doc {
|
||||
Doc::new(
|
||||
timestamp.to_string(),
|
||||
hex_hash(&content),
|
||||
doc_type,
|
||||
timestamp,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn equals_some(&self, other: &Option<Self>) -> bool {
|
||||
if let Some(other) = other {
|
||||
self.get_version() == other.get_version() && self.get_hash() == other.get_hash()
|
||||
self.equals(other)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
pub fn equals(&self, other: &Self) -> bool {
|
||||
self.get_version() == other.get_version() && self.get_hash() == other.get_hash()
|
||||
self.get_hash() == other.get_hash()
|
||||
}
|
||||
|
||||
pub fn get_version(&self) -> String {
|
||||
|
|
@ -41,34 +48,36 @@ impl Doc {
|
|||
self.hash.clone()
|
||||
}
|
||||
pub fn get_time(&self) -> u64 {
|
||||
self.timestamp.clone()
|
||||
self.timestamp
|
||||
}
|
||||
pub fn get_content(&self) -> String {
|
||||
load_file(
|
||||
format!("docs/{}/", self.doc_type.to_str()).as_str(),
|
||||
format!("{}.md", self.version).as_str(),
|
||||
)
|
||||
#[cfg(test)]
|
||||
fn timestamp(&self) -> u64 {
|
||||
self.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> JsonValue {
|
||||
let mut json = JsonValue::new_object();
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Doc;
|
||||
use crate::terms_getter::Type;
|
||||
use iota_util::crypto_helper::hex_hash;
|
||||
|
||||
let _ = json.insert("version", self.version.clone());
|
||||
let _ = json.insert("hash", self.hash.clone());
|
||||
let _ = json.insert("unix", self.timestamp.clone());
|
||||
#[test]
|
||||
fn raw_documents_use_a_local_timestamp_and_content_hash() {
|
||||
let content = "# EULA\n".to_owned();
|
||||
let document = Doc::from_raw(Type::EULA, content.clone(), 123);
|
||||
|
||||
json
|
||||
assert_eq!(document.doc_type, Type::EULA);
|
||||
assert_eq!(document.get_version(), "123");
|
||||
assert_eq!(document.timestamp(), 123);
|
||||
assert_eq!(document.get_hash(), hex_hash(&content));
|
||||
}
|
||||
pub fn from_json(doc_type: Type, json: Object) -> Option<Self> {
|
||||
let hash = json.get("hash")?.as_str()?.to_string();
|
||||
let version = json.get("version")?.as_str()?.to_string();
|
||||
let timestamp = json.get("unix")?.as_u64()?;
|
||||
|
||||
Some(Doc {
|
||||
version,
|
||||
hash,
|
||||
doc_type,
|
||||
timestamp,
|
||||
})
|
||||
#[test]
|
||||
fn matching_documents_ignore_the_fetch_timestamp() {
|
||||
let earlier = Doc::from_raw(Type::EULA, "# EULA\n".to_owned(), 123);
|
||||
let later = Doc::from_raw(Type::EULA, "# EULA\n".to_owned(), 456);
|
||||
|
||||
assert!(earlier.equals(&later));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ pub mod terms_getter;
|
|||
pub use terms_getter::Type as TermsType;
|
||||
pub use terms_getter::get_current_docs;
|
||||
pub use terms_getter::get_link;
|
||||
pub use terms_getter::get_newest_docs;
|
||||
// pub use terms_getter::get_newest_docs;
|
||||
pub use terms_getter::get_newest_link;
|
||||
pub use terms_getter::get_terms;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use json::JsonValue::Object;
|
||||
|
||||
use crate::doc::Doc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Type {
|
||||
|
|
@ -13,8 +12,8 @@ impl Type {
|
|||
pub fn to_str(&self) -> &str {
|
||||
match self {
|
||||
Self::EULA => "eula",
|
||||
Self::TOS => "tos",
|
||||
Self::PP => "privacy",
|
||||
Self::TOS => "terms-of-service",
|
||||
Self::PP => "privacy-policy",
|
||||
}
|
||||
}
|
||||
pub fn to_string(&self) -> String {
|
||||
|
|
@ -27,79 +26,73 @@ impl Type {
|
|||
}
|
||||
|
||||
pub fn get_link(terms_type: Type) -> String {
|
||||
format!("https://legal.tensamin.net/{}/", terms_type.to_str())
|
||||
format!(
|
||||
"https://legal.methanium.net/tensamin/{}",
|
||||
terms_type.to_str()
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* The legal service exposes only its latest raw documents. Keep this helper
|
||||
* for the dormant pre-emptive-acceptance UI until it has a source of future
|
||||
* document versions again.
|
||||
*/
|
||||
pub fn get_newest_link(terms_type: Type) -> String {
|
||||
format!("https://legal.tensamin.net/{}/newest/", terms_type.to_str())
|
||||
get_link(terms_type)
|
||||
}
|
||||
|
||||
pub async fn get_current_docs() -> Option<(Doc, Doc, Doc)> {
|
||||
let body = reqwest::get("https://legal.tensamin.net/api/current/")
|
||||
.await
|
||||
.ok()?
|
||||
.text()
|
||||
.await
|
||||
.ok()?;
|
||||
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
|
||||
let (eula, tos, privacy) = tokio::join!(
|
||||
get_terms(Type::EULA),
|
||||
get_terms(Type::TOS),
|
||||
get_terms(Type::PP),
|
||||
);
|
||||
|
||||
let json = json::parse(&body).ok()?;
|
||||
|
||||
if let Object(eula) = &json["eula"] {
|
||||
if let Object(tos) = &json["tos"] {
|
||||
if let Object(pp) = &json["pp"] {
|
||||
Some((
|
||||
Doc::from_json(Type::EULA, eula.clone())?,
|
||||
Doc::from_json(Type::TOS, tos.clone())?,
|
||||
Doc::from_json(Type::PP, pp.clone())?,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Some((
|
||||
Doc::from_raw(Type::EULA, eula?, timestamp),
|
||||
Doc::from_raw(Type::TOS, tos?, timestamp),
|
||||
Doc::from_raw(Type::PP, privacy?, timestamp),
|
||||
))
|
||||
}
|
||||
|
||||
/*
|
||||
* Future documents are unavailable from the raw endpoint. Restore this API
|
||||
* with the pre-emptive-acceptance flow when the service provides them again.
|
||||
*
|
||||
pub async fn get_newest_docs() -> Option<(Doc, Doc, Doc)> {
|
||||
let body = reqwest::get("https://legal.tensamin.net/api/newest/")
|
||||
.await
|
||||
.ok()?
|
||||
.text()
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
let json = json::parse(&body).ok()?;
|
||||
|
||||
if let Object(eula) = &json["eula"] {
|
||||
if let Object(tos) = &json["tos"] {
|
||||
if let Object(pp) = &json["pp"] {
|
||||
Some((
|
||||
Doc::from_json(Type::EULA, eula.clone())?,
|
||||
Doc::from_json(Type::TOS, tos.clone())?,
|
||||
Doc::from_json(Type::PP, pp.clone())?,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
None
|
||||
}
|
||||
*/
|
||||
|
||||
pub async fn get_terms(terms_type: Type) -> Option<String> {
|
||||
let body = reqwest::get(format!(
|
||||
"https://legal.tensamin.net/api/text/{}/",
|
||||
reqwest::get(format!(
|
||||
"https://legal.methanium.net/tensamin/{}/raw",
|
||||
terms_type.to_str()
|
||||
))
|
||||
.await
|
||||
.ok()?
|
||||
.text()
|
||||
.await
|
||||
.ok()?;
|
||||
.ok()
|
||||
}
|
||||
|
||||
Some(body)
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Type, get_link};
|
||||
|
||||
#[test]
|
||||
fn maps_document_types_to_tensamin_raw_document_names() {
|
||||
assert_eq!(Type::EULA.to_str(), "eula");
|
||||
assert_eq!(Type::TOS.to_str(), "terms-of-service");
|
||||
assert_eq!(Type::PP.to_str(), "privacy-policy");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn links_to_the_tensamin_document_page() {
|
||||
assert_eq!(
|
||||
get_link(Type::TOS),
|
||||
"https://legal.methanium.net/tensamin/terms-of-service"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ edition = "2024"
|
|||
|
||||
[dependencies]
|
||||
iota-paths = { path = "../iota-paths" }
|
||||
<<<<<<< HEAD
|
||||
|
||||
=======
|
||||
>>>>>>> refs/remotes/origin/main
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
sha2 = "0.11.0"
|
||||
hex = "*"
|
||||
|
|
|
|||
|
|
@ -11,9 +11,12 @@ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [
|
|||
|
||||
reqwest = "0.13.2"
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
sysinfo = "0.39.0"
|
||||
sysinfo = "0.38.0"
|
||||
uuid = { version = "*", features = ["v4"] }
|
||||
walkdir = "2.5.0"
|
||||
zip = "6.0.0"
|
||||
base64 = "0.23.0"
|
||||
hex = "*"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
|
|
|||
158
iota-util/src/atomic_file.rs
Normal file
158
iota-util/src/atomic_file.rs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
static TEMPORARY_ID: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/* Persist small state files without exposing a partially written version after
|
||||
* a crash. Backups give operators a local recovery point for keys and config. */
|
||||
pub fn replace(path: &Path, contents: &[u8], backup_limit: usize) -> io::Result<()> {
|
||||
replace_with_mode(path, contents, backup_limit, false)
|
||||
}
|
||||
|
||||
pub fn replace_private(path: &Path, contents: &[u8], backup_limit: usize) -> io::Result<()> {
|
||||
replace_with_mode(path, contents, backup_limit, true)
|
||||
}
|
||||
|
||||
fn replace_with_mode(
|
||||
path: &Path,
|
||||
contents: &[u8],
|
||||
backup_limit: usize,
|
||||
private: bool,
|
||||
) -> io::Result<()> {
|
||||
let parent = path.parent().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"persistent file has no parent directory",
|
||||
)
|
||||
})?;
|
||||
fs::create_dir_all(parent)?;
|
||||
|
||||
if backup_limit > 0 && path.is_file() {
|
||||
create_backup(path, backup_limit)?;
|
||||
}
|
||||
|
||||
let temporary = temporary_path(path)?;
|
||||
let write_result = (|| {
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&temporary)?;
|
||||
set_private_permissions(&temporary, private)?;
|
||||
file.write_all(contents)?;
|
||||
file.sync_all()?;
|
||||
fs::rename(&temporary, path)?;
|
||||
sync_directory(parent)
|
||||
})();
|
||||
if write_result.is_err() {
|
||||
let _ = fs::remove_file(&temporary);
|
||||
}
|
||||
write_result
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn set_private_permissions(path: &Path, private: bool) -> io::Result<()> {
|
||||
if private {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn set_private_permissions(_path: &Path, _private: bool) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_backup(path: &Path, backup_limit: usize) -> io::Result<()> {
|
||||
let parent = path.parent().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"persistent file has no parent directory",
|
||||
)
|
||||
})?;
|
||||
let name = path.file_name().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "persistent file has no name")
|
||||
})?;
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis();
|
||||
let id = TEMPORARY_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let backup = parent.join(format!(
|
||||
".{}.backup-{timestamp}-{id}",
|
||||
name.to_string_lossy()
|
||||
));
|
||||
fs::copy(path, &backup)?;
|
||||
File::open(&backup)?.sync_all()?;
|
||||
sync_directory(parent)?;
|
||||
|
||||
let prefix = format!(".{}.backup-", name.to_string_lossy());
|
||||
let mut backups = fs::read_dir(parent)?
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| entry.file_name().to_string_lossy().starts_with(&prefix))
|
||||
.collect::<Vec<_>>();
|
||||
backups.sort_by_key(|entry| entry.file_name());
|
||||
let obsolete = backups.len().saturating_sub(backup_limit);
|
||||
for entry in backups.into_iter().take(obsolete) {
|
||||
fs::remove_file(entry.path())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn temporary_path(path: &Path) -> io::Result<PathBuf> {
|
||||
let parent = path.parent().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"persistent file has no parent directory",
|
||||
)
|
||||
})?;
|
||||
let name = path.file_name().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "persistent file has no name")
|
||||
})?;
|
||||
let id = TEMPORARY_ID.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(parent.join(format!(
|
||||
".{}.{}.{}.tmp",
|
||||
name.to_string_lossy(),
|
||||
std::process::id(),
|
||||
id
|
||||
)))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn sync_directory(path: &Path) -> io::Result<()> {
|
||||
File::open(path)?.sync_all()
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn sync_directory(_path: &Path) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::replace;
|
||||
|
||||
#[test]
|
||||
fn replace_preserves_a_previous_version_as_a_backup() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().join("state");
|
||||
replace(&path, b"first", 2).unwrap();
|
||||
replace(&path, b"second", 2).unwrap();
|
||||
|
||||
assert_eq!(std::fs::read(&path).unwrap(), b"second");
|
||||
let backups = std::fs::read_dir(directory.path())
|
||||
.unwrap()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| {
|
||||
entry
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.contains(".state.backup-")
|
||||
})
|
||||
.count();
|
||||
assert_eq!(backups, 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,10 +6,17 @@ pub fn generate_keyring() -> Keyring {
|
|||
}
|
||||
|
||||
pub fn keyring_to_base64(keyring: &Keyring) -> String {
|
||||
<<<<<<< HEAD
|
||||
keyring
|
||||
.try_to_bytes()
|
||||
.map(|bytes| STANDARD.encode(bytes))
|
||||
.unwrap_or_default()
|
||||
=======
|
||||
let bytes = keyring
|
||||
.try_to_bytes()
|
||||
.expect("keyring fields must fit the wire format");
|
||||
STANDARD.encode(bytes)
|
||||
>>>>>>> refs/remotes/origin/main
|
||||
}
|
||||
|
||||
pub fn keyring_from_base64(s: &str) -> Option<Keyring> {
|
||||
|
|
@ -18,10 +25,17 @@ pub fn keyring_from_base64(s: &str) -> Option<Keyring> {
|
|||
}
|
||||
|
||||
pub fn public_key_bundle_to_base64(bundle: &PublicKeyBundle) -> String {
|
||||
<<<<<<< HEAD
|
||||
bundle
|
||||
.try_as_bytes()
|
||||
.map(|bytes| STANDARD.encode(bytes))
|
||||
.unwrap_or_default()
|
||||
=======
|
||||
let bytes = bundle
|
||||
.try_as_bytes()
|
||||
.expect("public key bundle fields must fit the wire format");
|
||||
STANDARD.encode(bytes)
|
||||
>>>>>>> refs/remotes/origin/main
|
||||
}
|
||||
|
||||
pub fn public_key_bundle_from_base64(s: &str) -> Option<PublicKeyBundle> {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod atomic_file;
|
||||
pub mod crypto_helper;
|
||||
pub mod crypto_util;
|
||||
pub mod file_util;
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ enum CliCommand {
|
|||
yes: bool,
|
||||
},
|
||||
Components,
|
||||
Health,
|
||||
Logs {
|
||||
#[arg(long, default_value_t = 100)]
|
||||
limit: usize,
|
||||
|
|
@ -209,6 +210,12 @@ enum DaemonAction {
|
|||
#[arg(long)]
|
||||
operator: Option<String>,
|
||||
},
|
||||
Bootstrap {
|
||||
#[arg(long)]
|
||||
bundle: String,
|
||||
#[arg(long)]
|
||||
operator: Option<String>,
|
||||
},
|
||||
}
|
||||
#[derive(Args, Debug)]
|
||||
struct UpdateArgs {
|
||||
|
|
@ -276,6 +283,10 @@ pub enum Command {
|
|||
bundle: String,
|
||||
operator: Option<String>,
|
||||
},
|
||||
Bootstrap {
|
||||
bundle: String,
|
||||
operator: Option<String>,
|
||||
},
|
||||
Status,
|
||||
Tasks,
|
||||
UsersList,
|
||||
|
|
@ -329,6 +340,7 @@ pub enum Command {
|
|||
confirmed: bool,
|
||||
},
|
||||
Components,
|
||||
Health,
|
||||
Logs {
|
||||
limit: usize,
|
||||
},
|
||||
|
|
@ -388,6 +400,7 @@ impl CliInvocation {
|
|||
Some(CliCommand::Status) => Command::Status,
|
||||
Some(CliCommand::Tasks) => Command::Tasks,
|
||||
Some(CliCommand::Components) => Command::Components,
|
||||
Some(CliCommand::Health) => Command::Health,
|
||||
Some(CliCommand::Completions { shell }) => Command::Completions { shell },
|
||||
Some(CliCommand::Man) => Command::ManPage,
|
||||
Some(CliCommand::Users(users)) => match users.action {
|
||||
|
|
@ -463,6 +476,9 @@ impl CliInvocation {
|
|||
DaemonAction::RestartService => Command::DaemonRestartService,
|
||||
DaemonAction::StopService => Command::DaemonStopService,
|
||||
DaemonAction::Install { bundle, operator } => Command::Install { bundle, operator },
|
||||
DaemonAction::Bootstrap { bundle, operator } => {
|
||||
Command::Bootstrap { bundle, operator }
|
||||
}
|
||||
},
|
||||
};
|
||||
Ok(Self {
|
||||
|
|
@ -541,6 +557,12 @@ mod tests {
|
|||
assert_eq!(invocation.command, Command::UsersList);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_health() {
|
||||
let invocation = CliInvocation::parse(["health".into()]).unwrap();
|
||||
assert_eq!(invocation.command, Command::Health);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_terminal_capability_overrides() {
|
||||
let invocation =
|
||||
|
|
@ -599,6 +621,26 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_bootstrap_operator() {
|
||||
let invocation = CliInvocation::parse([
|
||||
"daemon".into(),
|
||||
"bootstrap".into(),
|
||||
"--bundle".into(),
|
||||
"release.zip".into(),
|
||||
"--operator".into(),
|
||||
"alice".into(),
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
invocation.command,
|
||||
Command::Bootstrap {
|
||||
bundle: "release.zip".into(),
|
||||
operator: Some("alice".into()),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_unconfirmed_destructive_commands_explicitly() {
|
||||
let invocation = CliInvocation::parse(["daemon".into(), "stop".into()]).unwrap();
|
||||
|
|
|
|||
|
|
@ -40,33 +40,6 @@ async fn run() -> Result<(), StartupError> {
|
|||
unicode,
|
||||
command,
|
||||
} = invocation;
|
||||
let local_endpoint = match iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
|
||||
.map_err(|error| StartupError::Other(format!("Cannot resolve user IPC path: {error}")))?
|
||||
.ipc_endpoint
|
||||
{
|
||||
iota_paths::IpcEndpoint::UnixSocket(path) => path,
|
||||
iota_paths::IpcEndpoint::WindowsPipe(name) => {
|
||||
return Err(StartupError::Other(format!(
|
||||
"Windows IPC endpoint {name} is not supported by this client build"
|
||||
)));
|
||||
}
|
||||
};
|
||||
let system_endpoint = match iota_paths::IotaPaths::resolve(iota_paths::Scope::System)
|
||||
.map_err(|error| StartupError::Other(format!("Cannot resolve system IPC path: {error}")))?
|
||||
.ipc_endpoint
|
||||
{
|
||||
iota_paths::IpcEndpoint::UnixSocket(path) => path,
|
||||
iota_paths::IpcEndpoint::WindowsPipe(name) => {
|
||||
return Err(StartupError::Other(format!(
|
||||
"Windows IPC endpoint {name} is not supported by this client build"
|
||||
)));
|
||||
}
|
||||
};
|
||||
let endpoints = daemon_setup_flow::DaemonEndpoints {
|
||||
local: local_endpoint,
|
||||
system: system_endpoint,
|
||||
};
|
||||
|
||||
match command {
|
||||
Command::Help => {
|
||||
print_help();
|
||||
|
|
@ -91,7 +64,12 @@ async fn run() -> Result<(), StartupError> {
|
|||
)
|
||||
.map_err(|error| StartupError::Other(format!("Installation failed: {error}")))
|
||||
}
|
||||
Command::Bootstrap { bundle, operator } => {
|
||||
iota_installer::bootstrap_linux_bundle(Path::new(&bundle), operator.as_deref())
|
||||
.map_err(|error| StartupError::Other(format!("Bootstrap failed: {error}")))
|
||||
}
|
||||
command => {
|
||||
let endpoints = resolve_endpoints()?;
|
||||
if matches!(
|
||||
command,
|
||||
Command::DaemonEnable { .. }
|
||||
|
|
@ -123,6 +101,32 @@ async fn run() -> Result<(), StartupError> {
|
|||
}
|
||||
}
|
||||
|
||||
fn resolve_endpoints() -> Result<daemon_setup_flow::DaemonEndpoints, StartupError> {
|
||||
let local = match iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
|
||||
.map_err(|error| StartupError::Other(format!("Cannot resolve user IPC path: {error}")))?
|
||||
.ipc_endpoint
|
||||
{
|
||||
iota_paths::IpcEndpoint::UnixSocket(path) => path,
|
||||
iota_paths::IpcEndpoint::WindowsPipe(name) => {
|
||||
return Err(StartupError::Other(format!(
|
||||
"Windows IPC endpoint {name} is not supported by this client build"
|
||||
)));
|
||||
}
|
||||
};
|
||||
let system = match iota_paths::IotaPaths::resolve(iota_paths::Scope::System)
|
||||
.map_err(|error| StartupError::Other(format!("Cannot resolve system IPC path: {error}")))?
|
||||
.ipc_endpoint
|
||||
{
|
||||
iota_paths::IpcEndpoint::UnixSocket(path) => path,
|
||||
iota_paths::IpcEndpoint::WindowsPipe(name) => {
|
||||
return Err(StartupError::Other(format!(
|
||||
"Windows IPC endpoint {name} is not supported by this client build"
|
||||
)));
|
||||
}
|
||||
};
|
||||
Ok(daemon_setup_flow::DaemonEndpoints { local, system })
|
||||
}
|
||||
|
||||
async fn run_startup_command(command: Command) -> Result<(), StartupError> {
|
||||
let manager = iota_process_manager::detect()
|
||||
.await
|
||||
|
|
@ -406,6 +410,7 @@ fn print_help() {
|
|||
println!(" config get Show current configuration");
|
||||
println!(" config set <KEY> <VAL> Set a configuration value");
|
||||
println!(" config reload Reload configuration");
|
||||
println!(" health Show component health");
|
||||
println!(" components Show component health");
|
||||
println!(" logs [--limit N] Show recent log entries");
|
||||
println!(" update check Check for updates");
|
||||
|
|
@ -422,6 +427,7 @@ fn print_help() {
|
|||
println!(" daemon restart-service Restart the daemon service");
|
||||
println!(" daemon stop-service Stop the daemon service");
|
||||
println!(" daemon install Install from a bundle");
|
||||
println!(" daemon bootstrap Install and enable a Linux systemd bundle");
|
||||
println!(" help Show this help message");
|
||||
println!(" completions <SHELL> Generate shell completions");
|
||||
println!(" man Show the man page");
|
||||
|
|
@ -630,6 +636,7 @@ async fn run_command(
|
|||
Command::ConfigGet => LocalRequest::GetConfig,
|
||||
Command::ConfigSet { key, value } => LocalRequest::SetConfig { key, value },
|
||||
Command::ConfigReload => LocalRequest::ReloadConfig,
|
||||
Command::Health => LocalRequest::ListComponents,
|
||||
Command::Components => LocalRequest::ListComponents,
|
||||
Command::Logs { limit } => LocalRequest::GetLogs { limit },
|
||||
Command::UpdateCheck => LocalRequest::CheckUpdate,
|
||||
|
|
@ -657,6 +664,7 @@ async fn run_command(
|
|||
| Command::Completions { .. }
|
||||
| Command::ManPage
|
||||
| Command::Install { .. }
|
||||
| Command::Bootstrap { .. }
|
||||
| Command::TermsStatus { .. }
|
||||
| Command::TermsShow { .. }
|
||||
| Command::TermsAccept { .. }
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit a297dcce60bc6e84696c6a16f5fd510beb2ca643
|
||||
Subproject commit 486541b9483356ff49ff3ec7016f87d3ecbeaa0e
|
||||
171
omikron-connector/src/omikron_connection.rs
Executable file → Normal file
171
omikron-connector/src/omikron_connection.rs
Executable file → Normal file
|
|
@ -42,11 +42,18 @@ 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<PathBuf> = std::sync::OnceLock::new();
|
||||
static OMIKRON_PUBLIC_KEY_PATH: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
|
||||
|
||||
/// Must be called by the daemon before any Omikron connection is attempted.
|
||||
/// It keeps identity material independent from the working directory.
|
||||
/*
|
||||
* Keeps identity and pinned Omikron key files independent from the process
|
||||
* working directory, so restarts use the same trusted material.
|
||||
*/
|
||||
pub fn configure_identity_path(path: PathBuf) {
|
||||
let key_path = path.parent().map(|parent| parent.join("omikron.mpkb"));
|
||||
let _ = IDENTITY_PATH.set(path);
|
||||
if let Some(key_path) = key_path {
|
||||
let _ = OMIKRON_PUBLIC_KEY_PATH.set(key_path);
|
||||
}
|
||||
}
|
||||
fn identity_path() -> &'static Path {
|
||||
IDENTITY_PATH
|
||||
|
|
@ -54,7 +61,51 @@ fn identity_path() -> &'static Path {
|
|||
.map(PathBuf::as_path)
|
||||
.unwrap_or_else(|| Path::new(IOTA_KEYRING_PATH))
|
||||
}
|
||||
const OMIKRON_PUBLIC_KEY_PATH: &str = "omikron.mpkb";
|
||||
fn omikron_public_key_path() -> &'static Path {
|
||||
OMIKRON_PUBLIC_KEY_PATH
|
||||
.get()
|
||||
.map(PathBuf::as_path)
|
||||
.unwrap_or_else(|| Path::new("omikron.mpkb"))
|
||||
}
|
||||
|
||||
fn save_keyring(keyring: &Keyring, path: &Path) -> Result<(), String> {
|
||||
let temporary = serialization_path(path)?;
|
||||
mtp::files::save_keyring_raw(keyring, &temporary)
|
||||
.map_err(|error| format!("serialize keyring: {error}"))?;
|
||||
let bytes =
|
||||
std::fs::read(&temporary).map_err(|error| format!("read serialized keyring: {error}"));
|
||||
let _ = std::fs::remove_file(&temporary);
|
||||
let bytes = bytes?;
|
||||
iota_util::atomic_file::replace_private(path, &bytes, 3)
|
||||
.map_err(|error| format!("write {}: {error}", path.display()))
|
||||
}
|
||||
|
||||
fn save_omikron_public_key(key: &PublicKeyBundle, path: &Path) -> Result<(), String> {
|
||||
let temporary = serialization_path(path)?;
|
||||
mtp::files::save_public_key_bundle(key, &temporary)
|
||||
.map_err(|error| format!("serialize Omikron public key: {error}"))?;
|
||||
let bytes = std::fs::read(&temporary)
|
||||
.map_err(|error| format!("read serialized Omikron public key: {error}"));
|
||||
let _ = std::fs::remove_file(&temporary);
|
||||
let bytes = bytes?;
|
||||
iota_util::atomic_file::replace(path, &bytes, 3)
|
||||
.map_err(|error| format!("write {}: {error}", path.display()))
|
||||
}
|
||||
|
||||
fn serialization_path(path: &Path) -> Result<PathBuf, String> {
|
||||
let parent = path
|
||||
.parent()
|
||||
.ok_or_else(|| format!("{} has no parent directory", path.display()))?;
|
||||
let name = path
|
||||
.file_name()
|
||||
.ok_or_else(|| format!("{} has no file name", path.display()))?;
|
||||
Ok(parent.join(format!(
|
||||
".{}.serialize-{}",
|
||||
name.to_string_lossy(),
|
||||
Uuid::new_v4()
|
||||
)))
|
||||
}
|
||||
|
||||
const RECONNECT_DELAY: Duration = Duration::from_secs(5);
|
||||
const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(300);
|
||||
const CONNECTION_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
|
@ -558,8 +609,46 @@ impl OmikronConnection {
|
|||
// Identity (own Keyring, migrated from the legacy base64-in-config format)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
<<<<<<< HEAD
|
||||
/*
|
||||
* `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) = save_keyring(&keyring, path) {
|
||||
log!("Failed to persist {}: {}", path.display(), e);
|
||||
}
|
||||
|
||||
keyring
|
||||
=======
|
||||
async fn load_or_migrate_keyring(&self, passphrase: &[u8]) -> Result<Keyring, IdentityError> {
|
||||
load_or_migrate_keyring_at(identity_path(), CONFIG.load().keyring.clone(), passphrase)
|
||||
>>>>>>> refs/remotes/origin/main
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -590,21 +679,29 @@ impl OmikronConnection {
|
|||
let port: u16 = port_str
|
||||
.parse()
|
||||
.map_err(|_| format!("Invalid OMIKRON_PORT: {}", port_str))?;
|
||||
let public_key = mtp::files::load_public_key_bundle(OMIKRON_PUBLIC_KEY_PATH)
|
||||
let key_path = omikron_public_key_path();
|
||||
let public_key = mtp::files::load_public_key_bundle(key_path)
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Failed to load Omikron public key bundle from {}: {}. Obtain {} from the Omikron operator and place it in the working directory.",
|
||||
OMIKRON_PUBLIC_KEY_PATH, e, OMIKRON_PUBLIC_KEY_PATH
|
||||
"Failed to load Omikron public key bundle from {}: {}. Obtain {} from the Omikron operator and place it at that path.",
|
||||
key_path.display(), e, key_path.display()
|
||||
)
|
||||
})?;
|
||||
return Ok((host, port, public_key));
|
||||
}
|
||||
|
||||
let cached_key = mtp::files::load_public_key_bundle(OMIKRON_PUBLIC_KEY_PATH).ok();
|
||||
let key_path = omikron_public_key_path();
|
||||
let cached_key = mtp::files::load_public_key_bundle(key_path).ok();
|
||||
let cached_host_port = {
|
||||
let conf = CONFIG.load();
|
||||
match (&conf.omikron_host, conf.omikron_port) {
|
||||
(Some(host), Some(port)) => Some((host.clone(), port)),
|
||||
(Some(host), Some(port)) if !host.trim().is_empty() && port != 0 => {
|
||||
Some((host.clone(), port))
|
||||
}
|
||||
(Some(_), Some(_)) => {
|
||||
log!("Ignoring invalid cached Omikron endpoint in Iota configuration");
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
|
@ -629,6 +726,33 @@ impl OmikronConnection {
|
|||
})?;
|
||||
match &cached_key {
|
||||
Some(cached) => {
|
||||
<<<<<<< HEAD
|
||||
let keys_match =
|
||||
match (cached.try_as_bytes(), endpoint.public_key.try_as_bytes()) {
|
||||
(Ok(cached_bytes), Ok(discovered_bytes)) => {
|
||||
cached_bytes == discovered_bytes
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !keys_match {
|
||||
log!(
|
||||
"Fetched Omikron public key differs from the cached {} - keeping the \
|
||||
cached key. Delete {} manually if this is an expected key rotation.",
|
||||
key_path.display(),
|
||||
key_path.display()
|
||||
);
|
||||
if let Some((cached_host, cached_port)) = &cached_host_port {
|
||||
(cached_host.clone(), *cached_port, cached.clone())
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Omega returned an Omikron key that differs from {} and no validated cached endpoint is available",
|
||||
key_path.display()
|
||||
));
|
||||
}
|
||||
} else {
|
||||
(endpoint.host, endpoint.port, cached.clone())
|
||||
}
|
||||
=======
|
||||
let cached_key_bytes = cached.try_as_bytes().map_err(|error| {
|
||||
format!("Failed to serialize cached Omikron public key: {error}")
|
||||
})?;
|
||||
|
|
@ -641,12 +765,10 @@ impl OmikronConnection {
|
|||
);
|
||||
}
|
||||
(endpoint.host, endpoint.port, cached.clone())
|
||||
>>>>>>> refs/remotes/origin/main
|
||||
}
|
||||
None => {
|
||||
if let Err(e) = mtp::files::save_public_key_bundle(
|
||||
&endpoint.public_key,
|
||||
OMIKRON_PUBLIC_KEY_PATH,
|
||||
) {
|
||||
if let Err(e) = save_omikron_public_key(&endpoint.public_key, key_path) {
|
||||
log!("Failed to cache Omikron public key: {}", e);
|
||||
}
|
||||
(endpoint.host, endpoint.port, endpoint.public_key)
|
||||
|
|
@ -2115,7 +2237,11 @@ impl OmikronConnection {
|
|||
))
|
||||
})?;
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
save_keyring(&keyring, path).map_err(|error| {
|
||||
=======
|
||||
save_protected_keyring_verified(&keyring, path, &identity_secret).map_err(|error| {
|
||||
>>>>>>> refs/remotes/origin/main
|
||||
OmikronError::Internal(format!(
|
||||
"could not save new identity {}: {error}",
|
||||
path.display()
|
||||
|
|
@ -2284,6 +2410,26 @@ impl OmikronClient for OmikronConnection {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
<<<<<<< HEAD
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn durable_keyring_save_preserves_mtp_format() {
|
||||
let directory = std::env::temp_dir().join(format!("iota-keyring-test-{}", Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&directory).unwrap();
|
||||
let path = directory.join(IOTA_KEYRING_PATH);
|
||||
let keyring = crypto_helper::generate_keyring();
|
||||
|
||||
save_keyring(&keyring, &path).unwrap();
|
||||
|
||||
let loaded = mtp::files::load_keyring_raw(&path).unwrap();
|
||||
assert_eq!(
|
||||
keyring.try_to_bytes().unwrap(),
|
||||
loaded.try_to_bytes().unwrap()
|
||||
);
|
||||
std::fs::remove_dir_all(directory).unwrap();
|
||||
=======
|
||||
mod identity_tests {
|
||||
use super::*;
|
||||
|
||||
|
|
@ -2357,5 +2503,6 @@ mod identity_tests {
|
|||
}
|
||||
|
||||
assert!(jittered_reconnect_delay(Duration::from_secs(600)) <= MAX_RECONNECT_DELAY);
|
||||
>>>>>>> refs/remotes/origin/main
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,7 +128,10 @@ async fn users_remove(
|
|||
return forbidden();
|
||||
}
|
||||
|
||||
let uuid = payload.get("uuid").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
let uuid = match user_id(&payload) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
iota_storage::users::user_manager::remove_user(uuid);
|
||||
iota_storage::users::user_manager::save_users();
|
||||
|
|
@ -194,7 +197,14 @@ fn success() -> HttpResponse {
|
|||
}
|
||||
|
||||
fn error() -> HttpResponse {
|
||||
HttpResponse::Ok().json(json!({ "type": "error" }))
|
||||
HttpResponse::BadRequest().json(json!({ "type": "error" }))
|
||||
}
|
||||
|
||||
fn user_id(payload: &Value) -> Result<i64, HttpResponse> {
|
||||
payload
|
||||
.get("uuid")
|
||||
.and_then(Value::as_i64)
|
||||
.ok_or_else(error)
|
||||
}
|
||||
|
||||
fn is_allowed(addr: SocketAddr, ssl: bool) -> bool {
|
||||
|
|
@ -208,3 +218,20 @@ fn is_allowed_req(req: &HttpRequest, ssl: bool) -> bool {
|
|||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn error_response_is_bad_request() {
|
||||
assert_eq!(error().status(), actix_web::http::StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_id_rejects_missing_or_non_integer_uuid() {
|
||||
assert!(user_id(&json!({})).is_err());
|
||||
assert!(user_id(&json!({ "uuid": "0" })).is_err());
|
||||
assert_eq!(user_id(&json!({ "uuid": 0 })).ok(), Some(0));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue