diff --git a/.gitignore b/.gitignore index d0f40134..1fecf573 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ docs/_build man/*8 package/ +.tmp/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 84660d3f..a4ed1e64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,189 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash", + "base64", + "bitflags 2.6.0", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2 0.3.26", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.87", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -17,6 +200,30 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -26,6 +233,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -43,9 +265,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.17" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -58,9 +280,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" @@ -90,12 +312,45 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "aws-lc-rs" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd82dba44d209fddb11c190e0a94b78651f95299598e472215667417a03ff1d" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7a4168111d7eb622a31b214057b8509c0a7e1794f44c546d742330dc793972" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -111,6 +366,18 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bincode" version = "1.3.3" @@ -120,6 +387,29 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.87", + "which", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -132,6 +422,45 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -150,15 +479,44 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +[[package]] +name = "bytestring" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" +dependencies = [ + "bytes", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" -version = "1.1.31" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -185,20 +543,41 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" dependencies = [ "anstream", "anstyle", @@ -208,9 +587,18 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" + +[[package]] +name = "cmake" +version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] [[package]] name = "colorchoice" @@ -228,12 +616,54 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -268,11 +698,21 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "csv" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" dependencies = [ "csv-core", "itoa", @@ -290,40 +730,166 @@ dependencies = [ ] [[package]] -name = "dirs-next" -version = "2.0.0" +name = "curve25519-dalek" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "dirs-sys-next", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", ] [[package]] -name = "dirs-sys-next" -version = "0.1.2" +name = "curve25519-dalek-derive" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ - "libc", - "redox_users", - "winapi", + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] -name = "either" -version = "1.13.0" +name = "der" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] [[package]] -name = "encode_unicode" -version = "1.0.0" +name = "deranged" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] [[package]] -name = "equivalent" +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.87", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "merlin", + "rand_core", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" @@ -338,6 +904,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fastrand" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "flate2" version = "1.0.34" @@ -348,12 +926,166 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "fst" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a" +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -371,11 +1103,55 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" [[package]] name = "hermit-abi" @@ -395,6 +1171,153 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.6", + "http 1.1.0", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "hyphenation" version = "0.8.4" @@ -441,6 +1364,151 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-more" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae21c3177a27788957044151cc2800043d127acaa460a47ebb9b84dfa2c6aa0" + [[package]] name = "indexmap" version = "2.6.0" @@ -449,6 +1517,26 @@ checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", "hashbrown", + "serde", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", ] [[package]] @@ -462,6 +1550,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + [[package]] name = "is-terminal" version = "0.4.13" @@ -479,12 +1573,30 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.72" @@ -494,17 +1606,57 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.161" +version = "0.2.162" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libm" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" @@ -520,18 +1672,29 @@ dependencies = [ name = "libsysinspect" version = "0.2.0" dependencies = [ + "base64", "chrono", "colored", + "hex", "indexmap", "lazy_static", "log", "nix", + "once_cell", + "pem", + "pest", + "pest_derive", "prettytable-rs", + "rand", "regex", + "rsa", "serde", "serde_json", "serde_yaml", + "sha2", + "sysinfo 0.32.0", "textwrap", + "tokio", "unicode-segmentation", "walkdir", ] @@ -548,6 +1711,29 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + [[package]] name = "lock_api" version = "0.4.12" @@ -579,6 +1765,30 @@ dependencies = [ "autocfg", ] +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.0" @@ -596,10 +1806,34 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi 0.3.9", "libc", + "log", "wasi", "windows-sys 0.52.0", ] +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "neli" version = "0.6.4" @@ -653,6 +1887,16 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "ntapi" version = "0.4.1" @@ -662,6 +1906,49 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -669,6 +1956,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -686,6 +1974,61 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "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.87", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -693,7 +2036,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -704,23 +2061,184 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.7", "smallvec", "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64", + "serde", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "pest_meta" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "pin-project-lite" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2", + "scrypt", + "sha2", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "pkcs5", + "rand_core", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "pocket-resources" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c135f38778ad324d9e9ee68690bac2c1a51f340fdf96ca13e2ab3914eb2e51d8" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +dependencies = [ + "proc-macro2", + "syn 2.0.87", +] + [[package]] name = "prettytable-rs" version = "0.10.0" @@ -767,27 +2285,57 @@ dependencies = [ "flate2", "hex", "procfs-core", - "rustix 0.38.38", + "rustix 0.38.40", ] [[package]] name = "procfs-core" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" +checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" +dependencies = [ + "bitflags 2.6.0", + "chrono", + "hex", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ - "bitflags 2.6.0", - "chrono", - "hex", + "ppv-lite86", + "rand_core", ] [[package]] -name = "quote" -version = "1.0.37" +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "proc-macro2", + "getrandom", ] [[package]] @@ -810,6 +2358,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.7" @@ -844,21 +2401,107 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.6", + "http 1.1.0", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "sha1", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "run" version = "0.2.0" @@ -876,6 +2519,21 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.37.27" @@ -892,9 +2550,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.38" +version = "0.38.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" dependencies = [ "bitflags 2.6.0", "errno", @@ -903,6 +2561,48 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.23.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.18" @@ -915,6 +2615,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -924,30 +2633,79 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "serde" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -962,6 +2720,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -975,6 +2745,38 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", + "sha2-asm", +] + +[[package]] +name = "sha2-asm" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b845214d6175804686b2bd482bcffe96651bb2d1200742b712003504a2dac1ab" +dependencies = [ + "cc", +] + [[package]] name = "shlex" version = "1.3.0" @@ -990,6 +2792,41 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "sled" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot 0.11.2", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -1012,12 +2849,40 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -1031,53 +2896,158 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.85" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "sysinfo" version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows", +] + +[[package]] +name = "sysinfo" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ae3f4f7d64646c46c4cae4e3f01d1c5d255c7406fdd7c7f999a94e488791" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows", +] + +[[package]] +name = "sysinspect" +version = "0.2.0" +dependencies = [ + "chrono", + "clap", + "colored", + "libsysinspect", + "log", + "sysinfo 0.31.4", +] + +[[package]] +name = "sysmaster" +version = "0.1.0" +dependencies = [ + "actix-web", + "clap", + "colored", + "ed25519-dalek", + "futures", + "libc", + "libsysinspect", + "log", + "rand", + "rsa", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_yaml", + "sled", + "tokio", + "tokio-rustls", + "uuid", +] + +[[package]] +name = "sysminion" +version = "0.1.0" +dependencies = [ + "clap", + "colored", + "ed25519-dalek", + "glob", + "indexmap", + "libsysinspect", + "log", + "once_cell", + "rand", + "regex", + "reqwest", + "rsa", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_yaml", + "sysinfo 0.32.0", + "tokio", + "uuid", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "core-foundation-sys", - "libc", - "memchr", - "ntapi", - "rayon", - "windows", + "bitflags 2.6.0", + "core-foundation", + "system-configuration-sys", ] [[package]] -name = "sysinfo" -version = "0.32.0" +name = "system-configuration-sys" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ae3f4f7d64646c46c4cae4e3f01d1c5d255c7406fdd7c7f999a94e488791" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", - "memchr", - "ntapi", - "rayon", - "windows", ] [[package]] -name = "sysinspect" -version = "0.2.0" +name = "tempfile" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ - "chrono", - "clap", - "colored", - "libsysinspect", - "log", - "sysinfo 0.31.4", + "cfg-if", + "fastrand", + "once_cell", + "rustix 0.38.40", + "windows-sys 0.59.0", ] [[package]] @@ -1116,35 +3086,76 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.65" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.65" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", ] [[package]] name = "tokio" -version = "1.41.0" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", "bytes", "libc", "mio", - "parking_lot", + "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", "socket2", @@ -1160,9 +3171,87 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.13" @@ -1193,12 +3282,62 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "walkdir" version = "2.5.0" @@ -1209,6 +3348,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1237,10 +3385,22 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.95" @@ -1259,7 +3419,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1270,6 +3430,28 @@ version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.40", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1328,7 +3510,7 @@ checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ "windows-implement", "windows-interface", - "windows-result", + "windows-result 0.1.2", "windows-targets 0.52.6", ] @@ -1340,7 +3522,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", ] [[package]] @@ -1351,7 +3533,18 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.87", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result 0.2.0", + "windows-strings", + "windows-targets 0.52.6", ] [[package]] @@ -1363,6 +3556,25 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1510,3 +3722,151 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 8cad8e47..bf2c4787 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ sysinfo = { version = "0.31.4", features = ["linux-tmpfs"] } [workspace] resolver = "2" -members = ["modules/sys/*", "libsysinspect"] +members = ["modules/sys/*", "libsysinspect", "sysmaster", "sysminion"] [profile.release] strip = true diff --git a/docs/genusage/distributed_model.rst b/docs/genusage/distributed_model.rst new file mode 100644 index 00000000..78903131 --- /dev/null +++ b/docs/genusage/distributed_model.rst @@ -0,0 +1,160 @@ +.. raw:: html + + + +.. role:: u + :class: underlined + +.. role:: bi + :class: bolditalic + +.. _distributed_model: + +Distributed Model +================= + +Distributed model means that the entities of it might be scattered +across the system in different "boxes" or consist of different components, +those are found in different network-connected places. + +Each entity keeps some claims about it. For example, a network consists of +IP addresses, MAC addresses, some ports, some protocols etc. Each of these +elements are *"building bricks"* of the whole network entity. Suppose, +we need to ensure that the network across many "boxes" contains specific +ports, protocols, addresses, routes etc. And each "box" has different configuration +for it. This means each part of the network will have a different set of claims. + +For example, a router, which is :bi:`a part of the network entity`, has specific +requirements on its own inside of it, and they are claims. But they are not a +:bi:`global` claims, they are only claims that are specific to that router. +However, we cannot admit that the network is healthy if only router alone works. +We want to check many other places and settings all over the related "boxes" +and see if each box on its own has their claims admitted as actual proven facts. +Only if :bi:`all pieces comes together`, only if all checks on all boxes/places/segments +are verified and are "green", only then we can admit that the "Network Entity" +works as expected. + +To achieve this, we need to distribute the model. + +.. important:: + + The model is actually always distributed. It is just the way how do you write it. + However, the model is aimed to cover the entire :bi:`system`, which is not just + one or two "boxes" or software components, processes etc. + +Model Distribution Principle +---------------------------- + +The static model is always copied to each minion "as is". But that means that each +minion supposed to be exactly the same in order to return a success result. However, +in a complex system, some "boxes" are Database, some of them are routers, some of them +are just storage systems, some of them are running middleware/services etc. And thus +all of these "boxes" render a final heterogeneous distributed system. + +How Sysinspect dissects to the targets only what's needed? + +Let's take a look at the entity description: + +.. code-block:: yaml + + # General section for all entities + entities: + router: + claims: + $: + - foo: ... + + powered: + - foobar: ... + +In this case we have an entity, callsed **"router"**, which has some default common claims, +accessible by globbing ``$`` (e.g. some admin interfaces are always there, config of hypervisor +partitions etc), and it also has claims that are valid only when it is in ``powered`` +mode. + +.. pull-quote:: + + Now, what if we have :bi:`two different routers`, and yet they have to be + together, in order to let the whole network work as expected? How to split + facts between them? + +For this, an entity needs to match a minion by its traits, and claims are defined under +the section, where traits are filtering (or selecting) :bi:`the relevant part` of the claims +description in a declarative way. + +.. _splitting_claims: + +Splitting Claims +---------------- + +Let's continue with the same router example, but add another one. Say, the other one +is running on ARM-64 architecture instead of on Intel x86_64, has much less memory +and has a different hostname. By these traits we can distinguish those two routers, +splitting claims among them. + +Here is an example: + +.. code-block:: yaml + + # General section for all entities + entities: + router: + # Router 1 + my-big-one: + traits: + system.cpu.arch: x86_64 + system.mem.total: 16GB + net.hostname: crocodile.local + system.os.vendor: f5 + + # Specific flaims only for big, powerful "f5" Intel-based router + claims: + ... + + # Router 2 + my-small-one: + traits: + system.cpu.arch: ARM64 + system.mem.total: 4GB + net.hostname: frog.local + + # Specific flaims only for ARM-based router + claims: + ... + + # Common claims for both routers + claims: + $: + - foo: ... + + powered: + - foobar: ... + +The above is the entity description that is in the master Model. However, +each minion will not get the entire model, but only :bi:`a subset` of the Model, +which is relevant to only that specific minion. + +.. note:: + + Each minion will get only :bi:`a subset` of the Model, relevant only to + the current minion traits or other attributes! + +The mechanism works very similar to the Model Inheritance: matching section +will replace the default claims section from the section that matches the minion. +In case both sections are matching a minion, then they will be merged. If they +overlap, then first wins. Therefore it is very important to be careful to point out +the difference in traits or other attributes, ensuring the model is not overlapping +on the minion side or renders wrong. + +.. important:: + + It is important to use granular and detailed targeting, in order to avoid + claims overlap between the minions, rendering false results. \ No newline at end of file diff --git a/docs/genusage/overview.rst b/docs/genusage/overview.rst new file mode 100644 index 00000000..77c2cfc4 --- /dev/null +++ b/docs/genusage/overview.rst @@ -0,0 +1,50 @@ +Using Sysinspect +================ + +.. note:: + + This section explains how to use Sysinspect in "solo" mode and + in the network. + + +Sysinspect can be used in two modes: + +1. Solo mode, where the entire model is for only one "box" or hardware +2. Network-connected cluster, where entities can consist from more than one element + and Sysinspect needs to gather information from different places in order to construct + a final answer about a specific entity. Such entity, for example, can be the entire + network itself. + +19 Seconds Tutorial +------------------- + +So you wrote a Model, using "Model Description" documentation and placed it to +``/etc/sysinspect/models`` directory on your Master machine as ``my_model``. + +Then just call the entire model across all minions: + +.. code-block:: bash + + sysinspect "my_model" + +You can call only a subset of your module, such as a specific state of a specific entity. +For example: + +.. code-block:: bash + + sysinspect "my_model/my_entity/my_state" + +For more information go ahead and dive in! + +Diving In +--------- + +To better understand how to use Sysinspect in those situation, read through the following +sections: + +.. toctree:: + :maxdepth: 2 + + distributed_model + systraits + targeting diff --git a/docs/genusage/systraits.rst b/docs/genusage/systraits.rst new file mode 100644 index 00000000..34355185 --- /dev/null +++ b/docs/genusage/systraits.rst @@ -0,0 +1,215 @@ +.. raw:: html + + + +.. role:: u + :class: underlined + +.. role:: bi + :class: bolditalic + +.. _systraits: + +System Traits +============= + +.. note:: + + Definition and description of system traits and their purpose. + +Traits are essentially static attributes of a minion. They can be a literally anything +in a form of key/value. There are different kinds of traits: + +**Common** + + Common traits are given to each minion automatically. They are typical system + information and anything else that can be commonly fetched out of the "box": OS info, + kernel version, memory size, network settings, hostnames, machine Id etc. + +**Custom** + + Custom traits are static data that set explicity onto a minion. Any data in + key/value form. They are usually various labels, rack number, physical floor, + Asset Tag, serial number etc. + +**Dynamic** + + Dynamic traits are custom functions, where data obtained by relevant modules. + essentially, they are just like normal modules, except the resulting data is stored as + a criterion by which a specific minion is targeted. For example, *"memory less than X"*, + or *"runs process Y"* etc. + +Listing Traits +-------------- + +To list minion's traits, is enough to target a minion by its Id or hostname: + +.. code-block:: bash + + $ sysinspect --minions + ... + + $ sysinspect --info + +Using Traits in a Model +----------------------- + +Using traits in a model is described in :ref:`splitting_claims` chapter of the :ref:`distributed_model` document. + +Static Minion Traits +-------------------- + +Traits can be also custom static data, which is placed in a minion configuration. Traits are just +YAML files with key/value format, placed in ``$SYSINSPECT/traits`` directory of a minion. The naming +of those files is not important, they will be anyway merged into one tree. Important is to ensure +that trait keys do not repeat, so they do not overwrite each other. The ``$SYSINSPECT`` directory +is ``/etc/sysinspect`` by default or is defined in the minion configuration. + +Example of a trait file: + +.. code-block:: yaml + :caption: File: ``/etc/sysinspect/traits/example.trait`` + + traits: + name: Fred + +From now on, the minion can be targeded by the trait ``name``: + +.. code-block:: bash + :caption: Targeting a minion by a custom trait + + sysinspect "my_model/my_entity name:Fred" + +.. code-block:: + +Populating Static Traits +------------------------ + +Populating traits is done in two steps: + +1. Writing a specific static trait in a trait description +2. Populating the trait description to all targeted minions + +Synopsis of a trait description as follows: + +.. code-block:: text + :caption: Synopsis + + : + [machine-id]: + - [list] + [hostname]: + - [list] + [traits]: + [key]: [value] + : + [key]: [value] + + # Only for dynamic traits (functions) + [functions]: + - [list] + +For example, to make an alias for all Ubuntu running machines, the following valid trait description: + +.. code-block:: yaml + :caption: An alias to a system trait + + # This is to select what minions should have + # the following traits assigned + query: + traits: + - system.os.kernel.version: 6.* + + # Actual traits to be assigned + traits: + kernel: six + +Now it is possible to call all minions with any kernel of major version 6 like so: + +.. code-block:: bash + :caption: Target minions by own alias + + sysinspect "my_model/my_entity kernel:six" + +The section ``functions`` is used for the dynamic traits, described below. + +Dynamic Traits +-------------- + +Dynamic traits are functions that are doing something on the machine. Since those functions +are standalone executables, they do not accept any parameters. Functions are the same modules +like any other modules and using the same protocol with the JSON format. The difference is that +the module should return key/value structure. For example: + +.. code-block:: json + + { + "key": "value", + } + +Example of using a custom module: + +.. code-block:: bash + :caption: File: ``my_trait.sh`` + + #!/usr/bin/bash + kernel=$(uname -r) + echo $(printf '{"kernel.release": "%s"}' $kernel) + +The output of this script is a JSON key/value structure: + +.. code-block:: json + :caption: Example output + + { + "kernel.release": "5.19.0-50-generic" + } + +The function module must be portable, i.e. Minion has no responsibility to ensuring if the +function module is actionable or not on a target system. I.e. user must ensure that the target +system where the particular minion is running, should be equipped with Bash in ``/usr/bin`` +directory. + +Any modules that return non-zero return like system error more than ``1`` is simply ignored +and error is logged. + +Populating Dynamic Traits +------------------------- + +To populate dynamic trait there are three steps for this: + +1. Writing a specific trait in a Trait Description +2. Placing the trait module to the file server so the minions can download it +3. Populating the Trait Description to all targeted minions + +To write a specific trait in a Trait Description, the ``functions`` section must be specified. +Example: + +.. code-block:: yaml + + functions: + # Specify a relative path on the fileserver + - /functions/my_trait.sh + +The script ``my_trait.sh`` will be copied to ``$SYSINSPECT/functions``. When the minion starts, +it will execute each function in alphabetical oder, read the JSON output and merge the result +into the common traits tree. Then the traits tree will be synchronised with the Master. + +.. important:: + + While function traits are dynamic, they are still should be treated as static data. + +While function sounds dynamic, the trait is still an attribute :bi:`by which` a minion is queried. +This means if the attribute will be different at every minion startup, it might be useless +to target a minion by such attribute, unless it is matching to some regular expression. There +might be a rare use cases, such as *"select minion or not, depending on its mood"* (because the +function returns every time a different value), but generally this sort of dynamism is nearly +outside of the scope of traits system. \ No newline at end of file diff --git a/docs/genusage/targeting.rst b/docs/genusage/targeting.rst new file mode 100644 index 00000000..c8b1d99b --- /dev/null +++ b/docs/genusage/targeting.rst @@ -0,0 +1,167 @@ +.. raw:: html + + + +.. role:: u + :class: underlined + +.. role:: bi + :class: bolditalic + +Targeting Entities +================== + +.. note:: + + Entities are bound to the specific hardware, which is related to a specific minion. + This document explains how to target specific minions to complete entity description. + +General +------- + +Sysinspect has a query mechanism where master can target remote minions by a specific +criteria. Similarly, the Model itself can be called from different "entry points". + +**Checkbook** + + A Checkbook is basically a list of "entry points" to a complex trees of entities, + essentially a group of entities that form a feature or a set of features to be checked. + +**Entities** + + A regular Model entities. This type of entry is usually used to narrow down assessment + path. + + +Checkbook query is using path-like tuples to target a feature (a group of entities etc) +in the following format: + +.. code-block:: text + :caption: Precise model query synopsis + + "model:///[entity]/[state] [traits query]" + +.. code-block:: text + :caption: Checkbook model query synopsis + + "model:///[entity]:[checkbook labels]" + +Since there can be many models, it is essential to select one, therefore a model Id is +always required. If ``entity`` and/or ``state`` are not specified, they are defaulted to +``$`` (all). + +.. code-block:: bash + :caption: Example of Model targeting by precise query + + sysinspect "model://router/network,devices/online" + +In the example above, a network is verified in a router only when it supposed to be online. +Under the hood, omitted parts are translated to "all" (``$``). E.g. ``router/network`` is +translated as ``router/network/$``, or Model name alone e.g. ``router`` is translated to +``router/$/$`` etc. + +Traits query is separated with a space and essentially a list of possible traits with their +values and logical operations. See :ref:`query_targeting` for more details. + +.. code-block:: bash + :caption: Example of Model targeting by checkbook labels + + sysinspect "model://router:network,devices" + +The example above is the same as the previous one, except it is using Checkbook. Entities +in the Checkbook are basically the top-high groups of other entities. + +Using Traits +------------ + +Every minion, running on the system can be targeted with specific criterion, describing it. +Each :bi:`minion` has a set of attributes, called :ref:`systraits`. They are used to identify +and target minions directly or from the Model. + +.. warning:: + + Using dynamic or static traits strongly depends on the use case of the Model. In terms of + portability, even though static traits are "hard-coding" claims, they are stable to the + system architecture. Likewise dynamic traits are move flexible, but they can also be more + difficult to debug, when they clash with each other. + + +.. _query_targeting: + +Query Targeting +--------------- + +Additionally, traits can be incorporated in the query. The main use of traits are +within the model, but sometimes one needs to target only a specific entity that has scope +exclusively bound to a specific minion. In the nutshell, the idea is to filter-out other +irrelevant minions, carrying *similar* entities. + +Synopsis of the query is as following: + +.. code-block:: text + :caption: Query synopsis + + ... + +Query does not support grouping with `( ... )` parentheses and is read from left to right. +Example: + +.. code-block:: bash + + "system.os.vendor:Debian and system.os.arch:ARM64 + or system.os.vendor:RHEL and system.os.arch:x86_64" + +The expression above is telling Sysinspect to target minions, those are: + +1. Running Linux Debian on ARM-64 architecture +2. Running Linux RHEL on x86_64 architecture + +As it is very clear from the example above, the use of operators must be careful. Switch +of them differently will cause different results. For example: + +.. code-block:: bash + + "system.os.vendor:Debian or system.os.arch:ARM64 + and system.os.vendor:RHEL or system.os.arch:x86_64" + +The expression above is telling Sysinspect to target minions, those are: + +1. Running Linux Debian +2. Running Linux RHEL on x86_64 architecture +3. Running on ARM-64 architecture + + +Distributed Entity +------------------ + +.. warning:: + + ⚠️ Planned feature for future releases, not implemented yet. + +Some entities can be distributed across different boxes. For example, "Backup over WiFi" +may involve a router, a WiFi antennae online and a storage with all disks in the RAID online. +However, Sysinspect can query that feature directly by its label. + +The following synopsis of the distributed entity notation in Checkbook: + +.. code-block:: text + + : + : + +For example, the use case of "Backup over WiFi" would be expressed the following way: + +.. code-block:: yaml + + backup_over_wifi: + - antennae: 'status:online and freq_ghz:5' # Use of custom traits via functions + - raid: 'system.os.vendor:Debian and net.hostname:storage.local' + - router: 'system.os.mem:16GB and &raid' # References "raid" group by label diff --git a/docs/global_config.rst b/docs/global_config.rst index 03f55cde..900c54b0 100644 --- a/docs/global_config.rst +++ b/docs/global_config.rst @@ -1,34 +1,255 @@ +.. raw:: html + + + +.. role:: u + :class: underlined + +.. role:: bi + :class: bolditalic + +.. _configuration: + Configuration ============= .. note:: - Configuration of Sysinspect + This document describes the configuration of Sysinspect system. + +Sysinspect can run in two modes: -Setup ------ +- **Distributed Mode** using network-connected Minions, allowing many "boxes" to be subscribed + to a Master command center. This is a typical use. +- **Solo Mode**, i.e. locally, only on the current local "box", affecting nothing else. This usage + is for a very small embedded systems only. -Configuration files can be in three location and are searched in the following order: +Config Location +--------------- + +Configuration files can be in three locations and are searched in the following order: 1. Current directory from which the app was launched: ``sysinspect.conf``. 2. "Dot-file" in the current user's home ``~/.sysinspect`` -3. As ``/etc/sysinspect.conf``. +3. As ``/etc/sysinspect/sysinspect.conf``. Synopsis -------- -Configuration file supports the following format: +Configuration is located under ``config`` section in a YAML file. This section +has two important sub-sections: -.. code-block:: text - :caption: Configuration Synopsis +- ``master`` for all settings of Sysinspect Master +- ``minion`` for covering settings of Sysinspect Minion - config: - : +Config Section +-------------- + +Main section of the entire configuration is ``config``. It is located at the root +of the configuration file and contains the following directives: ``modules`` -^^^^^^^^^^^ -Section ``modules`` defines the root of built-in modules: + Path to location of the modules, used in the model and states. Default + value is ``/usr/share/sysinspect/modules`` according to the LSB standard. + +``master`` + + Sysinspect Master configuration. + +``minion`` + + Sysinspect Minion configuration. + + +Master +^^^^^^ + +Sysinspect Master configuration is located under earlier mentioned ``master`` section, +and contains the following directives: + +``socket`` + + Path for a FIFO socket to communicate with the ``sysinspect`` command, + which is issuing commands over the network. + + Default value is ``/tmp/sysinspect-master.socket``. + +``bind.ip`` + + IPv4 address on which the Master is listening for all incoming and outgoing traffic + with Minion communication. + + Default value is ``0.0.0.0``. + +``bind.port`` + + Network port number on which the Master is listening using ``bind.ip`` directive. + + Sysinspect Master port is ``4200``. + + +.. important:: + + Master runs a **File Server service**. This service is :bi:`very important` for all the minions, + as they are exchanging data with the master, by downloading all the required artefacts to be + processed on their targets. + +File Server service serves static data, which is continuously checked by each minion and updated, +if that data changes. In particular, the artefacts are modules, trait configs, models, states etc. +Typically, File Server service has the root of all the data in ``/etc/sysinspect/data``. + +.. warning:: + Even though as of current version, there is no specific layout of the static data on the + File Server service to manager all the artifacts. However, this is a **subject to change**. + +Within the *"/data"* directory, *currently* one is free to organise the layout as they want. +However, it is :bi:`strongly` advised to keep all the models, states and other artefacts +separated from each other, using their own directories and namespaces. Future releases will have +configurable default namespaces for each cathegory of the artefacts. + +Below are directives for the configuration of the File Server service: + +``fileserver.bind.ip`` + + Same as ``bind.ip``, but for the internal File Server service. + +``fileserver.bind.port`` + + Network port number on which the File Server service is listening. + + File Server service port is ``4201``. + +``fileserver.models.root`` + + Relative path where are the master models kept. + +``fileserver.models`` + + List of subdirectories within ``fileserver.models.root``, exporting models. If a model is not + in the list, it will not be available for the minions. + +Example configuration for the Sysinspect Master: + +.. code-block:: yaml + + config: + master: + socket: /tmp/sysinspect-master.socket + bind.ip: 0.0.0.0 + bind.port: 4200 + + fileserver.bind.ip: 0.0.0.0 + fileserver.bind.port: 4201 + + fileserver.models.root: /models + fileserver.models: + - my_model + - my_other_model + + +Minion +^^^^^^ + +Sysinspect Minion configuration is located under earlier mentioned ``minion`` section, +and contains the following directives: + +``root`` + + Typically, Minion if running standard, the root of all data kept by a Minion is + defaulted to ``/etc/sysinspect``, same as Master. However, in an embedded and custom + systems this might not be possible, especially if the system is usually read-only + and writable directories are limited to only a few. In this case *root* must be + set according to the system setup. + +``master.ip`` + + Corresponds to ``bind.ip`` of Master node and should be identical. + +``master.port`` + + Corresponds to ``bind.ip.port`` of Master node and should be identical. + +Example configuration for the Sysinspect Minion: .. code-block:: yaml - modules: /opt/sysinspect/modules + config: + minion: + # Root directory where minion keeps all data. + # Default: /etc/sysinspect — same as for master + root: /etc/sysinspect + master.ip: 192.168.2.31 + master.port: 4200 + +Layout of ``/etc/sysinspect`` +----------------------------- + +Ideally, both Master and Minion have the same location of configuration and data collection, +which is defaulted to ``/etc/sysinspect``. This directory has many objects stored and has +a specific structure and purpose. For more making paths more short, this directory will be +referred as ``$SR`` *(Sysinspect Root)*. + +Common +^^^^^^ + +There are directories that are same on both Master and Minion: + +``$SR/functions`` + + Directory, containing custom trait functions. They are meant to be defined on the Master side + and then sync'ed to all the minions. + +Only on Master +^^^^^^^^^^^^^^ + +Public and private RSA keys of Master are: + +``$SR/master.rsa`` + + Master's private RSA key. + +``$SR/master.rsa.pub`` + + Master's public RSA key. + +``$SR/minion-keys`` + + Public keys from registered minions in format ``.rsa.pub``. + + Each registered minion has its own Id. Typically it is ``/etc/machine-id`` or automatically + generated one, if this file does not exist. + +``$SR/minion-registry`` + + A binary cache of minion's data, such as minion traits, data about currently connected minions etc. + This is fully purge-able directory, i.e. data can be freely deleted. However, Sysinspect Master + needs to be restarted and all minions needs to reconnect. + +Only on Minion +^^^^^^^^^^^^^^ + +Public and private RSA keys of Master are: + +``$SR/master.rsa`` + + Minion's private RSA key. + +``$SR/master.rsa.pub`` + + Minion's public RSA key. + +``$SR/traits`` + + Directory, containing custom static traits of a Minion. + +``$SR/models`` + + Directory, containing models. diff --git a/docs/index.rst b/docs/index.rst index 9dbf824a..4e96438f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,6 +9,7 @@ Welcome to the Project Sysinspect! :maxdepth: 1 global_config + genusage/overview modeldescr/overview moddev/overview moddescr/overview diff --git a/docs/modeldescr/overview.rst b/docs/modeldescr/overview.rst index 9052ac2c..6b630176 100644 --- a/docs/modeldescr/overview.rst +++ b/docs/modeldescr/overview.rst @@ -4,6 +4,8 @@ Model Description .. note:: Document explains the Model Definition and its components. +.. _model_description: + The Model is essentially a configuration of a system. It is written in YAML format, and it is following a specific expression schema and logic. diff --git a/libsysinspect/Cargo.toml b/libsysinspect/Cargo.toml index 6c2477a4..8185bdf8 100644 --- a/libsysinspect/Cargo.toml +++ b/libsysinspect/Cargo.toml @@ -4,17 +4,28 @@ version = "0.2.0" edition = "2021" [dependencies] +base64 = "0.22.1" chrono = "0.4.38" colored = "2.1.0" -indexmap = "2.6.0" +hex = "0.4.3" +indexmap = { version = "2.6.0", features = ["serde"] } lazy_static = "1.5.0" log = "0.4.22" nix = { version = "0.29.0", features = ["net", "user"] } +once_cell = "1.20.2" +pem = "3.0.4" +pest = "2.7.14" +pest_derive = "2.7.14" prettytable-rs = "0.10.0" +rand = "0.8.5" regex = "1.10.6" +rsa = { version = "0.9.6", features = ["pkcs5", "sha1", "sha2"] } serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0.128" serde_yaml = "0.9.34" +sha2 = "0.10.8" +sysinfo = { version = "0.32.0", features = ["linux-tmpfs"] } textwrap = { version = "0.16.1", features = ["hyphenation", "terminal_size"] } +tokio = { version = "1.41.1", features = ["full"] } unicode-segmentation = "1.12.0" walkdir = "2.5.0" diff --git a/libsysinspect/src/cfg/mmconf.rs b/libsysinspect/src/cfg/mmconf.rs new file mode 100644 index 00000000..e508c218 --- /dev/null +++ b/libsysinspect/src/cfg/mmconf.rs @@ -0,0 +1,189 @@ +use crate::{intp::functions::get_by_namespace, SysinspectError}; +use serde::{Deserialize, Serialize}; +use serde_yaml::{from_str, from_value, Value}; +use std::{fs, path::PathBuf}; + +// Network +pub static DEFAULT_ADDR: &str = "0.0.0.0"; +pub static DEFAULT_PORT: u32 = 4200; +pub static DEFAULT_FILESERVER_PORT: u32 = 4201; + +// Default directories +pub static DEFAULT_SOCKET: &str = "/var/run/sysinspect-master.socket"; +pub static DEFAULT_SYSINSPECT_ROOT: &str = "/etc/sysinspect"; +pub static DEFAULT_MODULES_ROOT: &str = "/usr/share/sysinspect/modules"; + +// All directories are relative to the sysinspect root +pub static CFG_MINION_KEYS: &str = "minion-keys"; +pub static CFG_MINION_REGISTRY: &str = "minion-registry"; +pub static CFG_FILESERVER_ROOT: &str = "data"; +pub static CFG_MODELS_ROOT: &str = "models"; +pub static CFG_TRAITS_ROOT: &str = "traits"; +pub static CFG_TRAIT_FUNCTIONS_ROOT: &str = "functions"; +pub static CFG_DB: &str = "registry"; + +// Key names +pub static CFG_MASTER_KEY_PUB: &str = "master.rsa.pub"; +pub static CFG_MASTER_KEY_PRI: &str = "master.rsa"; +pub static CFG_MINION_RSA_PUB: &str = "minion.rsa.pub"; +pub static CFG_MINION_RSA_PRV: &str = "minion.rsa"; + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +pub struct MinionConfig { + /// Root directory where minion keeps all data. + /// Default: /etc/sysinspect — same as for master + root: Option, + + /// IP address of Master + #[serde(rename = "master.ip")] + master_ip: String, + + /// Port of Master. Default: 4200 + #[serde(rename = "master.port")] + master_port: Option, + + /// Port of Master's fileserver. Default: 4201 + #[serde(rename = "master.fileserver.port")] + master_fileserver_port: Option, +} + +impl MinionConfig { + pub fn new(p: PathBuf) -> Result { + let cp = p.as_os_str().to_str().unwrap_or_default(); + if !p.exists() { + return Err(SysinspectError::ConfigError(format!("File not found: {}", cp))); + } + + if let Some(cfgv) = get_by_namespace(Some(from_str::(&fs::read_to_string(&p)?)?), "config.minion") { + return Ok(from_value::(cfgv)?); + } + + Err(SysinspectError::ConfigError(format!("Unable to read config at: {}", cp))) + } + + /// Return master addr + pub fn master(&self) -> String { + format!("{}:{}", self.master_ip, self.master_port.unwrap_or(DEFAULT_PORT)) + } + + /// Return master fileserver addr + pub fn fileserver(&self) -> String { + format!("{}:{}", self.master_ip, self.master_fileserver_port.unwrap_or(DEFAULT_FILESERVER_PORT)) + } + + /// Get minion root directory + pub fn root_dir(&self) -> PathBuf { + PathBuf::from(self.root.clone().unwrap_or(DEFAULT_SYSINSPECT_ROOT.to_string())) + } + + /// Get root directory for models + pub fn models_dir(&self) -> PathBuf { + self.root_dir().join(CFG_MODELS_ROOT) + } + /// Get root directory for functions + pub fn functions_dir(&self) -> PathBuf { + self.root_dir().join(CFG_TRAIT_FUNCTIONS_ROOT) + } + + /// Get root directory for drop-in traits + pub fn traits_dir(&self) -> PathBuf { + self.root_dir().join(CFG_TRAITS_ROOT) + } +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +pub struct MasterConfig { + // Bind IP listener. Default "the world", i.e. 0.0.0.0 + #[serde(rename = "bind.ip")] + bind_ip: Option, + + // Bind port. Default 4200 + #[serde(rename = "bind.port")] + bind_port: Option, + + // Path to FIFO socket. Default: /var/run/sysinspect-master.socket + socket: Option, + + #[serde(rename = "fileserver.bind.ip")] + fsr_ip: Option, + + #[serde(rename = "fileserver.bind.port")] + fsr_port: Option, + + // Exported models path root on the fileserver + #[serde(rename = "fileserver.models.root")] + fsr_models_root: String, + + // Exported models on the fileserver + #[serde(rename = "fileserver.models")] + fsr_models: Vec, +} + +impl MasterConfig { + pub fn new(p: PathBuf) -> Result { + let cp = p.as_os_str().to_str().unwrap_or_default(); + if !p.exists() { + return Err(SysinspectError::ConfigError(format!("File not found: {}", cp))); + } + + if let Some(cfgv) = get_by_namespace(Some(from_str::(&fs::read_to_string(&p)?)?), "config.master") { + return Ok(from_value::(cfgv)?); + } + + Err(SysinspectError::ConfigError(format!("Unable to read config at: {}", cp))) + } + + /// Return master addr + pub fn bind_addr(&self) -> String { + format!("{}:{}", self.bind_ip.to_owned().unwrap_or(DEFAULT_ADDR.to_string()), self.bind_port.unwrap_or(DEFAULT_PORT)) + } + + /// Get socket address + pub fn socket(&self) -> String { + self.socket.to_owned().unwrap_or(DEFAULT_SOCKET.to_string()) + } + + /// Return fileserver addr + pub fn fileserver_bind_addr(&self) -> String { + format!( + "{}:{}", + self.fsr_ip.to_owned().unwrap_or(DEFAULT_ADDR.to_string()), + self.fsr_port.unwrap_or(DEFAULT_FILESERVER_PORT) + ) + } + + /// Get a list of exported models from the fileserver + pub fn fileserver_models(&self) -> &Vec { + &self.fsr_models + } + + /// Get fileserver root + pub fn fileserver_root(&self) -> PathBuf { + self.root_dir().join(CFG_FILESERVER_ROOT) + } + + /// Get models root on the fileserver + pub fn fileserver_mdl_root(&self, alone: bool) -> PathBuf { + let mr = PathBuf::from(&self.fsr_models_root.strip_prefix("/").unwrap_or_default()); + if alone { + mr + } else { + self.fileserver_root().join(mr) + } + } + + /// Get default sysinspect root. For master it is always /etc/sysinspect + pub fn root_dir(&self) -> PathBuf { + PathBuf::from(DEFAULT_SYSINSPECT_ROOT.to_string()) + } + + /// Get minion keys store + pub fn keyman_root(&self) -> PathBuf { + self.root_dir().join(CFG_MINION_KEYS) + } + + /// Get minion registry + pub fn minion_registry_root(&self) -> PathBuf { + self.root_dir().join(CFG_MINION_REGISTRY) + } +} diff --git a/libsysinspect/src/cfg/mod.rs b/libsysinspect/src/cfg/mod.rs new file mode 100644 index 00000000..c5de2349 --- /dev/null +++ b/libsysinspect/src/cfg/mod.rs @@ -0,0 +1,52 @@ +/* +Config reader + */ + +pub mod mmconf; + +use crate::SysinspectError; +use nix::unistd::Uid; +use std::{env, path::PathBuf}; + +pub const APP_CONF: &str = "sysinspect.conf"; +pub const APP_DOTCONF: &str = ".sysinspect"; +pub const APP_HOME: &str = "/etc/sysinspect"; + +/// Select app conf +pub fn select_config(p: Option) -> Result { + // Override path from options + if let Some(ovrp) = p { + let ovrp = PathBuf::from(ovrp); + if ovrp.exists() { + return Ok(ovrp); + } + } + + // Current + let cfp: PathBuf = env::current_dir()?.canonicalize()?.join(APP_CONF); + if cfp.exists() { + return Ok(cfp); + } + + // Dot-file + let cfp = env::var_os("HOME").map(PathBuf::from).or_else(|| { + #[cfg(unix)] + { + Some(PathBuf::from(format!("/home/{}", Uid::current()))) + } + }); + if let Some(cfp) = cfp { + let cfp = cfp.join(APP_DOTCONF); + if cfp.exists() { + return Ok(cfp); + } + } + + // Global conf + let cfp = PathBuf::from(format!("{APP_HOME}/{APP_CONF}")); + if cfp.exists() { + return Ok(cfp); + } + + Err(SysinspectError::ConfigError("No config has been found".to_string())) +} diff --git a/libsysinspect/src/inspector.rs b/libsysinspect/src/inspector.rs new file mode 100644 index 00000000..f212843b --- /dev/null +++ b/libsysinspect/src/inspector.rs @@ -0,0 +1,86 @@ +use crate::{ + intp::{self, inspector::SysInspector}, + mdescr::mspec, + reactor::evtproc::EventProcessor, +}; +use intp::actproc::response::ActionResponse; + +#[derive(Debug, Default)] +pub struct SysInspectRunner { + model_pth: String, + state: Option, + entities: Vec, + + // Check book labels + cb_labels: Vec, +} + +impl SysInspectRunner { + pub fn new() -> SysInspectRunner { + SysInspectRunner { ..Default::default() } + } + + /// Set model path + pub fn set_model_path(&mut self, p: &str) { + self.model_pth = p.to_string() + } + + /// Set process state + pub fn set_state(&mut self, state: Option) { + self.state = state; + } + + /// Set entities to query + pub fn set_entities(&mut self, entities: Vec) { + self.entities = entities; + } + + /// Set checkbook labels + pub fn set_checkbook_labels(&mut self, labels: Vec) { + self.cb_labels = labels; + } + + pub fn start(&self) { + log::info!("Starting sysinspect runner"); + match mspec::load(&self.model_pth) { + Ok(spec) => { + log::debug!("Initalising inspector"); + match SysInspector::new(spec) { + Ok(isp) => { + // Setup event processor + let mut evtproc = EventProcessor::new().set_config(isp.cfg()); + + let actions = if !self.cb_labels.is_empty() { + isp.actions_by_relations(self.cb_labels.to_owned(), self.state.to_owned()) + } else { + isp.actions_by_entities(self.entities.to_owned(), self.state.to_owned()) + }; + + match actions { + Ok(actions) => { + for ac in actions { + match ac.run() { + Ok(response) => { + let response = response.unwrap_or(ActionResponse::default()); + evtproc.receiver().register(response.eid().to_owned(), response); + } + Err(err) => { + log::error!("{err}") + } + } + } + evtproc.process(); + } + Err(err) => { + log::error!("{}", err); + } + } + } + Err(err) => log::error!("{err}"), + } + log::debug!("Done"); + } + Err(err) => log::error!("Error loading mspec: {}", err), + }; + } +} diff --git a/libsysinspect/src/intp/README.txt b/libsysinspect/src/intp/README.txt new file mode 100644 index 00000000..cf08bd8c --- /dev/null +++ b/libsysinspect/src/intp/README.txt @@ -0,0 +1 @@ +Package interpreting module spec (runtime) \ No newline at end of file diff --git a/libsysinspect/src/intp/conf.rs b/libsysinspect/src/intp/conf.rs index c34362a5..85e04ca7 100644 --- a/libsysinspect/src/intp/conf.rs +++ b/libsysinspect/src/intp/conf.rs @@ -1,4 +1,4 @@ -use crate::{util, SysinspectError}; +use crate::{cfg::mmconf::DEFAULT_MODULES_ROOT, util, SysinspectError}; use serde::{Deserialize, Serialize}; use serde_yaml::Value; use std::{collections::HashMap, path::PathBuf}; @@ -69,7 +69,7 @@ impl EventConfig { /// The entire config #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct Config { - modules: PathBuf, + modules: Option, // EventId to config, added later events: Option>, @@ -87,7 +87,7 @@ impl Config { /// Get module from the namespace pub fn get_module(&self, namespace: &str) -> Result { // Fool-proof cleanup, likely a bad idea - let modpath = self.modules.join( + let modpath = &self.modules.to_owned().unwrap_or(PathBuf::from(DEFAULT_MODULES_ROOT)).join( namespace .trim_start_matches('.') .trim_end_matches('.') @@ -102,7 +102,7 @@ impl Config { return Err(SysinspectError::ModuleError(format!("Module \"{}\" was not found at {:?}", namespace, modpath))); } - Ok(modpath) + Ok(modpath.to_owned()) } /// Set events config diff --git a/libsysinspect/src/lib.rs b/libsysinspect/src/lib.rs index 6883283c..80aa2476 100644 --- a/libsysinspect/src/lib.rs +++ b/libsysinspect/src/lib.rs @@ -1,16 +1,22 @@ use std::{ error::Error, + ffi::NulError, fmt::{Display, Formatter, Result}, io, }; use mdescr::mspec; +pub mod cfg; +pub mod inspector; pub mod intp; pub mod logger; pub mod mdescr; pub mod modlib; +pub mod proto; pub mod reactor; +pub mod rsa; +pub mod traits; pub mod util; #[derive(Debug)] @@ -20,11 +26,17 @@ pub enum SysinspectError { ModelDSLError(String), ModuleError(String), ConfigError(String), + MasterGeneralError(String), + MinionGeneralError(String), + ProtoError(String), // Wrappers for the system errors IoErr(io::Error), SerdeYaml(serde_yaml::Error), SerdeJson(serde_json::Error), + FFINullError(NulError), + DynError(Box), + AsynDynError(Box), } impl Error for SysinspectError { @@ -48,6 +60,12 @@ impl Display for SysinspectError { SysinspectError::ModelDSLError(err) => format!("(DSL) {err}"), SysinspectError::ModuleError(err) => format!("(Module) {err}"), SysinspectError::ConfigError(err) => format!("(Config) {err}"), + SysinspectError::FFINullError(err) => format!("(System) {err}"), + SysinspectError::MasterGeneralError(err) => format!("(Master) {err}"), + SysinspectError::MinionGeneralError(err) => format!("(Minion) {err}"), + SysinspectError::ProtoError(err) => format!("(Protocol) {err}"), + SysinspectError::DynError(err) => format!("(General) {err}"), + SysinspectError::AsynDynError(err) => format!("(General part) {err}"), }; write!(f, "{msg}")?; @@ -75,3 +93,23 @@ impl From for SysinspectError { SysinspectError::SerdeJson(err) } } + +/// Handle FFI Nul error +impl From for SysinspectError { + fn from(err: NulError) -> Self { + SysinspectError::FFINullError(err) + } +} + +// Implement From> for SysinspectError +impl From> for SysinspectError { + fn from(err: Box) -> SysinspectError { + SysinspectError::DynError(err) + } +} + +impl From> for SysinspectError { + fn from(err: Box) -> SysinspectError { + SysinspectError::DynError(err) + } +} diff --git a/libsysinspect/src/mdescr/README.txt b/libsysinspect/src/mdescr/README.txt new file mode 100644 index 00000000..2d79db6f --- /dev/null +++ b/libsysinspect/src/mdescr/README.txt @@ -0,0 +1 @@ +Package to work with the model spec (before actual runtime) \ No newline at end of file diff --git a/libsysinspect/src/mdescr/mspec.rs b/libsysinspect/src/mdescr/mspec.rs index 7faeea95..755ed7a2 100644 --- a/libsysinspect/src/mdescr/mspec.rs +++ b/libsysinspect/src/mdescr/mspec.rs @@ -1,9 +1,7 @@ use super::{datapatch, mspecdef::ModelSpec}; -use crate::SysinspectError; -use nix::unistd::Uid; +use crate::{cfg::select_config, SysinspectError}; use serde_yaml::Value; use std::{ - env::{self}, fs::{self}, path::{Path, PathBuf}, }; @@ -11,8 +9,6 @@ use walkdir::WalkDir; pub const MODEL_INDEX: &str = "model.cfg"; pub const MODEL_FILE_EXT: &str = ".cfg"; -pub const APP_CONF: &str = "sysinspect.conf"; -pub const APP_DOTCONF: &str = ".sysinspect"; /// Spec loader object struct SpecLoader { @@ -70,7 +66,6 @@ impl SpecLoader { fn merge_parts(&mut self, chunks: &mut Vec) -> Result { if chunks.is_empty() { return Err(SysinspectError::ModelMultipleIndex("No data found".to_string())); - // XXX: Add one more exception } let mut base = chunks.remove(0); @@ -104,37 +99,6 @@ impl SpecLoader { Ok(base) } - /// Select app conf - fn select_config(&self) -> Result { - // Current - let cfp: PathBuf = env::current_dir()?.canonicalize()?.join(APP_CONF); - if cfp.exists() { - return Ok(cfp); - } - - // Dot-file - let cfp = env::var_os("HOME").map(PathBuf::from).or_else(|| { - #[cfg(unix)] - { - Some(PathBuf::from(format!("/home/{}", Uid::current()))) - } - }); - if let Some(cfp) = cfp { - let cfp = cfp.join(APP_DOTCONF); - if cfp.exists() { - return Ok(cfp); - } - } - - // Global conf - let cfp = PathBuf::from(format!("/etc/{}", APP_CONF)); - if cfp.exists() { - return Ok(cfp); - } - - Err(SysinspectError::ConfigError("No config has been found".to_string())) - } - /// Load model spec by merging all the data parts and validating /// its content. fn load(&mut self) -> Result { @@ -157,7 +121,7 @@ impl SpecLoader { } // Load app config and merge to the main model - base.push(serde_yaml::from_str::(&fs::read_to_string(self.select_config()?)?)?); + base.push(serde_yaml::from_str::(&fs::read_to_string(select_config(None)?)?)?); let mut base = self.merge_parts(&mut base)?; if !iht.is_empty() { diff --git a/libsysinspect/src/modlib/README.txt b/libsysinspect/src/modlib/README.txt new file mode 100644 index 00000000..fc766f85 --- /dev/null +++ b/libsysinspect/src/modlib/README.txt @@ -0,0 +1 @@ +Package to work with the external inspection modules. \ No newline at end of file diff --git a/libsysinspect/src/proto/README.md b/libsysinspect/src/proto/README.md new file mode 100644 index 00000000..6cf345c5 --- /dev/null +++ b/libsysinspect/src/proto/README.md @@ -0,0 +1,176 @@ +# Master/minion protocol + +Protocol description about message exchange between master and a minion. + +## Message Structure + +### Master message structure + +The following is a message structure for a master: + +```json +{ + // Target Destinations + "t": [], + + // Request Type + "r": "", + + // Payload Data in base64 (anything) + "d": "", + + // Return code int + "c": 0, +} +``` + +The following types for "`r`" are available: + +- `add` — Minion registration request. Payload contains an RSA public key of a master. +- `cmd` — A regular command to a minion(s). +- `tr` — Request to return all minion traits for the database sync (payload is empty) +or push new (payload exists). This must be used together with the targeting. +- `rm` — Minion un-registration. + +## Targeting + +Type `t` (target) is a list of target structures. A target structure can target minions +by the following criterias: + +1. Hostnames with UNIX type globbing. E.g.: `web*.com`. +2. Machine Id +3. Traits (any) + +### Target Structure + +```json +{ + // Trait Targeted traits + "t": {}, + + // Minion Id List of minion Ids + "id": [], + + // Hostnames List of minion hostnames + "h": [], +} +``` + +Example targeting by an IPv4 trait, using globbing: + +```json +{ + "t": {"system.net.*.ipv4": "192.168.*"}, +} +``` + +Example targeting by an Id: + +```json +{ + "id": "30006546535e428aba0a0caa6712e225", +} +``` + +Example targeting by hosts, starting their domain names as "web": + +```json +{ + "h": "web*", +} +``` + +Example targeting all minions: + +```json +{ + "h": ["*"], +} +``` + +### Minion message structure + +The following is a message structure for a minion: + +```json +{ + // Id Machine id or pre-generated equivalent if none + "id": "", + + // Request Type + "r": "", + + // Payload Data in base64 (anything) + "d": "", + + // Return code int + "c": 0, +} +``` + +The following request types for "`r`" are available: + +- `add` — Minion registration context. Payload contains nothing. + In this case Master responds with `add` request, containing its RSA public key. + A Minion needs to accept it by a fingerprint. + +- `rsp` — A regular response to any command. +- `ehlo` — Hello notice for a newly connected minion (any). Contains Minion Id RSA cipher. + +## Types + +### Request/Response + +- `add` — Add a minion, registration request. +- `rm` — Remove a minion, un-registration. +- `rsp` — Regular response to any Master command. +- `cmd` — Regular command to any Minion. +- `tr` — Request to return all minion traits. +- `ehlo` — Hello message to initiate protocol. +- `retry` — Retry connect (e.g. after the registration). +- `pi` — Ping request. +- `po` — Pong response. +- `undef` — Unknown agent. + +### Return Codes + +- `Undef`: 0 — No specific return code or code is ignorable. +- `Success`: 1 — Successfully completed the routine. +- `GeneralFailure`: 2 — General failure, unspecified. Equal to 1 of POSIX. +- `NotRegistered`: 3 — Minion is not registered. Registration sequence required. +- `AlreadyRegistered`: 4 — Minion is already registered. +- `AlreadyConnected`: 5 — Minion connection duplicate. +- `Unknown`: N/A — Internal designator of unrecognised incoming error code. + +## Hello (ehlo) + +This sequence requires no established connection. + +1. Master listens. +2. Minion sends type `ehlo` request with no payload. +3. Master checks if the Id is registered. +4. In case there is no Id registered, Master responds with the error code and kills +the connection. +5. Master responds with a non-zero return code, mapped to a successful connection. + +## Minion Registration Sequence + +This sequence requires no established connection. + +1. Master listens. +2. Minion sends a request type `add` with an empty payload. +3. Master checks if the Id is registered. +4. In case there is an Id registered, Master responds with the error code and kills +the connection, awaiting `ehlo` instead. +5. Master sends type `add` response with RSA public key. +6. Minion accepts the key, storing it and responds with type `ehlo`, containing own +Id within RSA cipher, using Master's public key. +7. Master responds with a non-zero return code, mapped to a successful connection. + +## Minion Call + +This sequence requires established successful connection. + +1. Master broadcasts type `cmd` to all minions with the destination mask. +2. Each minion accepts the message and looks if a target matches it. +3. Each Minion responds back with type `rsp`. diff --git a/libsysinspect/src/proto/errcodes.rs b/libsysinspect/src/proto/errcodes.rs new file mode 100644 index 00000000..68f75a63 --- /dev/null +++ b/libsysinspect/src/proto/errcodes.rs @@ -0,0 +1,25 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum ProtoErrorCode { + /// No code + Undef = 0, + + /// Successfully completed + Success = 1, + + /// General unspecified failure + GeneralFailure = 2, + + /// Minion is not registered + NotRegistered = 3, + + /// Minion is already registered + AlreadyRegistered = 4, + + /// Minion is already connected + AlreadyConnected = 5, + + /// Unassigned, unknown + Unknown, +} diff --git a/libsysinspect/src/proto/mod.rs b/libsysinspect/src/proto/mod.rs new file mode 100644 index 00000000..de9d9fc1 --- /dev/null +++ b/libsysinspect/src/proto/mod.rs @@ -0,0 +1,219 @@ +pub mod errcodes; +pub mod payload; +pub mod query; +pub mod rqtypes; + +use crate::SysinspectError; +use errcodes::ProtoErrorCode; +use rqtypes::RequestType; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value; + +/// Master message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MasterMessage { + #[serde(rename = "t")] + target: MinionTarget, + + #[serde(rename = "r")] + request: RequestType, + + #[serde(rename = "d")] + data: Value, + + #[serde(rename = "c")] + retcode: usize, +} + +impl MasterMessage { + /// Master message constructor + pub fn new(rtype: RequestType, data: Value) -> MasterMessage { + MasterMessage { target: Default::default(), request: rtype, data, retcode: ProtoErrorCode::Undef as usize } + } + + /// Add a target. + pub fn set_target(&mut self, t: MinionTarget) { + self.target = t; + } + + /// Set return code + pub fn set_retcode(&mut self, retcode: ProtoErrorCode) { + self.retcode = retcode as usize; + } + + /// Get return code + pub fn get_retcode(&self) -> ProtoErrorCode { + match &self.retcode { + 0 => ProtoErrorCode::Undef, + 1 => ProtoErrorCode::Success, + 2 => ProtoErrorCode::GeneralFailure, + 3 => ProtoErrorCode::NotRegistered, + 4 => ProtoErrorCode::AlreadyRegistered, + 5 => ProtoErrorCode::AlreadyConnected, + _ => ProtoErrorCode::Unknown, + } + } + + /// Request type + pub fn req_type(&self) -> &RequestType { + &self.request + } + + /// Get payload + pub fn payload(&self) -> &Value { + &self.data + } + + /// Get targeting means + pub fn get_target(&self) -> &MinionTarget { + &self.target + } +} + +/// Minion message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MinionMessage { + id: String, + sid: String, // Temporary session Id + + #[serde(rename = "r")] + request: RequestType, + + #[serde(rename = "d")] + data: String, + + #[serde(rename = "c")] + retcode: usize, +} + +impl MinionMessage { + /// Message constructor + pub fn new(id: String, rtype: RequestType, data: String) -> MinionMessage { + MinionMessage { id, request: rtype, data, retcode: ProtoErrorCode::Undef as usize, sid: "".to_string() } + } + + /// Set return code + pub fn set_retcode(&mut self, retcode: ProtoErrorCode) { + self.retcode = retcode as usize; + } + + /// Get return code + pub fn get_retcode(&self) -> ProtoErrorCode { + match &self.retcode { + 0 => ProtoErrorCode::Undef, + 1 => ProtoErrorCode::Success, + 2 => ProtoErrorCode::GeneralFailure, + 3 => ProtoErrorCode::NotRegistered, + 4 => ProtoErrorCode::AlreadyRegistered, + 5 => ProtoErrorCode::AlreadyConnected, + _ => ProtoErrorCode::Unknown, + } + } + + /// Set Session Id + pub fn set_sid(&mut self, sid: String) { + self.sid = sid + } + + /// Request type + pub fn req_type(&self) -> &RequestType { + &self.request + } + + /// Get minion Id + pub fn id(&self) -> &str { + &self.id + } + + /// Get payload + pub fn payload(&self) -> &str { + &self.data + } +} + +/// Minion target +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct MinionTarget { + /// List of minion Ids + id: String, + + /// Session Id + sid: String, // XXX: Should be gone + + /// Scheme to call (model:// or state://) + #[serde(rename = "s")] + scheme: String, + + /// Traits query that needs to be parsed at Minion + #[serde(rename = "qt")] + traits_query: String, + + #[serde(rename = "h")] + hostnames: Vec, +} + +impl MinionTarget { + pub fn new(mid: &str, sid: &str) -> MinionTarget { + MinionTarget { id: mid.to_string(), sid: sid.to_string(), ..Default::default() } + } + + /// Add hostnames + pub fn add_hostname(&mut self, hostname: &str) { + self.hostnames.push(hostname.to_string()); + } + + pub fn id(&self) -> &String { + &self.id + } + + pub fn sid(&self) -> &String { + &self.sid + } + + pub fn hostnames(&self) -> &Vec { + &self.hostnames + } + + /// Get scheme + pub fn scheme(&self) -> &String { + &self.scheme + } + + /// Set scheme + pub fn set_scheme(&mut self, scheme: &str) { + self.scheme = scheme.to_string(); + } + + /// Set traits query + pub fn set_traits_query(&mut self, traits: &str) { + self.traits_query = traits.to_string(); + } + + /// Traits query itself. + pub fn traits_query(&self) -> &String { + &self.traits_query + } +} + +pub trait ProtoConversion: Serialize + DeserializeOwned { + fn serialise(&self) -> Result; + fn sendable(&self) -> Result, SysinspectError>; +} + +impl ProtoConversion for T +where + T: Serialize + DeserializeOwned, +{ + /// Serialise self + fn serialise(&self) -> Result { + match serde_json::to_string(self) { + Ok(out) => Ok(out), + Err(err) => Err(SysinspectError::MinionGeneralError(format!("{err}"))), + } + } + + /// Serialise self to bytes + fn sendable(&self) -> Result, SysinspectError> { + Ok(self.serialise()?.as_bytes().to_vec()) + } +} diff --git a/libsysinspect/src/proto/payload.rs b/libsysinspect/src/proto/payload.rs new file mode 100644 index 00000000..09ab9924 --- /dev/null +++ b/libsysinspect/src/proto/payload.rs @@ -0,0 +1,87 @@ +// XXX: Refactor: move all message types-related code here! + +/* +Payload types and their deserialisation. +*/ + +use serde::{Deserialize, Serialize}; +use serde_json::{from_value, Value}; +use std::collections::HashMap; + +/// Payload types +pub enum PayloadType { + ModelOrStatement(ModStatePayload), + Undef(Value), +} + +impl TryFrom for PayloadType { + type Error = serde_json::Error; + + fn try_from(value: Value) -> Result { + if let Ok(v) = from_value::(value.clone()) { + return Ok(PayloadType::ModelOrStatement(v)); + } + Ok(PayloadType::Undef(value)) + } +} + +/// Message is sent to the Minion +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ModStatePayload { + // Each file has a SHA1 checksum to prevent huge bogus traffic + files: HashMap, + + // Root where models starts. It corresponds to "fileserver.models.root" conf of Master. + // It will be substracted from each file path when saving + models_root: String, + + // session Id + sid: String, + + // sysinspect URI + uri: String, +} + +impl ModStatePayload { + pub fn new(sid: String) -> Self { + ModStatePayload { sid, ..Default::default() } + } + + /// Set URI + pub fn set_uri(mut self, uri: String) -> Self { + self.uri = uri; + self + } + + /// Add files + pub fn add_files(mut self, files: HashMap) -> Self { + self.files.extend(files); + self + } + + /// Set models root + pub fn set_models_root(mut self, mr: &str) -> Self { + self.models_root = mr.to_string(); + self + } + + /// Get list of files to download + pub fn files(&self) -> &HashMap { + &self.files + } + + /// Get SID + pub fn sid(&self) -> &str { + &self.sid + } + + /// Get URI + pub fn uri(&self) -> &str { + &self.uri + } + + /// Get root of models + pub fn models_root(&self) -> &str { + &self.models_root + } +} diff --git a/libsysinspect/src/proto/query.rs b/libsysinspect/src/proto/query.rs new file mode 100644 index 00000000..e10dd450 --- /dev/null +++ b/libsysinspect/src/proto/query.rs @@ -0,0 +1,99 @@ +use std::sync::{Arc, Mutex}; + +use crate::SysinspectError; + +/// Targeting schemes +pub static SCHEME_MODEL: &str = "model://"; +pub static SCHEME_STATE: &str = "state://"; + +/// +/// Query parser (scheme). +/// It has the following format: +/// +/// /[entity]/[state] +/// :[checkbook labels] +/// +/// If `"entity"` and/or `"state"` are omitted, they are globbed to `"$"` (all). +#[derive(Debug, Clone, Default)] +pub struct MinionQuery { + src: String, + entity: Option, + state: Option, + scheme: String, + labels: Option, +} + +impl MinionQuery { + pub fn new(q: &str) -> Result>, SysinspectError> { + let q = q.trim(); + if !q.starts_with(SCHEME_STATE) && !q.starts_with(SCHEME_MODEL) { + return Err(SysinspectError::ProtoError("Query has unknown scheme".to_string())); + } + + let sq: Vec<&str> = q.split("://").collect(); + if sq.len() != 2 { + return Err(SysinspectError::ProtoError("Unable to parse scheme".to_string())); + } + + let mut instance = Self { ..Default::default() }; + instance.scheme = sq[0].to_owned(); + + let precise = sq[1].contains('/'); + let sq: Vec<&str> = sq[1].split(if precise { '/' } else { ':' }).filter(|s| !s.is_empty()).collect(); + match sq.len() { + 0 => { + return Err(SysinspectError::ProtoError("No model has been targeted".to_string())); + } + 1 => instance.src = sq[0].to_string(), + 2 => { + instance.src = sq[0].to_string(); + if precise { + instance.entity = Some(sq[1].to_string()); + } else { + instance.labels = Some(sq[1].to_string()); + } + } + 3 => { + instance.src = sq[0].to_string(); + if precise { + instance.entity = Some(sq[1].to_string()); + instance.state = Some(sq[2].to_string()); + } + } + _ => {} + } + + Ok(Arc::new(Mutex::new(instance))) + } + + /// Get target model name + pub fn target(&self) -> &str { + &self.src + } + + /// Get entities, comma-separated + pub fn entities(&self) -> Vec { + if let Some(entity) = &self.entity { + return entity.split(',').map(|s| s.to_string()).collect(); + } + + vec![] + } + + /// Get checkbook labels, comma-separated + pub fn checkbook_labels(&self) -> Vec { + if let Some(l) = &self.labels { + return l.split(',').map(|s| s.to_string()).collect(); + } + vec![] + } + + /// Get desired state of the model + pub fn state(&self) -> Option { + if let Some(state) = &self.state { + return Some(state.to_owned()); + } + + None + } +} diff --git a/libsysinspect/src/proto/rqtypes.rs b/libsysinspect/src/proto/rqtypes.rs new file mode 100644 index 00000000..025edfb6 --- /dev/null +++ b/libsysinspect/src/proto/rqtypes.rs @@ -0,0 +1,44 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum RequestType { + /// Minion registration request or context. + #[serde(rename = "add")] + Add, + + /// Minion un-registration request. + #[serde(rename = "rm")] + Remove, + + /// Regular response to any Master command + #[serde(rename = "rsp")] + Response, + + /// Regular command to any Minion + #[serde(rename = "cmd")] + Command, + + /// Request to return all minion traits + #[serde(rename = "tr")] + Traits, + + /// Hello/ehlo + #[serde(rename = "ehlo")] + Ehlo, + + /// Retry connect (e.g. after the registration) + #[serde(rename = "retry")] + Reconnect, + + /// Unknown agent + #[serde(rename = "undef")] + AgentUnknown, + + /// Ping + #[serde(rename = "pi")] + Ping, + + /// Pong + #[serde(rename = "po")] + Pong, +} diff --git a/libsysinspect/src/reactor/README.txt b/libsysinspect/src/reactor/README.txt new file mode 100644 index 00000000..b82bbdc9 --- /dev/null +++ b/libsysinspect/src/reactor/README.txt @@ -0,0 +1,2 @@ +Package that implements reactor, formatters, handlers etc. +Everything related to them should go here. \ No newline at end of file diff --git a/libsysinspect/src/rsa/keys.rs b/libsysinspect/src/rsa/keys.rs new file mode 100644 index 00000000..79a349a4 --- /dev/null +++ b/libsysinspect/src/rsa/keys.rs @@ -0,0 +1,155 @@ +use rand::rngs::OsRng; +use rsa::{ + pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey, EncodeRsaPrivateKey, EncodeRsaPublicKey}, + pkcs1v15::{Signature, SigningKey, VerifyingKey}, + sha2::{Digest, Sha256}, + signature::SignerMut, + signature::{Keypair, SignatureEncoding, Verifier}, + Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey, +}; +use std::{error::Error, fs, io, path::PathBuf}; + +use crate::SysinspectError; + +/// Default key size. +pub static DEFAULT_KEY_SIZE: usize = 1048; + +#[allow(clippy::large_enum_variant)] +pub enum RsaKey { + Private(RsaPrivateKey), + Public(RsaPublicKey), +} + +/// Generate RSA keys +pub fn keygen(bits: usize) -> Result<(RsaPrivateKey, RsaPublicKey), Box> { + let mut rng = OsRng; + let prk = RsaPrivateKey::new(&mut rng, bits)?; + let pbk = RsaPublicKey::from(&prk); + + Ok((prk, pbk)) +} + +/// Serializes RSA private and public keys to PEM format. +pub fn to_pem( + prk: Option<&RsaPrivateKey>, pbk: Option<&RsaPublicKey>, +) -> Result<(Option, Option), Box> { + Ok(( + if prk.is_some() { + Some(pem::encode(&pem::Pem::new("RSA PRIVATE KEY", prk.unwrap().to_pkcs1_der()?.as_bytes().to_vec()))) + } else { + None + }, + if pbk.is_some() { + Some(pem::encode(&pem::Pem::new("RSA PUBLIC KEY", pbk.unwrap().to_pkcs1_der()?.as_bytes().to_vec()))) + } else { + None + }, + )) +} + +/// Deserializes RSA private and public keys from PEM format. +pub fn from_pem( + prk_pem: Option<&str>, pbk_pem: Option<&str>, +) -> Result<(Option, Option), Box> { + Ok(( + if prk_pem.is_some() { + Some(RsaPrivateKey::from_pkcs1_der(pem::parse(prk_pem.unwrap_or_default())?.contents())?) + } else { + None + }, + if pbk_pem.is_some() { + Some(RsaPublicKey::from_pkcs1_der(pem::parse(pbk_pem.unwrap_or_default())?.contents())?) + } else { + None + }, + )) +} + +/// Sign data with the private key +pub fn sign_data(prk: RsaPrivateKey, data: &[u8]) -> Result, Box> { + let mut sk = SigningKey::::new(prk); + let sig = sk.sign(data); + sk.verifying_key().verify(data, &sig)?; + + Ok((*sig.to_bytes()).to_vec()) +} + +/// Verify signature from the pubic key +pub fn verify_sign(pbk: &RsaPublicKey, data: &[u8], sig: Vec) -> Result> { + Ok(VerifyingKey::::new(pbk.clone()).verify(data, &Signature::try_from(sig.as_slice())?).is_ok()) +} + +/// Get fingerprint of a public key +#[allow(clippy::format_collect)] +pub fn get_fingerprint(pbk: &RsaPublicKey) -> Result> { + let mut digest = Sha256::new(); + digest.update(pbk.to_pkcs1_der()?.as_bytes()); + Ok(digest.finalize().iter().map(|byte| format!("{:02x}", byte)).collect()) +} + +// Encrypt data +pub fn encrypt(pbk: RsaPublicKey, data: Vec) -> Result, Box> { + Ok(pbk.encrypt(&mut rand::thread_rng(), Pkcs1v15Encrypt, &data[..])?) +} + +// Decrypt data +pub fn decrypt(prk: RsaPrivateKey, cipher: Vec) -> Result, Box> { + Ok(prk.decrypt(Pkcs1v15Encrypt, &cipher)?) +} + +/// Write private or a public key to a file +pub fn key_to_file(prk: &RsaKey, p: &str, name: &str) -> Result<(), SysinspectError> { + let p = PathBuf::from(p).join(name); + if p.exists() { + return Err(SysinspectError::IoErr(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("File {} already exists", p.to_str().unwrap_or_default()), + ))); + } + + let mut pem = String::default(); + match prk { + RsaKey::Private(prk) => { + if let Ok((prk_pem, _)) = to_pem(Some(prk), None) { + if let Some(prk_pem) = prk_pem { + pem = prk_pem; + } + } else { + return Err(SysinspectError::IoErr(io::Error::new(io::ErrorKind::InvalidData, "Unable to create PEM key"))); + } + } + RsaKey::Public(pbk) => { + if let Ok((_, pbk_pem)) = to_pem(None, Some(pbk)) { + if let Some(pbk_pem) = pbk_pem { + pem = pbk_pem; + } + } else { + return Err(SysinspectError::IoErr(io::Error::new(io::ErrorKind::InvalidData, "Unable to create PEM key"))); + } + } + } + fs::write(&p, pem.as_bytes())?; + log::debug!("Wrote PEM file as {}", p.to_str().unwrap_or_default()); + + Ok(()) +} + +/// Read private or a public key from a file +pub fn key_from_file(p: &str) -> Result, SysinspectError> { + let pth = PathBuf::from(p); + if !pth.exists() { + return Err(SysinspectError::IoErr(io::Error::new(io::ErrorKind::NotFound, format!("File {} not found", p)))); + } + + let data = &fs::read_to_string(pth)?; + + if data.contains("RSA PRIVATE KEY") { + if let Ok((Some(prk), _)) = from_pem(Some(data), None) { + return Ok(Some(RsaKey::Private(prk))); + } + } else if let Ok((_, Some(pbk))) = from_pem(None, Some(data)) { + return Ok(Some(RsaKey::Public(pbk))); + } + + Ok(None) +} diff --git a/libsysinspect/src/rsa/mod.rs b/libsysinspect/src/rsa/mod.rs new file mode 100644 index 00000000..703bc087 --- /dev/null +++ b/libsysinspect/src/rsa/mod.rs @@ -0,0 +1 @@ +pub mod keys; diff --git a/libsysinspect/src/traits/README.txt b/libsysinspect/src/traits/README.txt new file mode 100644 index 00000000..b014612c --- /dev/null +++ b/libsysinspect/src/traits/README.txt @@ -0,0 +1 @@ +System traits package. \ No newline at end of file diff --git a/libsysinspect/src/traits/mod.rs b/libsysinspect/src/traits/mod.rs new file mode 100644 index 00000000..123720bf --- /dev/null +++ b/libsysinspect/src/traits/mod.rs @@ -0,0 +1,97 @@ +pub mod systraits; + +use std::collections::HashMap; + +use crate::SysinspectError; +use pest::Parser; +use pest_derive::Parser; +use serde_json::Value; +use systraits::SystemTraits; + +/// Standard Traits +pub static SYS_ID: &str = "system.id"; +pub static SYS_OS_KERNEL: &str = "system.kernel"; +pub static SYS_OS_VERSION: &str = "system.os.version"; +pub static SYS_OS_NAME: &str = "system.os.name"; +pub static SYS_OS_DISTRO: &str = "system.os.distribution"; + +pub static SYS_NET_HOSTNAME: &str = "system.hostname"; +pub static HW_MEM: &str = "hardware.memory"; +pub static HW_SWAP: &str = "hardware.swap"; +pub static HW_CPU_TOTAL: &str = "hardware.cpu.total"; +pub static HW_CPU_BRAND: &str = "hardware.cpu.brand"; +pub static HW_CPU_FREQ: &str = "hardware.cpu.frequency"; +pub static HW_CPU_VENDOR: &str = "hardware.cpu.vendor"; +pub static HW_CPU_CORES: &str = "hardware.cpu.cores"; + +#[derive(Parser)] +#[grammar = "traits/traits_query.pest"] +struct QueryParser; + +/// Parse a very simple traits query. It returns an array of OR arrays, containing AND values. +/// Example: +/// +/// "foo and bar or baz" +/// +/// This yields to the following structure: +/// +/// [[foo, bar], [baz]] +/// +/// Each inner array should be treated with AND operator. +pub fn parse_traits_query(input: &str) -> Result>, SysinspectError> { + let pairs = QueryParser::parse(Rule::expression, input) + .map_err(|err| SysinspectError::ModelDSLError(format!("Invalid query: {err}")))?; + + let mut out = Vec::new(); + + for expr in pairs { + if expr.as_rule() == Rule::expression { + for group_pair in expr.into_inner() { + if group_pair.as_rule() == Rule::group { + let mut terms = Vec::new(); + for term_pair in group_pair.into_inner() { + if term_pair.as_rule() == Rule::term { + terms.push(term_pair.as_str().trim().to_string()); + } + } + out.push(terms); + } + } + } + } + + Ok(out) +} + +/// Parse trait query to trait typed (JSON) query +pub fn to_typed_query(qt: Vec>) -> Result>>, SysinspectError> { + let mut out: Vec>> = Vec::default(); + for and_op in qt { + let mut out_op: Vec> = Vec::default(); + for op in and_op { + let x = op.replace(":", ": "); + match serde_yaml::from_str::>(&x) { + Ok(v) => out_op.push(v), + Err(e) => return Err(SysinspectError::MinionGeneralError(format!("Broken traits query: {e}"))), + }; + } + out.push(out_op); + } + Ok(out) +} + +pub fn matches_traits(qt: Vec>>, traits: SystemTraits) -> bool { + let mut or_op_c: Vec = Vec::default(); + for and_op in qt { + let mut and_op_c: Vec = Vec::default(); + for ophm in and_op { + // op hashmap has always just one key and one value + for (opk, opv) in ophm { + and_op_c.push(traits.get(&opk).map(|x| x.eq(&opv)).unwrap_or(false)); + } + } + or_op_c.push(!and_op_c.contains(&false)); + } + + or_op_c.contains(&true) +} diff --git a/libsysinspect/src/traits/systraits.rs b/libsysinspect/src/traits/systraits.rs new file mode 100644 index 00000000..99a23db2 --- /dev/null +++ b/libsysinspect/src/traits/systraits.rs @@ -0,0 +1,269 @@ +use crate::{ + cfg::mmconf::MinionConfig, + traits::{ + HW_CPU_BRAND, HW_CPU_CORES, HW_CPU_FREQ, HW_CPU_TOTAL, HW_CPU_VENDOR, HW_MEM, HW_SWAP, SYS_ID, SYS_NET_HOSTNAME, + SYS_OS_DISTRO, SYS_OS_KERNEL, SYS_OS_NAME, SYS_OS_VERSION, + }, + SysinspectError, +}; +use indexmap::IndexMap; +use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; +use std::{ + collections::HashMap, + fs::{self}, + os::unix::fs::PermissionsExt, + path::PathBuf, + process::Command, +}; + +/// SystemTraits contains a key/value of a system properties. +#[derive(Debug, Clone, Default)] +pub struct SystemTraits { + data: IndexMap, + cfg: MinionConfig, + checksum: String, +} + +impl SystemTraits { + pub fn new(cfg: MinionConfig) -> SystemTraits { + log::debug!("Initialising system traits"); + let mut traits = SystemTraits { cfg, ..Default::default() }; + traits.get_system(); + traits.get_network(); + if let Err(err) = traits.get_defined() { + log::error!("Unable to load custom traits: {err}"); + } + + if let Err(err) = traits.get_functions() { + log::error!("Unable to load trait functions: {err}"); + } + + traits + } + + /// Put a JSON value into traits structure + pub fn put(&mut self, path: String, data: Value) { + self.data.insert(path, data); + } + + /// Get a trait value in JSON + pub fn get(&self, path: &str) -> Option { + self.data.get(path).cloned() + } + + /// Check if trait is present + pub fn has(&self, path: &str) -> bool { + self.get(path).is_some() + } + + /// Check if trait matches the requested value. + pub fn matches(&self, path: &str, v: Value) -> bool { + if let Some(t) = self.get(path) { + return t.eq(&v); + } + + false + } + + /// Return known trait items + pub fn items(&self) -> Vec { + let mut items = self.data.keys().map(|s| s.to_string()).collect::>(); + items.sort(); + + items + } + + /// Return checksum of traits. + /// This is done by calculating checksum of the *keys*, as values can change on every restart, + /// e.g. IPv6 data, which is usually irrelevant or any other things that *meant* to change. + pub fn checksum(&mut self) -> String { + if !self.checksum.is_empty() { + return self.checksum.to_owned(); + } + + let mut keys = self.data.keys().map(|s| s.to_string()).collect::>(); + keys.sort(); + + self.checksum = format!("{:x}", Sha256::digest(keys.join("|").as_bytes())); + self.checksum.to_owned() + } + + /// Proxypass the error logging + fn proxy_log_error(result: Result, context: &str) -> Option { + match result { + Ok(v) => Some(v), + Err(err) => { + log::error!("{}: {}", context, err); + None + } + } + } + + /// Read standard system traits + fn get_system(&mut self) { + log::info!("Loading system traits data"); + let system = sysinfo::System::new_all(); + + // Common + if let Some(v) = sysinfo::System::host_name() { + self.put(SYS_NET_HOSTNAME.to_string(), json!(v)); + } + + if let Some(v) = sysinfo::System::kernel_version() { + self.put(SYS_OS_KERNEL.to_string(), json!(v)); + } + + if let Some(v) = sysinfo::System::os_version() { + self.put(SYS_OS_VERSION.to_string(), json!(v)); + } + + if let Some(v) = sysinfo::System::name() { + self.put(SYS_OS_NAME.to_string(), json!(v)); + } + + self.put(SYS_OS_DISTRO.to_string(), json!(sysinfo::System::distribution_id())); + + // Machine Id (not always there) + let mip = PathBuf::from("/etc/machine-id"); + let mut mid = String::default(); + if mip.exists() { + if let Ok(id) = fs::read_to_string(mip) { + mid = id.trim().to_string(); + } + } + self.put(SYS_ID.to_string(), json!(mid)); + + // Memory + self.put(HW_MEM.to_string(), json!(system.total_memory())); + self.put(HW_SWAP.to_string(), json!(system.total_swap())); + + // Load CPU data + self.put(HW_CPU_TOTAL.to_string(), json!(system.cpus().len())); + self.put(HW_CPU_BRAND.to_string(), json!(system.cpus()[0].brand())); + self.put(HW_CPU_FREQ.to_string(), json!(system.cpus()[0].frequency())); + self.put(HW_CPU_VENDOR.to_string(), json!(system.cpus()[0].vendor_id())); + if let Some(pcrc) = system.physical_core_count() { + self.put(HW_CPU_CORES.to_string(), json!(pcrc)); + } + } + + /// Load network data + fn get_network(&mut self) { + log::info!("Lading network traits data"); + let net = sysinfo::Networks::new_with_refreshed_list(); + for (ifs, data) in net.iter() { + self.put(format!("system.net.{}.mac", ifs), json!(data.mac_address().to_string())); + for ipn in data.ip_networks() { + let tp = if ipn.addr.is_ipv4() { "4" } else { "6" }; + self.put(format!("system.net.{}.ipv{}", ifs, tp), json!(ipn.addr.to_string())); + } + } + } + + /// Read defined/configured static traits + fn get_defined(&mut self) -> Result<(), SysinspectError> { + log::debug!("Loading custom static traits data"); + + for f in fs::read_dir(self.cfg.traits_dir())?.flatten() { + let fname = f.file_name(); + let fname = fname.to_str().unwrap_or_default(); + if fname.ends_with(".cfg") { + let content = Self::proxy_log_error( + fs::read_to_string(f.path()), + format!("Unable to read custom trait file at {}", fname).as_str(), + ) + .unwrap_or_default(); + + if content.is_empty() { + continue; + } + + let content: Option = + Self::proxy_log_error(serde_yaml::from_str(&content), "Custom trait file has broken YAML"); + + let content: Option = content.as_ref().and_then(|v| { + Self::proxy_log_error(serde_json::to_value(v), "Unable to convert existing YAML to JSON format") + }); + + if content.is_none() { + log::error!("Unable to load custom traits from {}", f.file_name().to_str().unwrap_or_default()); + continue; + } + + let content = content.as_ref().and_then(|v| { + Self::proxy_log_error( + serde_json::from_value::>(v.clone()), + "Unable to parse JSON", + ) + }); + + if let Some(content) = content { + for (k, v) in content { + self.put(k, json!(v)); + } + } else { + log::error!("Custom traits data is empty or in a wrong format"); + } + } + } + Ok(()) + } + + /// Load custom functions + fn get_functions(&mut self) -> Result<(), SysinspectError> { + log::info!("Loading trait functions"); + for f in fs::read_dir(self.cfg.functions_dir())?.flatten() { + let fname = f.path(); + let fname = fname.file_name().unwrap_or_default().to_str().unwrap_or_default(); + + log::info!("Calling function {fname}"); + + let is_exec = match fs::metadata(f.path()) { + Ok(m) => { + #[cfg(unix)] + { + m.permissions().mode() & 0o111 != 0 + } + } + Err(_) => false, + }; + + if is_exec { + let out = Command::new(f.path()).output()?; + if out.status.success() { + let data = Self::proxy_log_error( + String::from_utf8(out.stdout), + format!("Unable to load content from the function {}", fname).as_str(), + ); + if data.is_none() { + log::error!("Function {fname} returned no content"); + continue; + } + let data = data.unwrap_or_default(); + let data = Self::proxy_log_error( + serde_json::from_str::>(&data), + format!("Unable to parse JSON output from trait function at {fname}").as_str(), + ); + if let Some(data) = data { + for (k, v) in data { + self.put(k, json!(v)); + } + } else { + log::error!("Custom traits data is empty or in a wrong format"); + } + } else { + log::error!("Error running {fname}"); + } + } else { + log::warn!("Function {fname} is not an executable, skipping"); + } + } + Ok(()) + } + + /// Convert the data to the JSON body + pub fn to_json_string(&self) -> Result { + Ok(serde_json::to_string(&json!(self.data))?) + } +} diff --git a/libsysinspect/src/traits/traits_query.pest b/libsysinspect/src/traits/traits_query.pest new file mode 100644 index 00000000..9c543a3c --- /dev/null +++ b/libsysinspect/src/traits/traits_query.pest @@ -0,0 +1,16 @@ +WHITESPACE = _{ " " | "\t" } + +// Define a term as one or more alphanumeric characters, or a quoted value +term = { key ~ ":" ~ value } +key = @{ (ASCII_ALPHANUMERIC | "-" | "_" | ".")+ } +value = @{ (ASCII_ALPHANUMERIC | "-" | "_")+ | quoted_value } +quoted_value = @{ "\"" ~ (!"\"" ~ ANY)* ~ "\"" } + +and_op = _{ "and" } +or_op = _{ "or" } + +// Define a group: one or more terms connected by "and" +group = { term ~ (and_op ~ term)* } + +// Define an expression: one or more groups connected by "or" +expression = { group ~ (or_op ~ group)* } diff --git a/libsysinspect/src/util/README.txt b/libsysinspect/src/util/README.txt new file mode 100644 index 00000000..35646e63 --- /dev/null +++ b/libsysinspect/src/util/README.txt @@ -0,0 +1 @@ +Various utilities: data conversion, filesystem reads etc. \ No newline at end of file diff --git a/libsysinspect/src/util/iofs.rs b/libsysinspect/src/util/iofs.rs new file mode 100644 index 00000000..cde37c79 --- /dev/null +++ b/libsysinspect/src/util/iofs.rs @@ -0,0 +1,54 @@ +/* +Various unsorted utils with the filesystem, IO, files, byte arrays etc +*/ + +use crate::SysinspectError; +use hex::encode; +use sha2::{Digest, Sha256}; +use std::{ + collections::HashMap, + io::{BufReader, Read}, +}; +use std::{fs::File, path::PathBuf}; +use walkdir::WalkDir; + +/// Calculate an SHA265 checksum of a file on the file system. +pub fn get_file_sha256(pth: PathBuf) -> Result { + let mut dg = Sha256::new(); + let mut buf = [0u8; 0x2000]; + let mut reader = BufReader::new(File::open(&pth)?); + loop { + let brd = reader.read(&mut buf)?; + if brd == 0 { + break; + } + dg.update(&buf[..brd]); + } + + Ok(encode(dg.finalize())) +} + +/// Scan a given root for any file. +/// Returns a `HashMap` with format `path` to `checksum`. +pub fn scan_files_sha256(pth: PathBuf, ext: Option<&str>) -> HashMap { + let ext = ext.map(|e| e.trim_start_matches('.')); // Just in case :) + WalkDir::new(&pth) + .into_iter() + .filter_map(|entry| { + let entry = entry.ok()?; + + if entry.file_type().is_file() { + if ext.is_some() && entry.path().extension().and_then(|e| e.to_str()) == ext || ext.is_none() { + Some(( + entry.path().strip_prefix(&pth).ok()?.to_string_lossy().to_string(), + get_file_sha256(entry.path().to_path_buf()).unwrap_or_default(), + )) + } else { + None + } + } else { + None + } + }) + .collect::>() +} diff --git a/libsysinspect/src/util/mod.rs b/libsysinspect/src/util/mod.rs index 77c2b205..03fef808 100644 --- a/libsysinspect/src/util/mod.rs +++ b/libsysinspect/src/util/mod.rs @@ -1 +1,2 @@ pub mod dataconv; +pub mod iofs; diff --git a/libsysinspect/tests/rsa_keys.rs b/libsysinspect/tests/rsa_keys.rs new file mode 100644 index 00000000..ee52e918 --- /dev/null +++ b/libsysinspect/tests/rsa_keys.rs @@ -0,0 +1,91 @@ +#[cfg(test)] +mod rsa_test { + use libsysinspect::rsa::keys::{ + decrypt, encrypt, key_from_file, key_to_file, keygen, sign_data, to_pem, verify_sign, + RsaKey::{Private, Public}, + DEFAULT_KEY_SIZE, + }; + use std::fs; + + #[test] + fn test_keygen() { + let r = keygen(DEFAULT_KEY_SIZE); + assert!(r.is_ok(), "Error generating RSA keys"); + } + + #[test] + fn test_to_pem() { + let (pr, pb) = keygen(DEFAULT_KEY_SIZE).unwrap(); + let r = to_pem(Some(&pr), Some(&pb)); + + assert!(r.is_ok(), "Unable to convert RSA keys to PEM"); + + let (prp, pbp) = r.unwrap(); + + assert!(prp.is_some(), "Unable to convert private PEM key"); + assert!(pbp.is_some(), "Unable to convert public PEM key"); + + let prp = prp.unwrap_or_default(); + let pbp = pbp.unwrap_or_default(); + + assert!(prp.contains(" PRIVATE "), "Not a private key"); + assert!(pbp.contains(" PUBLIC "), "Not a public key"); + } + + #[test] + fn test_sign() { + let (pr, pb) = keygen(DEFAULT_KEY_SIZE).unwrap(); + + let data = "Sysinspect can also configure systems!"; + let sig = sign_data(pr.clone(), data.as_bytes()).unwrap(); + + assert!(!sig.is_empty(), "Sig should not be empty"); + + let r = verify_sign(&pb, data.as_bytes(), sig); + + assert!(r.is_ok(), "Verification failed to proceed"); + assert!(r.unwrap(), "Verification didn't succeed"); + } + + #[test] + fn test_cipher() { + let (pr, pb) = keygen(DEFAULT_KEY_SIZE).unwrap(); + let data = "Sysinspect can also configure systems!"; + let cipher = encrypt(pb.to_owned(), data.as_bytes().to_vec()).unwrap(); + + assert!(!cipher.is_empty(), "No cipher found"); + + let rdata = String::from_utf8(decrypt(pr.to_owned(), cipher).unwrap()).unwrap_or_default(); + + assert!(rdata.eq(data), "Data wasn't properly decryped"); + } + + #[test] + fn test_to_file() { + _ = fs::remove_file("priv.key.pem"); + _ = fs::remove_file("pub.key.pem"); + + let (pr, pb) = keygen(DEFAULT_KEY_SIZE).unwrap(); + + if let Err(err) = key_to_file(&Private(pr), "", "priv.key.pem") { + assert!(false, "Private key error: {}", err); + }; + + if let Err(err) = key_to_file(&Public(pb), "", "pub.key.pem") { + assert!(false, "Public key error: {}", err); + }; + + match key_from_file("priv.key.pem").unwrap().unwrap() { + Private(_) => assert!(true), + Public(_) => assert!(false, "Not a public key"), + } + + match key_from_file("pub.key.pem").unwrap().unwrap() { + Private(_) => assert!(false, "Not a private"), + Public(_) => assert!(true), + } + + _ = fs::remove_file("priv.key.pem"); + _ = fs::remove_file("pub.key.pem"); + } +} diff --git a/src/clidef.rs b/src/clidef.rs index 708357ac..56bf0c80 100644 --- a/src/clidef.rs +++ b/src/clidef.rs @@ -17,6 +17,30 @@ pub fn cli(version: &'static str) -> Command { .about(format!("{} - {}", APPNAME.bright_magenta().bold(), "is a tool for anomaly detection and root cause analysis in a known system")) .override_usage(format!("{} [OPTIONS] [FILTERS]", APPNAME)) + // Sysinspect + .next_help_heading("Main") + .arg( + Arg::new("scheme") + .help("Specify scheme that needs to be requested (model:// or state://)") + .required(false) + .index(1) + ) + .arg( + Arg::new("query") + .help("Minions to query") + .required(false) + .index(2) + ) + .arg( + Arg::new("traits") + .short('t') + .long("traits") + .help("Specify traits to select remote systems") + ) + + // Local + .next_help_heading("Local") + // Config .arg( Arg::new("model") @@ -56,6 +80,12 @@ pub fn cli(version: &'static str) -> Command { // Other .next_help_heading("Other") + .arg( + Arg::new("config") + .short('c') + .long("config") + .help("Specify alternative configuration") + ) .arg( Arg::new("debug") .short('d') diff --git a/src/main.rs b/src/main.rs index e8ed6a73..a72f4af0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,16 @@ +use clap::ArgMatches; use colored::Colorize; use libsysinspect::{ - intp::actproc::response::ActionResponse, + cfg::{mmconf::MasterConfig, select_config}, + inspector::SysInspectRunner, logger, - reactor::{evtproc::EventProcessor, handlers}, + reactor::handlers, + SysinspectError, }; use log::LevelFilter; -use std::env; +use std::{env, fs::OpenOptions, io::Write}; mod clidef; -mod mcf; static VERSION: &str = "0.2.0"; static LOGGER: logger::STDOUTLogger = logger::STDOUTLogger; @@ -23,6 +25,33 @@ fn print_event_handlers() { println!(); } +/// Call master via FIFO +fn call_master_fifo(model: &str, query: &str, traits: Option<&String>, fifo: &str) -> Result<(), SysinspectError> { + let payload = format!("{model};{query};{}\n", traits.unwrap_or(&"".to_string())); + OpenOptions::new().write(true).open(fifo)?.write_all(payload.as_bytes())?; + + log::debug!("Message sent to the master via FIFO: {:?}", payload); + Ok(()) +} + +/// Set logger +fn set_logger(p: &ArgMatches) { + if let Err(err) = log::set_logger(&LOGGER).map(|()| { + log::set_max_level(match p.get_count("debug") { + 0 => LevelFilter::Info, + 1 => LevelFilter::Debug, + 2.. => LevelFilter::max(), + }) + }) { + println!("{}", err) + } +} + +/// Get configuration of the master +fn get_cfg(p: &ArgMatches) -> Result { + MasterConfig::new(select_config(p.get_one::("config").cloned())?) +} + fn main() { let args: Vec = env::args().collect(); let mut cli = clidef::cli(VERSION); @@ -36,6 +65,9 @@ fn main() { // Our main params let params = cli.to_owned().get_matches(); + // Set logger + set_logger(¶ms); + // Print help? if *params.get_one::("help").unwrap() { return { @@ -55,60 +87,27 @@ fn main() { return; } - // Setup logger - if let Err(err) = log::set_logger(&LOGGER).map(|()| { - log::set_max_level(match params.get_count("debug") { - 0 => LevelFilter::Info, - 1 => LevelFilter::Debug, - 2.. => LevelFilter::max(), - }) - }) { - return println!("{}", err); - } + // Get master config + let cfg = match get_cfg(¶ms) { + Ok(cfg) => cfg, + Err(err) => { + log::error!("Unable to get master configuration: {err}"); + std::process::exit(1); + } + }; - if let Some(mpath) = params.get_one::("model") { - match libsysinspect::mdescr::mspec::load(mpath) { - Ok(spec) => { - log::debug!("Initalising inspector"); - match libsysinspect::intp::inspector::SysInspector::new(spec) { - Ok(isp) => { - // Setup event processor - let mut evtproc = EventProcessor::new().set_config(isp.cfg()); - - let arg_state = params.get_one::("state").cloned(); - let arg_labels = clidef::split_by(¶ms, "labels", None); - - let actions = if !arg_labels.is_empty() { - isp.actions_by_relations(arg_labels, arg_state.to_owned()) - } else { - isp.actions_by_entities(clidef::split_by(¶ms, "entities", None), arg_state) - }; - - match actions { - Ok(actions) => { - for ac in actions { - match ac.run() { - Ok(response) => { - let response = response.unwrap_or(ActionResponse::default()); - evtproc.receiver().register(response.eid().to_owned(), response); - } - Err(err) => { - log::error!("{err}") - } - } - } - evtproc.process(); - } - Err(err) => { - log::error!("{}", err); - } - } - } - Err(err) => log::error!("{err}"), - } - log::debug!("Done"); - } - Err(err) => log::error!("Error: {}", err), - }; + if let Some(model) = params.get_one::("scheme") { + let query = params.get_one::("query"); + let traits = params.get_one::("traits"); + if let Err(err) = call_master_fifo(model, query.unwrap_or(&"".to_string()), traits, &cfg.socket()) { + log::error!("Cannot reach master: {err}"); + } + } else if let Some(mpath) = params.get_one::("model") { + let mut sr = SysInspectRunner::new(); + sr.set_model_path(mpath); + sr.set_state(params.get_one::("state").cloned()); + sr.set_entities(clidef::split_by(¶ms, "entities", None)); + sr.set_checkbook_labels(clidef::split_by(¶ms, "labels", None)); + sr.start(); } } diff --git a/src/mcf/README.md b/src/mcf/README.md deleted file mode 100644 index ad5bf45a..00000000 --- a/src/mcf/README.md +++ /dev/null @@ -1,22 +0,0 @@ -Model design - -A model is a directory with the following layout: - -``` - -| -+-- entities/ -| // Here are all entities -| -+-- relations/ -| // Here are all relations between entities -| -+-- actions/ -| // Here are all actions on each relation -| -+-- constraints/ -| // Here are all constraints for each entity -| -+-- model.cfg - // Main model description -``` diff --git a/src/mcf/cfread.rs b/src/mcf/cfread.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/src/mcf/mod.rs b/src/mcf/mod.rs deleted file mode 100644 index 636e8144..00000000 --- a/src/mcf/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod cfread; diff --git a/sysinspect.conf b/sysinspect.conf index f61790ef..1b056541 100644 --- a/sysinspect.conf +++ b/sysinspect.conf @@ -1,2 +1,31 @@ config: - modules: target/debug + # Path to the custom location where modules resides + # Default: /usr/share/sysinspect/modules + # + # modules: /path/to/custom/location + + # Configuration that is present only on master node + master: + # Command socket. Default: /var/run/sysinspect.socket + socket: /tmp/sysinspect-master.socket + bind.ip: 0.0.0.0 + bind.port: 4200 + + fileserver.bind.ip: 0.0.0.0 + fileserver.bind.port: 4201 + + # Path to the models in fileserver root + fileserver.models.root: /models + + # Exported models + fileserver.models: + - router + - inherited + + # Configuration that is present only on a minion node + minion: + # Root directory where minion keeps all data. + # Default: /etc/sysinspect — same as for master + root: /etc/sysinspect + master.ip: 192.168.2.31 + master.port: 4200 diff --git a/sysmaster/Cargo.toml b/sysmaster/Cargo.toml new file mode 100644 index 00000000..701b4323 --- /dev/null +++ b/sysmaster/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "sysmaster" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5.20", features = ["unstable-styles"] } +colored = "2.1.0" +ed25519-dalek = { version = "2.1.1", features = [ + "asm", + "batch", + "digest", + "merlin", + "pem", + "pkcs8", + "rand_core", + "signature", + "serde", +] } +futures = "0.3.31" +libc = "0.2.161" +rand = "0.8.5" +rustls = "0.23.16" +rustls-pemfile = "2.2.0" +serde = { version = "1.0.214", features = ["derive"] } +serde_json = "1.0.132" +serde_yaml = "0.9.34" +tokio = { version = "1.41.0", features = ["full"] } +tokio-rustls = "0.26.0" +libsysinspect = { path = "../libsysinspect" } +log = "0.4.22" +sled = "0.34.7" +rsa = { version = "0.9.6", features = ["pkcs5", "sha1", "sha2"] } +uuid = { version = "1.11.0", features = ["v4"] } +actix-web = "4.9.0" diff --git a/sysmaster/src/clidef.rs b/sysmaster/src/clidef.rs new file mode 100644 index 00000000..e6af7415 --- /dev/null +++ b/sysmaster/src/clidef.rs @@ -0,0 +1,74 @@ +use clap::builder::styling; +use clap::{Arg, ArgAction, Command}; +use colored::Colorize; + +/// Define CLI arguments and styling +pub fn cli(version: &'static str, appname: &'static str) -> Command { + let styles = styling::Styles::styled() + .header(styling::AnsiColor::Yellow.on_default()) + .usage(styling::AnsiColor::Yellow.on_default()) + .literal(styling::AnsiColor::BrightGreen.on_default()) + .placeholder(styling::AnsiColor::BrightMagenta.on_default()); + + Command::new(appname) + .version(version) + .about(format!("{} - {}", appname.bright_magenta().bold(), "is a master node to operate minion agents")) + .override_usage(format!("{} [OPTIONS]", appname)) + + // Config + .arg( + Arg::new("config") + .short('c') + .long("config") + .help("Alternative path to the config") + ) + .arg( + Arg::new("query") + .short('q') + .long("query") + .help("Target model for operations") + ) + .arg( + Arg::new("start") + .long("start") + .action(ArgAction::SetTrue) + .help("Start master") + ) + + .next_help_heading("Info") + .arg( + Arg::new("status") + .long("status") + .action(ArgAction::SetTrue) + .help("Show connected minions") + ) + + + // Other + .next_help_heading("Other") + .arg( + Arg::new("debug") + .short('d') + .long("debug") + .action(ArgAction::Count) + .help("Set debug mode for more verbose output. Increase this flag for more verbosity."), + ) + .arg( + Arg::new("help") + .short('h') + .long("help") + .action(ArgAction::SetTrue) + .help("Display help"), + ) + .arg( + Arg::new("version") + .short('v') + .long("version") + .action(ArgAction::SetTrue) + .help("Get current version."), + ) + .disable_help_flag(true) // Otherwise it is displayed in a wrong position + .disable_version_flag(true) + .disable_colored_help(false) + .styles(styles) +} diff --git a/sysmaster/src/dataserv/fls.rs b/sysmaster/src/dataserv/fls.rs new file mode 100644 index 00000000..67c6174b --- /dev/null +++ b/sysmaster/src/dataserv/fls.rs @@ -0,0 +1,47 @@ +use actix_web::{rt::System, web, App, HttpResponse, HttpServer, Responder}; +use libsysinspect::{ + cfg::mmconf::{MasterConfig, CFG_FILESERVER_ROOT, DEFAULT_SYSINSPECT_ROOT}, + SysinspectError, +}; +use std::{ + fs, + path::{Path, PathBuf}, + thread, +}; + +// Separate handler on every HTTP call +async fn serve_file(path: web::Path, _cfg: web::Data) -> impl Responder { + let pth = Path::new(DEFAULT_SYSINSPECT_ROOT).join(CFG_FILESERVER_ROOT).join(path.into_inner()); + log::debug!("Requested local file: {:?}", pth); + if pth.is_file() { + return HttpResponse::Ok().body(fs::read(pth).unwrap()); + } + log::error!("File {:?} was not found", pth); + HttpResponse::NotFound().body("File not found") +} + +/// Start fileserver +pub async fn start(cfg: MasterConfig) -> Result<(), SysinspectError> { + thread::spawn(move || { + let c_cfg = cfg.clone(); + System::new().block_on(async move { + let server = HttpServer::new(move || { + App::new().app_data(web::Data::new(cfg.clone())).service(web::resource("/{path:.*}").to(serve_file)) + }) + .bind(c_cfg.fileserver_bind_addr()); + + match server { + Ok(server) => { + if let Err(err) = server.run().await { + Err(err) + } else { + Ok(()) + } + } + Err(err) => Err(err), + } + }) + }); + + Ok(()) +} diff --git a/sysmaster/src/dataserv/mod.rs b/sysmaster/src/dataserv/mod.rs new file mode 100644 index 00000000..5d975762 --- /dev/null +++ b/sysmaster/src/dataserv/mod.rs @@ -0,0 +1 @@ +pub mod fls; diff --git a/sysmaster/src/main.rs b/sysmaster/src/main.rs new file mode 100644 index 00000000..a6fbb8c2 --- /dev/null +++ b/sysmaster/src/main.rs @@ -0,0 +1,75 @@ +mod clidef; +mod dataserv; +mod master; +mod registry; +mod rmt; + +use clidef::cli; +use libsysinspect::{ + cfg::{mmconf::MasterConfig, select_config}, + logger, SysinspectError, +}; +use log::LevelFilter; +use rmt::send_message; +use std::env; +use std::path::PathBuf; + +static APPNAME: &str = "sysmaster"; +static VERSION: &str = "0.0.1"; +static LOGGER: logger::STDOUTLogger = logger::STDOUTLogger; + +#[tokio::main] +async fn main() -> Result<(), SysinspectError> { + let mut cli = cli(VERSION, APPNAME); + let params = cli.to_owned().get_matches(); + + // Print help? + if env::args().collect::>().len() == 1 || *params.get_one::("help").unwrap() { + cli.print_help()?; + std::process::exit(1); + } + + // Print version? + if *params.get_one::("version").unwrap() { + println!("Version: {} {}", APPNAME, VERSION); + return Ok(()); + } + + // Setup logger + if let Err(err) = log::set_logger(&LOGGER).map(|()| { + log::set_max_level(match params.get_count("debug") { + 0 => LevelFilter::Info, + 1 => LevelFilter::Debug, + 2.. => LevelFilter::max(), + }) + }) { + println!("Error setting logger output: {}", err); + } + + // Get config + let mut cfp = PathBuf::from(params.get_one::("config").unwrap_or(&"".to_string()).to_owned()); + if !cfp.exists() { + cfp = match select_config(None) { + Ok(cfp) => { + log::debug!("Reading config at {}", cfp.to_str().unwrap_or_default()); + cfp + } + Err(err) => { + log::error!("{}", err); + std::process::exit(1); + } + }; + } + let cfg = MasterConfig::new(cfp)?; + + // Mode + let query = params.get_one::("query").unwrap_or(&"".to_string()).to_owned(); + if *params.get_one::("start").unwrap() { + master::master(cfg).await?; + } else if !query.is_empty() { + log::info!("Query: {}", query); + send_message(&query, &cfg.socket()).await? + } + + Ok(()) +} diff --git a/sysmaster/src/master.rs b/sysmaster/src/master.rs new file mode 100644 index 00000000..b83aef61 --- /dev/null +++ b/sysmaster/src/master.rs @@ -0,0 +1,462 @@ +use crate::{ + dataserv::fls, + registry::{ + mkb::MinionsKeyRegistry, + mreg::MinionRegistry, + session::{self, SessionKeeper}, + }, +}; +use libsysinspect::{ + cfg::mmconf::MasterConfig, + mdescr::mspec::MODEL_FILE_EXT, + proto::{ + self, errcodes::ProtoErrorCode, payload::ModStatePayload, rqtypes::RequestType, MasterMessage, MinionMessage, + MinionTarget, ProtoConversion, + }, + util::iofs::scan_files_sha256, + SysinspectError, +}; +use serde_json::json; +use std::{ + collections::{HashMap, HashSet}, + path::Path, + sync::Arc, +}; +use tokio::net::TcpListener; +use tokio::select; +use tokio::sync::{broadcast, mpsc}; +use tokio::time::{sleep, Duration}; +use tokio::{fs::OpenOptions, sync::Mutex}; +use tokio::{ + io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader as TokioBufReader}, + time, +}; + +#[derive(Debug)] +pub struct SysMaster { + cfg: MasterConfig, + broadcast: broadcast::Sender>, + mkr: MinionsKeyRegistry, + mreg: MinionRegistry, + to_drop: HashSet, + session: session::SessionKeeper, +} + +impl SysMaster { + pub fn new(cfg: MasterConfig) -> Result { + let (tx, _) = broadcast::channel::>(100); + let mkr = MinionsKeyRegistry::new(cfg.keyman_root())?; + let mreg = MinionRegistry::new(cfg.minion_registry_root())?; + Ok(SysMaster { cfg, broadcast: tx, mkr, to_drop: HashSet::default(), session: SessionKeeper::new(30), mreg }) + } + + /// Open FIFO socket for command-line communication + fn open_socket(&self, path: &str) -> Result<(), SysinspectError> { + if !Path::new(path).exists() { + if unsafe { libc::mkfifo(std::ffi::CString::new(path)?.as_ptr(), 0o600) } != 0 { + return Err(SysinspectError::ConfigError(format!("{}", std::io::Error::last_os_error()))); + } + log::info!("Socket opened at {}", path); + } + Ok(()) + } + + /// Parse minion request + fn to_request(&self, data: &str) -> Option { + match serde_json::from_str::(data) { + Ok(request) => { + return Some(request); + } + Err(err) => { + log::error!("Error parse minion response: {err}"); + } + } + + None + } + + /// Start sysmaster + pub async fn init(&mut self) -> Result<(), SysinspectError> { + log::info!("Starting master at {}", self.cfg.bind_addr()); + self.open_socket(&self.cfg.socket())?; + Ok(()) + } + + pub fn cfg(&self) -> MasterConfig { + self.cfg.to_owned() + } + + pub fn broadcast(&self) -> broadcast::Sender> { + self.broadcast.clone() + } + + pub async fn listener(&self) -> Result { + Ok(TcpListener::bind(self.cfg.bind_addr()).await?) + } + + /// Get Minion key registry + fn mkr(&mut self) -> &mut MinionsKeyRegistry { + &mut self.mkr + } + + /// Construct a Command message to the minion + fn msg_query(&mut self, payload: &str) -> Option { + let query = payload.split(";").map(|s| s.to_string()).collect::>(); + + if let [scheme, query, traits] = query.as_slice() { + let mut tgt = MinionTarget::default(); + tgt.set_scheme(scheme); + tgt.set_traits_query(traits); + for hostname in query.split(",") { + tgt.add_hostname(hostname); + } + + // Collect downloadable model(s) files + let mut out: HashMap = HashMap::default(); + for em in self.cfg.fileserver_models() { + for (n, cs) in scan_files_sha256(self.cfg.fileserver_mdl_root(false).join(em), Some(MODEL_FILE_EXT)) { + out.insert( + format!("/{}/{em}/{n}", self.cfg.fileserver_mdl_root(false).file_name().unwrap().to_str().unwrap()), + cs, + ); + } + } + + let mut msg = MasterMessage::new( + RequestType::Command, + json!(ModStatePayload::new("12345".to_string()) + .set_uri(scheme.to_string()) + .add_files(out) + .set_models_root(self.cfg.fileserver_mdl_root(true).to_str().unwrap_or_default())), // TODO: SID part + ); + msg.set_target(tgt); + msg.set_retcode(ProtoErrorCode::Success); + + return Some(msg); + } + + None + } + + /// Request minion to sync its traits + fn msg_request_traits(&mut self, mid: String, sid: String) -> MasterMessage { + let mut m = MasterMessage::new(RequestType::Traits, json!(sid)); + let tgt = MinionTarget::new(&mid, &sid); + m.set_target(tgt); + m.set_retcode(ProtoErrorCode::Success); + + m + } + + /// Already connected + fn msg_already_connected(&mut self, mid: String, sid: String) -> MasterMessage { + let mut m = MasterMessage::new(RequestType::Command, json!(sid)); + m.set_target(MinionTarget::new(&mid, &sid)); + m.set_retcode(ProtoErrorCode::AlreadyConnected); + + m + } + + /// Bounce message + fn msg_not_registered(&mut self, mid: String) -> MasterMessage { + let mut m = + MasterMessage::new(RequestType::AgentUnknown, json!(self.mkr().get_master_key_pem().clone().unwrap().to_string())); + m.set_target(MinionTarget::new(&mid, "")); + m.set_retcode(ProtoErrorCode::Success); + + m + } + + /// Accept registration + fn msg_registered(&self, mid: String, msg: &str) -> MasterMessage { + let mut m = MasterMessage::new(RequestType::Reconnect, json!(msg)); // XXX: Should it be already encrypted? + m.set_target(MinionTarget::new(&mid, "")); + m.set_retcode(ProtoErrorCode::Success); + + m + } + + /// Process incoming minion messages + #[allow(clippy::while_let_loop)] + pub async fn do_incoming(master: Arc>, mut rx: tokio::sync::mpsc::Receiver<(Vec, String)>) { + log::trace!("Init incoming channel"); + let bcast = master.lock().await.broadcast(); + tokio::spawn(async move { + loop { + if let Some((msg, minion_addr)) = rx.recv().await { + let msg = String::from_utf8_lossy(&msg).to_string(); + log::trace!("Minion response: {}: {}", minion_addr, msg); + if let Some(req) = master.lock().await.to_request(&msg) { + match req.req_type() { + RequestType::Add => { + let c_master = Arc::clone(&master); + let c_bcast = bcast.clone(); + let c_mid = req.id().to_string(); + tokio::spawn(async move { + log::info!("Minion \"{}\" requested registration", minion_addr); + let mut guard = c_master.lock().await; + let resp_msg: &str; + if !guard.mkr().is_registered(&c_mid) { + if let Err(err) = guard.mkr().add_mn_key(&c_mid, &minion_addr, req.payload()) { + log::error!("Unable to add minion RSA key: {err}"); + } + guard.to_drop.insert(minion_addr.to_owned()); + resp_msg = "Minion registration has been accepted"; + log::info!("Registered a minion at {minion_addr} ({})", c_mid); + } else { + resp_msg = "Minion already registered"; + log::warn!("Minion {minion_addr} ({}) is already registered", c_mid); + } + _ = c_bcast.send(guard.msg_registered(req.id().to_string(), resp_msg).sendable().unwrap()); + }); + } + RequestType::Response => { + log::info!("Response"); + } + RequestType::Ehlo => { + log::info!("EHLO from {}", req.id()); + + let c_master = Arc::clone(&master); + let c_bcast = bcast.clone(); + let c_id = req.id().to_string(); + let c_payload = req.payload().to_string(); + tokio::spawn(async move { + let mut guard = c_master.lock().await; + if !guard.mkr().is_registered(&c_id) { + log::info!("Minion at {minion_addr} ({}) is not registered", req.id()); + guard.to_drop.insert(minion_addr); + _ = c_bcast.send(guard.msg_not_registered(req.id().to_string()).sendable().unwrap()); + } else if guard.session.exists(&c_id) { + log::info!("Minion at {minion_addr} ({}) is already connected", req.id()); + guard.to_drop.insert(minion_addr); + _ = c_bcast.send( + guard.msg_already_connected(req.id().to_string(), c_payload).sendable().unwrap(), + ); + } else { + log::info!("{} connected successfully", c_id); + guard.session.ping(&c_id, &c_payload); + _ = c_bcast + .send(guard.msg_request_traits(req.id().to_string(), c_payload).sendable().unwrap()); + log::info!("Syncing traits with minion at {}", c_id); + } + }); + } + + RequestType::Pong => { + let c_master = Arc::clone(&master); + let c_id = req.id().to_string(); + let c_payload = req.payload().to_string(); + tokio::spawn(async move { + let mut guard = c_master.lock().await; + guard.session.ping(&c_id, &c_payload); + let uptime = guard.session.uptime(req.id()).unwrap_or_default(); + log::debug!( + "Update last contacted for {} (alive for {:.2} min)", + req.id().to_string(), + uptime as f64 / 60.0 + ); + }); + } + + RequestType::Traits => { + log::debug!("Syncing traits from {}", req.id()); + let c_master = Arc::clone(&master); + let c_id = req.id().to_string(); + let c_payload = req.payload().to_string(); + tokio::spawn(async move { + let mut guard = c_master.lock().await; + guard.on_traits(c_id, c_payload).await; + }); + } + _ => { + log::error!("Minion sends unknown request type"); + } + } + } + } else { + break; + } + } + }); + } + + pub async fn on_traits(&mut self, mid: String, payload: String) { + let traits = serde_json::from_str::>(&payload).unwrap_or_default(); + if !traits.is_empty() { + if let Err(err) = self.mreg.refresh(&mid, traits) { + log::error!("Unable to sync traits: {err}"); + } else { + log::info!("Traits added"); + } + } + } + + pub async fn do_fifo(master: Arc>) { + log::trace!("Init local command channel"); + tokio::spawn(async move { + let bcast = master.lock().await.broadcast(); + let cfg = master.lock().await.cfg(); + loop { + match OpenOptions::new().read(true).open(cfg.socket()).await { + Ok(file) => { + let reader = TokioBufReader::new(file); + let mut lines = reader.lines(); + + loop { + select! { + line = lines.next_line() => { + match line { + Ok(Some(payload)) => { + log::info!("Querying minions: {}", payload); + if let Some(msg) = master.lock().await.msg_query(&payload) { + let _ = bcast.send(msg.sendable().unwrap()); + } + } + Ok(None) => break, // End of file, re-open the FIFO + Err(e) => { + log::error!("Error reading from FIFO: {}", e); + break; + } + } + } + } + } + } + Err(e) => { + log::error!("Failed to open FIFO: {}", e); + sleep(Duration::from_secs(1)).await; // Retry after a sec + } + } + } + }); + } + + pub async fn do_heartbeat(master: Arc>) { + log::trace!("Starting heartbeat"); + let bcast = master.lock().await.broadcast(); + tokio::spawn(async move { + loop { + _ = time::sleep(Duration::from_secs(5)).await; + let mut p = MasterMessage::new(RequestType::Ping, json!("")); + let mut t = MinionTarget::default(); + t.add_hostname("*"); + p.set_target(t); + let _ = bcast.send(p.sendable().unwrap()); + } + }); + } + + pub async fn do_outgoing(master: Arc>, tx: mpsc::Sender<(Vec, String)>) -> Result<(), SysinspectError> { + log::trace!("Init outgoing channel"); + let listener = master.lock().await.listener().await?; + tokio::spawn(async move { + let bcast = master.lock().await.broadcast(); + + loop { + tokio::select! { + // Accept a new connection + Ok((socket, _)) = listener.accept() => { + let mut bcast_sub = bcast.subscribe(); + let client_tx = tx.clone(); + let local_addr = socket.local_addr().unwrap(); + let (reader, writer) = socket.into_split(); + let c_master = Arc::clone(&master); + let (cancel_tx, cancel_rx) = tokio::sync::watch::channel(false); + + // Task to send messages to the client + tokio::spawn(async move { + let mut writer = writer; + log::info!("Minion {} connected. Ready to send messages.", local_addr.to_string()); + + loop { + if let Ok(msg) = bcast_sub.recv().await { + log::trace!("Sending message to minion at {} length of {}", local_addr.to_string(), msg.len()); + if writer.write_all(&(msg.len() as u32).to_be_bytes()).await.is_err() + || writer.write_all(&msg).await.is_err() + || writer.flush().await.is_err() + { + if let Err(err) = cancel_tx.send(true) { + log::error!("Sending cancel notification: {err}"); + } + break; + } + + if c_master.lock().await.to_drop.contains(&local_addr.to_string()) { + c_master.lock().await.to_drop.remove(&local_addr.to_string()); + log::info!("Dropping minion: {}", &local_addr.to_string()); + if let Err(err) = writer.shutdown().await { + log::error!("Error shutting down outgoing: {err}"); + } + if let Err(err) = cancel_tx.send(true) { + log::error!("Sending cancel notification: {err}"); + } + + return; + } + } + } + }); + + // Task to read messages from the client + tokio::spawn(async move { + let mut reader = TokioBufReader::new(reader); + loop { + if *cancel_rx.borrow() { + log::info!("Process terminated"); + return; + } + + let mut len_buf = [0u8; 4]; + if reader.read_exact(&mut len_buf).await.is_err() { + return; + } + + let msg_len = u32::from_be_bytes(len_buf) as usize; + let mut msg = vec![0u8; msg_len]; + if reader.read_exact(&mut msg).await.is_err() { + return; + } + + if client_tx.send((msg, local_addr.to_string())).await.is_err() { + break; + } + + } + }); + } + } + } + }); + + Ok(()) + } +} + +pub(crate) async fn master(cfg: MasterConfig) -> Result<(), SysinspectError> { + let master = Arc::new(Mutex::new(SysMaster::new(cfg.to_owned())?)); + { + let mut m = master.lock().await; + m.init().await?; + } + + let (client_tx, client_rx) = mpsc::channel::<(Vec, String)>(100); + + // Start internal fileserver for minions + fls::start(cfg).await?; + + // Task to read from the FIFO and broadcast messages to clients + SysMaster::do_fifo(Arc::clone(&master)).await; + + // Handle incoming messages from minions + SysMaster::do_incoming(Arc::clone(&master), client_rx).await; + + // Accept connections and spawn tasks for each client + SysMaster::do_outgoing(Arc::clone(&master), client_tx).await?; + + SysMaster::do_heartbeat(Arc::clone(&master)).await; + + // Listen for shutdown signal and cancel tasks + tokio::signal::ctrl_c().await.expect("Failed to listen for ctrl_c"); + log::info!("Received shutdown signal."); + std::process::exit(0); +} diff --git a/sysmaster/src/registry/mkb.rs b/sysmaster/src/registry/mkb.rs new file mode 100644 index 00000000..bb2895bf --- /dev/null +++ b/sysmaster/src/registry/mkb.rs @@ -0,0 +1,135 @@ +use ::rsa::{RsaPrivateKey, RsaPublicKey}; +use libsysinspect::{ + cfg::mmconf::{CFG_MASTER_KEY_PRI, CFG_MASTER_KEY_PUB}, + rsa, SysinspectError, +}; +use std::{collections::HashMap, fs, path::PathBuf}; + +/// Registered minion base. +/// Essentially this is just a directory, +/// where collected all public keys from all minions. + +#[derive(Debug, Default, Clone)] +pub struct MinionsKeyRegistry { + root: PathBuf, + keys: HashMap>, + + // Master RSA + ms_prk: Option, + ms_pbk: Option, + ms_pbk_pem: Option, +} + +impl MinionsKeyRegistry { + pub fn new(root: PathBuf) -> Result { + let mut reg = MinionsKeyRegistry { root, ..MinionsKeyRegistry::default() }; + reg.setup()?; + + Ok(reg) + } + + /// Generate keys, if none + fn init_keys(&mut self) -> Result<(), SysinspectError> { + let prk_pth = self.root.parent().unwrap().join(CFG_MASTER_KEY_PRI); + let pbk_pth = self.root.parent().unwrap().join(CFG_MASTER_KEY_PUB); + + if prk_pth.exists() || pbk_pth.exists() { + let prk_pem = fs::read_to_string(prk_pth)?; + self.ms_pbk_pem = Some(fs::read_to_string(pbk_pth)?); + (self.ms_prk, self.ms_pbk) = rsa::keys::from_pem(Some(&prk_pem), self.ms_pbk_pem.as_deref())?; + + if self.ms_pbk.is_none() || self.ms_pbk.is_none() { + return Err(SysinspectError::MasterGeneralError("Unable to initialise RSA keys".to_string())); + } + + return Ok(()); + } + + log::debug!("Generating RSA keys..."); + + let (prk, pbk) = rsa::keys::keygen(rsa::keys::DEFAULT_KEY_SIZE)?; + let (prk_pem, pbk_pem) = rsa::keys::to_pem(Some(&prk), Some(&pbk))?; + + if prk_pem.is_none() || pbk_pem.is_none() { + return Err(SysinspectError::MasterGeneralError("Unable to generate RSA keys".to_string())); + } + + fs::write(prk_pth, prk_pem.unwrap().as_bytes())?; + fs::write(pbk_pth, pbk_pem.clone().unwrap().as_bytes())?; + + self.ms_pbk_pem = pbk_pem; + + log::debug!("RSA keys saved to the disk"); + + Ok(()) + } + + /// Sets up the registry + fn setup(&mut self) -> Result<(), SysinspectError> { + if !self.root.exists() { + fs::create_dir_all(&self.root)?; + } else { + for e in fs::read_dir(&self.root)?.flatten() { + self.keys.insert(e.file_name().to_str().and_then(|e| e.split('.').next()).unwrap_or_default().to_string(), None); + } + } + + self.init_keys() + } + + /// Returns a method if a minion Id is known to the key registry. + pub fn is_registered(&self, mid: &str) -> bool { + self.keys.contains_key(mid) + } + + /// Get a fingerprint of a master key + pub fn get_master_key_pem(&self) -> &Option { + &self.ms_pbk_pem + } + + /// Add minion key + pub fn add_mn_key(&mut self, mid: &str, addr: &str, pbk_pem: &str) -> Result<(), SysinspectError> { + let k_pth = self.root.join(format!("{}.rsa.pub", mid)); + log::debug!("Adding minion key for {mid} at {addr} as {}", k_pth.as_os_str().to_str().unwrap_or_default()); + fs::write(k_pth, pbk_pem)?; + + let (_, pbk) = rsa::keys::from_pem(None, Some(pbk_pem))?; + if let Some(pbk) = pbk { + self.keys.insert(mid.to_string(), Some(pbk)); + } + Ok(()) + } + + /// Lazy-load minion key. By start all keys are only containing minion Ids. + /// If a key is requested, it is loaded from the disk on demand. + fn get_mn_key(&mut self, mid: &str) -> Option { + log::debug!("Loading RSA key for {mid}"); + + if let Some(pbk) = self.keys.get(mid).and_then(|s| s.clone()) { + return Some(pbk); + } + + let k_pth = self.root.join(format!("{}.rsa.pub", mid)); + if !k_pth.exists() { + log::error!("Minion {mid} requests RSA key, but the key is not found!"); + return None; + } + + match fs::read_to_string(k_pth) { + Ok(pbk_pem) => { + if let Ok((_, Some(pbk))) = rsa::keys::from_pem(None, Some(&pbk_pem)) { + self.keys.insert(mid.to_string(), Some(pbk.to_owned())); + return Some(pbk); + } + } + Err(err) => log::error!("Unable to read minion RSA key: {err}"), + } + None + } + + pub fn remove_mn_key(&self) {} + + pub fn encrypt_with_mn_key(&self) {} + + pub fn encrypt_with_mst_key(&self) {} +} diff --git a/sysmaster/src/registry/mod.rs b/sysmaster/src/registry/mod.rs new file mode 100644 index 00000000..34ecf763 --- /dev/null +++ b/sysmaster/src/registry/mod.rs @@ -0,0 +1,8 @@ +/* +Minion registry. It contains minion tasks, traits, location and other data + */ + +pub mod mkb; +pub mod mreg; +pub mod rec; +pub mod session; diff --git a/sysmaster/src/registry/mreg.rs b/sysmaster/src/registry/mreg.rs new file mode 100644 index 00000000..f67bf78b --- /dev/null +++ b/sysmaster/src/registry/mreg.rs @@ -0,0 +1,147 @@ +use super::rec::MinionRecord; +use libsysinspect::SysinspectError; +use serde_json::{json, Value}; +use sled::{Db, Tree}; +use std::{collections::HashMap, fs, path::PathBuf}; + +const DB_MINIONS: &str = "minions"; + +#[derive(Debug)] +pub struct MinionRegistry { + conn: Db, +} + +impl MinionRegistry { + pub fn new(pth: PathBuf) -> Result { + if !pth.exists() { + fs::create_dir_all(&pth)?; + } + + Ok(MinionRegistry { + conn: match sled::open(pth) { + Ok(db) => db, + Err(err) => return Err(SysinspectError::MasterGeneralError(format!("{err}"))), + }, + }) + } + + fn get_tree(&self, tid: &str) -> Result { + let tree = self.conn.open_tree(tid); + if let Err(err) = tree { + return Err(SysinspectError::MasterGeneralError(format!("Unable to open {tid} database: {err}"))); + } + Ok(tree.unwrap()) + } + + /// Add or update traits + pub fn refresh(&mut self, mid: &str, traits: HashMap) -> Result<(), SysinspectError> { + let minions = self.get_tree(DB_MINIONS)?; + match minions.contains_key(mid) { + Ok(exists) => { + if exists { + if let Err(err) = minions.remove(mid) { + return Err(SysinspectError::MasterGeneralError(format!( + "Unable to remove previous data for {mid} from the database: {err}" + ))); + } else { + log::debug!("Traits for {mid} pre-removed"); + } + } + } + Err(err) => return Err(SysinspectError::MasterGeneralError(format!("Unable to access the database: {err}"))), + }; + + self.add(mid, MinionRecord::new(mid.to_string(), traits))?; + + Ok(()) + } + + fn add(&mut self, mid: &str, mrec: MinionRecord) -> Result<(), SysinspectError> { + let minions = self.get_tree(DB_MINIONS)?; + if let Err(err) = minions.insert(mid, json!(mrec).to_string().as_bytes().to_vec()) { + return Err(SysinspectError::MasterGeneralError(format!("{err}"))); + } + + Ok(()) + } + + pub fn get(&mut self, mid: &str) -> Result, SysinspectError> { + let minions = self.get_tree(DB_MINIONS)?; + let data = match minions.get(mid) { + Ok(data) => data, + Err(err) => return Err(SysinspectError::MasterGeneralError(format!("{err}"))), + }; + + if let Some(data) = data { + return Ok(Some(match String::from_utf8(data.to_vec()) { + Ok(data) => match serde_json::from_str::(&data) { + Ok(mrec) => mrec, + Err(err) => return Err(SysinspectError::MasterGeneralError(format!("{err}"))), + }, + Err(err) => return Err(SysinspectError::MasterGeneralError(format!("{err}"))), + })); + } + + Ok(None) + } + + pub fn remove(&mut self, mid: &str) -> Result<(), SysinspectError> { + let minions = self.get_tree(DB_MINIONS)?; + let contains = match minions.contains_key(mid) { + Ok(res) => res, + Err(err) => return Err(SysinspectError::MasterGeneralError(format!("{err}"))), + }; + + if contains { + if let Err(err) = minions.remove(mid) { + return Err(SysinspectError::MasterGeneralError(format!("{err}"))); + }; + } + + Ok(()) + } + + /// Select minions by trait criterias + pub fn select(&self, traits: HashMap) -> Result, SysinspectError> { + let minions = self.get_tree(DB_MINIONS)?; + let mut mns: Vec = Vec::default(); + + // XXX: Pretty much crude-dumb implementation which doesn't scale. But good enough for 0.x version. :-) + for entry in minions.iter() { + match entry { + Ok((k_ent, v_ent)) => { + let mid = String::from_utf8(k_ent.to_vec()).unwrap_or_default(); + let mrec = serde_json::from_str::(&String::from_utf8(v_ent.to_vec()).unwrap_or_default()); + let mrec = match mrec { + Ok(mrec) => mrec, + Err(err) => { + return Err(SysinspectError::MasterGeneralError(format!("Unable to read minion record: {err}"))) + } + }; + + let mut matches = false; + for (kreq, vreq) in &traits { + if let Some(v) = mrec.get_traits().get(kreq) { + if vreq.eq(v) { + matches = true; + } else { + matches = true; + break; + } + } else { + matches = false; + break; + } + } + + if matches { + mns.push(mid); + } + } + Err(err) => return Err(SysinspectError::MasterGeneralError(format!("Minion database seems corrupt: {err}"))), + }; + } + + Ok(mns) + } +} diff --git a/sysmaster/src/registry/rec.rs b/sysmaster/src/registry/rec.rs new file mode 100644 index 00000000..d3527aaa --- /dev/null +++ b/sysmaster/src/registry/rec.rs @@ -0,0 +1,25 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct MinionRecord { + id: String, + traits: HashMap, +} + +impl MinionRecord { + pub fn new(id: String, traits: HashMap) -> Self { + MinionRecord { id, traits } + } + + /// Check if the record matches the value + pub fn matches(&self, attr: &str, v: Value) -> bool { + self.traits.get(attr).map(|f| f.eq(&v)).unwrap_or(false) + } + + pub fn get_traits(&self) -> &HashMap { + &self.traits + } +} diff --git a/sysmaster/src/registry/session.rs b/sysmaster/src/registry/session.rs new file mode 100644 index 00000000..c966f814 --- /dev/null +++ b/sysmaster/src/registry/session.rs @@ -0,0 +1,89 @@ +/* +Session keeper. +Keeps connected minions and updates their uptime via heartbeat. +This prevents simultaenous connection of multiple minions on the same machine. + */ + +use std::{collections::HashMap, time::Instant}; + +#[derive(Debug, Clone)] +struct Session { + uptime: Instant, + last: Instant, + sid: String, +} + +impl Session { + pub fn new(sid: &str) -> Session { + Session { last: Instant::now(), uptime: Instant::now(), sid: sid.to_string() } + } + + pub fn age_sec(&self) -> u64 { + self.last.elapsed().as_secs() + } + + pub fn update(&mut self) { + self.last = Instant::now() + } + + pub fn uptime_sec(&self) -> u64 { + self.uptime.elapsed().as_secs() + } + + pub fn session_id(&self) -> String { + self.sid.to_string() + } +} + +#[derive(Debug, Default, Clone)] +pub struct SessionKeeper { + sessions: HashMap, + lifetime: u64, +} + +impl SessionKeeper { + pub fn new(lifetime: u64) -> SessionKeeper { + SessionKeeper { lifetime, ..Default::default() } + } + + /// Collect the garbage (outdated sessions) + #[allow(clippy::useless_conversion)] // Not useless: it has to be a copy because it self-shooting itself + fn gc(&mut self) { + for s in self.sessions.keys().into_iter().map(|s| s.to_string()).collect::>() { + self.alive(&s); + } + } + + /// Create a new session or update the existing + pub fn ping(&mut self, mid: &str, sid: &str) { + self.sessions.entry(mid.to_string()).or_insert_with(|| Session::new(sid)).update(); + self.gc(); + } + + /// Return uptime for a minion (seconds) + pub fn uptime(&self, mid: &str) -> Option { + self.sessions.get(mid).map(|s| s.uptime_sec()) + } + + /// Returns true if a minion is alive. + pub fn alive(&mut self, mid: &str) -> bool { + if let Some(session) = self.sessions.get(mid) { + if session.age_sec() < self.lifetime { + return true; + } + + self.sessions.remove(mid); + } + false + } + + pub(crate) fn exists(&mut self, mid: &str) -> bool { + self.gc(); + self.sessions.contains_key(mid) + } + + /// Get session Id for the minion + pub(crate) fn get_id(&self, mid: &str) -> Option { + self.sessions.get(mid).map(|s| s.session_id()) + } +} diff --git a/sysmaster/src/rmt/mod.rs b/sysmaster/src/rmt/mod.rs new file mode 100644 index 00000000..85ab45aa --- /dev/null +++ b/sysmaster/src/rmt/mod.rs @@ -0,0 +1,8 @@ +use libsysinspect::SysinspectError; +use tokio::{fs::OpenOptions, io::AsyncWriteExt}; + +pub(crate) async fn send_message(msg: &str, fifo: &str) -> Result<(), SysinspectError> { + OpenOptions::new().write(true).open(fifo).await?.write_all(format!("{}\n", msg).as_bytes()).await?; + log::debug!("Message sent to FIFO: {}", msg); + Ok(()) +} diff --git a/sysminion/Cargo.toml b/sysminion/Cargo.toml new file mode 100644 index 00000000..a5f43c0d --- /dev/null +++ b/sysminion/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "sysminion" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5.20", features = ["unstable-styles"] } +colored = "2.1.0" +ed25519-dalek = { version = "2.1.1", features = [ + "asm", + "batch", + "digest", + "merlin", + "pem", + "pkcs8", + "rand_core", + "signature", + "serde", +] } +rand = "0.8.5" +rustls = "0.23.16" +rustls-pemfile = "2.2.0" +tokio = { version = "1.41.0", features = ["full"] } +libsysinspect = { path = "../libsysinspect" } +log = "0.4.22" +serde_yaml = "0.9.34" +serde = { version = "1.0.214", features = ["derive"] } +serde_json = "1.0.132" +indexmap = "2.6.0" +once_cell = "1.20.2" +sysinfo = { version = "0.32.0", features = ["linux-tmpfs"] } +rsa = { version = "0.9.6", features = ["pkcs5", "sha1", "sha2"] } +uuid = { version = "1.11.0", features = ["v4"] } +reqwest = "0.12.9" +regex = "1.11.1" +glob = "0.3.1" diff --git a/sysminion/src/clidef.rs b/sysminion/src/clidef.rs new file mode 100644 index 00000000..8b9581d7 --- /dev/null +++ b/sysminion/src/clidef.rs @@ -0,0 +1,75 @@ +use clap::builder::styling; +use clap::{Arg, ArgAction, Command}; +use colored::Colorize; + +/// Define CLI arguments and styling +pub fn cli(version: &'static str, appname: &'static str) -> Command { + let styles = styling::Styles::styled() + .header(styling::AnsiColor::Yellow.on_default()) + .usage(styling::AnsiColor::Yellow.on_default()) + .literal(styling::AnsiColor::BrightGreen.on_default()) + .placeholder(styling::AnsiColor::BrightMagenta.on_default()); + + Command::new(appname) + .version(version) + .about(format!("{} - {}", appname.bright_magenta().bold(), "is an agent client on a remote device")) + .override_usage(format!("{} [OPTIONS]", appname)) + + // Config + .arg( + Arg::new("config") + .short('c') + .long("config") + .help("Alternative path to the config") + ) + .arg( + Arg::new("register") + .short('r') + .long("register") + .conflicts_with("start") + .help("Register to the master by its fingerprint") // XXX: This must be a key fingerprint in a future + ) + .arg( + Arg::new("start") + .long("start") + .action(ArgAction::SetTrue) + .help("Start minion") + ) + + .next_help_heading("Info") + .arg( + Arg::new("status") + .long("status") + .action(ArgAction::SetTrue) + .help("Show minion status") + ) + + + // Other + .next_help_heading("Other") + .arg( + Arg::new("debug") + .short('d') + .long("debug") + .action(ArgAction::Count) + .help("Set debug mode for more verbose output. Increase this flag for more verbosity."), + ) + .arg( + Arg::new("help") + .short('h') + .long("help") + .action(ArgAction::SetTrue) + .help("Display help"), + ) + .arg( + Arg::new("version") + .short('v') + .long("version") + .action(ArgAction::SetTrue) + .help("Get current version."), + ) + .disable_help_flag(true) // Otherwise it is displayed in a wrong position + .disable_version_flag(true) + .disable_colored_help(false) + .styles(styles) +} diff --git a/sysminion/src/filedata.rs b/sysminion/src/filedata.rs new file mode 100644 index 00000000..13f4aff3 --- /dev/null +++ b/sysminion/src/filedata.rs @@ -0,0 +1,51 @@ +/* +Filedata manager + */ + +use libsysinspect::{mdescr::mspec::MODEL_FILE_EXT, util::iofs::scan_files_sha256, SysinspectError}; +use std::{collections::HashMap, path::PathBuf}; + +#[derive(Debug, Default, Clone)] +pub struct MinionFiledata { + // Checksum to file + stack: HashMap, + + // Path to models (root) + mpth: PathBuf, +} + +impl MinionFiledata { + /// Constructor + pub fn new(mpth: PathBuf) -> Result { + let mut instance = Self { mpth, ..Default::default() }; + instance.init(); + + Ok(instance) + } + + pub fn init(&mut self) { + self.stack = scan_files_sha256(self.mpth.to_owned(), Some(MODEL_FILE_EXT)) + .iter() + .map(|(f, cs)| (cs.to_owned(), PathBuf::from(f.to_owned()))) + .collect::>(); + } + + /// Verify if a corresponding file matches the checksum + pub fn check_sha256(&self, pth: String, cs: String, relative: bool) -> bool { + if !self.stack.contains_key(&cs) { + // Unknown checksum, (re)download required + return false; + } + + if let Some(p) = self.stack.get(&cs) { + let pth = PathBuf::from(pth); + if relative { + return pth.ends_with(p); + } else { + return pth.eq(p); + } + } + + false + } +} diff --git a/sysminion/src/main.rs b/sysminion/src/main.rs new file mode 100644 index 00000000..a497e761 --- /dev/null +++ b/sysminion/src/main.rs @@ -0,0 +1,59 @@ +mod clidef; +mod filedata; +mod minion; +mod proto; +mod rsa; + +use clidef::cli; +use libsysinspect::logger; +use log::LevelFilter; +use std::env; + +static APPNAME: &str = "sysminion"; +static VERSION: &str = "0.0.1"; +static LOGGER: logger::STDOUTLogger = logger::STDOUTLogger; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + let mut cli = cli(VERSION, APPNAME); + if env::args().collect::>().len() == 1 { + cli.print_help()?; + std::process::exit(1); + } + + let params = cli.to_owned().get_matches(); + + // Print help? + if *params.get_one::("help").unwrap() { + return cli.print_help(); + } + + // Print version? + if *params.get_one::("version").unwrap() { + println!("Version: {} {}", APPNAME, VERSION); + return Ok(()); + } + + // Setup logger + if let Err(err) = log::set_logger(&LOGGER).map(|()| { + log::set_max_level(match params.get_count("debug") { + 0 => LevelFilter::Info, + 1 => LevelFilter::Debug, + 2.. => LevelFilter::max(), + }) + }) { + println!("Error setting logger output: {}", err); + } + + // Start + let fp = params.get_one::("register").cloned(); + if *params.get_one::("start").unwrap_or(&false) || fp.is_some() { + let cfp = params.get_one::("config"); + if let Err(err) = minion::minion(cfp.map_or("", |v| v), fp).await { + log::error!("Unable to start minion: {}", err); + return Ok(()); + } + } + + Ok(()) +} diff --git a/sysminion/src/minion.rs b/sysminion/src/minion.rs new file mode 100644 index 00000000..e06b138d --- /dev/null +++ b/sysminion/src/minion.rs @@ -0,0 +1,462 @@ +use crate::{filedata::MinionFiledata, proto, rsa::MinionRSAKeyManager}; +use libsysinspect::{ + cfg::{self, mmconf::MinionConfig}, + inspector::SysInspectRunner, + proto::{ + errcodes::ProtoErrorCode, + payload::{ModStatePayload, PayloadType}, + query::MinionQuery, + rqtypes::RequestType, + MasterMessage, MinionMessage, ProtoConversion, + }, + rsa, + traits::{self, systraits::SystemTraits}, + util::dataconv, + SysinspectError, +}; +use once_cell::sync::{Lazy, OnceCell}; +use std::{fs, path::PathBuf, sync::Arc, vec}; +use tokio::io::AsyncReadExt; +use tokio::net::{tcp::OwnedReadHalf, TcpStream}; +use tokio::sync::Mutex; +use tokio::{io::AsyncWriteExt, net::tcp::OwnedWriteHalf}; +use uuid::Uuid; + +/// Session Id of the minion +pub static MINION_SID: Lazy = Lazy::new(|| Uuid::new_v4().to_string()); +/* +Traits are system properties and attributes on which a minion is running. + +P.S. These are not Rust traits. :-) + */ + +/// System traits instance +static _TRAITS: OnceCell = OnceCell::new(); + +/// Returns a copy of initialised traits. +pub fn get_minion_traits() -> SystemTraits { + _TRAITS.get().expect("Traits are not initialised!?").to_owned() +} + +pub struct SysMinion { + cfg: MinionConfig, + fingerprint: Option, + kman: MinionRSAKeyManager, + + rstm: Arc>, + wstm: Arc>, + + filedata: Mutex, +} + +impl SysMinion { + pub async fn new(cfp: &str, fingerprint: Option) -> Result, SysinspectError> { + let mut cfp = PathBuf::from(cfp); + if !cfp.exists() { + cfp = cfg::select_config(None)?; + } + + let cfg = MinionConfig::new(cfp)?; + + // Init traits + _TRAITS.get_or_init(|| SystemTraits::new(cfg.clone())); + + let (rstm, wstm) = TcpStream::connect(cfg.master()).await.unwrap().into_split(); + let instance = SysMinion { + cfg: cfg.clone(), + fingerprint, + kman: MinionRSAKeyManager::new(cfg.root_dir())?, + rstm: Arc::new(Mutex::new(rstm)), + wstm: Arc::new(Mutex::new(wstm)), + filedata: Mutex::new(MinionFiledata::new(cfg.models_dir())?), + }; + instance.init()?; + + Ok(Arc::new(instance)) + } + + /// Initialise minion. + /// This creates all directory structures if none etc. + fn init(&self) -> Result<(), SysinspectError> { + log::info!("Initialising minion"); + // Place for models + if !self.cfg.models_dir().exists() { + log::debug!( + "Creating directory for the models at {}", + self.cfg.models_dir().as_os_str().to_str().unwrap_or_default() + ); + fs::create_dir_all(self.cfg.models_dir())?; + } + + // Place for traits.d + if !self.cfg.traits_dir().exists() { + log::debug!( + "Creating directory for the drop-in traits at {}", + self.cfg.traits_dir().as_os_str().to_str().unwrap_or_default() + ); + fs::create_dir_all(self.cfg.traits_dir())?; + } + + // Place for trait functions + if !self.cfg.functions_dir().exists() { + log::debug!( + "Creating directory for the custom trait functions at {}", + self.cfg.functions_dir().as_os_str().to_str().unwrap_or_default() + ); + fs::create_dir_all(self.cfg.functions_dir())?; + } + + let mut out: Vec = vec![]; + for t in get_minion_traits().items() { + out.push(format!("{}: {}", t.to_owned(), dataconv::to_string(get_minion_traits().get(&t)).unwrap_or_default())); + } + log::debug!("Minion traits:\n{}", out.join("\n")); + + Ok(()) + } + + pub fn as_ptr(self: &Arc) -> Arc { + Arc::clone(self) + } + + /// Get current minion Id + fn get_minion_id(&self) -> String { + dataconv::as_str(get_minion_traits().get(traits::SYS_ID)) + } + + /// Talk-back to the master + async fn request(&self, msg: Vec) { + let mut stm = self.wstm.lock().await; + + if let Err(e) = stm.write_all(&(msg.len() as u32).to_be_bytes()).await { + log::error!("Failed to send message length to master: {}", e); + return; + } + + if let Err(e) = stm.write_all(&msg).await { + log::error!("Failed to send message to master: {}", e); + return; + } + + if let Err(e) = stm.flush().await { + log::error!("Failed to flush writer to master: {}", e); + } else { + log::trace!("To master: {}", String::from_utf8_lossy(&msg)); + } + } + + pub async fn do_proto(self: Arc) -> Result<(), SysinspectError> { + let rstm = Arc::clone(&self.rstm); + + tokio::spawn(async move { + loop { + let mut buff = [0u8; 4]; + if let Err(e) = rstm.lock().await.read_exact(&mut buff).await { + log::trace!("Unknown message length from the master: {}", e); + break; + } + let msg_len = u32::from_be_bytes(buff) as usize; + + let mut msg = vec![0u8; msg_len]; + if let Err(e) = rstm.lock().await.read_exact(&mut msg).await { + log::error!("Invalid message from the master: {}", e); + break; + } + + let msg = match proto::msg::payload_to_msg(msg) { + Ok(msg) => msg, + Err(err) => { + log::error!("Error getting network payload as message: {err}"); + continue; + } + }; + + log::trace!("Received: {:#?}", msg); + + match msg.req_type() { + RequestType::Add => { + log::debug!("Master accepts registration"); + } + + RequestType::Reconnect => { + log::debug!("Master requires reconnection"); + log::info!("{}", msg.payload()); + std::process::exit(0); + } + + RequestType::Remove => { + log::debug!("Master asks to unregister"); + } + RequestType::Command => { + log::debug!("Master sends a command"); + match msg.get_retcode() { + ProtoErrorCode::Success => { + let cls = self.as_ptr().clone(); + tokio::spawn(async move { + cls.dispatch(msg.to_owned()).await; + }); + } + ProtoErrorCode::AlreadyConnected => { + if MINION_SID.eq(msg.payload()) { + log::error!("Another minion from this machine is already connected"); + std::process::exit(1); + } + } + ret => { + log::debug!("Return code {:?} not yet implemented", ret); + } + } + } + RequestType::Traits => { + log::debug!("Master requests traits"); + if let Err(err) = self.as_ptr().send_traits().await { + log::error!("Unable to send traits: {err}"); + } + } + RequestType::AgentUnknown => { + let pbk_pem = dataconv::as_str(Some(msg.payload()).cloned()); // Expected PEM RSA pub key + let (_, pbk) = rsa::keys::from_pem(None, Some(&pbk_pem)).unwrap(); + let fpt = rsa::keys::get_fingerprint(&pbk.unwrap()).unwrap(); + + log::error!("Minion is not registered"); + log::info!("Master fingerprint: {}", fpt); + std::process::exit(1); + } + RequestType::Ping => { + self.request(proto::msg::get_pong()).await; + } + _ => { + log::error!("Unknown request type"); + } + } + } + }); + Ok(()) + } + + pub async fn send_traits(self: Arc) -> Result<(), SysinspectError> { + let mut r = MinionMessage::new(self.get_minion_id(), RequestType::Traits, get_minion_traits().to_json_string()?); + r.set_sid(MINION_SID.to_string()); + self.request(r.sendable().unwrap()).await; // XXX: make a better error handling for Tokio + Ok(()) + } + + /// Send ehlo + pub async fn send_ehlo(self: Arc) -> Result<(), SysinspectError> { + let mut r = MinionMessage::new( + dataconv::as_str(get_minion_traits().get(traits::SYS_ID)), + RequestType::Ehlo, + MINION_SID.to_string(), + ); + r.set_sid(MINION_SID.to_string()); + + log::info!("Ehlo on {}", self.cfg.master()); + self.request(r.sendable()?).await; + Ok(()) + } + + /// Send registration request + pub async fn send_registration(self: Arc, pbk_pem: String) -> Result<(), SysinspectError> { + let r = MinionMessage::new(dataconv::as_str(get_minion_traits().get(traits::SYS_ID)), RequestType::Add, pbk_pem); + + log::info!("Registration request to {}", self.cfg.master()); + self.request(r.sendable()?).await; + Ok(()) + } + + /// Download a file from master + async fn download_file(self: Arc, fname: &str) -> Result, SysinspectError> { + async fn fetch_file(url: &str, filename: &str) -> Result { + let url = format!("http://{}/{}", url.trim_end_matches('/'), filename.to_string().trim_start_matches('/')); + let rsp = match reqwest::get(url.to_owned()).await { + Ok(rsp) => rsp, + Err(err) => { + return Err(SysinspectError::MinionGeneralError(format!("Unable to get file at {url}: {err}"))); + } + }; + + Ok(match rsp.status() { + reqwest::StatusCode::OK => match rsp.text().await { + Ok(data) => data, + Err(err) => { + return Err(SysinspectError::MinionGeneralError(format!("Unable to get text from the file: {}", err))); + } + }, + reqwest::StatusCode::NOT_FOUND => return Err(SysinspectError::MinionGeneralError("File not found".to_string())), + _ => return Err(SysinspectError::MinionGeneralError("Unknown status".to_string())), + }) + } + let addr = self.cfg.fileserver(); + let fname = fname.to_string(); + let h = tokio::spawn(async move { + match fetch_file(&addr, &fname).await { + Ok(data) => { + log::debug!("Filename: {fname} contains {} bytes", data.len()); + Some(data.into_bytes()) + } + Err(err) => { + log::error!("Error while downloading file {fname}: {err}"); + None + } + } + }); + + if let Ok(Some(data)) = h.await { + return Ok(data); + } + + Err(SysinspectError::MinionGeneralError("File was not downloaded".to_string())) + } + + /// Launch sysinspect + async fn launch_sysinspect(self: Arc, scheme: &str, msp: &ModStatePayload) { + // Get the query first + let mqr = match MinionQuery::new(scheme) { + Ok(mqr) => mqr, + Err(err) => { + log::error!("Query error: {err}"); + return; + } + }; + + // Auto-sync all data files + let mut dirty = false; + for (uri_file, fcs) in msp.files() { + let dst = self + .as_ptr() + .cfg + .models_dir() + .join(uri_file.trim_start_matches(&format!("/{}", msp.models_root())).strip_prefix("/").unwrap_or_default()); + + if self.as_ptr().filedata.lock().await.check_sha256(uri_file.to_owned(), fcs.to_owned(), true) { + continue; + } + log::debug!("File {uri_file} has different checksum"); + + match self.as_ptr().download_file(uri_file).await { + Ok(data) => { + let dst_dir = dst.parent().unwrap(); + if !dst_dir.exists() { + log::debug!("Creating directory: {:?}", dst_dir); + if let Err(err) = fs::create_dir_all(dst_dir) { + log::error!("Unable to create directories for model download: {err}"); + return; + } + } + + log::debug!("Saving URI {uri_file} as {:?}", dst_dir); + if let Err(err) = fs::write(&dst, data) { + log::error!("Unable to save downloaded file to {:?}: {err}", dst); + return; + } + dirty = true; + } + Err(_) => todo!(), + } + } + if dirty { + self.as_ptr().filedata.lock().await.init(); + } + + // Render DSL + + // Run the model + log::debug!("Launching model for sysinspect for: {scheme}"); + let mqr_l = mqr.lock().unwrap(); + let mut sr = SysInspectRunner::new(); + sr.set_model_path(self.as_ptr().cfg.models_dir().join(mqr_l.target()).to_str().unwrap_or_default()); + sr.set_state(mqr_l.state()); + sr.set_entities(mqr_l.entities()); + sr.set_checkbook_labels(mqr_l.checkbook_labels()); + sr.start(); + log::debug!("Sysinspect model cycle finished"); + } + + async fn dispatch(self: Arc, cmd: MasterMessage) { + log::debug!("Dispatching message"); + let tgt = cmd.get_target(); + + // Is command minion-specific? + if !tgt.id().is_empty() && tgt.id().ne(&self.get_minion_id()) { + log::trace!("Command was dropped as it was specifically addressed for another minion"); + return; + } else if tgt.id().is_empty() { + let traits = get_minion_traits(); + + // Is matching this host? + let mut skip = true; + let hostname = dataconv::as_str(traits.get("system.hostname")); + if !hostname.is_empty() { + for hq in tgt.hostnames() { + if let Ok(hq) = glob::Pattern::new(hq) { + if hq.matches(&hostname) { + skip = false; + break; + } + } + } + if skip { + log::trace!("Command was dropped as it is specifically targeting different hosts"); + return; + } + } + + // Can match the host, but might not match by traits. + // For example, web* can match "webschool.com" or "webshop.com", + // but traits for those hosts might be different. + let tq = tgt.traits_query(); + if !tq.is_empty() { + match traits::parse_traits_query(tq) { + Ok(q) => { + match traits::to_typed_query(q) { + Ok(tpq) => { + if !traits::matches_traits(tpq, get_minion_traits()) { + log::trace!("Command was dropped as it does not match the traits"); + return; + } + } + Err(e) => log::error!("{e}"), + }; + } + Err(e) => log::error!("{e}"), + }; + } + } // else: this minion is directly targeted by its Id. + + match PayloadType::try_from(cmd.payload().clone()) { + Ok(PayloadType::ModelOrStatement(pld)) => { + self.launch_sysinspect(cmd.get_target().scheme(), &pld).await; + log::debug!("Command dispatched"); + log::trace!("Command payload: {:#?}", pld); + } + Ok(PayloadType::Undef(pld)) => { + log::error!("Unknown command: {:#?}", pld); + } + Err(err) => { + log::error!("Error dispatching command: {err}"); + } + } + } +} + +pub async fn minion(cfp: &str, fingerprint: Option) -> Result<(), SysinspectError> { + let minion = SysMinion::new(cfp, fingerprint).await?; + minion.as_ptr().do_proto().await?; + + // Example downloading file + //minion.as_ptr().download_file("models/inherited/model.cfg".to_string()).await; + + // Messages + if minion.fingerprint.is_some() { + minion.as_ptr().send_registration(minion.kman.get_pubkey_pem()).await?; + } else { + // ehlo + minion.as_ptr().send_ehlo().await?; + } + + // Keep the client alive until Ctrl+C is pressed + tokio::signal::ctrl_c().await.expect("Failed to listen for ctrl_c"); + log::info!("Shutting down client."); + + Ok(()) +} diff --git a/sysminion/src/proto.rs b/sysminion/src/proto.rs new file mode 100644 index 00000000..17d3326d --- /dev/null +++ b/sysminion/src/proto.rs @@ -0,0 +1,44 @@ +pub mod msg { + use crate::minion::{get_minion_traits, MINION_SID}; + use libsysinspect::{ + proto::{rqtypes::RequestType, MinionMessage}, + traits, + util::dataconv, + }; + use libsysinspect::{ + proto::{MasterMessage, ProtoConversion}, + SysinspectError, + }; + + /// Make pong message + pub fn get_pong() -> Vec { + let p = MinionMessage::new( + dataconv::as_str(get_minion_traits().get(traits::SYS_ID)), + RequestType::Pong, + MINION_SID.to_string(), + ); + + if let Ok(data) = p.sendable() { + return data; + } + vec![] + } + + /// Get message + pub fn payload_to_msg(data: Vec) -> Result { + let data = match String::from_utf8(data) { + Ok(data) => data, + Err(err) => return Err(SysinspectError::ProtoError(format!("unable to parse master message: {err}"))), + }; + + let msg = match serde_json::from_str::(&data) { + Ok(msg) => msg, + Err(err) => { + log::trace!("Broken JSON message: {data}"); + return Err(SysinspectError::ProtoError(format!("broken JSON from master message: {err}"))); + } + }; + + Ok(msg) + } +} diff --git a/sysminion/src/rsa.rs b/sysminion/src/rsa.rs new file mode 100644 index 00000000..7ba81447 --- /dev/null +++ b/sysminion/src/rsa.rs @@ -0,0 +1,76 @@ +/* +RSA keys manager + */ + +use libsysinspect::{ + cfg::mmconf::{CFG_MINION_RSA_PRV, CFG_MINION_RSA_PUB}, + SysinspectError, +}; +use rsa::{RsaPrivateKey, RsaPublicKey}; +use std::{fs, path::PathBuf}; + +#[derive(Debug, Default, Clone)] +pub struct MinionRSAKeyManager { + root: PathBuf, + + // RSA + mn_prk: Option, + mn_pbk: Option, + mn_pbk_pem: String, +} + +impl MinionRSAKeyManager { + /// Initiate Minion's RSA key manager. Parameter `root` is + /// optional, if configuration contains alternative Minion root. + pub fn new(root: PathBuf) -> Result { + let mut keyman = MinionRSAKeyManager { root, ..Default::default() }; + + keyman.setup()?; + Ok(keyman) + } + + /// Initialise RSA keys, if none + fn init_keys(&mut self) -> Result<(), SysinspectError> { + let prk_pth = self.root.join(CFG_MINION_RSA_PRV); + let pbk_pth = self.root.join(CFG_MINION_RSA_PUB); + + // Exists already? + if prk_pth.exists() || pbk_pth.exists() { + let prk_pem = fs::read_to_string(prk_pth)?; + let pbk_pem = fs::read_to_string(pbk_pth)?; + (self.mn_prk, self.mn_pbk) = libsysinspect::rsa::keys::from_pem(Some(&prk_pem), Some(&pbk_pem))?; + self.mn_pbk_pem = pbk_pem; + + return Ok(()); + } + + // Create RSA keypair + log::info!("Creating RSA keypair..."); + + let (prk, pbk) = libsysinspect::rsa::keys::keygen(libsysinspect::rsa::keys::DEFAULT_KEY_SIZE)?; + let (prk_pem, pbk_pem) = libsysinspect::rsa::keys::to_pem(Some(&prk), Some(&pbk))?; + + if prk_pem.is_none() || pbk_pem.is_none() { + return Err(SysinspectError::MinionGeneralError("Error generating new RSA keys".to_string())); + } + + self.mn_pbk_pem = pbk_pem.to_owned().unwrap(); + fs::write(prk_pth, prk_pem.unwrap())?; + fs::write(pbk_pth, pbk_pem.unwrap())?; + + log::info!("RSA keypair created"); + + Ok(()) + } + + /// Setup the RSA key manager + fn setup(&mut self) -> Result<(), SysinspectError> { + self.init_keys()?; + Ok(()) + } + + /// Get RSA PEM pubkey + pub fn get_pubkey_pem(&self) -> String { + self.mn_pbk_pem.to_owned() + } +}