diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 00000000..9f3ad1f5 --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1,2 @@ +target +.adminapirc diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 00000000..391c9a13 --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,2320 @@ +# 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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "adminapi" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "env_logger", + "futures", + "hmac", + "log", + "reqwest", + "serde", + "serde_json", + "sha1", + "signature", + "ssh-agent-client-rs", + "ssh-encoding", + "ssh-key", + "tokio", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[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 = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[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", +] + +[[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 = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dsa" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689" +dependencies = [ + "digest", + "num-bigint-dig", + "num-traits", + "pkcs8", + "rfc6979", + "sha2", + "signature", + "zeroize", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "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", + "sha2", + "subtle", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[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" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[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", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[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.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core 0.6.4", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "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 = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.1", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[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 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "redox_syscall" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +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.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +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 = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +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 = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +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 = "ssh-agent-client-rs" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc9798a2e1390157853f81735b9815be2a45dd2200c36646aad942de9074d78" +dependencies = [ + "bytes", + "signature", + "ssh-encoding", + "ssh-key", + "thiserror 1.0.69", +] + +[[package]] +name = "ssh-cipher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +dependencies = [ + "cipher", + "ssh-encoding", +] + +[[package]] +name = "ssh-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +dependencies = [ + "base64ct", + "pem-rfc7468", + "sha2", +] + +[[package]] +name = "ssh-key" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3" +dependencies = [ + "dsa", + "ed25519-dalek", + "num-bigint-dig", + "p256", + "p384", + "p521", + "rand_core 0.6.4", + "rsa", + "sec1", + "serde", + "sha1", + "sha2", + "signature", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +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.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "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-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[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-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "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-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +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.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[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 = "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 = "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.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +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 = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" +dependencies = [ + "rustls-pki-types", +] + +[[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.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +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 = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 00000000..fbf8adc5 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "adminapi" +version = "0.1.0" +edition = "2021" +description = "The ServerAdmin API available in Rust" +license = "MIT" +readme = "README.md" +authors = ["InnoGames System Administration "] +repository = "https://github.com/innogames/serveradmin/" + +[dependencies] +anyhow = "1.0" +hmac = "0.12" +log = "0.4" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sha1 = "0.10" +signature = "2.2" +ssh-agent-client-rs = "0.9" +ssh-encoding = { version = "0.2", features = ["base64"] } +ssh-key = { version = "0.6", features = ["serde", "ed25519", "dsa", "crypto", "rsa"] } + +[dev-dependencies] +env_logger = "0.11" +tokio = { version = "1.38", features = ["full"] } +clap = "4.5" +futures = "0.3" + +[profile.release] +lto = true +opt-level = "z" +strip = true diff --git a/rust/README.md b/rust/README.md new file mode 100644 index 00000000..ca76e261 --- /dev/null +++ b/rust/README.md @@ -0,0 +1,21 @@ +# Adminapi in Rust! + +This is the client library for ServerAdmin in Rust! + +## ServerAdmin + +Serveradmin is the central server database management system of InnoGames. +It has an HTTP web interface and an HTTP JSON API. Check out `the documentation +`_ or watch `this FOSDEM 19 +talk `_ for a deepdive how +InnoGames works with serveradmin. + +## Features of this library + +| Feature | Status | Note | +|--------------|-----------------|----------------------------------------------------------------------------------------------------------------------------------------| +| Querying | Implemented | Querying works by either a generic `serde_json::Value`, or with a `serde` supported type. | +| Creating | Implemented | Creating/Changing/Deleting is implemented via `adminapi::commit::Commit` object. (may change later) | +| *other* APIs | Not implemented | The adminapi supports other APIs such as the "firewall" api. The support for those is currently not implemented | +| Token Auth | Implemented | | +| SSH Auth | Implemented | The implementation either uses the SSH agent using the `SSH_AUTH_SOCK` env variable or reads a private key from `SERVERADMIN_KEY_PATH` | diff --git a/rust/examples/adminapi.rs b/rust/examples/adminapi.rs new file mode 100644 index 00000000..26e80aec --- /dev/null +++ b/rust/examples/adminapi.rs @@ -0,0 +1,24 @@ +use adminapi::cli::parse_filter_args; +use adminapi::query::Query; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + + let mut args = std::env::args(); + args.next(); + + let filters = parse_filter_args(args)?; + + let query = Query::builder() + .filters(filters) + .restrict(["hostname", "responsible_admin", "os"]) + .build(); + + let servers = query.request().await?; + for server in servers.into_iter() { + println!("{server:#?}"); + } + + Ok(()) +} diff --git a/rust/examples/adminclone.rs b/rust/examples/adminclone.rs new file mode 100644 index 00000000..90566f5f --- /dev/null +++ b/rust/examples/adminclone.rs @@ -0,0 +1,79 @@ +use std::collections::HashSet; + +use adminapi::{new_object::NewObject, query::Query}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + let args = clap::Command::new("adminclone") + .arg(clap::arg!( "The origin object identified by it's hostname")) + .arg( + clap::arg!(-a "Adds a value to a multi-attribute field") + .action(clap::ArgAction::Append), + ) + .arg( + clap::arg!(-d "Deletes a value from a multi-attribute field") + .action(clap::ArgAction::Append), + ) + .arg( + clap::arg!(-s "Sets the value for an attribute") + .action(clap::ArgAction::Append), + ) + .arg(clap::arg!(-c "Clears an attribute").action(clap::ArgAction::Append)) + .get_matches(); + + let hostname = args + .get_one::("from") + .ok_or(anyhow::anyhow!("Missing from argument")) + .cloned()?; + + let mut query = Query::builder().filter("hostname", hostname).build(); + query.restrict = HashSet::new(); // We want to get all attributes here + let mut server = query.request().await?.one()?; + server.attributes.clear("hostname"); + let servertype = server.get("servertype"); + let mut server = NewObject::from_dataset(server.attributes); + server.set("servertype", servertype)?; + + for pair in args.get_many::("set_attribute").unwrap_or_default() { + let Some((name, value)) = pair.split_once("=") else { + return Err(anyhow::anyhow!("Got attribute set without '=': {pair}")); + }; + + server.set(name.to_string(), value.to_string())?; + } + + for pair in args.get_many::("add_attribute").unwrap_or_default() { + let Some((name, value)) = pair.split_once("=") else { + return Err(anyhow::anyhow!("Got attribute set without '=': {pair}")); + }; + + server.attributes.add(name.to_string(), value.to_string()); + } + + for pair in args + .get_many::("delete_attribute") + .unwrap_or_default() + { + let Some((name, value)) = pair.split_once("=") else { + return Err(anyhow::anyhow!("Got attribute set without '=': {pair}")); + }; + + server + .attributes + .remove(name.to_string(), value.to_string()); + } + + for name in args + .get_many::("clear_attribute") + .unwrap_or_default() + { + server.attributes.clear(name.to_string()); + } + + server.commit().await?; + + log::info!("Server cloned"); + + Ok(()) +} diff --git a/rust/examples/commit.rs b/rust/examples/commit.rs new file mode 100644 index 00000000..12019557 --- /dev/null +++ b/rust/examples/commit.rs @@ -0,0 +1,41 @@ +use adminapi::filter::{empty, not, regexp}; +use adminapi::new_object::NewObject; +use adminapi::query::Query; + +#[tokio::main] +pub async fn main() -> anyhow::Result<()> { + env_logger::init(); + let mut server = Query::builder() + .filter("hostname", regexp(".*payment-staging.*")) + .filter("os", not(empty())) + .restrict(["hostname", "responsible_admin", "os"]) + .build() + .request() + .await? + .all() + .pop() + .ok_or(anyhow::anyhow!("No servers returned"))?; + + server + .set("os", "bookworm")? + .add("responsible_admin", "yannik.schwiegerr")? + .remove("responsible_admin", "yannik.schwieger")?; + + server.commit().await?; + + let mut new_sg = NewObject::request_new("service_group").await?; + new_sg + .set("hostname", "yannik-adminapi-rs-2.test.sg")? + .set("project", "test")? + .add("responsible_admin", "yannik.schwieger")? + .add("protocol_ports_inbound", "tcp443")?; + let mut created_sg = new_sg.commit().await?; + created_sg + .add("protocol_ports_inbound", "tcp80")? + .add("sg_allow_from", "yannik-adminapi-rs-2.test.sg")? + .add("sg_allow_to", "yannik-adminapi-rs-2.test.sg")? + .commit() + .await?; + + Ok(()) +} diff --git a/rust/examples/parse_args.rs b/rust/examples/parse_args.rs new file mode 100644 index 00000000..2cf80a51 --- /dev/null +++ b/rust/examples/parse_args.rs @@ -0,0 +1,15 @@ +use adminapi::cli::parse_filter_args; + +pub fn main() { + let mut args = std::env::args(); + args.next(); + + match parse_filter_args(args) { + Ok(arg) => { + println!("{arg:#?}"); + } + Err(err) => { + eprintln!("{err:#?}"); + } + } +} diff --git a/rust/examples/prepare_os_upgrade.rs b/rust/examples/prepare_os_upgrade.rs new file mode 100644 index 00000000..d9ac5613 --- /dev/null +++ b/rust/examples/prepare_os_upgrade.rs @@ -0,0 +1,99 @@ +use adminapi::cli::parse_filter_args; +use adminapi::commit::AttributeValue; +use adminapi::query::Query; + +#[tokio::main] +pub async fn main() -> anyhow::Result<()> { + env_logger::init(); + + let clap = clap::Command::new("prepare_os_upgrade") + .author("Yannik, yannik.schwieger@innogames.com") + .arg(clap::arg!([OS_RELEASE] "Sets the OS release name").required(true)) + .arg( + clap::arg!([QUERY]) + .action(clap::ArgAction::Append) + .help("Query for the preparation") + .value_parser(clap::value_parser!(String)) + .num_args(1..) + .required(true), + ) + .arg( + clap::arg!(--"puppet-environment" "Sets an optional puppet environment") + .value_parser(clap::value_parser!(String)) + .required(false), + ) + .arg( + clap::arg!(--"maintenance" "Sets the servers to maintenance") + .action(clap::ArgAction::SetTrue) + .required(false), + ); + + let matches = clap.get_matches(); + let os_release = matches + .get_one::("OS_RELEASE") + .expect("OS_RELEASE is required!"); + let query = matches + .get_many::("QUERY") + .unwrap() + .cloned() + .collect::>(); + + if !["buster", "bullseye", "bookworm", "trixie"].contains(&os_release.as_str()) { + return Err(anyhow::anyhow!( + "Error: {os_release} is not a valid Debian release!" + )); + } + + let filters = parse_filter_args(query.into_iter())?; + + let query = Query::builder() + .filters(filters) + .restrict(["os", "repositories", "puppet_environment", "state"]) + .build(); + let response = query.request().await?; + let mut updates = vec![]; + + for mut server in response.all() { + let AttributeValue::String(base_os) = server.get("os") else { + return Err(anyhow::anyhow!("Unexpected value for os")); + }; + + let AttributeValue::Array(repos) = server.get("repositories") else { + return Err(anyhow::anyhow!("Unexpected value for repositories")); + }; + + server.set("os", os_release.to_string())?; + if matches.get_flag("maintenance") { + server.set("state", "maintenance")?; + } + + for repo in repos { + let AttributeValue::String(repo) = repo else { + return Err(anyhow::anyhow!("Unexpected value for repository")); + }; + + if !repo.contains(&base_os) { + continue; + } + + server.add("repositories", repo.replace(&base_os, os_release))?; + server.remove("repositories", repo)?; + + if let Some(environment) = matches.get_one::("puppet-environment") { + if !server.get("puppet_environment").is_null() { + return Err(anyhow::anyhow!( + "Puppet environment is already set. Aborting!" + )); + } + + server.set("puppet_environment", environment.clone())?; + } + } + + updates.push(async move { server.commit().await }); + } + + futures::future::try_join_all(updates).await?; + + Ok(()) +} diff --git a/rust/src/api.rs b/rust/src/api.rs new file mode 100644 index 00000000..bbe95681 --- /dev/null +++ b/rust/src/api.rs @@ -0,0 +1,317 @@ +use std::fmt::Display; + +use signature::Signer; +use ssh_encoding::Encode; + +use crate::commit::{ + AttributeChange, AttributeValue, Changeset, Commit, Dataset, IntoAttributeValue, +}; +use crate::config::Config; +use crate::query::Query; + +pub const QUERY_ENDPOINT: &str = "/api/dataset/query"; +pub const COMMIT_ENDPOINT: &str = "/api/dataset/commit"; +pub const NEW_OBJECT_ENDPOINT: &str = "/api/dataset/new_object"; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct QueryResponse { + #[serde(default)] + pub status: String, + pub result: Vec>, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct NewObjectResponse { + pub result: Dataset, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct CommitResponse { + #[serde(default)] + pub status: String, + #[serde(default)] + pub message: Option, +} + +pub async fn query_objects( + query: &Query, +) -> anyhow::Result> { + let config = Config::build_from_environment()?; + let response = request_api(QUERY_ENDPOINT, serde_json::to_value(query)?, config).await?; + let response = response.error_for_status()?; + + Ok(response.json().await?) +} + +pub async fn new_object(servertype: impl Display) -> anyhow::Result { + let config = Config::build_from_environment()?; + let response = request_api( + format!("{NEW_OBJECT_ENDPOINT}?servertype={servertype}"), + serde_json::Value::Null, + config, + ) + .await?; + let response = response.error_for_status()?; + + Ok(response.json().await?) +} + +pub async fn commit_changes(commit: &Commit) -> anyhow::Result { + let config = Config::build_from_environment()?; + let response = request_api(COMMIT_ENDPOINT, serde_json::to_value(commit)?, config).await?; + let status = response.status(); + let body = response.json::().await?; + + if status.is_client_error() || status.is_server_error() { + return Err(anyhow::anyhow!("Unable to process request").context(format!("{:?}", body))); + } + + if body.status == "error" { + return Err(anyhow::anyhow!( + "Error while committing {}", + body.message + .unwrap_or_else(|| String::from("Unknown commit error")) + )); + } + + Ok(body) +} + +pub async fn request_api( + endpoint: impl Display, + data: serde_json::Value, + config: Config, +) -> anyhow::Result { + let client = reqwest::Client::new(); + let token = config.auth_token.unwrap_or_default(); + let body = serde_json::to_string(&data)?; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs(); + let sign_body = format!("{now}:{body}"); + let url = format!("{}{endpoint}", config.base_url); + let mut request = client + .post(url) + .header("Content-Type", "application/json") + .header("X-Timestamp", now.to_string()) + .header("X-API-Version", config.api_version) + .body(body.into_bytes()); + + if let Some(signer) = &config.ssh_signer { + let signature = signer.try_sign(sign_body.as_bytes())?; + let len = Encode::encoded_len_prefixed(&signature)?; + let base64_len = (((len.saturating_mul(4)) / 3).saturating_add(3)) & !3; + let mut buf = vec![0; base64_len]; + let mut writer = ssh_encoding::Base64Writer::new(&mut buf)?; + signature.encode(&mut writer)?; + let signature = writer.finish()?; + + request = request + .header("X-PublicKeys", signer.get_public_key()) + .header("X-Signatures", signature.to_string()); + } else { + request = request + .header( + "X-SecurityToken", + calculate_security_token(&token, &sign_body), + ) + .header("X-Application", calculate_app_id(&token)) + } + + Ok(request.send().await?) +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct Server { + pub object_id: u64, + #[serde(flatten)] + pub attributes: T, + #[serde(skip, default)] + pub(crate) changes: Changeset, +} + +impl IntoIterator for QueryResponse { + type Item = Server; + type IntoIter = std::vec::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + self.result.into_iter() + } +} + +fn calculate_security_token(token: &String, sign_body: &str) -> String { + use hmac::Mac; + + type HmacSha1 = hmac::Hmac; + let mut hmac = + HmacSha1::new_from_slice(token.as_bytes()).expect("Hmac can accept any size of key"); + hmac.update(sign_body.as_bytes()); + let result = hmac.finalize(); + + result + .into_bytes() + .iter() + .fold(String::new(), |hash, byte| format!("{hash}{byte:02x}")) +} + +fn calculate_app_id(token: &String) -> String { + use sha1::Digest; + let mut hasher = sha1::Sha1::new(); + hasher.update(token.as_bytes()); + let result = hasher.finalize(); + + result + .iter() + .fold(String::new(), |hash, byte| format!("{hash}{byte:02x}")) +} + +impl Server { + pub fn clear(&mut self, attribute: impl ToString) -> anyhow::Result<&mut Self> { + self.attributes.clear(attribute.to_string()); + + Ok(self) + } + + pub fn set( + &mut self, + attribute: impl ToString, + value: impl IntoAttributeValue + 'static, + ) -> anyhow::Result<&mut Self> { + let new = value.into_attribute_value(); + let attribute = attribute.to_string(); + + if self.attributes.get(&attribute).is_array() { + return Err(anyhow::anyhow!( + "Attribute {attribute} is a multi attribute, set is not supported!" + )); + } + + let old = self.attributes.get(&attribute); + self.attributes.set(attribute.clone(), new.clone()); + self.changes + .attributes + .insert(attribute, AttributeChange::Update { old, new }); + + Ok(self) + } + + pub fn add( + &mut self, + attribute: impl ToString, + value: impl IntoAttributeValue + 'static, + ) -> anyhow::Result<&mut Self> { + let value = value.into_attribute_value(); + let attribute = attribute.to_string(); + + if !self.attributes.get(&attribute).is_array() { + return Err(anyhow::anyhow!( + "add is only supported with multi attributes" + )); + } + + if self + .attributes + .get(&attribute) + .as_array() + .unwrap() + .contains(&value) + { + return Ok(self); + } + + self.attributes.add(attribute.clone(), value.clone()); + let entry = self + .changes + .attributes + .entry(attribute) + .or_insert(AttributeChange::Multi { + remove: vec![], + add: vec![], + }); + + if let AttributeChange::Multi { add, .. } = entry { + add.push(value); + } + + Ok(self) + } + + pub fn remove( + &mut self, + attribute: impl ToString, + value: impl IntoAttributeValue + 'static, + ) -> anyhow::Result<&mut Self> { + let value = value.into_attribute_value(); + let attribute = attribute.to_string(); + + if !self.attributes.get(&attribute).is_array() { + return Err(anyhow::anyhow!( + "remove is only supported with multi attributes" + )); + } + + if !self + .attributes + .get(&attribute) + .as_array() + .unwrap() + .contains(&value) + { + return Ok(self); + } + + self.attributes.remove(attribute.clone(), value.clone()); + let entry = self + .changes + .attributes + .entry(attribute) + .or_insert(AttributeChange::Multi { + remove: vec![], + add: vec![], + }); + + if let AttributeChange::Multi { remove, .. } = entry { + remove.push(value); + } + + Ok(self) + } + + pub fn get(&self, name: &str) -> AttributeValue { + self.attributes.get(name) + } + + pub fn changeset(&self) -> Changeset { + let mut set = self.changes.clone(); + set.object_id = self.object_id; + + set + } + + /// Rolls back the changes. + /// + /// Returns the reverted changes + pub fn rollback(&mut self) -> Changeset { + let old_changes = self.changeset(); + + self.changes = Changeset::default(); + + old_changes + } +} + +impl QueryResponse { + pub fn one(mut self) -> anyhow::Result> { + if self.result.len() > 1 { + return Err(anyhow::anyhow!("Result has more then one item!")); + } + + self.result + .pop() + .ok_or(anyhow::anyhow!("No result returned!")) + } + + pub fn all(self) -> Vec> { + self.result + } +} diff --git a/rust/src/cli.rs b/rust/src/cli.rs new file mode 100644 index 00000000..ab6e7c0f --- /dev/null +++ b/rust/src/cli.rs @@ -0,0 +1,112 @@ +use std::str::Chars; + +use crate::filter; +use crate::filter::{AttributeFilter, FilterValue, IntoFilterValue}; + +pub fn parse_filter_args( + args: impl Iterator + 'static, +) -> anyhow::Result { + let mut filter = AttributeFilter::default(); + + for arg in args { + let mut split = arg.split('='); + let Some(attribute) = split.next() else { + continue; + }; + let tail = split.collect::(); + if tail.is_empty() { + return Err(anyhow::anyhow!("Attribute value is missing")); + } + + filter.insert(attribute.to_string(), parse_filter_arg(tail)); + } + + Ok(filter) +} + +pub fn parse_filter_arg(arg: String) -> FilterValue { + let mut chars = arg.chars(); + + lookup_function(&mut chars) +} + +pub fn lookup_function(chars: &mut Chars) -> FilterValue { + let mut buffer = String::new(); + let mut fn_name = String::new(); + let mut depth = 0; + let mut inner = Vec::new(); + + for char in chars.by_ref() { + if char == '(' { + depth += 1; + } + if char == ')' { + depth -= 1; + } + + if char == '(' && depth == 1 { + fn_name.extend(buffer.drain(0..)); + } + + if char == '(' && depth == 1 { + continue; + } + + if char == ')' && depth == 0 { + continue; + } + + if depth == 0 && char == ' ' { + inner.push(buffer.clone()); + buffer.clear(); + + continue; + } + + buffer.push(char); + } + + if fn_name.is_empty() && inner.is_empty() { + return buffer.into_filter_value(); + } + + if !buffer.is_empty() { + inner.push(buffer); + } + + let filter_fn = get_filter_function(&fn_name.to_lowercase()); + let mut inner_filters = inner + .into_iter() + .map(|filter| { + let mut chars = filter.chars(); + + lookup_function(&mut chars) + }) + .collect::>(); + + if inner_filters.len() == 1 { + filter_fn(inner_filters.pop().unwrap()) + } else { + filter_fn(FilterValue::Array(inner_filters)) + } +} + +fn get_filter_function(name: &str) -> Box FilterValue> { + match name { + "all" => Box::new(filter::all), + "any" => Box::new(filter::any), + "containedby" => Box::new(filter::contained_by), + "containedonlyby" => Box::new(filter::contained_only_by), + "contains" => Box::new(filter::contains), + "empty" => Box::new(|_| filter::empty()), + "greaterthan" => Box::new(filter::greater_than), + "greaterthanorequals" => Box::new(filter::greater_than_or_equals), + "lessthan" => Box::new(filter::less_than), + "lessthanorequals" => Box::new(filter::less_than_or_equals), + "not" => Box::new(filter::not), + "overlaps" => Box::new(filter::overlaps), + "regexp" => Box::new(filter::regexp), + "startswith" => Box::new(filter::starts_with), + _name => Box::new(IntoFilterValue::into_filter_value), + } +} diff --git a/rust/src/commit.rs b/rust/src/commit.rs new file mode 100644 index 00000000..0ee0e22a --- /dev/null +++ b/rust/src/commit.rs @@ -0,0 +1,237 @@ +use std::collections::{HashMap, HashSet}; + +use serde_json::Number; + +pub type AttributeValue = serde_json::Value; + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct Dataset(HashMap); + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(tag = "action", rename_all = "snake_case")] +pub enum AttributeChange { + Update { + old: AttributeValue, + new: AttributeValue, + }, + Multi { + remove: Vec, + add: Vec, + }, +} + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct Changeset { + pub object_id: u64, + #[serde(flatten)] + pub attributes: HashMap, +} + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct Commit { + #[serde(skip_serializing_if = "Vec::is_empty")] + pub created: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub changed: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub deleted: Vec, +} + +pub trait IntoDataset { + fn into_dataset(self) -> Dataset; +} + +pub trait IntoAttributeValue { + fn into_attribute_value(self) -> AttributeValue; +} + +impl Changeset { + pub fn has_changes(&self) -> bool { + self.attributes + .iter() + .filter(|(_, change)| { + if let AttributeChange::Multi { add, remove } = change { + return add.len() + remove.len() > 0; + } + + if let AttributeChange::Update { new, old } = change { + return new.ne(old); + } + + true + }) + .any(|_| true) + } +} + +impl Commit { + pub fn new() -> Self { + Self { + created: Default::default(), + changed: Default::default(), + deleted: Default::default(), + } + } + + pub fn create(mut self, attrs: impl IntoDataset + 'static) -> Self { + self.created.push(attrs.into_dataset()); + + self + } + + pub fn update(mut self, mut changeset: Changeset) -> Self { + let filtered = changeset + .attributes + .into_iter() + .filter(|(_, change)| { + if let AttributeChange::Update { new, old } = change { + return new.ne(old); + } + + if let AttributeChange::Multi { add, remove } = change { + return add.len() + remove.len() > 0; + } + + true + }) + .collect(); + + changeset.attributes = filtered; + + self.changed.push(changeset); + + self + } + + pub fn delete(mut self, object_id: u64) -> Self { + self.deleted.push(object_id); + + self + } +} + +impl IntoDataset for Dataset { + fn into_dataset(self) -> Dataset { + self + } +} + +impl IntoAttributeValue for String { + fn into_attribute_value(self) -> AttributeValue { + AttributeValue::String(self) + } +} + +impl IntoAttributeValue for &String { + fn into_attribute_value(self) -> AttributeValue { + AttributeValue::String(self.clone()) + } +} + +impl IntoAttributeValue for () { + fn into_attribute_value(self) -> AttributeValue { + AttributeValue::Null + } +} + +impl IntoAttributeValue for f32 { + fn into_attribute_value(self) -> AttributeValue { + AttributeValue::from(self) + } +} + +impl IntoAttributeValue for i32 { + fn into_attribute_value(self) -> AttributeValue { + AttributeValue::Number(Number::from(self)) + } +} + +impl IntoAttributeValue for Vec { + fn into_attribute_value(self) -> AttributeValue { + AttributeValue::from_iter( + self.into_iter() + .map(IntoAttributeValue::into_attribute_value), + ) + } +} + +impl IntoAttributeValue for AttributeValue { + fn into_attribute_value(self) -> AttributeValue { + self + } +} + +impl IntoAttributeValue for &str { + fn into_attribute_value(self) -> AttributeValue { + AttributeValue::String(self.to_string()) + } +} + +impl Dataset { + pub fn new() -> Self { + Self(Default::default()) + } + + pub fn clear(&mut self, name: impl ToString) -> &mut Self { + self.0.remove(&name.to_string()); + + self + } + + pub fn set( + &mut self, + name: impl ToString, + attr: impl IntoAttributeValue + 'static, + ) -> &mut Self { + self.0.insert(name.to_string(), attr.into_attribute_value()); + + self + } + + pub fn add( + &mut self, + name: impl ToString, + attr: impl IntoAttributeValue + 'static, + ) -> &mut Self { + let name = name.to_string(); + let attr = attr.into_attribute_value(); + + let value = self.0.entry(name).or_insert(AttributeValue::Array(vec![])); + if let serde_json::Value::Array(array) = value { + array.push(attr); + } + + self + } + + pub fn remove( + &mut self, + name: impl ToString, + attr: impl IntoAttributeValue + 'static, + ) -> &mut Self { + let name = name.to_string(); + let attr = attr.into_attribute_value(); + + let Some(AttributeValue::Array(value)) = self.0.get_mut(&name) else { + return self; + }; + + if let Some(index) = value.iter().position(|val| val == &attr) { + value.remove(index); + } + + if value.is_empty() { + self.0.remove(&name); + } + + self + } + + pub fn get(&self, name: &str) -> AttributeValue { + self.0.get(name).cloned().unwrap_or(AttributeValue::Null) + } + + pub fn keys(&self) -> HashSet { + self.0.keys().cloned().collect() + } +} diff --git a/rust/src/config.rs b/rust/src/config.rs new file mode 100644 index 00000000..3bb40820 --- /dev/null +++ b/rust/src/config.rs @@ -0,0 +1,124 @@ +use std::fmt::{Debug, Formatter}; +use std::path::Path; +use std::sync::{Arc, Mutex}; + +use signature::Signer; + +use crate::API_VERSION; + +pub const ENV_NAME_BASE_URL: &str = "SERVERADMIN_BASE_URL"; +pub const ENV_NAME_TOKEN: &str = "SERVERADMIN_TOKEN"; +pub const ENV_NAME_KEY_PATH: &str = "SERVERADMIN_KEY_PATH"; +pub const ENV_NAME_SSH_AGENT: &str = "SSH_AUTH_SOCK"; + +#[derive(Clone, Debug, Default)] +pub struct Config { + pub base_url: String, + pub api_version: String, + pub ssh_signer: Option, + pub auth_token: Option, +} + +#[derive(Clone)] +pub enum SshSigner { + Agent( + Box, + Box>>, + ), + Key(Box), +} + +impl Config { + pub fn build_from_environment() -> anyhow::Result { + let config = Self { + base_url: std::env::var(ENV_NAME_BASE_URL)? + .trim_end_matches('/') + .trim_end_matches("/api") + .to_string(), + api_version: API_VERSION.to_string(), + ssh_signer: Self::get_signing_key()?, + auth_token: std::env::var(ENV_NAME_TOKEN).ok(), + }; + + Ok(config) + } + + fn get_signing_key() -> anyhow::Result> { + if let Ok(path) = std::env::var(ENV_NAME_KEY_PATH) { + let key = ssh_key::PrivateKey::read_openssh_file(Path::new(&path))?; + + return Ok(Some(SshSigner::Key(Box::new(key)))); + } + + let path = std::env::var(ENV_NAME_SSH_AGENT).unwrap_or_default(); + let client = ssh_agent_client_rs::Client::connect(Path::new(&path)) + .map_err(|error| log::debug!("Unable to connect to SSH agent: {error}")) + .ok(); + + if let Some(mut client) = client { + let identities = client.list_identities()?; + for key in identities { + log::debug!( + "Test signing with SSH key {}", + key.fingerprint(ssh_key::HashAlg::Sha256) + ); + + if client.sign(&key, b"mest message").is_ok() { + log::debug!( + "Found compatible key: {}", + key.fingerprint(ssh_key::HashAlg::Sha256) + ); + + return Ok(Some(SshSigner::Agent( + Box::new(key), + Box::new(Arc::new(Mutex::new(client))), + ))); + } + } + } + + Ok(None) + } +} + +impl Debug for SshSigner { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SshSigner::Key(_key) => f.write_fmt(format_args!("Key: {}", self.get_public_key())), + SshSigner::Agent(_key, _) => { + f.write_fmt(format_args!("Agent: {}", self.get_public_key())) + } + } + } +} + +impl SshSigner { + pub fn get_public_key(&self) -> String { + let public_key = match self { + SshSigner::Key(key) => key.public_key(), + SshSigner::Agent(key, _) => key, + }; + + let key = public_key.to_openssh().unwrap(); + let mut key = key.split(' '); + key.next(); + + key.next().map(ToString::to_string).unwrap_or_default() + } +} + +impl Signer for SshSigner { + fn try_sign(&self, msg: &[u8]) -> Result { + match self { + SshSigner::Key(key) => key.try_sign(msg), + SshSigner::Agent(key, agent) => agent + .lock() + .unwrap() + .sign(key, msg) + .map_err(signature::Error::from_source), + } + } +} + +unsafe impl Send for SshSigner {} +unsafe impl Sync for SshSigner {} diff --git a/rust/src/filter.rs b/rust/src/filter.rs new file mode 100644 index 00000000..a37d3df6 --- /dev/null +++ b/rust/src/filter.rs @@ -0,0 +1,123 @@ +use std::collections::HashMap; + +pub type FilterValue = serde_json::Value; + +pub type AttributeFilter = HashMap; + +pub trait IntoFilterValue { + fn into_filter_value(self) -> FilterValue; +} + +/// ServerAdmin All Filter +pub fn all(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("All", value) +} + +/// ServerAdmin Any Filter +pub fn any(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("Any", value) +} + +/// ServerAdmin ContainedBy Filter +pub fn contained_by(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("ContainedBy", value) +} + +/// ServerAdmin ContainedOnlyBy Filter +pub fn contained_only_by(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("ContainedOnlyBy", value) +} + +/// ServerAdmin Contains Filter +pub fn contains(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("Contains", value) +} + +/// ServerAdmin Empty Filter +pub fn empty() -> FilterValue { + create_filter("Empty", ()) +} + +/// ServerAdmin GreaterThan Filter +pub fn greater_than(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("GreaterThan", value) +} + +/// ServerAdmin GreaterThanOrEquals Filter +pub fn greater_than_or_equals(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("GreaterThanOrEquals", value) +} + +/// ServerAdmin LessThan Filter +pub fn less_than(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("LessThan", value) +} + +/// ServerAdmin LessThanOrEquals Filter +pub fn less_than_or_equals(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("LessThanOrEquals", value) +} + +/// ServerAdmin Not Filter +pub fn not(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("Not", value) +} + +/// ServerAdmin Overlaps Filter +pub fn overlaps(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("Overlaps", value) +} + +/// ServerAdmin Regexp Filter +pub fn regexp(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("Regexp", value) +} + +/// ServerAdmin StartsWith Filter +pub fn starts_with(value: impl IntoFilterValue + 'static) -> FilterValue { + create_filter("StartsWith", value) +} + +impl IntoFilterValue for () { + fn into_filter_value(self) -> FilterValue { + FilterValue::Null + } +} + +impl IntoFilterValue for String { + fn into_filter_value(self) -> FilterValue { + FilterValue::String(self) + } +} + +impl IntoFilterValue for &str { + fn into_filter_value(self) -> FilterValue { + FilterValue::String(self.to_string()) + } +} + +impl IntoFilterValue for i32 { + fn into_filter_value(self) -> FilterValue { + FilterValue::from(self) + } +} + +impl IntoFilterValue for Vec { + fn into_filter_value(self) -> FilterValue { + FilterValue::from_iter(self.into_iter().map(IntoFilterValue::into_filter_value)) + } +} + +impl IntoFilterValue for serde_json::Value { + fn into_filter_value(self) -> FilterValue { + self + } +} + +/// Filters on an attribute +fn create_filter(filter_name: impl ToString, value: impl IntoFilterValue + 'static) -> FilterValue { + let mut filter = HashMap::new(); + filter.insert(filter_name.to_string(), value.into_filter_value()); + + FilterValue::from_iter(filter) +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 00000000..78fdcd8a --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,9 @@ +pub const API_VERSION: &str = "4.9.0"; + +pub mod api; +pub mod cli; +pub mod commit; +pub mod config; +pub mod filter; +pub mod new_object; +pub mod query; diff --git a/rust/src/new_object.rs b/rust/src/new_object.rs new file mode 100644 index 00000000..f5449b0c --- /dev/null +++ b/rust/src/new_object.rs @@ -0,0 +1,151 @@ +use std::ops::{Deref, DerefMut}; + +use crate::api::{commit_changes, new_object, NewObjectResponse, Server}; +use crate::commit::{AttributeValue, Changeset, Commit, Dataset}; +use crate::query::Query; + +#[derive(Clone, Debug)] +pub struct NewObject { + object_id: Option, + server: Server, + deferred_changes: Changeset, +} + +impl NewObject { + pub fn from_dataset(dataset: Dataset) -> Self { + Self { + object_id: None, + server: Server { + object_id: 0, + attributes: dataset, + changes: Default::default(), + }, + deferred_changes: Default::default(), + } + } + + pub async fn request_new(servertype: impl ToString) -> anyhow::Result { + let servertype = servertype.to_string(); + let NewObjectResponse { result } = new_object(&servertype).await?; + + Ok(Self { + object_id: None, + server: Server { + object_id: 0, + attributes: result, + changes: Default::default(), + }, + deferred_changes: Default::default(), + }) + } + + pub async fn get_or_create( + servertype: impl ToString, + hostname: impl ToString, + ) -> anyhow::Result { + let mut new_object = Self::request_new(servertype.to_string()).await?; + + if let Ok(server) = Query::builder() + .filter("hostname", hostname.to_string()) + .restrict(new_object.server.attributes.keys()) + .build() + .request() + .await? + .one() + { + new_object.object_id = Some(server.object_id); + new_object.server = server; + } + + Ok(new_object) + } + + pub fn is_new(&self) -> bool { + self.object_id.is_none() + } + + pub fn has_changes(&self) -> bool { + if self.is_new() { + return true; + } + + self.server.has_changes() || self.deferred_changes.has_changes() + } + + /// + /// Commits the new object + /// + /// The changes done in [NewObject::deferred] will not be submitted yet, but the returned [Server] + /// object is preloaded with them. + /// + pub async fn commit(mut self) -> anyhow::Result { + let AttributeValue::String(hostname) = self.server.get("hostname") else { + return Err(anyhow::anyhow!("Required attribute 'hostname' is missing")); + }; + + if self.is_new() { + commit_changes(&Commit::new().create(self.server.attributes)).await?; + } else { + self.server.commit().await?; + } + + let mut server = Query::builder() + .filter("hostname", hostname) + .build() + .request() + .await? + .one()?; + + server.changes = self.deferred_changes; + + Ok(server) + } + + /// + /// Gets the initial commit data and the follow-up commit of deferred changes + /// + pub fn get_commit(self) -> (Commit, Commit) { + ( + Commit::new().create(self.server.attributes), + Commit::new().update(self.deferred_changes), + ) + } + + /// + /// The deferred method allows you to pre-update the newly created object + /// + /// It allows you to already prepare relations before other objects are created, making the new + /// object creation with relations a very simple 2-stage process. + /// + /// The input [Server] object for the `callback` is loaded with the [Dataset] of the current + /// object. Keep in mind though, that there are some attributes that are only filled after the + /// object is created + /// + pub fn deferred(&mut self, callback: impl FnOnce(&mut Server) -> R) -> R { + let mut server = Server { + object_id: 0, + attributes: self.server.attributes.clone(), + changes: std::mem::take(&mut self.deferred_changes), + }; + + let output = callback(&mut server); + + self.deferred_changes = std::mem::take(&mut server.changes); + + output + } +} + +impl Deref for NewObject { + type Target = Server; + + fn deref(&self) -> &Self::Target { + &self.server + } +} + +impl DerefMut for NewObject { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.server + } +} diff --git a/rust/src/query.rs b/rust/src/query.rs new file mode 100644 index 00000000..3c7ff84b --- /dev/null +++ b/rust/src/query.rs @@ -0,0 +1,95 @@ +use std::collections::HashSet; + +use crate::api::{commit_changes, query_objects, CommitResponse, QueryResponse, Server}; +use crate::commit::{Changeset, Commit}; +use crate::filter::{AttributeFilter, IntoFilterValue}; + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct Query { + pub filters: AttributeFilter, + pub restrict: HashSet, + #[serde(skip_serializing_if = "Option::is_none")] + pub order_by: Option, +} + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct QueryBuilder(Query); + +impl Query { + pub fn new() -> Self { + Default::default() + } + + pub fn builder() -> QueryBuilder { + Default::default() + } + + pub async fn request(&self) -> anyhow::Result { + query_objects(self).await + } + + pub async fn request_typed( + &self, + ) -> anyhow::Result> { + query_objects::(self).await + } +} + +impl QueryBuilder { + pub fn new() -> Self { + Default::default() + } + + pub fn filters(mut self, filter: AttributeFilter) -> Self { + self.0.filters.extend(filter); + + self + } + + pub fn filter( + mut self, + attribute: impl ToString, + value: impl IntoFilterValue + 'static, + ) -> Self { + self.0 + .filters + .insert(attribute.to_string(), value.into_filter_value()); + + self + } + + pub fn restrict>(mut self, attributes: I) -> Self { + self.0.restrict = HashSet::from_iter(attributes.into_iter().map(|v| v.to_string())); + + self + } + + pub fn order_by>>(mut self, value: T) -> Self { + self.0.order_by = value.into().as_ref().map(ToString::to_string); + + self + } + + pub fn build(mut self) -> Query { + if self.0.restrict.is_empty() { + self.0.restrict.insert(String::from("hostname")); + } + + self.0.restrict.insert(String::from("object_id")); + + self.0 + } +} + +impl Server { + pub async fn commit(&mut self) -> anyhow::Result { + let commit = Commit::new().update(self.changeset()); + self.changes = Changeset::default(); + + commit_changes(&commit).await + } + + pub fn has_changes(&self) -> bool { + self.changes.has_changes() + } +}