commit 6580cc6a7509d013b7b24a81a6584971fa6e24a0 Author: ghe0 Date: Mon Mar 17 00:25:34 2025 +0200 code migration diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..17c0d8f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2497 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[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 = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + +[[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 = "brain-mock" +version = "0.1.0" +dependencies = [ + "bs58", + "chrono", + "dashmap", + "detee-shared", + "ed25519-dalek", + "env_logger", + "log", + "prost", + "prost-types", + "reqwest", + "serde", + "serde_yaml", + "thiserror", + "tokio", + "tokio-stream", + "tonic", + "tonic-build", + "uuid", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" + +[[package]] +name = "cc" +version = "1.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[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 = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", + "serde", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "detee-shared" +version = "0.1.0" +source = "git+ssh://git@gitea.detee.cloud/noormohammedb/detee-shared?branch=stable_01#099f0a0488bce8e59c9c9e9a5e9b1f24998f1633" +dependencies = [ + "base64", + "prost", + "serde", + "serde_yaml", + "thiserror", + "tonic", + "tonic-build", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "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", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[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 = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[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 = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[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-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.7.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +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", +] + +[[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", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "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", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +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", +] + +[[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 = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" + +[[package]] +name = "native-tls" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" + +[[package]] +name = "openssl" +version = "0.10.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" +dependencies = [ + "bitflags", + "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", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.7.1", +] + +[[package]] +name = "pin-project" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f3e5beed80eb580c68e2c600937ac2c4eedabdfd5ef1e5b7ea4f3fba84497b" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc" +dependencies = [ + "prost", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +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 = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "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 0.5.2", + "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 0.2.15", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[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.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7" +dependencies = [ + "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.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +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 = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +dependencies = [ + "itoa", + "memchr", + "ryu", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.7.1", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "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 = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "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 = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom 0.3.1", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2", + "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +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 = "unicode-ident" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" + +[[package]] +name = "unsafe-libyaml" +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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +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.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" +dependencies = [ + "getrandom 0.3.1", +] + +[[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 = "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "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", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[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", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d91d473 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "brain-mock" +version = "0.1.0" +edition = "2021" + +[dependencies] +bs58 = "0.5.1" +chrono = { version = "0.4.39", features = ["serde"] } +dashmap = { version = "6.1.0", features = ["serde"] } +ed25519-dalek = "2.1.1" +env_logger = "0.11.6" +log = "0.4.22" +prost = "0.13.4" +prost-types = "0.13.4" +reqwest = "0.12.10" +serde = { version = "1.0.217", features = ["derive"] } +serde_yaml = "0.9.34" +thiserror = "2.0.11" +tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread"] } +tokio-stream = "0.1.17" +tonic = "0.12" +uuid = { version = "1.11.0", features = ["v4"] } + +detee-shared = { git = "ssh://git@gitea.detee.cloud/noormohammedb/detee-shared", branch = "stable_01" } +# detee-shared = { path = "../detee-shared" } + +[build-dependencies] +tonic-build = "0.12" diff --git a/README.md b/README.md new file mode 100644 index 0000000..a818250 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Brain mock + +eval "$(ssh-agent -s)" && ssh-add ~/.ssh/id_ed25519 \ No newline at end of file diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..469b7d1 --- /dev/null +++ b/build.rs @@ -0,0 +1,6 @@ +fn main() { + tonic_build::configure() + .build_server(true) + .compile_protos(&["vm.proto"], &["proto"]) + .unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e)); +} diff --git a/src/data.rs b/src/data.rs new file mode 100644 index 0000000..92a1ad4 --- /dev/null +++ b/src/data.rs @@ -0,0 +1,1524 @@ +use crate::grpc::snp_proto::{self as grpc}; +use chrono::Utc; +use dashmap::DashMap; +use log::{debug, info, warn}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use std::sync::RwLock; +use std::{ + collections::{HashMap, HashSet}, + fs::File, + io::Write, +}; +use tokio::sync::mpsc::Sender; +use tokio::sync::oneshot::Sender as OneshotSender; + +use detee_shared::sgx::pb::brain::brain_message_app; +use detee_shared::sgx::pb::brain::AppContract as AppContractPB; +use detee_shared::sgx::pb::brain::AppNodeFilters; +use detee_shared::sgx::pb::brain::AppNodeListResp; +use detee_shared::sgx::pb::brain::AppNodeResources; +use detee_shared::sgx::pb::brain::AppResource as AppResourcePB; +use detee_shared::sgx::pb::brain::BrainMessageApp; +use detee_shared::sgx::pb::brain::DelAppReq; +use detee_shared::sgx::pb::brain::MappedPort; +use detee_shared::sgx::pb::brain::NewAppReq; +use detee_shared::sgx::pb::brain::NewAppRes; +const DATA_PATH: &str = "/etc/detee/brain-mock/saved_data.yaml"; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("We do not allow locking of more than 100000 LP.")] + TxTooBig, + #[error("Escrow must be at least 5000 LP.")] + MinimalEscrow, + #[error("Account has insufficient funds for this operation")] + InsufficientFunds, + #[error("Could not find contract {0}")] + VmContractNotFound(String), + #[error("This error should never happen.")] + ImpossibleError, + #[error("You don't have the required permissions for this operation.")] + AccessDenied, + + #[error("Could not find contract {0}")] + AppContractNotFound(String), +} + +#[derive(Clone, Default, Serialize, Deserialize, Debug)] +pub struct AccountData { + pub balance: u64, + pub tmp_locked: u64, + // holds reasons why VMs of this account got kicked + pub kicked_for: Vec, + pub last_kick: chrono::DateTime, + // holds accounts that banned this account + pub banned_by: HashSet, +} + +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct OperatorData { + pub escrow: u64, + pub email: String, + pub banned_users: HashSet, + pub vm_nodes: HashSet, + pub app_nodes: HashSet, +} + +impl From for grpc::AccountBalance { + fn from(value: AccountData) -> Self { + grpc::AccountBalance { + balance: value.balance, + tmp_locked: value.tmp_locked, + } + } +} + +#[derive(Eq, PartialEq, Clone, Debug, Default, Serialize, Deserialize)] +pub struct VmNode { + pub public_key: String, + pub operator_wallet: String, + pub country: String, + pub region: String, + pub city: String, + pub ip: String, + pub avail_mem_mb: u32, + pub avail_vcpus: u32, + pub avail_storage_gbs: u32, + pub avail_ipv4: u32, + pub avail_ipv6: u32, + pub avail_ports: u32, + pub max_ports_per_vm: u32, + // nanoLP per unit per minute + pub price: u64, + // 1st String is user wallet and 2nd String is report message + pub reports: HashMap, + pub offline_minutes: u64, +} + +impl Into for VmNode { + fn into(self) -> grpc::VmNodeListResp { + grpc::VmNodeListResp { + operator: self.operator_wallet, + node_pubkey: self.public_key, + country: self.country, + region: self.region, + city: self.city, + ip: self.ip, + price: self.price, + reports: self.reports.into_values().collect(), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct VmContract { + pub uuid: String, + pub hostname: String, + pub admin_pubkey: String, + pub node_pubkey: String, + pub exposed_ports: Vec, + pub public_ipv4: String, + pub public_ipv6: String, + pub disk_size_gb: u32, + pub vcpus: u32, + pub memory_mb: u32, + pub kernel_sha: String, + pub dtrfs_sha: String, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + // recommended value is 20000 + /// price per unit per minute + pub price_per_unit: u64, + pub locked_nano: u64, + pub collected_at: chrono::DateTime, +} + +impl VmContract { + /// total hardware units of this VM + fn total_units(&self) -> u64 { + // TODO: Optimize this based on price of hardware. + // I tried, but this can be done better. + // Storage cost should also be based on tier + (self.vcpus as u64 * 10) + + ((self.memory_mb + 256) as u64 / 200) + + (self.disk_size_gb as u64 / 10) + + (!self.public_ipv4.is_empty() as u64 * 10) + } + + /// Returns price per minute in nanoLP + fn price_per_minute(&self) -> u64 { + self.total_units() * self.price_per_unit + } +} + +impl Into for VmContract { + fn into(self) -> grpc::VmContract { + let nano_per_minute = self.price_per_minute(); + grpc::VmContract { + uuid: self.uuid, + hostname: self.hostname, + admin_pubkey: self.admin_pubkey, + node_pubkey: self.node_pubkey, + exposed_ports: self.exposed_ports, + public_ipv4: self.public_ipv4, + public_ipv6: self.public_ipv6, + disk_size_gb: self.disk_size_gb, + vcpus: self.vcpus, + memory_mb: self.memory_mb, + kernel_sha: self.kernel_sha, + dtrfs_sha: self.dtrfs_sha, + created_at: self.created_at.to_rfc3339(), + updated_at: self.updated_at.to_rfc3339(), + nano_per_minute, + locked_nano: self.locked_nano, + collected_at: self.collected_at.to_rfc3339(), + } + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct AppContract { + pub uuid: String, + pub package_url: String, + pub admin_pubkey: String, + pub node_pubkey: String, + pub mapped_ports: Vec<(u16, u16)>, + pub host_ipv4: String, + pub disk_size_mb: u32, + pub vcpus: u32, + pub memory_mb: u32, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + // price per unit per minute + // recommended value is 20000 + pub price_per_unit: u64, + pub locked_nano: u64, + pub collected_at: chrono::DateTime, + pub hratls_pubkey: String, + pub public_package_mr_enclave: Option>, + pub app_name: String, +} + +impl AppContract { + fn total_units(&self) -> f64 { + // TODO: Optimize this based on price of hardware. + (self.vcpus as f64 * 5f64) + + (self.memory_mb as f64 / 200f64) + + (self.disk_size_mb as f64 / 10000f64) + } + + /// Returns price per minute in nanoLP + fn price_per_minute(&self) -> u64 { + (self.total_units() * self.price_per_unit as f64) as u64 + } +} + +impl From for AppContractPB { + fn from(value: AppContract) -> Self { + let mapped_ports = value + .mapped_ports + .clone() + .into_iter() + .map(MappedPort::from) + .collect(); + + let nano_per_minute = value.price_per_minute(); + let resource = Some(AppResourcePB { + memory_mb: value.memory_mb, + disk_mb: value.disk_size_mb, + vcpu: value.vcpus, + ports: value.mapped_ports.iter().map(|p| p.1 as u32).collect(), + }); + Self { + uuid: value.uuid, + package_url: value.package_url, + admin_pubkey: value.admin_pubkey, + node_pubkey: value.node_pubkey, + mapped_ports, + public_ipv4: value.host_ipv4, + resource, + created_at: value.created_at.to_rfc3339(), + updated_at: value.updated_at.to_rfc3339(), + nano_per_minute, + locked_nano: value.locked_nano, + collected_at: value.collected_at.to_rfc3339(), + hratls_pubkey: value.hratls_pubkey, + public_package_mr_enclave: value.public_package_mr_enclave, + app_name: value.app_name, + } + } +} + +#[derive(Eq, Hash, PartialEq, Clone, Debug, Default, Serialize, Deserialize)] +pub struct AppNode { + pub node_pubkey: String, + pub operator_wallet: String, + pub country: String, + pub region: String, + pub city: String, + pub ip: String, + pub avail_mem_mb: u32, + pub avail_vcpus: u32, + pub avail_storage_mb: u32, + pub avail_no_of_port: u32, + pub max_ports_per_app: u32, + // nanotokens per unit per minute + pub price: u64, + + pub offline_minutes: u64, +} + +impl From for AppNodeListResp { + fn from(value: AppNode) -> Self { + Self { + operator: value.operator_wallet, + node_pubkey: value.node_pubkey, + country: value.country, + region: value.region, + city: value.city, + ip: value.ip, + price: value.price, + reports: Vec::new(), + } + } +} + +#[derive(Default, Serialize, Deserialize)] +pub struct BrainData { + accounts: DashMap, + operators: DashMap, + vm_nodes: RwLock>, + vm_contracts: RwLock>, + #[serde(skip_serializing, skip_deserializing)] + tmp_newvm_reqs: DashMap)>, + #[serde(skip_serializing, skip_deserializing)] + tmp_updatevm_reqs: DashMap)>, + #[serde(skip_serializing, skip_deserializing)] + daemon_tx: DashMap>, + + app_nodes: RwLock>, + #[serde(skip_serializing, skip_deserializing)] + app_daemon_tx: DashMap>, + #[serde(skip_serializing, skip_deserializing)] + tmp_new_container_reqs: DashMap)>, + app_contracts: RwLock>, +} + +impl BrainData { + pub fn save_to_disk(&self) -> Result<(), Box> { + let mut file = File::create(DATA_PATH)?; + file.write_all(serde_yaml::to_string(self)?.as_bytes())?; + Ok(()) + } + + fn load_from_disk() -> Result> { + let content = std::fs::read_to_string(DATA_PATH)?; + let data: Self = serde_yaml::from_str(&content)?; + Ok(data) + } + + pub fn new() -> Self { + match Self::load_from_disk() { + Ok(data) => data, + Err(e) => { + warn!("Could not data {DATA_PATH} due to error: {e:?}"); + info!("Creating new instance of brain."); + Self { + accounts: DashMap::new(), + operators: DashMap::new(), + vm_nodes: RwLock::new(Vec::new()), + vm_contracts: RwLock::new(Vec::new()), + tmp_newvm_reqs: DashMap::new(), + tmp_updatevm_reqs: DashMap::new(), + daemon_tx: DashMap::new(), + + app_nodes: RwLock::new(Vec::new()), + app_daemon_tx: DashMap::new(), + tmp_new_container_reqs: DashMap::new(), + app_contracts: RwLock::new(Vec::new()), + } + } + } + } + + pub fn get_balance(&self, account: &str) -> AccountData { + if let Some(account) = self.accounts.get(account) { + return account.value().clone(); + } else { + let balance = AccountData { + balance: 0, + tmp_locked: 0, + kicked_for: Vec::new(), + banned_by: HashSet::new(), + last_kick: chrono::Utc::now(), + }; + return balance; + } + } + + pub fn give_airdrop(&self, account: &str, tokens: u64) { + warn!("Airdropping {tokens} to {account}."); + self.add_nano_to_wallet(account, tokens.saturating_mul(1_000_000_000)); + } + + pub fn slash_account(&self, account: &str, tokens: u64) { + warn!("Slashing {tokens} from {account}."); + self.rm_nano_from_wallet(account, tokens.saturating_mul(1_000_000_000)); + } + + fn add_nano_to_wallet(&self, account: &str, nano_lp: u64) { + log::debug!("Adding {nano_lp} nanoLP to {account}"); + self.accounts + .entry(account.to_string()) + .and_modify(|d| d.balance += nano_lp) + .or_insert(AccountData { + balance: nano_lp, + ..Default::default() + }); + } + + fn rm_nano_from_wallet(&self, account: &str, nano_lp: u64) { + log::debug!("Slashing {nano_lp} nanoLP to {account}"); + self.accounts.entry(account.to_string()).and_modify(|d| { + d.balance = d.balance.saturating_sub(nano_lp); + }); + } + + /// This is written to run every minute + pub async fn vm_nodes_cron(&self) { + log::debug!("Running vm nodes cron..."); + let mut nodes = self.vm_nodes.write().unwrap(); + let mut vm_contracts = self.vm_contracts.write().unwrap(); + for node in nodes.iter_mut() { + if self.daemon_tx.contains_key(&node.public_key) { + node.offline_minutes = 0; + continue; + } + let mut operator = match self + .operators + .iter_mut() + .find(|o| o.vm_nodes.contains(&node.public_key)) + { + Some(op) => op, + None => continue, + }; + node.offline_minutes += 1; + // compensate contract admin if the node is offline more then 5 minutes + if node.offline_minutes > 5 { + for c in vm_contracts + .iter() + .filter(|c| c.node_pubkey == node.public_key) + { + let compensation = c.price_per_minute() * 10; + if compensation < operator.escrow { + operator.escrow -= compensation; + self.add_nano_to_wallet(&c.admin_pubkey, compensation); + } + } + } + } + // delete nodes that are offline more than 3 hours, and clean contracts + nodes.retain(|n| { + if n.offline_minutes > 1600 { + vm_contracts.retain_mut(|c| { + if c.node_pubkey == n.public_key { + self.add_nano_to_wallet(&c.admin_pubkey, c.locked_nano); + } + c.node_pubkey != n.public_key + }); + for mut op in self.operators.iter_mut() { + op.vm_nodes.remove(&n.public_key); + } + } + n.offline_minutes <= 180 + }); + } + + pub async fn vm_contracts_cron(&self) { + let mut deleted_contracts: Vec<(String, String)> = Vec::new(); + log::debug!("Running contracts cron..."); + { + let mut contracts = self.vm_contracts.write().unwrap(); + contracts.retain_mut(|c| { + let node = self.find_node_by_pubkey(&c.node_pubkey).unwrap(); + if node.offline_minutes == 0 { + let operator_wallet = node.operator_wallet.clone(); + let minutes_to_collect = (Utc::now() - c.collected_at).num_minutes() as u64; + c.collected_at = Utc::now(); + let mut nanolp_to_collect = + c.price_per_minute().saturating_mul(minutes_to_collect); + if nanolp_to_collect > c.locked_nano { + nanolp_to_collect = c.locked_nano; + } + log::debug!("Removing {nanolp_to_collect} nanoLP from {}", c.uuid); + c.locked_nano -= nanolp_to_collect; + let escrow_multiplier = match self.operators.get(&operator_wallet) { + Some(op) if op.escrow > 5000 => match self.operators.get(&c.admin_pubkey) { + Some(user_is_op) if user_is_op.escrow > 5000 => 1, + _ => 5, + }, + _ => 1, + }; + self.add_nano_to_wallet( + &operator_wallet, + nanolp_to_collect * escrow_multiplier, + ); + if c.locked_nano == 0 { + deleted_contracts.push((c.uuid.clone(), c.node_pubkey.clone())); + } + } + c.locked_nano > 0 + }); + } + // inform daemons of the deletion of the contracts + for (uuid, node_pubkey) in deleted_contracts.iter() { + if let Some(daemon_tx) = self.daemon_tx.get(&node_pubkey.clone()) { + let msg = grpc::BrainVmMessage { + msg: Some(grpc::brain_vm_message::Msg::DeleteVm(grpc::DeleteVmReq { + uuid: uuid.to_string(), + admin_pubkey: String::new(), + })), + }; + let daemon_tx = daemon_tx.clone(); + let _ = daemon_tx.send(msg).await; + } + } + } + + pub fn register_node(&self, node: VmNode) { + info!("Registering node {node:?}"); + self.add_vmnode_to_operator(&node.operator_wallet, &node.public_key); + let mut nodes = self.vm_nodes.write().unwrap(); + for n in nodes.iter_mut() { + if n.public_key == node.public_key { + // TODO: figure what to do in this case. + warn!("VM Node {} already exists. Updating data.", n.public_key); + *n = node; + return; + } + } + nodes.push(node); + } + + // todo: this should also support Apps + /// Receives: operator, contract uuid, reason of kick + pub async fn kick_contract( + &self, + operator: &str, + uuid: &str, + reason: &str, + ) -> Result { + log::debug!("Operator {operator} requested a kick of {uuid} for reason: {reason}"); + let contract = self.find_contract_by_uuid(uuid)?; + let mut operator_data = self + .operators + .get_mut(operator) + .ok_or(Error::AccessDenied)?; + if !operator_data.vm_nodes.contains(&contract.node_pubkey) { + return Err(Error::AccessDenied); + } + + let mut minutes_to_refund = chrono::Utc::now() + .signed_duration_since(contract.updated_at) + .num_minutes() + .abs() as u64; + // cap refund at 1 week + if minutes_to_refund > 10080 { + minutes_to_refund = 10080; + } + + let mut refund_amount = minutes_to_refund * contract.price_per_minute(); + let mut admin_account = self + .accounts + .get_mut(&contract.admin_pubkey) + .ok_or(Error::ImpossibleError)?; + + // check if he got kicked within the last day + if !chrono::Utc::now() + .signed_duration_since(admin_account.last_kick) + .gt(&chrono::Duration::days(1)) + { + refund_amount = 0; + } + + if operator_data.escrow < refund_amount { + refund_amount = operator_data.escrow; + } + + log::debug!( + "Removing {refund_amount} escrow from {} and giving it to {}", + operator_data.key(), + admin_account.key() + ); + admin_account.balance += refund_amount; + admin_account.kicked_for.push(reason.to_string()); + operator_data.escrow -= refund_amount; + + let admin_pubkey = contract.admin_pubkey.clone(); + drop(admin_account); + drop(contract); + + self.delete_vm(grpc::DeleteVmReq { + uuid: uuid.to_string(), + admin_pubkey, + }) + .await?; + + Ok(refund_amount) + } + + pub fn ban_user(&self, operator: &str, user: &str) { + self.accounts + .entry(user.to_string()) + .and_modify(|a| { + a.banned_by.insert(operator.to_string()); + }) + .or_insert(AccountData { + banned_by: HashSet::from([operator.to_string()]), + ..Default::default() + }); + self.operators + .entry(operator.to_string()) + .and_modify(|o| { + o.banned_users.insert(user.to_string()); + }) + .or_insert(OperatorData { + banned_users: HashSet::from([user.to_string()]), + ..Default::default() + }); + } + + pub fn report_node(&self, admin_pubkey: String, node: &str, report: String) { + let mut nodes = self.vm_nodes.write().unwrap(); + if let Some(node) = nodes.iter_mut().find(|n| n.public_key == node) { + node.reports.insert(admin_pubkey, report); + } + } + + pub fn lock_nanotockens(&self, account: &str, nano_lp: u64) -> Result<(), Error> { + if nano_lp > 100_000_000_000_000 { + return Err(Error::TxTooBig); + } + if let Some(mut account) = self.accounts.get_mut(account) { + if nano_lp > account.balance { + return Err(Error::InsufficientFunds); + } + account.balance = account.balance.saturating_sub(nano_lp); + account.tmp_locked = account.tmp_locked.saturating_add(nano_lp); + Ok(()) + } else { + Err(Error::InsufficientFunds) + } + } + + pub fn extend_vm_contract_time( + &self, + uuid: &str, + wallet: &str, + nano_lp: u64, + ) -> Result<(), Error> { + if nano_lp > 100_000_000_000_000 { + return Err(Error::TxTooBig); + } + let mut account = match self.accounts.get_mut(wallet) { + Some(account) => account, + None => return Err(Error::InsufficientFunds), + }; + match self + .vm_contracts + .write() + .unwrap() + .iter_mut() + .find(|c| c.uuid == uuid) + { + Some(contract) => { + if contract.admin_pubkey != wallet { + return Err(Error::VmContractNotFound(uuid.to_string())); + } + if account.balance + contract.locked_nano < nano_lp { + return Err(Error::InsufficientFunds); + } + account.balance = account.balance + contract.locked_nano - nano_lp; + contract.locked_nano = nano_lp; + Ok(()) + } + None => Err(Error::VmContractNotFound(uuid.to_string())), + } + } + + pub fn submit_node_resources(&self, res: grpc::VmNodeResources) { + let mut nodes = self.vm_nodes.write().unwrap(); + for n in nodes.iter_mut() { + if n.public_key == res.node_pubkey { + debug!( + "Found node {}. Updating resources to {:?}", + n.public_key, res + ); + n.avail_ipv4 = res.avail_ipv4; + n.avail_ipv6 = res.avail_ipv6; + n.avail_vcpus = res.avail_vcpus; + n.avail_mem_mb = res.avail_memory_mb; + n.avail_storage_gbs = res.avail_storage_gb; + n.max_ports_per_vm = res.max_ports_per_vm; + n.avail_ports = res.avail_ports; + return; + } + } + debug!( + "VM Node {} not found when trying to update resources.", + res.node_pubkey + ); + debug!("VM Node list:\n{:?}", nodes); + } + + pub fn add_daemon_tx(&self, node_pubkey: &str, tx: Sender) { + self.daemon_tx.insert(node_pubkey.to_string(), tx); + } + + pub fn del_daemon_tx(&self, node_pubkey: &str) { + self.daemon_tx.remove(node_pubkey); + } + + pub async fn delete_vm(&self, delete_vm: grpc::DeleteVmReq) -> Result<(), Error> { + log::debug!("Starting deletion of VM {}", delete_vm.uuid); + let contract = self.find_contract_by_uuid(&delete_vm.uuid)?; + if contract.admin_pubkey != delete_vm.admin_pubkey { + return Err(Error::AccessDenied); + } + info!("Found vm {}. Deleting...", delete_vm.uuid); + if let Some(daemon_tx) = self.daemon_tx.get(&contract.node_pubkey) { + debug!( + "TX for daemon {} found. Informing daemon about deletion of {}.", + contract.node_pubkey, delete_vm.uuid + ); + let msg = grpc::BrainVmMessage { + msg: Some(grpc::brain_vm_message::Msg::DeleteVm(delete_vm.clone())), + }; + if let Err(e) = daemon_tx.send(msg).await { + warn!( + "Failed to send deletion request to {} due to error: {e:?}", + contract.node_pubkey + ); + info!("Deleting daemon TX for {}", contract.node_pubkey); + self.del_daemon_tx(&contract.node_pubkey); + } + } + + self.add_nano_to_wallet(&contract.admin_pubkey, contract.locked_nano); + let mut contracts = self.vm_contracts.write().unwrap(); + contracts.retain(|c| c.uuid != delete_vm.uuid); + Ok(()) + } + + // also unlocks nanotokens in case VM creation failed + pub async fn submit_newvm_resp(&self, new_vm_resp: grpc::NewVmResp) { + let new_vm_req = match self.tmp_newvm_reqs.remove(&new_vm_resp.uuid) { + Some((_, r)) => r, + None => { + log::error!( + "Received confirmation for ghost NewVMReq {}", + new_vm_resp.uuid + ); + return; + } + }; + if let Err(_) = new_vm_req.1.send(new_vm_resp.clone()) { + log::error!( + "CLI RX for {} dropped before receiving confirmation {:?}.", + &new_vm_req.0.admin_pubkey, + new_vm_resp, + ); + } + if new_vm_resp.error != "" { + if let Some(mut admin_wallet) = self.accounts.get_mut(&new_vm_req.0.admin_pubkey) { + admin_wallet.balance += new_vm_req.0.locked_nano; + admin_wallet.tmp_locked -= new_vm_req.0.locked_nano; + } + return; + } + + let mut public_ipv4 = String::new(); + let mut public_ipv6 = String::new(); + + let args = new_vm_resp.args.as_ref().unwrap(); + for ip in args.ips.iter() { + if let Ok(ipv4_addr) = std::net::Ipv4Addr::from_str(&ip.address) { + if !ipv4_addr.is_private() && !ipv4_addr.is_link_local() { + public_ipv4 = ipv4_addr.to_string(); + } + continue; + } + if let Ok(ipv6_addr) = std::net::Ipv6Addr::from_str(&ip.address) { + public_ipv6 = ipv6_addr.to_string(); + } + } + + if let Some(mut admin_wallet) = self.accounts.get_mut(&new_vm_req.0.admin_pubkey) { + admin_wallet.tmp_locked -= new_vm_req.0.locked_nano; + } + + let contract = VmContract { + uuid: new_vm_resp.uuid, + exposed_ports: args.exposed_ports.clone(), + public_ipv4, + public_ipv6, + created_at: Utc::now(), + updated_at: Utc::now(), + hostname: new_vm_req.0.hostname, + admin_pubkey: new_vm_req.0.admin_pubkey, + node_pubkey: new_vm_req.0.node_pubkey.clone(), + disk_size_gb: new_vm_req.0.disk_size_gb, + vcpus: new_vm_req.0.vcpus, + memory_mb: new_vm_req.0.memory_mb, + kernel_sha: new_vm_req.0.kernel_sha, + dtrfs_sha: new_vm_req.0.dtrfs_sha, + price_per_unit: new_vm_req.0.price_per_unit, + locked_nano: new_vm_req.0.locked_nano, + collected_at: Utc::now(), + }; + info!("Created new contract: {contract:?}"); + self.vm_contracts.write().unwrap().push(contract); + } + + pub async fn submit_updatevm_resp(&self, mut update_vm_resp: grpc::UpdateVmResp) { + let update_vm_req = match self.tmp_updatevm_reqs.remove(&update_vm_resp.uuid) { + Some((_, r)) => r, + None => { + log::error!( + "Received confirmation for ghost UpdateVMRequest {}", + update_vm_resp.uuid + ); + update_vm_resp.error = + "Received confirmation for ghost UpdateVMRequest.".to_string(); + return; + } + }; + if let Err(e) = update_vm_req.1.send(update_vm_resp.clone()) { + log::warn!( + "CLI RX dropped before receiving UpdateVMResp {update_vm_resp:?}. Error: {e:?}" + ); + } + if update_vm_resp.error != "" { + return; + } + let mut contracts = self.vm_contracts.write().unwrap(); + match contracts.iter_mut().find(|c| c.uuid == update_vm_resp.uuid) { + Some(contract) => { + if update_vm_req.0.vcpus != 0 { + contract.vcpus = update_vm_req.0.vcpus; + } + if update_vm_req.0.memory_mb != 0 { + contract.memory_mb = update_vm_req.0.memory_mb; + } + if update_vm_req.0.disk_size_gb != 0 { + contract.disk_size_gb = update_vm_req.0.disk_size_gb; + } + if !update_vm_req.0.kernel_sha.is_empty() { + debug!( + "Updating kernel sha for {} to {}", + contract.uuid, update_vm_req.0.kernel_sha + ); + contract.kernel_sha = update_vm_req.0.kernel_sha; + } + if !update_vm_req.0.dtrfs_sha.is_empty() { + debug!( + "Updating dtrfs sha for {} to {}", + contract.uuid, update_vm_req.0.dtrfs_sha + ); + contract.dtrfs_sha = update_vm_req.0.dtrfs_sha; + } + contract.updated_at = Utc::now(); + } + None => { + log::error!("VM Contract not found for {}.", update_vm_req.0.uuid); + update_vm_resp.error = "VM Contract not found.".to_string(); + } + } + } + + pub async fn submit_newvm_req( + &self, + mut req: grpc::NewVmReq, + tx: OneshotSender, + ) { + if let Err(e) = self.lock_nanotockens(&req.admin_pubkey, req.locked_nano) { + let _ = tx.send(grpc::NewVmResp { + uuid: String::new(), + error: e.to_string(), + args: None, + }); + return; + } + req.uuid = uuid::Uuid::new_v4().to_string(); + info!("Inserting new vm request in memory: {req:?}"); + self.tmp_newvm_reqs + .insert(req.uuid.clone(), (req.clone(), tx)); + if let Some(daemon_tx) = self.daemon_tx.get(&req.node_pubkey) { + debug!( + "Found daemon TX for {}. Sending newVMReq {}", + req.node_pubkey, req.uuid + ); + let msg = grpc::BrainVmMessage { + msg: Some(grpc::brain_vm_message::Msg::NewVmReq(req.clone())), + }; + if let Err(e) = daemon_tx.send(msg).await { + warn!( + "Failed to send new VM request to {} due to error: {e:?}", + req.node_pubkey + ); + info!("Deleting daemon TX for {}", req.node_pubkey); + self.del_daemon_tx(&req.node_pubkey); + self.submit_newvm_resp(grpc::NewVmResp { + error: "Daemon is offline.".to_string(), + uuid: req.uuid, + args: None, + }) + .await; + } + } else { + self.submit_newvm_resp(grpc::NewVmResp { + error: "Daemon is offline.".to_string(), + uuid: req.uuid, + args: None, + }) + .await; + } + } + + pub async fn submit_updatevm_req( + &self, + req: grpc::UpdateVmReq, + tx: OneshotSender, + ) { + let uuid = req.uuid.clone(); + info!("Inserting new vm update request in memory: {req:?}"); + let node_pubkey = match self.find_contract_by_uuid(&req.uuid) { + Ok(contract) => { + if contract.admin_pubkey != req.admin_pubkey { + let _ = tx.send(grpc::UpdateVmResp { + uuid, + error: "VM Contract does not exist.".to_string(), + args: None, + }); + return; + } + contract.node_pubkey + } + Err(_) => { + log::warn!( + "Received UpdateVMReq for a contract that does not exist: {}", + req.uuid + ); + let _ = tx.send(grpc::UpdateVmResp { + uuid, + error: "VM Contract does not exist.".to_string(), + args: None, + }); + return; + } + }; + self.tmp_updatevm_reqs + .insert(req.uuid.clone(), (req.clone(), tx)); + if let Some(server_tx) = self.daemon_tx.get(&node_pubkey) { + debug!( + "Found daemon TX for {}. Sending updateVMReq {}", + node_pubkey, req.uuid + ); + let msg = grpc::BrainVmMessage { + msg: Some(grpc::brain_vm_message::Msg::UpdateVmReq(req.clone())), + }; + match server_tx.send(msg).await { + Ok(_) => { + debug!("Successfully sent updateVMReq to {}", node_pubkey); + return; + } + Err(e) => { + warn!("Failed to send update VM request to {node_pubkey} due to error {e}"); + info!("Deleting daemon TX for {}", node_pubkey); + self.del_daemon_tx(&node_pubkey); + self.submit_updatevm_resp(grpc::UpdateVmResp { + uuid, + error: "Daemon is offline.".to_string(), + args: None, + }) + .await; + } + } + } else { + warn!("No daemon TX found for {}", node_pubkey); + self.submit_updatevm_resp(grpc::UpdateVmResp { + uuid, + error: "Daemon is offline.".to_string(), + args: None, + }) + .await; + } + } + + pub fn find_node_by_pubkey(&self, public_key: &str) -> Option { + let nodes = self.vm_nodes.read().unwrap(); + nodes.iter().cloned().find(|n| n.public_key == public_key) + } + + pub fn is_user_banned_by_node(&self, user_wallet: &str, node_pubkey: &str) -> bool { + if let Some(node) = self.find_node_by_pubkey(&node_pubkey) { + if let Some(account) = self.accounts.get(user_wallet) { + if account.banned_by.contains(&node.operator_wallet) { + return true; + } + } + } + false + } + + pub fn add_vmnode_to_operator(&self, operator_wallet: &str, node_pubkey: &str) { + self.operators + .entry(operator_wallet.to_string()) + .and_modify(|op| { + op.vm_nodes.insert(node_pubkey.to_string()); + }) + .or_insert(OperatorData { + escrow: 0, + email: String::new(), + banned_users: HashSet::new(), + vm_nodes: HashSet::from([node_pubkey.to_string()]), + app_nodes: HashSet::new(), + }); + } + + pub fn register_operator(&self, req: grpc::RegOperatorReq) -> Result<(), Error> { + let mut operator = match self.operators.get(&req.pubkey) { + Some(o) => (*(o.value())).clone(), + None => OperatorData { + ..Default::default() + }, + }; + if req.escrow < 5000 { + return Err(Error::MinimalEscrow); + } + let escrow = req.escrow * 1_000_000_000; + if let Some(mut account) = self.accounts.get_mut(&req.pubkey) { + if (account.balance + operator.escrow) < escrow { + return Err(Error::InsufficientFunds); + } + account.balance = account.balance + operator.escrow - escrow; + operator.escrow = escrow; + } else { + return Err(Error::InsufficientFunds); + } + operator.email = req.email; + self.operators.insert(req.pubkey, operator); + Ok(()) + } + + pub fn find_vm_nodes_by_operator(&self, operator_wallet: &str) -> Vec { + let nodes = self.vm_nodes.read().unwrap(); + nodes + .iter() + .filter(|node| node.operator_wallet == operator_wallet) + .cloned() + .collect() + } + + pub fn total_operator_reports(&self, operator_wallet: &str) -> usize { + let nodes = self.vm_nodes.read().unwrap(); + nodes + .iter() + .cloned() + .filter(|n| n.operator_wallet == operator_wallet) + .map(|node| node.reports.len()) + .sum() + } + + pub fn find_vm_nodes_by_filters( + &self, + filters: &crate::grpc::snp_proto::VmNodeFilters, + ) -> Vec { + let nodes = self.vm_nodes.read().unwrap(); + nodes + .iter() + .filter(|n| { + n.avail_ports >= filters.free_ports + && (!filters.offers_ipv4 || n.avail_ipv4 > 0) + && (!filters.offers_ipv6 || n.avail_ipv6 > 0) + && n.avail_vcpus >= filters.vcpus + && n.avail_mem_mb >= filters.memory_mb + && n.avail_storage_gbs >= filters.storage_gb + && (filters.country.is_empty() || (n.country == filters.country)) + && (filters.city.is_empty() || (n.city == filters.city)) + && (filters.region.is_empty() || (n.region == filters.region)) + && (filters.ip.is_empty() || (n.ip == filters.ip)) + }) + .cloned() + .collect() + } + + // TODO: sort by rating + pub fn get_one_node_by_filters( + &self, + filters: &crate::grpc::snp_proto::VmNodeFilters, + ) -> Option { + let nodes = self.vm_nodes.read().unwrap(); + nodes + .iter() + .find(|n| { + n.avail_ports >= filters.free_ports + && (!filters.offers_ipv4 || n.avail_ipv4 > 0) + && (!filters.offers_ipv6 || n.avail_ipv6 > 0) + && n.avail_vcpus >= filters.vcpus + && n.avail_mem_mb >= filters.memory_mb + && n.avail_storage_gbs >= filters.storage_gb + && (filters.country.is_empty() || (n.country == filters.country)) + && (filters.city.is_empty() || (n.city == filters.city)) + && (filters.region.is_empty() || (n.region == filters.region)) + && (filters.ip.is_empty() || (n.ip == filters.ip)) + && (filters.node_pubkey.is_empty() || (n.public_key == filters.node_pubkey)) + }) + .cloned() + } + + pub fn find_contract_by_uuid(&self, uuid: &str) -> Result { + let contracts = self.vm_contracts.read().unwrap(); + contracts + .iter() + .cloned() + .find(|c| c.uuid == uuid) + .ok_or(Error::VmContractNotFound(uuid.to_string())) + } + + pub fn list_all_contracts(&self) -> Vec { + let contracts = self.vm_contracts.read().unwrap(); + contracts.iter().cloned().collect() + } + + pub fn list_accounts(&self) -> Vec { + self.accounts + .iter() + .map(|a| grpc::Account { + pubkey: a.key().to_string(), + balance: a.balance, + tmp_locked: a.tmp_locked, + }) + .collect() + } + + pub fn list_operators(&self) -> Vec { + self.operators + .iter() + .map(|op| grpc::ListOperatorsResp { + pubkey: op.key().to_string(), + escrow: op.escrow / 1_000_000_000, + email: op.email.clone(), + app_nodes: 0, + vm_nodes: op.vm_nodes.len() as u64, + reports: self.total_operator_reports(op.key()) as u64, + }) + .collect() + } + + pub fn inspect_operator(&self, wallet: &str) -> Option { + self.operators.get(wallet).map(|op| { + let nodes = self + .find_vm_nodes_by_operator(wallet) + .into_iter() + .map(|n| n.into()) + .collect(); + grpc::InspectOperatorResp { + operator: Some(grpc::ListOperatorsResp { + pubkey: op.key().to_string(), + escrow: op.escrow, + email: op.email.clone(), + app_nodes: 0, + vm_nodes: op.vm_nodes.len() as u64, + reports: self.total_operator_reports(op.key()) as u64, + }), + nodes, + } + }) + } + + pub fn find_vm_contracts_by_operator(&self, wallet: &str) -> Vec { + debug!("Searching contracts for operator {wallet}"); + let nodes = match self.operators.get(wallet) { + Some(op) => op.vm_nodes.clone(), + None => return Vec::new(), + }; + let contracts: Vec = self + .vm_contracts + .read() + .unwrap() + .iter() + .filter(|c| nodes.contains(&c.node_pubkey)) + .cloned() + .collect(); + contracts + } + + pub fn find_vm_contracts_by_admin(&self, admin_wallet: &str) -> Vec { + debug!("Searching contracts for admin pubkey {admin_wallet}"); + let contracts: Vec = self + .vm_contracts + .read() + .unwrap() + .iter() + .filter(|c| c.admin_pubkey == admin_wallet) + .cloned() + .collect(); + contracts + } + + pub fn find_vm_contracts_by_node(&self, node_pubkey: &str) -> Vec { + let contracts = self.vm_contracts.read().unwrap(); + contracts + .iter() + .filter(|c| c.node_pubkey == node_pubkey) + .cloned() + .collect() + } +} + +impl BrainData { + pub fn add_app_daemon_tx(&self, node_pubkey: &str, tx: Sender) { + self.app_daemon_tx.insert(node_pubkey.to_string(), tx); + } + + pub fn del_app_daemon_tx(&self, node_pubkey: &str) { + self.app_daemon_tx.remove(node_pubkey); + } + + pub fn register_app_node(&self, node: AppNode) { + info!("Registering app node {node:?}"); + self.add_app_node_to_operator(&node.operator_wallet, &node.node_pubkey); + let mut nodes = self.app_nodes.write().unwrap(); + for n in nodes.iter_mut() { + if n.node_pubkey == node.node_pubkey { + // TODO: figure what to do in this case. + warn!("Node {} already exists. Updating data.", n.node_pubkey); + *n = node; + return; + } + } + nodes.push(node); + } + + pub fn add_app_node_to_operator(&self, operator_wallet: &str, node_pubkey: &str) { + self.operators + .entry(operator_wallet.to_string()) + .and_modify(|op| { + op.app_nodes.insert(node_pubkey.to_string()); + }) + .or_insert(OperatorData { + escrow: 0, + email: String::new(), + banned_users: HashSet::new(), + vm_nodes: HashSet::new(), + app_nodes: HashSet::from([node_pubkey.to_string()]), + }); + } + + pub fn find_app_nodes_by_filters(&self, filters: &AppNodeFilters) -> Vec { + let nodes = self.app_nodes.read().unwrap(); + nodes + .iter() + .filter(|n| { + n.avail_vcpus >= filters.vcpus + && n.avail_mem_mb >= filters.memory_mb + && n.avail_storage_mb >= filters.storage_mb + && (filters.country.is_empty() || (n.country == filters.country)) + && (filters.city.is_empty() || (n.city == filters.city)) + && (filters.region.is_empty() || (n.region == filters.region)) + && (filters.ip.is_empty() || (n.ip == filters.ip)) + }) + .cloned() + .collect() + } + + // TODO: sort by rating + pub fn get_one_app_node_by_filters(&self, filters: &AppNodeFilters) -> Option { + let nodes = self.app_nodes.read().unwrap(); + nodes + .iter() + .find(|n| { + n.avail_vcpus >= filters.vcpus + && n.avail_mem_mb >= filters.memory_mb + && n.avail_storage_mb >= filters.storage_mb + && (filters.country.is_empty() || (n.country == filters.country)) + && (filters.city.is_empty() || (n.city == filters.city)) + && (filters.region.is_empty() || (n.region == filters.region)) + && (filters.ip.is_empty() || (n.ip == filters.ip)) + && (filters.node_pubkey.is_empty() || (n.node_pubkey == filters.node_pubkey)) + }) + .cloned() + } + + pub fn find_app_contract_by_uuid(&self, uuid: &str) -> Result { + let contracts = self.app_contracts.read().unwrap(); + contracts + .iter() + .find(|c| c.uuid == uuid) + .cloned() + .ok_or(Error::AppContractNotFound(uuid.to_string())) + } + + pub fn find_app_node_by_pubkey(&self, public_key: &str) -> Option { + let nodes = self.app_nodes.read().unwrap(); + nodes.iter().cloned().find(|n| n.node_pubkey == public_key) + } + + pub fn find_app_contracts_by_admin_pubkey(&self, admin_pubkey: &str) -> Vec { + debug!("Searching contracts for admin pubkey {admin_pubkey}"); + let contracts: Vec = self + .app_contracts + .read() + .unwrap() + .iter() + .filter(|c| c.admin_pubkey == admin_pubkey) + .cloned() + .collect(); + debug!("Found {} contracts or {admin_pubkey}.", contracts.len()); + contracts + } + + pub fn list_all_app_contracts(&self) -> Vec { + let contracts = self.app_contracts.read().unwrap(); + contracts.iter().cloned().collect() + } + + pub fn find_app_contracts_by_node(&self, node_pubkey: &str) -> Vec { + let app_contracts = self.app_contracts.read().unwrap(); + app_contracts + .iter() + .filter(|c| c.node_pubkey == node_pubkey) + .cloned() + .collect() + } + + pub async fn app_contracts_cron(&self) { + let mut deleted_app_contracts: Vec<(String, String)> = Vec::new(); + log::debug!("Running app contracts cron..."); + { + let mut app_contracts = self.app_contracts.write().unwrap(); + app_contracts.retain_mut(|c| { + let node = self.find_app_node_by_pubkey(&c.node_pubkey).unwrap(); + if node.offline_minutes == 0 { + let operator_wallet = node.operator_wallet.clone(); + let minutes_to_collect = (Utc::now() - c.collected_at).num_minutes() as u64; + c.collected_at = Utc::now(); + dbg!(&minutes_to_collect); + dbg!(&c.price_per_minute()); + let mut nanolp_to_collect = + c.price_per_minute().saturating_mul(minutes_to_collect); + if nanolp_to_collect > c.locked_nano { + nanolp_to_collect = c.locked_nano; + } + dbg!(&nanolp_to_collect); + log::debug!("Removing {nanolp_to_collect} nanoLP from {}", c.uuid); + c.locked_nano -= nanolp_to_collect; + let escrow_multiplier = match self.operators.get(&operator_wallet) { + Some(op) if op.escrow > 5000 => match self.operators.get(&c.admin_pubkey) { + Some(user_is_op) if user_is_op.escrow > 5000 => 1, + _ => 5, + }, + _ => 1, + }; + self.add_nano_to_wallet( + &operator_wallet, + nanolp_to_collect * escrow_multiplier, + ); + if c.locked_nano == 0 { + deleted_app_contracts.push((c.uuid.clone(), c.node_pubkey.clone())); + } + } + c.locked_nano > 0 + }); + } + // inform daemons of the deletion of the contracts + for (uuid, node_pubkey) in deleted_app_contracts.iter() { + if let Some(app_daemon_tx) = self.app_daemon_tx.get(&node_pubkey.clone()) { + let msg = BrainMessageApp { + msg: Some(brain_message_app::Msg::DeleteAppReq(DelAppReq { + uuid: uuid.to_string(), + admin_pubkey: String::new(), + })), + }; + let app_daemon_tx = app_daemon_tx.clone(); + let _ = app_daemon_tx.send(msg).await; + } + } + } + + pub fn submit_app_node_resources(&self, node_resource: AppNodeResources) { + debug!("{:#?}", &node_resource); + let mut nodes = self.app_nodes.write().unwrap(); + for n in nodes.iter_mut() { + if n.node_pubkey == node_resource.node_pubkey { + debug!( + "Found node {}. Updating resources to {:?}", + n.node_pubkey, node_resource + ); + n.avail_vcpus = node_resource.avail_vcpus; + n.avail_mem_mb = node_resource.avail_memory_mb; + n.avail_storage_mb = node_resource.avail_storage_mb; + n.max_ports_per_app = node_resource.max_ports_per_app; + n.avail_no_of_port = node_resource.avail_no_of_port; + return; + } + } + debug!( + "VM Node {} not found when trying to update resources.", + node_resource.node_pubkey + ); + debug!("VM Node list:\n{:?}", nodes); + } + + pub async fn send_new_container_req(&self, mut req: NewAppReq, tx: OneshotSender) { + if let Err(e) = self.lock_nanotockens(&req.admin_pubkey, req.locked_nano) { + let _ = tx.send(NewAppRes { + uuid: String::new(), + error: e.to_string(), + status: "failed".to_string(), + ..Default::default() + }); + return; + } + + req.uuid = uuid::Uuid::new_v4().to_string(); + + info!("Inserting new container request in memory: {req:?}"); + self.tmp_new_container_reqs + .insert(req.uuid.clone(), (req.clone(), tx)); + + if let Some(app_daemon_tx) = self.app_daemon_tx.get(&req.node_pubkey) { + debug!( + "Found daemon TX for {}. Sending newVMReq {}", + req.node_pubkey, req.uuid + ); + let msg = BrainMessageApp { + msg: Some( + detee_shared::sgx::pb::brain::brain_message_app::Msg::NewAppReq(req.clone()), + ), + }; + if let Err(e) = app_daemon_tx.send(msg).await { + warn!( + "Failed to send new container request to {} due to error: {e:?}", + req.node_pubkey + ); + info!("Deleting daemon TX for {}", req.node_pubkey); + self.del_app_daemon_tx(&req.node_pubkey); + self.send_new_container_resp(NewAppRes { + uuid: req.uuid, + status: "failed".to_string(), + error: "Daemon is offline.".to_string(), + ..Default::default() + }) + .await; + } + } else { + self.send_new_container_resp(NewAppRes { + status: "failed".to_string(), + error: "Daemon is offline.".to_string(), + uuid: req.uuid, + ..Default::default() + }) + .await; + } + } + + pub async fn send_del_container_req(&self, req: DelAppReq) -> Result<(), Error> { + log::debug!("Starting deletion of app {}", req.uuid); + let app_contract = self.find_app_contract_by_uuid(&req.uuid)?; + + if app_contract.admin_pubkey != req.admin_pubkey { + return Err(Error::AccessDenied); + } + + info!("Found app contract {}. Deleting...", &req.uuid); + if let Some(app_daemon_tx) = self.app_daemon_tx.get(&app_contract.node_pubkey) { + debug!( + "TX for daemon {} found. Informing daemon about deletion of {}.", + app_contract.node_pubkey, &req.uuid + ); + let msg = BrainMessageApp { + msg: Some(brain_message_app::Msg::DeleteAppReq(req.clone())), + }; + + if let Err(e) = app_daemon_tx.send(msg).await { + warn!( + "Failed to send deletion request to {} due to error: {e:?}", + app_contract.node_pubkey + ); + info!("Deleting daemon TX for {}", app_contract.node_pubkey); + self.del_app_daemon_tx(&app_contract.node_pubkey); + } + } + + self.add_nano_to_wallet(&app_contract.admin_pubkey, app_contract.locked_nano); + let mut app_contracts = self.app_contracts.write().unwrap(); + app_contracts.retain(|c| c.uuid != req.uuid); + + Ok(()) + } + + pub async fn send_new_container_resp(&self, new_container_resp: NewAppRes) { + let new_container_req = match self.tmp_new_container_reqs.remove(&new_container_resp.uuid) { + Some((_, r)) => r, + None => { + log::error!( + "Received confirmation for ghost new container req {}", + new_container_resp.uuid + ); + return; + } + }; + if let Err(err) = new_container_req.1.send(new_container_resp.clone()) { + log::error!( + "CLI RX for {} dropped before receiving confirmation {:?}.\n{:?}", + &new_container_req.0.admin_pubkey, + new_container_resp, + err + ); + } + + if new_container_resp.error != "" { + if let Some(mut admin_wallet) = self.accounts.get_mut(&new_container_req.0.admin_pubkey) + { + admin_wallet.balance += new_container_req.0.locked_nano; + admin_wallet.tmp_locked -= new_container_req.0.locked_nano; + } + return; + } + + if let Some(mut admin_wallet) = self.accounts.get_mut(&new_container_req.0.admin_pubkey) { + admin_wallet.tmp_locked -= new_container_req.0.locked_nano; + } + + let requested_resource = new_container_req.0.resource.clone().unwrap_or_default(); + + let app_contracts = AppContract { + uuid: new_container_req.0.uuid, + package_url: new_container_req.0.package_url, + admin_pubkey: new_container_req.0.admin_pubkey, + node_pubkey: new_container_req.0.node_pubkey.clone(), + mapped_ports: new_container_resp + .mapped_ports + .iter() + .map(|p| (p.host_port as u16, p.app_port as u16)) + .collect::>(), + host_ipv4: new_container_resp.ip_address, + disk_size_mb: requested_resource.disk_mb, + vcpus: requested_resource.vcpu, + memory_mb: requested_resource.memory_mb, + created_at: Utc::now(), + updated_at: Utc::now(), + price_per_unit: new_container_req.0.price_per_unit, + locked_nano: new_container_req.0.locked_nano, + collected_at: Utc::now(), + hratls_pubkey: new_container_req.0.hratls_pubkey, + public_package_mr_enclave: new_container_req.0.public_package_mr_enclave, + app_name: new_container_req.0.app_name, + }; + log::info!("Created new app contract: {app_contracts:?}"); + self.app_contracts.write().unwrap().push(app_contracts); + } +} diff --git a/src/grpc.rs b/src/grpc.rs new file mode 100644 index 0000000..c5a7102 --- /dev/null +++ b/src/grpc.rs @@ -0,0 +1,857 @@ +pub mod snp_proto { + tonic::include_proto!("vm_proto"); +} + +use crate::data::BrainData; +use crate::grpc::vm_daemon_message; +use log::info; +use snp_proto::brain_cli_server::BrainCli; +use snp_proto::brain_vm_daemon_server::BrainVmDaemon; +use snp_proto::*; +use std::pin::Pin; +use std::sync::Arc; +use tokio::sync::mpsc; +use tokio_stream::{wrappers::ReceiverStream, Stream, StreamExt}; +use tonic::{Request, Response, Status, Streaming}; + +use detee_shared::sgx::pb::brain::brain_app_cli_server::BrainAppCli; +use detee_shared::sgx::pb::brain::brain_app_daemon_server::BrainAppDaemon; +use detee_shared::sgx::pb::brain::{ + AppContract, AppNodeFilters, AppNodeListResp, BrainMessageApp, DaemonMessageApp, DelAppReq, + ListAppContractsReq, NewAppReq, NewAppRes, RegisterAppNodeReq, +}; +const ADMIN_ACCOUNTS: &[&str] = &[ + "x52w7jARC5erhWWK65VZmjdGXzBK6ZDgfv1A283d8XK", + "FHuecMbeC1PfjkW2JKyoicJAuiU7khgQT16QUB3Q1XdL", + "H21Shi4iE7vgfjWEQNvzmpmBMJSaiZ17PYUcdNoAoKNc", +]; + +pub struct BrainDaemonMock { + data: Arc, +} + +impl BrainDaemonMock { + pub fn new(data: Arc) -> Self { + Self { data } + } +} + +pub struct BrainCliMock { + data: Arc, +} + +impl BrainCliMock { + pub fn new(data: Arc) -> Self { + Self { data } + } +} + +pub struct BrainAppCliMock { + data: Arc, +} + +impl BrainAppCliMock { + pub fn new(data: Arc) -> Self { + Self { data } + } +} + +pub struct BrainAppDaemonMock { + data: Arc, +} + +impl BrainAppDaemonMock { + pub fn new(data: Arc) -> Self { + Self { data } + } +} + +#[tonic::async_trait] +impl BrainVmDaemon for BrainDaemonMock { + type RegisterVmNodeStream = Pin> + Send>>; + async fn register_vm_node( + &self, + req: Request, + ) -> Result, Status> { + let req = check_sig_from_req(req)?; + info!("Starting registration process for {:?}", req); + let node = crate::data::VmNode { + public_key: req.node_pubkey.clone(), + operator_wallet: req.operator_wallet, + country: req.country, + region: req.region, + city: req.city, + ip: req.main_ip, + price: req.price, + ..Default::default() + }; + self.data.register_node(node); + + info!("Sending existing contracts to {}", req.node_pubkey); + let contracts = self.data.find_vm_contracts_by_node(&req.node_pubkey); + let (tx, rx) = mpsc::channel(6); + tokio::spawn(async move { + for contract in contracts { + let _ = tx.send(Ok(contract.into())).await; + } + }); + let output_stream = ReceiverStream::new(rx); + Ok(Response::new( + Box::pin(output_stream) as Self::RegisterVmNodeStream + )) + } + + type BrainMessagesStream = Pin> + Send>>; + async fn brain_messages( + &self, + req: Request, + ) -> Result, Status> { + let auth = req.into_inner(); + let pubkey = auth.pubkey.clone(); + check_sig_from_parts( + &pubkey, + &auth.timestamp, + &format!("{:?}", auth.contracts), + &auth.signature, + )?; + info!("Daemon {} connected to receive brain messages", pubkey); + let (tx, rx) = mpsc::channel(6); + self.data.add_daemon_tx(&pubkey, tx); + let output_stream = ReceiverStream::new(rx).map(|msg| Ok(msg)); + Ok(Response::new( + Box::pin(output_stream) as Self::BrainMessagesStream + )) + } + + async fn daemon_messages( + &self, + req: Request>, + ) -> Result, Status> { + let mut req_stream = req.into_inner(); + let pubkey: String; + if let Some(Ok(msg)) = req_stream.next().await { + log::debug!( + "demon_messages received the following auth message: {:?}", + msg.msg + ); + if let Some(vm_daemon_message::Msg::Auth(auth)) = msg.msg { + pubkey = auth.pubkey.clone(); + check_sig_from_parts( + &pubkey, + &auth.timestamp, + &format!("{:?}", auth.contracts), + &auth.signature, + )?; + } else { + return Err(Status::unauthenticated( + "Could not authenticate the daemon: could not extract auth signature", + )); + } + } else { + return Err(Status::unauthenticated("Could not authenticate the daemon")); + } + + // info!("Received a message from daemon {pubkey}: {daemon_message:?}"); + while let Some(daemon_message) = req_stream.next().await { + match daemon_message { + Ok(msg) => match msg.msg { + Some(vm_daemon_message::Msg::NewVmResp(new_vm_resp)) => { + self.data.submit_newvm_resp(new_vm_resp).await; + } + Some(vm_daemon_message::Msg::UpdateVmResp(update_vm_resp)) => { + self.data.submit_updatevm_resp(update_vm_resp).await; + } + Some(vm_daemon_message::Msg::VmNodeResources(node_resources)) => { + self.data.submit_node_resources(node_resources); + } + _ => {} + }, + Err(e) => { + log::warn!("Daemon disconnected: {e:?}"); + self.data.del_daemon_tx(&pubkey); + } + } + } + Ok(Response::new(Empty {})) + } +} + +#[tonic::async_trait] +impl BrainCli for BrainCliMock { + async fn get_balance(&self, req: Request) -> Result, Status> { + let req = check_sig_from_req(req)?; + Ok(Response::new(self.data.get_balance(&req.pubkey).into())) + } + + async fn new_vm(&self, req: Request) -> Result, Status> { + let req = check_sig_from_req(req)?; + info!("New VM requested via CLI: {req:?}"); + if self + .data + .is_user_banned_by_node(&req.admin_pubkey, &req.node_pubkey) + { + return Err(Status::permission_denied( + "This operator banned you. What did you do?", + )); + } + let admin_pubkey = req.admin_pubkey.clone(); + let (oneshot_tx, oneshot_rx) = tokio::sync::oneshot::channel(); + self.data.submit_newvm_req(req, oneshot_tx).await; + match oneshot_rx.await { + Ok(response) => { + info!("Sending VM confirmation to {admin_pubkey}: {response:?}"); + Ok(Response::new(response)) + } + Err(e) => { + log::error!("Something weird happened. Reached error {e:?}"); + Err(Status::unknown( + "Request failed due to unknown error. Please try again or contact the DeTEE devs team.", + )) + } + } + } + + async fn update_vm(&self, req: Request) -> Result, Status> { + let req = check_sig_from_req(req)?; + info!("Update VM requested via CLI: {req:?}"); + let (oneshot_tx, oneshot_rx) = tokio::sync::oneshot::channel(); + self.data.submit_updatevm_req(req, oneshot_tx).await; + match oneshot_rx.await { + Ok(response) => { + info!("Sending UpdateVMResp: {response:?}"); + Ok(Response::new(response)) + } + Err(e) => Err(Status::unknown(format!( + "Update VM request failed due to error: {e}" + ))), + } + } + + async fn extend_vm(&self, req: Request) -> Result, Status> { + let req = check_sig_from_req(req)?; + match self + .data + .extend_vm_contract_time(&req.uuid, &req.admin_pubkey, req.locked_nano) + { + Ok(()) => Ok(Response::new(Empty {})), + Err(e) => Err(Status::unknown(format!("Could not extend contract: {e}"))), + } + } + + async fn delete_vm(&self, req: Request) -> Result, Status> { + let req = check_sig_from_req(req)?; + match self.data.delete_vm(req).await { + Ok(()) => Ok(Response::new(Empty {})), + Err(e) => Err(Status::not_found(e.to_string())), + } + } + + async fn report_node(&self, req: Request) -> Result, Status> { + let req = check_sig_from_req(req)?; + match self.data.find_contract_by_uuid(&req.contract) { + Ok(contract) + if contract.admin_pubkey == req.admin_pubkey + && contract.node_pubkey == req.node_pubkey => + { + () + } + _ => return Err(Status::unauthenticated("No contract found by this ID.")), + }; + self.data + .report_node(req.admin_pubkey, &req.node_pubkey, req.reason); + Ok(Response::new(Empty {})) + } + + type ListVmContractsStream = Pin> + Send>>; + async fn list_vm_contracts( + &self, + req: Request, + ) -> Result, Status> { + let req = check_sig_from_req(req)?; + info!( + "CLI {} requested ListVMVmContractsStream. As operator: {}", + req.wallet, req.as_operator + ); + let mut contracts = Vec::new(); + if !req.uuid.is_empty() { + if let Ok(specific_contract) = self.data.find_contract_by_uuid(&req.uuid) { + if specific_contract.admin_pubkey == req.wallet { + contracts.push(specific_contract); + } + // TODO: allow operator to inspect contracts + } + } else { + if req.as_operator { + contracts.append(&mut self.data.find_vm_contracts_by_operator(&req.wallet)); + } else { + contracts.append(&mut self.data.find_vm_contracts_by_admin(&req.wallet)); + } + } + let (tx, rx) = mpsc::channel(6); + tokio::spawn(async move { + for contract in contracts { + let _ = tx.send(Ok(contract.into())).await; + } + }); + let output_stream = ReceiverStream::new(rx); + Ok(Response::new( + Box::pin(output_stream) as Self::ListVmContractsStream + )) + } + + type ListVmNodesStream = Pin> + Send>>; + async fn list_vm_nodes( + &self, + req: Request, + ) -> Result, tonic::Status> { + let req = check_sig_from_req(req)?; + info!("CLI requested ListVmNodesStream: {req:?}"); + let nodes = self.data.find_vm_nodes_by_filters(&req); + let (tx, rx) = mpsc::channel(6); + tokio::spawn(async move { + for node in nodes { + let _ = tx.send(Ok(node.into())).await; + } + }); + let output_stream = ReceiverStream::new(rx); + Ok(Response::new( + Box::pin(output_stream) as Self::ListVmNodesStream + )) + } + + async fn get_one_vm_node( + &self, + req: Request, + ) -> Result, Status> { + let req = check_sig_from_req(req)?; + info!("Unknown CLI requested ListVmNodesStream: {req:?}"); + match self.data.get_one_node_by_filters(&req) { + Some(node) => Ok(Response::new(node.into())), + None => Err(Status::not_found( + "Could not find any node based on your search criteria", + )), + } + } + + async fn register_operator( + &self, + req: Request, + ) -> Result, Status> { + let req = check_sig_from_req(req)?; + info!("Regitering new operator: {req:?}"); + match self.data.register_operator(req) { + Ok(()) => Ok(Response::new(Empty {})), + Err(e) => Err(Status::failed_precondition(e.to_string())), + } + } + + async fn kick_contract(&self, req: Request) -> Result, Status> { + let req = check_sig_from_req(req)?; + match self + .data + .kick_contract(&req.operator_wallet, &req.contract_uuid, &req.reason) + .await + { + Ok(nano_lp) => Ok(Response::new(KickResp { nano_lp })), + Err(e) => Err(Status::permission_denied(e.to_string())), + } + } + + async fn ban_user(&self, req: Request) -> Result, Status> { + let req = check_sig_from_req(req)?; + self.data.ban_user(&req.operator_wallet, &req.user_wallet); + Ok(Response::new(Empty {})) + } + + type ListOperatorsStream = + Pin> + Send>>; + async fn list_operators( + &self, + req: Request, + ) -> Result, Status> { + let _ = check_sig_from_req(req)?; + let operators = self.data.list_operators(); + let (tx, rx) = mpsc::channel(6); + tokio::spawn(async move { + for op in operators { + let _ = tx.send(Ok(op.into())).await; + } + }); + let output_stream = ReceiverStream::new(rx); + Ok(Response::new( + Box::pin(output_stream) as Self::ListOperatorsStream + )) + } + + async fn inspect_operator( + &self, + req: Request, + ) -> Result, Status> { + match self.data.inspect_operator(&req.into_inner().pubkey) { + Some(op) => Ok(Response::new(op.into())), + None => Err(Status::not_found( + "The wallet you specified is not an operator", + )), + } + } + + async fn airdrop(&self, req: Request) -> Result, Status> { + check_admin_key(&req)?; + let req = check_sig_from_req(req)?; + self.data.give_airdrop(&req.pubkey, req.tokens); + Ok(Response::new(Empty {})) + } + + async fn slash(&self, req: Request) -> Result, Status> { + check_admin_key(&req)?; + let req = check_sig_from_req(req)?; + self.data.slash_account(&req.pubkey, req.tokens); + Ok(Response::new(Empty {})) + } + + type ListAllVmContractsStream = Pin> + Send>>; + async fn list_all_vm_contracts( + &self, + req: Request, + ) -> Result, Status> { + check_admin_key(&req)?; + let _ = check_sig_from_req(req)?; + let contracts = self.data.list_all_contracts(); + let (tx, rx) = mpsc::channel(6); + tokio::spawn(async move { + for contract in contracts { + let _ = tx.send(Ok(contract.into())).await; + } + }); + let output_stream = ReceiverStream::new(rx); + Ok(Response::new( + Box::pin(output_stream) as Self::ListVmContractsStream + )) + } + + type ListAccountsStream = Pin> + Send>>; + async fn list_accounts( + &self, + req: Request, + ) -> Result, Status> { + check_admin_key(&req)?; + let _ = check_sig_from_req(req)?; + let accounts = self.data.list_accounts(); + let (tx, rx) = mpsc::channel(6); + tokio::spawn(async move { + for account in accounts { + let _ = tx.send(Ok(account.into())).await; + } + }); + let output_stream = ReceiverStream::new(rx); + Ok(Response::new( + Box::pin(output_stream) as Self::ListAccountsStream + )) + } +} + +trait PubkeyGetter { + fn get_pubkey(&self) -> Option; +} + +#[tonic::async_trait] +impl BrainAppCli for BrainAppCliMock { + type ListAppContractsStream = Pin> + Send>>; + type ListAppNodesStream = Pin> + Send>>; + type ListAllAppContractsStream = + Pin> + Send>>; + + async fn deploy_app( + &self, + req: tonic::Request, + ) -> Result, Status> { + let req_data = check_sig_from_req(req)?; + log::info!("Creating new container: {req_data:?}"); + let admin_pubkey = req_data.admin_pubkey.clone(); + let (oneshot_tx, oneshot_rx) = tokio::sync::oneshot::channel(); + self.data.send_new_container_req(req_data, oneshot_tx).await; + + match oneshot_rx.await { + Ok(response) => { + info!("responding container confirmation to {admin_pubkey}: {response:?}"); + Ok(Response::new(response)) + } + Err(e) => { + log::error!("Something went wrong. Reached error {e:?}"); + Err(Status::unknown( + "Request failed due to unknown error. Please try again or contact the DeTEE devs team.", + )) + } + } + } + + async fn delete_app( + &self, + req: tonic::Request, + ) -> Result, Status> { + let req_data = check_sig_from_req(req)?; + log::info!("deleting container: {}", req_data.uuid.clone()); + if let Err(er) = self.data.send_del_container_req(req_data).await { + info!("Could not delete container: {er}"); + return Err(Status::not_found("Could not find container")); + }; + + Ok(Response::new(detee_shared::sgx::pb::brain::Empty {})) + } + + async fn list_app_contracts( + &self, + req: tonic::Request, + ) -> Result, Status> { + let req_data = check_sig_from_req(req)?; + let app_contracts = self + .data + .find_app_contracts_by_admin_pubkey(&req_data.admin_pubkey); + + let (tx, rx) = mpsc::channel(6); + tokio::spawn(async move { + for contract in app_contracts { + let _ = tx.send(contract.into()).await; + } + }); + let output_stream = ReceiverStream::new(rx).map(Ok); + Ok(Response::new( + Box::pin(output_stream) as Self::ListAppContractsStream + )) + } + + async fn list_app_nodes( + &self, + req: tonic::Request, + ) -> Result, Status> { + let req = check_sig_from_req(req)?; + info!("CLI requested ListAppNodes: {req:?}"); + let nodes = self.data.find_app_nodes_by_filters(&req); + let (tx, rx) = mpsc::channel(6); + tokio::spawn(async move { + for node in nodes { + let _ = tx.send(Ok(node.into())).await; + } + }); + let output_stream = ReceiverStream::new(rx); + Ok(Response::new(Box::pin(output_stream))) + } + + async fn get_one_app_node( + &self, + req: tonic::Request, + ) -> Result, Status> { + let req = check_sig_from_req(req)?; + info!("CLI requested GetOneAppNode: {req:?}"); + match self.data.get_one_app_node_by_filters(&req) { + Some(node) => Ok(Response::new(node.into())), + None => Err(Status::not_found( + "Could not find any node based on your search criteria", + )), + } + } + + async fn list_all_app_contracts( + &self, + req: tonic::Request, + ) -> Result, Status> { + check_admin_key(&req)?; + let _ = check_sig_from_req(req)?; + let contracts = self.data.list_all_app_contracts(); + let (tx, rx) = mpsc::channel(6); + tokio::spawn(async move { + for contract in contracts { + let _ = tx.send(Ok(contract.into())).await; + } + }); + let output_stream = ReceiverStream::new(rx); + Ok(Response::new(Box::pin(output_stream))) + } +} + +#[tonic::async_trait] +impl BrainAppDaemon for BrainAppDaemonMock { + type RegisterAppNodeStream = Pin> + Send>>; + type BrainMessagesStream = Pin> + Send>>; + + async fn register_app_node( + &self, + req: tonic::Request, + ) -> Result, Status> { + let req_data = check_sig_from_req(req)?; + log::info!( + "registering app node_key : {}, operator_key: {}", + &req_data.node_pubkey, + &req_data.operator_wallet + ); + + let app_node = crate::data::AppNode { + node_pubkey: req_data.node_pubkey.clone(), + operator_wallet: req_data.operator_wallet, + ip: req_data.main_ip, + city: req_data.city, + region: req_data.region, + country: req_data.country, + ..Default::default() + }; + self.data.register_app_node(app_node); + + log::info!("Sending existing contracts to {}", &req_data.node_pubkey); + let app_contracts = self.data.find_app_contracts_by_node(&req_data.node_pubkey); + let (tx, rx) = mpsc::channel(6); + tokio::spawn(async move { + for contract in app_contracts { + let _ = tx.send(contract.into()).await; + } + }); + let output_stream = ReceiverStream::new(rx).map(Ok); + Ok(Response::new(Box::pin(output_stream))) + } + + async fn brain_messages( + &self, + req: tonic::Request, + ) -> Result, Status> { + let req_data = req.into_inner(); + let pubkey = req_data.pubkey.clone(); + check_sig_from_parts( + &pubkey, + &req_data.timestamp, + &format!("{:?}", req_data.contracts), + &req_data.signature, + )?; + + info!( + "Daemon {} connected to receive brain messages", + req_data.pubkey + ); + let (tx, rx) = mpsc::channel(6); + self.data.add_app_daemon_tx(&req_data.pubkey, tx); + let output_stream = ReceiverStream::new(rx).map(Ok); + Ok(Response::new( + Box::pin(output_stream) as Self::BrainMessagesStream + )) + } + + async fn daemon_messages( + &self, + req: tonic::Request>, + ) -> Result, Status> { + let mut req_stream = req.into_inner(); + let mut pubkey; + + if let Some(Ok(msg)) = req_stream.next().await { + log::debug!( + "demon_messages received the following auth message: {:?}", + msg.msg + ); + if let Some(detee_shared::sgx::pb::brain::daemon_message_app::Msg::Auth(auth)) = msg.msg + { + pubkey = auth.pubkey.clone(); + check_sig_from_parts( + &pubkey, + &auth.timestamp, + &format!("{:?}", auth.contracts), + &auth.signature, + )?; + } else { + return Err(Status::unauthenticated( + "Could not authenticate the daemon: could not extract auth signature", + )); + } + } else { + return Err(Status::unauthenticated("Could not authenticate the daemon")); + } + + while let Some(daemon_message) = req_stream.next().await { + match daemon_message { + Ok(msg) => match msg.msg { + Some(detee_shared::sgx::pb::brain::daemon_message_app::Msg::Auth( + daemon_auth, + )) => pubkey = daemon_auth.pubkey, + Some(detee_shared::sgx::pb::brain::daemon_message_app::Msg::NewAppRes( + new_app_res, + )) => self.data.send_new_container_resp(new_app_res).await, + Some( + detee_shared::sgx::pb::brain::daemon_message_app::Msg::AppNodeResources( + node_resource, + ), + ) => self.data.submit_app_node_resources(node_resource), + _ => { + dbg!("None"); + } + }, + Err(e) => { + log::warn!("Daemon disconnected: {e:?}"); + self.data.del_app_daemon_tx(&pubkey); + } + } + // + } + + Ok(Response::new(detee_shared::sgx::pb::brain::Empty {})) + } +} + +macro_rules! impl_pubkey_getter { + ($t:ty, $field:ident) => { + impl PubkeyGetter for $t { + fn get_pubkey(&self) -> Option { + Some(self.$field.clone()) + } + } + }; + ($t:ty) => { + impl PubkeyGetter for $t { + fn get_pubkey(&self) -> Option { + None + } + } + }; +} + +impl_pubkey_getter!(Pubkey, pubkey); +impl_pubkey_getter!(NewVmReq, admin_pubkey); +impl_pubkey_getter!(DeleteVmReq, admin_pubkey); +impl_pubkey_getter!(UpdateVmReq, admin_pubkey); +impl_pubkey_getter!(ExtendVmReq, admin_pubkey); +impl_pubkey_getter!(ReportNodeReq, admin_pubkey); +impl_pubkey_getter!(ListVmContractsReq, wallet); +impl_pubkey_getter!(RegisterVmNodeReq, node_pubkey); +impl_pubkey_getter!(RegOperatorReq, pubkey); +impl_pubkey_getter!(KickReq, operator_wallet); +impl_pubkey_getter!(BanUserReq, operator_wallet); + +impl_pubkey_getter!(VmNodeFilters); +impl_pubkey_getter!(Empty); +impl_pubkey_getter!(AirdropReq); +impl_pubkey_getter!(SlashReq); + +impl_pubkey_getter!(NewAppReq, admin_pubkey); +impl_pubkey_getter!(DelAppReq, admin_pubkey); +impl_pubkey_getter!(ListAppContractsReq, admin_pubkey); + +impl_pubkey_getter!(RegisterAppNodeReq); +impl_pubkey_getter!(detee_shared::sgx::pb::brain::Empty); +impl_pubkey_getter!(AppNodeFilters); + +fn check_sig_from_req(req: Request) -> Result { + let time = match req.metadata().get("timestamp") { + Some(t) => t.clone(), + None => return Err(Status::unauthenticated("Timestamp not found in metadata.")), + }; + let time = time + .to_str() + .map_err(|_| Status::unauthenticated("Timestamp in metadata is not a string"))?; + + let now = chrono::Utc::now(); + let parsed_time = chrono::DateTime::parse_from_rfc3339(time) + .map_err(|_| Status::unauthenticated("Coult not parse timestamp"))?; + let seconds_elapsed = now.signed_duration_since(parsed_time).num_seconds(); + if seconds_elapsed > 4 || seconds_elapsed < -4 { + return Err(Status::unauthenticated(format!( + "Date is not within 4 sec of the time of the server: CLI {} vs Server {}", + parsed_time, now + ))); + } + + let signature = match req.metadata().get("request-signature") { + Some(t) => t, + None => return Err(Status::unauthenticated("signature not found in metadata.")), + }; + let signature = bs58::decode(signature) + .into_vec() + .map_err(|_| Status::unauthenticated("signature is not a bs58 string"))?; + let signature = ed25519_dalek::Signature::from_bytes( + signature + .as_slice() + .try_into() + .map_err(|_| Status::unauthenticated("could not parse ed25519 signature"))?, + ); + + let pubkey_value = match req.metadata().get("pubkey") { + Some(p) => p.clone(), + None => return Err(Status::unauthenticated("pubkey not found in metadata.")), + }; + let pubkey = ed25519_dalek::VerifyingKey::from_bytes( + &bs58::decode(&pubkey_value) + .into_vec() + .map_err(|_| Status::unauthenticated("pubkey is not a bs58 string"))? + .try_into() + .map_err(|_| Status::unauthenticated("pubkey does not have the correct size."))?, + ) + .map_err(|_| Status::unauthenticated("could not parse ed25519 pubkey"))?; + + let req = req.into_inner(); + let message = format!("{time}{req:?}"); + use ed25519_dalek::Verifier; + pubkey + .verify(message.as_bytes(), &signature) + .map_err(|_| Status::unauthenticated("the signature is not valid"))?; + if let Some(req_pubkey) = req.get_pubkey() { + if pubkey_value.to_str().unwrap().to_string() != req_pubkey { + return Err(Status::unauthenticated( + "pubkey of signature does not match pubkey of request", + )); + } + } + Ok(req) +} + +fn check_sig_from_parts(pubkey: &str, time: &str, msg: &str, sig: &str) -> Result<(), Status> { + let now = chrono::Utc::now(); + let parsed_time = chrono::DateTime::parse_from_rfc3339(time) + .map_err(|_| Status::unauthenticated("Coult not parse timestamp"))?; + let seconds_elapsed = now.signed_duration_since(parsed_time).num_seconds(); + if seconds_elapsed > 4 || seconds_elapsed < -4 { + return Err(Status::unauthenticated(format!( + "Date is not within 4 sec of the time of the server: CLI {} vs Server {}", + parsed_time, now + ))); + } + + let signature = bs58::decode(sig) + .into_vec() + .map_err(|_| Status::unauthenticated("signature is not a bs58 string"))?; + let signature = ed25519_dalek::Signature::from_bytes( + signature + .as_slice() + .try_into() + .map_err(|_| Status::unauthenticated("could not parse ed25519 signature"))?, + ); + + let pubkey = ed25519_dalek::VerifyingKey::from_bytes( + &bs58::decode(&pubkey) + .into_vec() + .map_err(|_| Status::unauthenticated("pubkey is not a bs58 string"))? + .try_into() + .map_err(|_| Status::unauthenticated("pubkey does not have the correct size."))?, + ) + .map_err(|_| Status::unauthenticated("could not parse ed25519 pubkey"))?; + + let msg = time.to_string() + msg; + use ed25519_dalek::Verifier; + pubkey + .verify(msg.as_bytes(), &signature) + .map_err(|_| Status::unauthenticated("the signature is not valid"))?; + + Ok(()) +} + +fn check_admin_key(req: &Request) -> Result<(), Status> { + let pubkey = match req.metadata().get("pubkey") { + Some(p) => p.clone(), + None => return Err(Status::unauthenticated("pubkey not found in metadata.")), + }; + let pubkey = pubkey + .to_str() + .map_err(|_| Status::unauthenticated("could not parse pubkey metadata to str"))?; + + if !ADMIN_ACCOUNTS.contains(&pubkey) { + return Err(Status::unauthenticated( + "This operation is reserved to admin accounts", + )); + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f9b1041 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,50 @@ +mod data; +mod grpc; + +use data::BrainData; +use detee_shared::sgx::pb::brain::brain_app_cli_server::BrainAppCliServer; +use detee_shared::sgx::pb::brain::brain_app_daemon_server::BrainAppDaemonServer; +use grpc::snp_proto::brain_cli_server::BrainCliServer; +use grpc::snp_proto::brain_vm_daemon_server::BrainVmDaemonServer; +use grpc::BrainAppCliMock; +use grpc::BrainAppDaemonMock; +use grpc::BrainCliMock; +use grpc::BrainDaemonMock; +use std::sync::Arc; +use tonic::transport::Server; + +#[tokio::main] +async fn main() { + env_logger::builder() + .filter_level(log::LevelFilter::Debug) + .init(); + let data = Arc::new(BrainData::new()); + let data_clone = data.clone(); + tokio::spawn(async move { + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(60)).await; + data_clone.vm_nodes_cron().await; + data_clone.vm_contracts_cron().await; + data_clone.app_contracts_cron().await; + if let Err(e) = data_clone.save_to_disk() { + log::error!("Could not save data to disk due to error: {e}") + } + } + }); + let addr = "0.0.0.0:31337".parse().unwrap(); + + let daemon_server = BrainVmDaemonServer::new(BrainDaemonMock::new(data.clone())); + let cli_server = BrainCliServer::new(BrainCliMock::new(data.clone())); + + let sgx_cli_server = BrainAppCliServer::new(BrainAppCliMock::new(data.clone())); + let sgx_daemon_server = BrainAppDaemonServer::new(BrainAppDaemonMock::new(data.clone())); + + Server::builder() + .add_service(daemon_server) + .add_service(cli_server) + .add_service(sgx_cli_server) + .add_service(sgx_daemon_server) + .serve(addr) + .await + .unwrap(); +} diff --git a/vm.proto b/vm.proto new file mode 100644 index 0000000..f0f4d2e --- /dev/null +++ b/vm.proto @@ -0,0 +1,271 @@ +syntax = "proto3"; +package vm_proto; + +message Empty { +} + +message Pubkey { + string pubkey = 1; +} + +message AccountBalance { + uint64 balance = 1; + uint64 tmp_locked = 2; +} + +message VmContract { + string uuid = 1; + string hostname = 2; + string admin_pubkey = 3; + string node_pubkey = 4; + repeated uint32 exposed_ports = 5; + string public_ipv4 = 6; + string public_ipv6 = 7; + uint32 disk_size_gb = 8; + uint32 vcpus = 9; + uint32 memory_mb = 10; + string kernel_sha = 11; + string dtrfs_sha = 12; + string created_at = 13; + string updated_at = 14; + // total nanoLP cost per minute (for all units) + uint64 nano_per_minute = 15; + uint64 locked_nano = 16; + string collected_at = 17; +} + +message MeasurementArgs { + // this will be IP:Port of the dtrfs API + // actually not a measurement arg, but needed for the injector + string dtrfs_api_endpoint = 1; + repeated uint32 exposed_ports = 2; + string ovmf_hash = 5; + // This is needed to allow the CLI to build the kernel params from known data. + // The CLI will use the kernel params to get the measurement. + repeated MeasurementIP ips = 6; +} + +message MeasurementIP { + uint32 nic_index = 1; + string address = 2; + string mask = 3; + string gateway = 4; +} + +// This should also include a block hash or similar, for auth +message RegisterVmNodeReq { + string node_pubkey = 1; + string operator_wallet = 2; + string main_ip = 3; + string country = 4; + string region = 5; + string city = 6; + // nanoLP per unit per minute + uint64 price = 7; +} + +message VmNodeResources { + string node_pubkey = 1; + uint32 avail_ports = 2; + uint32 avail_ipv4 = 3; + uint32 avail_ipv6 = 4; + uint32 avail_vcpus = 5; + uint32 avail_memory_mb = 6; + uint32 avail_storage_gb = 7; + uint32 max_ports_per_vm = 8; +} + +message NewVmReq { + string uuid = 1; + string hostname = 2; + string admin_pubkey = 3; + string node_pubkey = 4; + repeated uint32 extra_ports = 5; + bool public_ipv4 = 6; + bool public_ipv6 = 7; + uint32 disk_size_gb = 8; + uint32 vcpus = 9; + uint32 memory_mb = 10; + string kernel_url = 11; + string kernel_sha = 12; + string dtrfs_url = 13; + string dtrfs_sha = 14; + uint64 price_per_unit = 15; + uint64 locked_nano = 16; +} + +message NewVmResp { + string uuid = 1; + string error = 2; + MeasurementArgs args = 3; +} + +message UpdateVmReq { + string uuid = 1; + string admin_pubkey = 2; + uint32 disk_size_gb = 3; + uint32 vcpus = 4; + uint32 memory_mb = 5; + string kernel_url = 6; + string kernel_sha = 7; + string dtrfs_url = 8; + string dtrfs_sha = 9; +} + +message UpdateVmResp { + string uuid = 1; + string error = 2; + MeasurementArgs args = 3; +} + +message DeleteVmReq { + string uuid = 1; + string admin_pubkey = 2; +} + +message BrainVmMessage { + oneof Msg { + NewVmReq new_vm_req = 1; + UpdateVmReq update_vm_req = 2; + DeleteVmReq delete_vm = 3; + } +} + +message DaemonStreamAuth { + string timestamp = 1; + string pubkey = 2; + repeated string contracts = 3; + string signature = 4; +} + +message VmDaemonMessage { + oneof Msg { + DaemonStreamAuth auth = 1; + NewVmResp new_vm_resp = 2; + UpdateVmResp update_vm_resp = 3; + VmNodeResources vm_node_resources = 4; + } +} + +service BrainVmDaemon { + rpc RegisterVmNode (RegisterVmNodeReq) returns (stream VmContract); + rpc BrainMessages (DaemonStreamAuth) returns (stream BrainVmMessage); + rpc DaemonMessages (stream VmDaemonMessage) returns (Empty); +} + +message ListVmContractsReq { + string wallet = 1; + bool as_operator = 2; + string uuid = 3; +} + +message VmNodeFilters { + uint32 free_ports = 1; + bool offers_ipv4 = 2; + bool offers_ipv6 = 3; + uint32 vcpus = 4; + uint32 memory_mb = 5; + uint32 storage_gb = 6; + string country = 7; + string region = 8; + string city = 9; + string ip = 10; + string node_pubkey = 11; +} + +message VmNodeListResp { + string operator = 1; + string node_pubkey = 2; + string country = 3; + string region = 4; + string city = 5; + string ip = 6; // required for latency test + repeated string reports = 7; // TODO: this will become an enum + uint64 price = 8; // nanoLP per unit per minute +} + +message ExtendVmReq { + string uuid = 1; + string admin_pubkey = 2; + uint64 locked_nano = 3; +} + +message AirdropReq { + string pubkey = 1; + uint64 tokens = 2; +} + +message SlashReq { + string pubkey = 1; + uint64 tokens = 2; +} + +message Account { + string pubkey = 1; + uint64 balance = 2; + uint64 tmp_locked = 3; +} + +message RegOperatorReq { + string pubkey = 1; + uint64 escrow = 2; + string email = 3; +} + +message ListOperatorsResp { + string pubkey = 1; + uint64 escrow = 2; + string email = 3; + uint64 app_nodes = 4; + uint64 vm_nodes = 5; + uint64 reports = 6; +} + +message InspectOperatorResp { + ListOperatorsResp operator = 1; + repeated VmNodeListResp nodes = 2; +} + +message ReportNodeReq { + string admin_pubkey = 1; + string node_pubkey = 2; + string contract = 3; + string reason = 4; +} + +message KickReq { + string operator_wallet = 1; + string contract_uuid = 2; + string reason = 3; +} + +message BanUserReq { + string operator_wallet = 1; + string user_wallet = 2; +} + +message KickResp { + uint64 nano_lp = 1; +} + +service BrainCli { + rpc GetBalance (Pubkey) returns (AccountBalance); + rpc NewVm (NewVmReq) returns (NewVmResp); + rpc ListVmContracts (ListVmContractsReq) returns (stream VmContract); + rpc ListVmNodes (VmNodeFilters) returns (stream VmNodeListResp); + rpc GetOneVmNode (VmNodeFilters) returns (VmNodeListResp); + rpc DeleteVm (DeleteVmReq) returns (Empty); + rpc UpdateVm (UpdateVmReq) returns (UpdateVmResp); + rpc ExtendVm (ExtendVmReq) returns (Empty); + rpc ReportNode (ReportNodeReq) returns (Empty); + rpc ListOperators (Empty) returns (stream ListOperatorsResp); + rpc InspectOperator (Pubkey) returns (InspectOperatorResp); + rpc RegisterOperator (RegOperatorReq) returns (Empty); + rpc KickContract (KickReq) returns (KickResp); + rpc BanUser (BanUserReq) returns (Empty); + // admin commands + rpc Airdrop (AirdropReq) returns (Empty); + rpc Slash (SlashReq) returns (Empty); + rpc ListAllVmContracts (Empty) returns (stream VmContract); + rpc ListAccounts (Empty) returns (stream Account); +}