diff --git a/Cargo.lock b/Cargo.lock index 3e19db9..c1ba82e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,6 +264,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[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" @@ -275,6 +285,20 @@ dependencies = [ "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 = "ahash" version = "0.8.11" @@ -282,6 +306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", + "const-random", "getrandom", "once_cell", "version_check", @@ -312,6 +337,21 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.15" @@ -382,6 +422,31 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "async-ssh2-tokio" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13084e4ade0a7b49651660f0e43faeccda2c74f74a929f9f3711ea04062467a2" +dependencies = [ + "async-trait", + "russh", + "russh-keys", + "russh-sftp", + "thiserror", + "tokio", +] + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -409,17 +474,43 @@ dependencies = [ "windows-targets 0.52.6", ] +[[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.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bcrypt-pbkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" +dependencies = [ + "blowfish", + "pbkdf2 0.12.2", + "sha2", +] + [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "block-buffer" @@ -430,6 +521,25 @@ 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 = "brotli" version = "6.0.0" @@ -519,6 +629,15 @@ 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.1.31" @@ -536,6 +655,31 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[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 = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + [[package]] name = "cipher" version = "0.4.4" @@ -592,6 +736,32 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -670,6 +840,24 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[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", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -677,6 +865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -701,12 +890,65 @@ dependencies = [ "memchr", ] +[[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", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + [[package]] name = "deflate64" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -761,6 +1003,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher", +] + [[package]] name = "digest" version = "0.10.7" @@ -768,6 +1019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -804,12 +1056,72 @@ dependencies = [ "syn", ] +[[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 = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "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", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.34" @@ -864,6 +1176,22 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "flate2" version = "1.0.34" @@ -874,6 +1202,18 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flurry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037030493fadfabb7b5638c2d665c0d2d2e393d8fc7aff27926524cf98efd8c0" +dependencies = [ + "ahash", + "num_cpus", + "parking_lot", + "seize", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1001,6 +1341,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1014,6 +1355,16 @@ dependencies = [ "wasi", ] +[[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 = "gimli" version = "0.31.1" @@ -1026,6 +1377,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "h2" version = "0.3.26" @@ -1082,6 +1444,21 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -1235,6 +1612,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -1286,6 +1686,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ + "block-padding", "generic-array", ] @@ -1364,11 +1765,26 @@ dependencies = [ ] [[package]] -name = "libc" +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "libredox" version = "0.1.3" @@ -1434,6 +1850,12 @@ dependencies = [ "crc", ] +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "mediatype" version = "0.19.18" @@ -1491,12 +1913,80 @@ dependencies = [ "tempfile", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "rand", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.36.5" @@ -1512,6 +2002,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl" version = "0.10.68" @@ -1571,6 +2067,44 @@ dependencies = [ "memchr", ] +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +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", + "sha2", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -1594,12 +2128,35 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "pbkdf2" version = "0.12.2" @@ -1610,6 +2167,15 @@ dependencies = [ "hmac", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1628,12 +2194,73 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2 0.12.2", + "scrypt", + "sha2", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "pkcs5", + "rand_core", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "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 = "powerfmt" version = "0.2.0" @@ -1649,6 +2276,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.87" @@ -1795,6 +2431,16 @@ dependencies = [ "windows-registry", ] +[[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.8" @@ -1818,6 +2464,7 @@ dependencies = [ "actix-web", "actix-web-lab", "anyhow", + "async-ssh2-tokio", "camino", "clap", "env_logger", @@ -1830,6 +2477,7 @@ dependencies = [ "reqwest", "serde", "serde_yaml", + "shell-escape", "shellexpand", "tempfile", "tokio", @@ -1837,6 +2485,149 @@ dependencies = [ "zip", ] +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "russh" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a229f2a03daea3f62cee897b40329ce548600cca615906d98d58b8db3029b19" +dependencies = [ + "aes", + "aes-gcm", + "async-trait", + "bitflags", + "byteorder", + "cbc", + "chacha20", + "ctr", + "curve25519-dalek", + "des", + "digest", + "elliptic-curve", + "flate2", + "futures", + "generic-array", + "hex-literal", + "hmac", + "log", + "num-bigint", + "once_cell", + "p256", + "p384", + "p521", + "poly1305", + "rand", + "rand_core", + "russh-cryptovec", + "russh-keys", + "sha1", + "sha2", + "ssh-encoding", + "ssh-key", + "subtle", + "thiserror", + "tokio", +] + +[[package]] +name = "russh-cryptovec" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadd2c0ab350e21c66556f94ee06f766d8bdae3213857ba7610bfd8e10e51880" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "russh-keys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89757474f7c9ee30121d8cc7fe293a954ba10b204a82ccf5850a5352a532ebc7" +dependencies = [ + "aes", + "async-trait", + "bcrypt-pbkdf", + "block-padding", + "byteorder", + "cbc", + "ctr", + "data-encoding", + "der", + "digest", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "futures", + "hmac", + "home", + "inout", + "log", + "md5", + "num-integer", + "p256", + "p384", + "p521", + "pbkdf2 0.11.0", + "pkcs1", + "pkcs5", + "pkcs8", + "rand", + "rand_core", + "rsa", + "russh-cryptovec", + "sec1", + "serde", + "sha1", + "sha2", + "spki", + "ssh-encoding", + "ssh-key", + "thiserror", + "tokio", + "tokio-stream", + "typenum", + "zeroize", +] + +[[package]] +name = "russh-sftp" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a72c8afe2041c17435eecd85d0b7291841486fd3d1c4082e0b212e5437ca42" +dependencies = [ + "async-trait", + "bitflags", + "bytes", + "chrono", + "flurry", + "log", + "serde", + "thiserror", + "tokio", + "tokio-util", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1910,6 +2701,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "schannel" version = "0.1.26" @@ -1925,6 +2725,31 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2 0.12.2", + "salsa20", + "sha2", +] + +[[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 = "2.11.1" @@ -1948,6 +2773,12 @@ dependencies = [ "libc", ] +[[package]] +name = "seize" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "689224d06523904ebcc9b482c6a3f4f7fb396096645c4cd10c0d2ff7371a34d3" + [[package]] name = "semver" version = "1.0.23" @@ -2045,6 +2876,23 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shell-escape" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" + [[package]] name = "shellexpand" version = "3.1.0" @@ -2071,6 +2919,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -2108,6 +2966,67 @@ 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", + "rsa", + "sec1", + "sha2", + "signature", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2225,6 +3144,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -2391,6 +3319,16 @@ 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" @@ -2536,6 +3474,37 @@ dependencies = [ "winsafe", ] +[[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-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-registry" version = "0.2.0" @@ -2780,7 +3749,7 @@ dependencies = [ "indexmap", "lzma-rs", "memchr", - "pbkdf2", + "pbkdf2 0.12.2", "rand", "sha1", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 816738d..3f5e248 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ actix-cors = "0.7.0" actix-web = "4.9.0" actix-web-lab = "0.23.0" anyhow = "1.0.90" +async-ssh2-tokio = "0.8.12" camino = { version = "1.1.9", features = ["serde"] } clap = { version = "4.5.20", features = ["derive"] } env_logger = "0.11.5" @@ -26,6 +27,7 @@ regex = "1.11.0" reqwest = "0.12.8" serde = { version = "1.0.211", features = ["derive"] } serde_yaml = "0.9.34" +shell-escape = "0.1.5" shellexpand = { version = "3.1.0", features = ["full"] } tempfile = "3.13.0" tokio = { version = "1.41.0", features = ["full"] } diff --git a/README.md b/README.md index 9dd0a2a..15f5231 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,18 @@ Repeat for multiple variables: robopages run -F function_name -A -D target=www.example.com -D foo=bar ``` +#### SSH + +The `run` and `serve` commands support an optional SSH connection string. If provided, commands will be executed over SSH on the given host. + +```bash +robopages serve --ssh user@host:port --ssh-key ~/.ssh/id_ed25519 +``` + +> [!IMPORTANT] +> * Setting a SSH connection string will override any container configuration. +> * If the function requires sudo, the remote host is expected to have passwordless sudo access. + ### Using with LLMs The examples folder contains integration examples for [Rigging](/examples/rigging_example.py), [OpenAI](/examples/openai_example.py), [Groq](/examples/groq_example.py), [OLLAMA](/examples/ollama_example.py) and [Nerve](/examples/nerve.md). \ No newline at end of file diff --git a/src/book/mod.rs b/src/book/mod.rs index 2dc2bec..e378cff 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -192,7 +192,7 @@ pub struct Book { impl Book { pub fn from_path(path: Utf8PathBuf, filter: Option) -> anyhow::Result { - log::info!("Searching for pages in {:?}", path); + log::debug!("Searching for pages in {:?}", path); let mut page_paths = Vec::new(); let path = Utf8PathBuf::from( @@ -203,20 +203,20 @@ impl Book { .canonicalize_utf8() .map_err(|e| anyhow::anyhow!("failed to canonicalize path: {}", e))?; - log::info!("Canonicalized path: {:?}", path); + log::debug!("canonicalized path: {:?}", path); if path.is_file() { - log::info!("Path is a file"); + log::debug!("path is a file"); eval_if_in_filter!(path, filter, page_paths.push(path.to_path_buf())); } else if path.is_dir() { - log::info!("Path is a directory, searching for .yml files"); + log::debug!("path is a directory, searching for .yml files"); let glob_pattern = path.join("**/*.yml").as_str().to_string(); - log::info!("Using glob pattern: {}", glob_pattern); + log::debug!("using glob pattern: {}", glob_pattern); for entry in glob(&glob_pattern)? { match entry { Ok(entry_path) => { - log::debug!("Found file: {:?}", entry_path); + log::debug!("found file: {:?}", entry_path); // skip files in hidden directories (starting with .) // but allow the root .robopages directory if let Ok(relative_path) = entry_path.strip_prefix(&path) { @@ -224,7 +224,7 @@ impl Book { let comp_str = component.as_os_str().to_string_lossy(); comp_str.starts_with(".") && comp_str != "." && comp_str != ".." }) { - log::debug!("Skipping hidden file/directory"); + log::debug!("skipping hidden file/directory"); continue; } } @@ -239,13 +239,13 @@ impl Book { } } Err(e) => { - log::error!("Error in glob: {}", e); + log::error!("error in glob: {}", e); } } } } - log::info!("Found {} page paths", page_paths.len()); + log::debug!("found {} page paths", page_paths.len()); if page_paths.is_empty() { return Err(anyhow::anyhow!("no pages found in {:?}", path)); diff --git a/src/book/runtime.rs b/src/book/runtime.rs index 8a3caa0..1f9caeb 100644 --- a/src/book/runtime.rs +++ b/src/book/runtime.rs @@ -39,15 +39,6 @@ impl ExecutionFlavor { ExecutionFlavor::Error(message) } - pub fn to_string(&self) -> String { - match self { - Self::Shell(shell) => shell.to_string(), - Self::Sudo => "sudo".to_string(), - Self::Docker(image) => format!("docker {}", image), - Self::Error(message) => message.to_string(), - } - } - fn get_current_shell() -> String { let shell_name = std::env::var("SHELL") .map(|s| s.split('/').last().unwrap_or("unknown").to_string()) @@ -111,6 +102,18 @@ impl ExecutionFlavor { } } +impl std::fmt::Display for ExecutionFlavor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + Self::Shell(shell) => shell.to_string(), + Self::Sudo => "sudo".to_string(), + Self::Docker(image) => format!("docker {}", image), + Self::Error(message) => message.to_string(), + }; + write!(f, "{}", s) + } +} + #[derive(Debug, Serialize, Deserialize)] pub enum ExecutionContext { #[serde(rename = "cmdline")] @@ -202,8 +205,8 @@ impl<'a> FunctionRef<'a> { let env_var = std::env::var(&env_var_name); let env_var_value = if let Ok(value) = env_var { value - } else if var_default.is_some() { - var_default.unwrap().to_string() + } else if let Some(def) = var_default { + def.to_string() } else { return Err(anyhow::anyhow!( "environment variable {} not set", @@ -217,8 +220,12 @@ impl<'a> FunctionRef<'a> { env_var_value } else if let Some(value) = arguments.get(var_name) { // if the value is empty and there's a default value, use the default value - if value.is_empty() && var_default.is_some() { - var_default.unwrap().to_string() + if value.is_empty() { + if let Some(def) = var_default { + def.to_string() + } else { + value.to_string() + } } else { // otherwise, use the provided value value.to_string() diff --git a/src/book/templates.rs b/src/book/templates.rs index 7790edb..1804948 100644 --- a/src/book/templates.rs +++ b/src/book/templates.rs @@ -51,13 +51,14 @@ impl Template { } } -impl ToString for Template { - fn to_string(&self) -> String { - match self { +impl std::fmt::Display for Template { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { Template::Basic => "basic".to_string(), Template::DockerImage => "docker_image".to_string(), Template::DockerBuild => "docker_build".to_string(), - } + }; + write!(f, "{}", s) } } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index f84f24a..c146aad 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -92,6 +92,15 @@ pub(crate) struct ServeArgs { /// Maximum number of parallel calls to execute. Leave to 0 to use all available cores. #[clap(long, default_value = "0")] workers: usize, + /// Optional SSH connection string, if set commands will be executed over SSH on the given host. + #[clap(long)] + ssh: Option, + /// SSH key to use for authentication if --ssh is set. + #[clap(long, default_value = "~/.ssh/id_ed25519")] + ssh_key: String, + /// SSH passphrase to unlock the key. + #[clap(long)] + ssh_key_passphrase: Option, } #[derive(Debug, Args)] @@ -108,6 +117,15 @@ pub(crate) struct RunArgs { /// Execute the function without user interaction. #[clap(long, short = 'A')] auto: bool, + /// Optional SSH connection string, if set commands will be executed over SSH on the given host. + #[clap(long)] + ssh: Option, + /// SSH key to use for authentication if --ssh is set. + #[clap(long, default_value = "~/.ssh/id_ed25519")] + ssh_key: String, + /// SSH passphrase to unlock the key. + #[clap(long)] + ssh_key_passphrase: Option, } #[derive(Debug, Args)] diff --git a/src/cli/run.rs b/src/cli/run.rs index 6f27525..7a8336e 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -2,12 +2,24 @@ use std::{collections::BTreeMap, sync::Arc}; use crate::{ book::{flavors::openai, Book}, - runtime::{self, prompt}, + runtime::{self, prompt, ssh::SSHConnection}, }; use super::RunArgs; pub(crate) async fn run(args: RunArgs) -> anyhow::Result<()> { + // parse and validate SSH connection string if provided + let ssh = if let Some(ssh_str) = args.ssh { + // parse + let conn = SSHConnection::from_str(&ssh_str, &args.ssh_key, args.ssh_key_passphrase)?; + // make sure we can connect + conn.test_connection().await?; + + Some(conn) + } else { + None + }; + let book = Arc::new(Book::from_path(args.path, None)?); let function = book.get_function(&args.function)?; @@ -39,9 +51,7 @@ pub(crate) async fn run(args: RunArgs) -> anyhow::Result<()> { call_type: "function".to_string(), }; - log::debug!("running function {:?}", function); - - let result = runtime::execute_call(!args.auto, 10, book, call).await?; + let result = runtime::execute_call(ssh, !args.auto, 10, book, call).await?; println!("\n{}", result.content); diff --git a/src/cli/serve.rs b/src/cli/serve.rs index bf6b161..ea10723 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -14,12 +14,14 @@ use crate::book::{ Book, }; use crate::runtime; +use crate::runtime::ssh::SSHConnection; use super::ServeArgs; struct AppState { max_running_tasks: usize, book: Arc, + ssh: Option, } async fn not_found() -> actix_web::Result { @@ -65,7 +67,15 @@ async fn process_calls( state: web::Data>, calls: web::Json>, ) -> actix_web::Result { - match runtime::execute(false, state.book.clone(), calls.0, state.max_running_tasks).await { + match runtime::execute( + state.ssh.clone(), + false, + state.book.clone(), + calls.0, + state.max_running_tasks, + ) + .await + { Ok(resp) => Ok(HttpResponse::Ok().json(resp)), Err(e) => Err(actix_web::error::ErrorBadRequest(e)), } @@ -76,6 +86,18 @@ pub(crate) async fn serve(args: ServeArgs) -> anyhow::Result<()> { log::warn!("external address specified, this is an unsafe configuration as no authentication is provided"); } + // parse and validate SSH connection string if provided + let ssh = if let Some(ssh_str) = args.ssh { + // parse + let conn = SSHConnection::from_str(&ssh_str, &args.ssh_key, args.ssh_key_passphrase)?; + // make sure we can connect + conn.test_connection().await?; + + Some(conn) + } else { + None + }; + let book = Arc::new(Book::from_path(args.path, args.filter)?); if !args.lazy { for page in book.pages.values() { @@ -103,6 +125,7 @@ pub(crate) async fn serve(args: ServeArgs) -> anyhow::Result<()> { let app_state = Arc::new(AppState { max_running_tasks, book, + ssh, }); HttpServer::new(move || { diff --git a/src/cli/view.rs b/src/cli/view.rs index 416629a..b25a157 100644 --- a/src/cli/view.rs +++ b/src/cli/view.rs @@ -12,7 +12,7 @@ pub(crate) async fn view(args: ViewArgs) -> anyhow::Result<()> { println!(" * {} : {}", function_name, function.description); println!( " running with: {}", - ExecutionFlavor::for_function(&function)?.to_string() + ExecutionFlavor::for_function(&function)? ); println!(" parameters:"); for (parameter_name, parameter) in &function.parameters { diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index b3bd7c6..a99c599 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -6,9 +6,11 @@ mod cmd; mod docker; pub(crate) mod prompt; +pub(crate) mod ssh; pub(crate) use cmd::CommandLine; pub(crate) use docker::ContainerSource; +use ssh::SSHConnection; static ACTIVE_TASKS: AtomicUsize = AtomicUsize::new(0); @@ -49,6 +51,7 @@ async fn wait_for_available_tasks(max_running_tasks: usize) { } pub(crate) async fn execute_call( + ssh: Option, interactive: bool, max_running_tasks: usize, book: Arc, @@ -80,14 +83,37 @@ pub(crate) async fn execute_call( // validate runtime requirements let container = function.function.container.as_ref(); - let needs_container = - // we're running in non-interactive mode, can't sudo - (command_line.sudo && !interactive) || - // app not in $PATH - !command_line.app_in_path || - // forced container use - (container.is_some() && container.unwrap().force); + let mut needs_container = false; + let mut can_ssh = false; + + // if --ssh was provided + if let Some(ssh) = ssh.as_ref() { + // check if the app is in $PATH on the ssh host + can_ssh = ssh.app_in_path(&command_line.app).await?; + if !can_ssh { + log::warn!( + "{} not found in $PATH on {}", + command_line.app, + ssh.to_string() + ); + } + } + + // we are not going to use ssh, so we need to check if we need a container + if !can_ssh { + if command_line.sudo && !interactive { + // we're running in non-interactive mode, can't sudo + needs_container = true; + } else if !command_line.app_in_path { + // app not in $PATH, we need a container + needs_container = true; + } else if container.is_some() && container.unwrap().force { + // forced container use + needs_container = true; + } + } + // wrap the command line in a container if needed let command_line = if needs_container { let container = match container { Some(c) => c, @@ -111,7 +137,15 @@ pub(crate) async fn execute_call( command_line }; - log::warn!("executing: {}", &command_line); + if can_ssh { + log::warn!( + "executing (as {}): {}", + ssh.as_ref().unwrap().to_string(), + &command_line + ); + } else { + log::warn!("executing: {}", &command_line); + } if interactive && prompt::ask( @@ -127,7 +161,17 @@ pub(crate) async fn execute_call( } // finally execute the command line - let content = command_line.execute().await?; + let content = if can_ssh { + // execute via ssh + ssh.as_ref() + .unwrap() + .execute(command_line.sudo, &command_line.app, &command_line.args) + .await? + } else { + // execute locally + command_line.execute().await? + }; + Ok(openai::CallResultMessage { role: "tool".to_string(), call_id: call.id.clone(), @@ -136,6 +180,7 @@ pub(crate) async fn execute_call( } pub(crate) async fn execute( + ssh: Option, interactive: bool, book: Arc, calls: Vec, @@ -144,6 +189,7 @@ pub(crate) async fn execute( let mut futures = Vec::new(); for call in calls { futures.push(tokio::spawn(execute_call( + ssh.clone(), interactive, max_running_tasks, book.clone(), @@ -213,7 +259,7 @@ mod tests { }, }); - let result = execute_call(false, 10, book, call).await.unwrap(); + let result = execute_call(None, false, 10, book, call).await.unwrap(); assert_eq!(result.role, "tool"); assert_eq!(result.call_id, Some("test_call".to_string())); @@ -283,7 +329,7 @@ mod tests { }, }); - let results = execute(false, book, calls, 10).await.unwrap(); + let results = execute(None, false, book, calls, 10).await.unwrap(); assert_eq!(results.len(), 2); assert_eq!(results[0].content, "test1\n"); @@ -317,7 +363,7 @@ mod tests { }, }]; - let result = execute(false, Arc::clone(&book), calls, 10).await; + let result = execute(None, false, Arc::clone(&book), calls, 10).await; assert!(result.is_err()); } @@ -362,7 +408,7 @@ mod tests { }, }]; - let result = execute(false, Arc::clone(&book), calls, 10).await; + let result = execute(None, false, Arc::clone(&book), calls, 10).await; assert!(result.is_err()); } } diff --git a/src/runtime/ssh.rs b/src/runtime/ssh.rs new file mode 100644 index 0000000..18f999f --- /dev/null +++ b/src/runtime/ssh.rs @@ -0,0 +1,222 @@ +use std::{borrow::Cow, path::PathBuf}; + +use async_ssh2_tokio::{AuthMethod, Client, ServerCheckMethod}; + +#[derive(Debug, Clone)] +pub struct SSHConnection { + host: String, + port: u16, + user: String, + auth_method: AuthMethod, +} + +impl SSHConnection { + pub fn from_str(s: &str, public_key: &str, passphrase: Option) -> anyhow::Result { + let mut user = std::env::var("USER").unwrap_or_else(|_| "root".to_string()); + let host; + let mut port = 22; + + if s.is_empty() { + return Err(anyhow::anyhow!("SSH connection string cannot be empty")); + } + + // split on @ first to separate user if present + let parts: Vec<&str> = s.split('@').collect(); + match parts.len() { + // only.host or only.host:port + 1 => { + let host_parts: Vec<&str> = parts[0].split(':').collect(); + match host_parts.len() { + 1 => host = host_parts[0].to_string(), + 2 => { + host = host_parts[0].to_string(); + port = host_parts[1].parse()?; + } + _ => return Err(anyhow::anyhow!("invalid host format")), + } + } + // user@host or user@host:port + 2 => { + user = parts[0].to_string(); + let host_parts: Vec<&str> = parts[1].split(':').collect(); + match host_parts.len() { + 1 => host = host_parts[0].to_string(), + 2 => { + host = host_parts[0].to_string(); + port = host_parts[1].parse()?; + } + _ => return Err(anyhow::anyhow!("invalid host format")), + } + } + _ => return Err(anyhow::anyhow!("invalid SSH connection string format")), + } + + let public_key = shellexpand::full(public_key)?.to_string(); + let public_key = PathBuf::from(public_key); + if !public_key.exists() { + return Err(anyhow::anyhow!( + "public key file {} does not exist", + public_key.display() + )); + } + let public_key = public_key.canonicalize()?.to_string_lossy().to_string(); + + let auth_method = AuthMethod::with_key_file(&public_key, passphrase.as_deref()); + + Ok(Self { + host, + port, + user, + auth_method, + }) + } + + async fn client(&self) -> anyhow::Result { + Client::connect( + (self.host.as_str(), self.port), + self.user.as_str(), + self.auth_method.clone(), + ServerCheckMethod::NoCheck, + ) + .await + .map_err(|e| anyhow::anyhow!("failed to connect to SSH server: {:?}", e)) + } + + fn create_command_line(with_sudo: bool, app: &str, args: &Vec) -> String { + let mut command = String::new(); + if with_sudo { + command.push_str("sudo "); + } + + command.push_str(&shell_escape::escape(Cow::Borrowed(app))); + + for arg in args { + command.push(' '); + command.push_str(&shell_escape::escape(Cow::Borrowed(arg))); + } + + command + } + + pub(crate) async fn execute( + &self, + with_sudo: bool, + app: &str, + args: &Vec, + ) -> anyhow::Result { + let command_line = Self::create_command_line(with_sudo, app, args); + let result = self.client().await?.execute(&command_line).await?; + + let mut parts = vec![]; + + if result.exit_status != 0 { + parts.push(format!("EXIT CODE: {}", &result.exit_status)); + } + + if !result.stdout.is_empty() { + parts.push(result.stdout.to_string()); + } + + if !result.stderr.is_empty() { + if result.exit_status == 0 { + parts.push(result.stderr.to_string()); + } else { + parts.push(format!("ERROR: {}", result.stderr)); + } + } + + Ok(parts.join("\n")) + } + + pub(crate) async fn test_connection(&self) -> anyhow::Result<()> { + log::info!("testing ssh connection to {}:{} ...", self.host, self.port); + let result = self.client().await?.execute("echo robopages").await?; + if result.exit_status != 0 { + return Err(anyhow::anyhow!("failed to execute command: {:?}", result)); + } else if result.stdout != "robopages\n" { + return Err(anyhow::anyhow!("unexpected output: {:?}", result)); + } + + Ok(()) + } + + pub(crate) async fn app_in_path(&self, app: &str) -> anyhow::Result { + let result = self + .client() + .await? + .execute(&format!("which {}", app)) + .await?; + + Ok(result.exit_status == 0) + } +} + +impl std::fmt::Display for SSHConnection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}@{}:{}", self.user, self.host, self.port) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_str_host_only() { + let conn = SSHConnection::from_str("example.com", "/dev/null", None).unwrap(); + assert_eq!(conn.host, "example.com"); + assert_eq!(conn.port, 22); + assert_eq!( + conn.user, + std::env::var("USER").unwrap_or_else(|_| "root".to_string()) + ); + } + + #[test] + fn test_from_str_host_and_port() { + let conn = SSHConnection::from_str("example.com:2222", "/dev/null", None).unwrap(); + assert_eq!(conn.host, "example.com"); + assert_eq!(conn.port, 2222); + assert_eq!( + conn.user, + std::env::var("USER").unwrap_or_else(|_| "root".to_string()) + ); + } + + #[test] + fn test_from_str_user_and_host() { + let conn = SSHConnection::from_str("testuser@example.com", "/dev/null", None).unwrap(); + assert_eq!(conn.host, "example.com"); + assert_eq!(conn.port, 22); + assert_eq!(conn.user, "testuser"); + } + + #[test] + fn test_from_str_full() { + let conn = SSHConnection::from_str("testuser@example.com:2222", "/dev/null", None).unwrap(); + assert_eq!(conn.host, "example.com"); + assert_eq!(conn.port, 2222); + assert_eq!(conn.user, "testuser"); + } + + #[test] + fn test_from_str_empty() { + assert!(SSHConnection::from_str("", "/dev/null", None).is_err()); + } + + #[test] + fn test_from_str_invalid_port() { + assert!(SSHConnection::from_str("example.com:invalid", "/dev/null", None).is_err()); + } + + #[test] + fn test_from_str_invalid_format() { + assert!(SSHConnection::from_str("user@host@extra", "/dev/null", None).is_err()); + assert!(SSHConnection::from_str("host:port:extra", "/dev/null", None).is_err()); + } + + #[test] + fn test_from_str_nonexistent_key() { + assert!(SSHConnection::from_str("example.com", "/nonexistent/key/path", None).is_err()); + } +}