Browse Source

replace ssh2 with russh

Eugene 1 year ago
parent
commit
aab7e285a9

+ 31 - 8
.github/workflows/build.yml

@@ -37,9 +37,15 @@ jobs:
       matrix:
       matrix:
         include:
         include:
           - arch: x86_64
           - arch: x86_64
+            rust_triple: x86_64-apple-darwin
           - arch: arm64
           - arch: arm64
+            rust_triple: aarch64-apple-darwin
       fail-fast: false
       fail-fast: false
 
 
+    env:
+      ARCH: ${{matrix.arch}}
+      RUST_TARGET_TRIPLE: ${{matrix.rust_triple}}
+
     steps:
     steps:
     - name: Checkout
     - name: Checkout
       uses: actions/checkout@v3
       uses: actions/checkout@v3
@@ -51,6 +57,8 @@ jobs:
       with:
       with:
         node-version: 18
         node-version: 18
 
 
+    - run: rustup target add ${{matrix.rust_triple}}
+
     - name: Install deps
     - name: Install deps
       run: |
       run: |
         sudo -H pip3 install setuptools
         sudo -H pip3 install setuptools
@@ -59,12 +67,6 @@ jobs:
       env:
       env:
         ARCH: ${{matrix.arch}}
         ARCH: ${{matrix.arch}}
 
 
-    - name: Fix cross build
-      run: |
-        rm -rf app/node_modules/cpu-features
-        rm -rf app/node_modules/ssh2/crypto/build
-      if: matrix.arch == 'arm64'
-
     - name: Webpack
     - name: Webpack
       run: yarn run build
       run: yarn run build
 
 
@@ -136,18 +138,24 @@ jobs:
         include:
         include:
           - build-arch: x64
           - build-arch: x64
             arch: amd64
             arch: amd64
+            rust_triple: x86_64-unknown-linux-gnu
           - build-arch: arm64
           - build-arch: arm64
             arch: arm64
             arch: arm64
+            rust_triple: aarch64-unknown-linux-gnu
             triplet: aarch64-linux-gnu-
             triplet: aarch64-linux-gnu-
           - build-arch: arm
           - build-arch: arm
             arch: armhf
             arch: armhf
+            rust_triple: arm-unknown-linux-gnueabihf
             triplet: arm-linux-gnueabihf-
             triplet: arm-linux-gnueabihf-
+      fail-fast: false
+
     env:
     env:
       CC: ${{matrix.triplet}}gcc
       CC: ${{matrix.triplet}}gcc
       CXX: ${{matrix.triplet}}g++
       CXX: ${{matrix.triplet}}g++
       ARCH: ${{matrix.build-arch}}
       ARCH: ${{matrix.build-arch}}
       npm_config_arch: ${{matrix.build-arch}}
       npm_config_arch: ${{matrix.build-arch}}
       npm_config_target_arch: ${{matrix.build-arch}}
       npm_config_target_arch: ${{matrix.build-arch}}
+      RUST_TARGET_TRIPLE: ${{matrix.rust_triple}}
 
 
     steps:
     steps:
     - name: Checkout
     - name: Checkout
@@ -160,6 +168,8 @@ jobs:
       with:
       with:
         node-version: 18
         node-version: 18
 
 
+    - run: rustup target add ${{matrix.rust_triple}}
+
     - name: Install dependencies
     - name: Install dependencies
       run: |
       run: |
         sudo apt-get update
         sudo apt-get update
@@ -280,17 +290,22 @@ jobs:
         path: tabby-web.tar.gz
         path: tabby-web.tar.gz
       if: matrix.build-arch == 'x64'
       if: matrix.build-arch == 'x64'
 
 
-
   Windows-Build:
   Windows-Build:
-    runs-on: windows-2022
+    runs-on: windows-latest
     needs: Lint
     needs: Lint
     strategy:
     strategy:
       matrix:
       matrix:
         include:
         include:
           - arch: x64
           - arch: x64
+            rust_triple: x86_64-pc-windows-msvc
           - arch: arm64
           - arch: arm64
+            rust_triple: aarch64-pc-windows-msvc
       fail-fast: false
       fail-fast: false
 
 
+    env:
+      RUST_TARGET_TRIPLE: ${{matrix.rust_triple}}
+      ARCH: ${{matrix.arch}}
+
     steps:
     steps:
     - name: Checkout
     - name: Checkout
       uses: actions/checkout@v3
       uses: actions/checkout@v3
@@ -302,6 +317,14 @@ jobs:
       with:
       with:
         node-version: 18
         node-version: 18
 
 
+    - run: npm i -g npx
+    - run: rustup target add ${{matrix.rust_triple}}
+
+    - name: Update node-gyp
+      run: |
+        npm install --global [email protected]
+        npm prefix -g | % {npm config set node_gyp "$_\node_modules\node-gyp\bin\node-gyp.js"}
+
     - name: Build
     - name: Build
       shell: powershell
       shell: powershell
       run: |
       run: |

+ 0 - 1
.gitignore

@@ -33,7 +33,6 @@ docs/api
 sentry.properties
 sentry.properties
 sentry-symbols.js
 sentry-symbols.js
 
 
-tabby-ssh/util/pagent.exe
 *.psd
 *.psd
 
 
 crowdin.yml
 crowdin.yml

+ 2 - 1
app/package.json

@@ -16,7 +16,7 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@electron/remote": "^2",
     "@electron/remote": "^2",
-    "node-pty": "^1.0",
+    "node-pty": "^1.1.0-beta.14",
     "any-promise": "^1.3.0",
     "any-promise": "^1.3.0",
     "electron-config": "2.0.0",
     "electron-config": "2.0.0",
     "electron-debug": "^3.2.0",
     "electron-debug": "^3.2.0",
@@ -30,6 +30,7 @@
     "native-process-working-directory": "^1.0.2",
     "native-process-working-directory": "^1.0.2",
     "npm": "6",
     "npm": "6",
     "rxjs": "^7.5.7",
     "rxjs": "^7.5.7",
+    "russh": "0.0.3",
     "source-map-support": "^0.5.20",
     "source-map-support": "^0.5.20",
     "v8-compile-cache": "^2.3.0",
     "v8-compile-cache": "^2.3.0",
     "yargs": "^17.7.2"
     "yargs": "^17.7.2"

+ 70 - 30
app/yarn.lock

@@ -28,6 +28,11 @@
     wrap-ansi "^8.1.0"
     wrap-ansi "^8.1.0"
     wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
     wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
 
 
+"@napi-rs/cli@^2.18.3":
+  version "2.18.4"
+  resolved "https://registry.yarnpkg.com/@napi-rs/cli/-/cli-2.18.4.tgz#12bebfb7995902fa7ab43cc0b155a7f5a2caa873"
+  integrity sha512-SgJeA4df9DE2iAEpr3M2H0OKl/yjtg1BnRI5/JyowS71tUWhrfSu2LT0V3vlHET+g1hBVlrO60PmEXwUEKp8Mg==
+
 "@ngx-translate/core@^14.0.0":
 "@ngx-translate/core@^14.0.0":
   version "14.0.0"
   version "14.0.0"
   resolved "https://registry.yarnpkg.com/@ngx-translate/core/-/core-14.0.0.tgz#af421d0e1a28376843f0fed375cd2fae7630a5ff"
   resolved "https://registry.yarnpkg.com/@ngx-translate/core/-/core-14.0.0.tgz#af421d0e1a28376843f0fed375cd2fae7630a5ff"
@@ -1490,25 +1495,26 @@ [email protected]:
     x11 "^2.3.0"
     x11 "^2.3.0"
 
 
 glob@^10.2.2, glob@^10.3.10:
 glob@^10.2.2, glob@^10.3.10:
-  version "10.3.10"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b"
-  integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==
+  version "10.4.5"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
+  integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
   dependencies:
   dependencies:
     foreground-child "^3.1.0"
     foreground-child "^3.1.0"
-    jackspeak "^2.3.5"
-    minimatch "^9.0.1"
-    minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
-    path-scurry "^1.10.1"
+    jackspeak "^3.1.2"
+    minimatch "^9.0.4"
+    minipass "^7.1.2"
+    package-json-from-dist "^1.0.0"
+    path-scurry "^1.11.1"
 
 
 glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
 glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
-  version "7.1.6"
-  resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz"
-  integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+  version "7.2.3"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
+  integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
   dependencies:
   dependencies:
     fs.realpath "^1.0.0"
     fs.realpath "^1.0.0"
     inflight "^1.0.4"
     inflight "^1.0.4"
     inherits "2"
     inherits "2"
-    minimatch "^3.0.4"
+    minimatch "^3.1.1"
     once "^1.3.0"
     once "^1.3.0"
     path-is-absolute "^1.0.0"
     path-is-absolute "^1.0.0"
 
 
@@ -1931,10 +1937,10 @@ isstream@~0.1.2:
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz"
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz"
   integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
   integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
 
 
-jackspeak@^2.3.5:
-  version "2.3.6"
-  resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8"
-  integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==
+jackspeak@^3.1.2:
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a"
+  integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==
   dependencies:
   dependencies:
     "@isaacs/cliui" "^8.0.2"
     "@isaacs/cliui" "^8.0.2"
   optionalDependencies:
   optionalDependencies:
@@ -2286,13 +2292,18 @@ lowercase-keys@^1.0.0:
   resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
   resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
   integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
   integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
 
 
-lru-cache@^10.0.1, "lru-cache@^9.1.1 || ^10.0.0":
+lru-cache@^10.0.1:
   version "10.0.2"
   version "10.0.2"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.2.tgz#34504678cc3266b09b8dfd6fab4e1515258271b7"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.2.tgz#34504678cc3266b09b8dfd6fab4e1515258271b7"
   integrity sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==
   integrity sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==
   dependencies:
   dependencies:
     semver "^7.3.5"
     semver "^7.3.5"
 
 
+lru-cache@^10.2.0:
+  version "10.4.3"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
+  integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
+
 lru-cache@^4.0.1:
 lru-cache@^4.0.1:
   version "4.1.5"
   version "4.1.5"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
@@ -2412,10 +2423,17 @@ minimatch@^3.0.4:
   dependencies:
   dependencies:
     brace-expansion "^1.1.7"
     brace-expansion "^1.1.7"
 
 
-minimatch@^9.0.1:
-  version "9.0.3"
-  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825"
-  integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==
+minimatch@^3.1.1:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+  integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimatch@^9.0.4:
+  version "9.0.5"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
+  integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
   dependencies:
   dependencies:
     brace-expansion "^2.0.1"
     brace-expansion "^2.0.1"
 
 
@@ -2488,6 +2506,11 @@ minipass@^5.0.0:
   resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c"
   resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c"
   integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==
   integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==
 
 
+minipass@^7.1.2:
+  version "7.1.2"
+  resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
+  integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
+
 minizlib@^1.3.3:
 minizlib@^1.3.3:
   version "1.3.3"
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
   resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
@@ -2640,6 +2663,11 @@ node-addon-api@^4.0.0, node-addon-api@^4.3.0:
   resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f"
   resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f"
   integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==
   integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==
 
 
+node-addon-api@^7.1.0:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
+  integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
+
 node-fetch-npm@^2.0.2:
 node-fetch-npm@^2.0.2:
   version "2.0.4"
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz#6507d0e17a9ec0be3bec516958a497cec54bf5a4"
   resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz#6507d0e17a9ec0be3bec516958a497cec54bf5a4"
@@ -2670,12 +2698,12 @@ node-gyp@^10.0.0, node-gyp@^5.0.2, node-gyp@^5.1.0:
     tar "^6.1.2"
     tar "^6.1.2"
     which "^4.0.0"
     which "^4.0.0"
 
 
-node-pty@^1.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.0.0.tgz#7daafc0aca1c4ca3de15c61330373af4af5861fd"
-  integrity sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==
+node-pty@^1.1.0-beta.14:
+  version "1.1.0-beta9"
+  resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.1.0-beta9.tgz#ed643cb3b398d031b4e31c216e8f3b0042435f1d"
+  integrity sha512-/Ue38pvXJdgRZ3+me1FgfglLd301GhJN0NStiotdt61tm43N5htUyR/IXOUzOKuNaFmCwIhy6nwb77Ky41LMbw==
   dependencies:
   dependencies:
-    nan "^2.17.0"
+    node-addon-api "^7.1.0"
 
 
 nopt@^4.0.3:
 nopt@^4.0.3:
   version "4.0.3"
   version "4.0.3"
@@ -3097,6 +3125,11 @@ p-try@^2.0.0:
   resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz"
   resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz"
   integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
   integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
 
 
+package-json-from-dist@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00"
+  integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==
+
 package-json@^4.0.0:
 package-json@^4.0.0:
   version "4.0.1"
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed"
   resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed"
@@ -3209,12 +3242,12 @@ path-parse@^1.0.6:
   resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
   resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
   integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
   integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
 
 
-path-scurry@^1.10.1:
-  version "1.10.1"
-  resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698"
-  integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==
+path-scurry@^1.11.1:
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2"
+  integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==
   dependencies:
   dependencies:
-    lru-cache "^9.1.1 || ^10.0.0"
+    lru-cache "^10.2.0"
     minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
     minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
 
 
 path-type@^2.0.0:
 path-type@^2.0.0:
@@ -3603,6 +3636,13 @@ run-queue@^1.0.0, run-queue@^1.0.3:
   dependencies:
   dependencies:
     aproba "^1.1.1"
     aproba "^1.1.1"
 
 
[email protected]:
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/russh/-/russh-0.0.3.tgz#bcb53d2efbe2b216857171bc5ca2131001ddfa46"
+  integrity sha512-iTW4M5W856zYjbjQYjlDFaSFSQ6pLBy+zsCYFwhivYuj8U5mZ7kF7TeGOUat9t4l25HVxAS36ivCt5l79p9lcQ==
+  dependencies:
+    "@napi-rs/cli" "^2.18.3"
+
 rxjs@^7.5.2, rxjs@^7.5.7:
 rxjs@^7.5.2, rxjs@^7.5.7:
   version "7.5.7"
   version "7.5.7"
   resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39"
   resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39"

+ 0 - 1
package.json

@@ -76,7 +76,6 @@
     "source-code-pro": "^2.38.0",
     "source-code-pro": "^2.38.0",
     "source-map-loader": "^4.0.1",
     "source-map-loader": "^4.0.1",
     "source-sans-pro": "3.6.0",
     "source-sans-pro": "3.6.0",
-    "ssh2": "^1.14.0",
     "style-loader": "^3.3.1",
     "style-loader": "^3.3.1",
     "svg-inline-loader": "^0.8.2",
     "svg-inline-loader": "^0.8.2",
     "thenby": "^1.3.4",
     "thenby": "^1.3.4",

+ 0 - 39
patches/ssh2+1.11.0.patch

@@ -1,39 +0,0 @@
-diff --git a/node_modules/ssh2/lib/protocol/keyParser.js b/node_modules/ssh2/lib/protocol/keyParser.js
-index 9860e3f..ee82e51 100644
---- a/node_modules/ssh2/lib/protocol/keyParser.js
-+++ b/node_modules/ssh2/lib/protocol/keyParser.js
-@@ -15,6 +15,7 @@ const {
-   sign: sign_,
-   verify: verify_,
- } = require('crypto');
-+const { createVerify: createVerifyDSS } = require('browserify-sign')
- const supportedOpenSSLCiphers = getCiphers();
- 
- const { Ber } = require('asn1');
-@@ -404,6 +405,17 @@ const BaseKey = {
-           return new Error('No public key available');
-         if (!algo || typeof algo !== 'string')
-           algo = this[SYM_HASH_ALGO];
-+
-+        if (algo === 'dss1') {
-+          const verifier = createVerifyDSS('DSA-SHA1');
-+          verifier.update(data);
-+          try {
-+            return verifier.verify(pem, signature);
-+          } catch (ex) {
-+            return ex;
-+          }
-+        }
-+
-         try {
-           return verify_(algo, data, pem, signature);
-         } catch (ex) {
-@@ -1343,7 +1355,7 @@ function parseDER(data, baseType, comment, fullType) {
-         return new Error('Malformed OpenSSH public key');
-       pubPEM = genOpenSSLDSAPub(p, q, g, y);
-       pubSSH = genOpenSSHDSAPub(p, q, g, y);
--      algo = 'sha1';
-+      algo = 'dss1';
-       break;
-     }
-     case 'ssh-ed25519': {

+ 11 - 9
tabby-core/src/api/platform.ts

@@ -63,22 +63,24 @@ export abstract class FileTransfer {
 }
 }
 
 
 export abstract class FileDownload extends FileTransfer {
 export abstract class FileDownload extends FileTransfer {
-    abstract write (buffer: Buffer): Promise<void>
+    abstract write (buffer: Uint8Array): Promise<void>
 }
 }
 
 
 export abstract class FileUpload extends FileTransfer {
 export abstract class FileUpload extends FileTransfer {
-    abstract read (): Promise<Buffer>
+    abstract read (): Promise<Uint8Array>
 
 
-    async readAll (): Promise<Buffer> {
-        const buffers: Buffer[] = []
+    async readAll (): Promise<Uint8Array> {
+        const result = new Uint8Array(this.getSize())
+        let pos = 0
         while (true) {
         while (true) {
             const buf = await this.read()
             const buf = await this.read()
             if (!buf.length) {
             if (!buf.length) {
                 break
                 break
             }
             }
-            buffers.push(Buffer.from(buf))
+            result.set(buf, pos)
+            pos += buf.length
         }
         }
-        return Buffer.concat(buffers)
+        return result
     }
     }
 }
 }
 
 
@@ -261,12 +263,12 @@ export class HTMLFileUpload extends FileUpload {
         return this.file.size
         return this.file.size
     }
     }
 
 
-    async read (): Promise<Buffer> {
+    async read (): Promise<Uint8Array> {
         const result: any = await this.reader.read()
         const result: any = await this.reader.read()
         if (result.done || !result.value) {
         if (result.done || !result.value) {
-            return Buffer.from('')
+            return new Uint8Array(0)
         }
         }
-        const chunk = Buffer.from(result.value)
+        const chunk = new Uint8Array(result.value)
         this.increaseProgress(chunk.length)
         this.increaseProgress(chunk.length)
         return chunk
         return chunk
     }
     }

+ 1 - 1
tabby-core/src/services/vault.service.ts

@@ -306,7 +306,7 @@ export class VaultFileProvider extends FileProvider {
                 id,
                 id,
                 description: `${description} (${transfer.getName()})`,
                 description: `${description} (${transfer.getName()})`,
             },
             },
-            value: (await transfer.readAll()).toString('base64'),
+            value: Buffer.from(await transfer.readAll()).toString('base64'),
         })
         })
         return `${this.prefix}${id}`
         return `${this.prefix}${id}`
     }
     }

+ 4 - 4
tabby-electron/src/services/platform.service.ts

@@ -300,12 +300,12 @@ class ElectronFileUpload extends FileUpload {
     private size: number
     private size: number
     private mode: number
     private mode: number
     private file: fs.FileHandle
     private file: fs.FileHandle
-    private buffer: Buffer
+    private buffer: Uint8Array
     private powerSaveBlocker = 0
     private powerSaveBlocker = 0
 
 
     constructor (private filePath: string, private electron: ElectronService) {
     constructor (private filePath: string, private electron: ElectronService) {
         super()
         super()
-        this.buffer = Buffer.alloc(256 * 1024)
+        this.buffer = new Uint8Array(256 * 1024)
         this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension')
         this.powerSaveBlocker = electron.powerSaveBlocker.start('prevent-app-suspension')
     }
     }
 
 
@@ -328,7 +328,7 @@ class ElectronFileUpload extends FileUpload {
         return this.size
         return this.size
     }
     }
 
 
-    async read (): Promise<Buffer> {
+    async read (): Promise<Uint8Array> {
         const result = await this.file.read(this.buffer, 0, this.buffer.length, null)
         const result = await this.file.read(this.buffer, 0, this.buffer.length, null)
         this.increaseProgress(result.bytesRead)
         this.increaseProgress(result.bytesRead)
         return this.buffer.slice(0, result.bytesRead)
         return this.buffer.slice(0, result.bytesRead)
@@ -370,7 +370,7 @@ class ElectronFileDownload extends FileDownload {
         return this.size
         return this.size
     }
     }
 
 
-    async write (buffer: Buffer): Promise<void> {
+    async write (buffer: Uint8Array): Promise<void> {
         let pos = 0
         let pos = 0
         while (pos < buffer.length) {
         while (pos < buffer.length) {
             const result = await this.file.write(buffer, pos, buffer.length - pos, null)
             const result = await this.file.write(buffer, pos, buffer.length - pos, null)

+ 19 - 14
tabby-electron/src/sftpContextMenu.ts

@@ -49,19 +49,24 @@ export class EditSFTPContextMenu extends SFTPContextMenuItemProvider {
         this.platform.openPath(tempPath)
         this.platform.openPath(tempPath)
 
 
         const events = new Subject<string>()
         const events = new Subject<string>()
-        const watcher = fs.watch(tempPath, event => events.next(event))
-        events.pipe(debounceTime(1000), debounce(async event => {
-            if (event === 'rename') {
-                watcher.close()
-            }
-            const upload = await this.platform.startUpload({ multiple: false }, [tempPath])
-            if (!upload.length) {
-                return
-            }
-            await sftp.upload(item.fullPath, upload[0])
-            await sftp.chmod(item.fullPath, item.mode)
-        })).subscribe()
-        watcher.on('close', () => events.complete())
-        sftp.closed$.subscribe(() => watcher.close())
+        fs.chmodSync(tempPath, 0o700)
+
+        // skip the first burst of events
+        setTimeout(() => {
+            const watcher = fs.watch(tempPath, event => events.next(event))
+            events.pipe(debounceTime(1000), debounce(async event => {
+                if (event === 'rename') {
+                    watcher.close()
+                }
+                const upload = await this.platform.startUpload({ multiple: false }, [tempPath])
+                if (!upload.length) {
+                    return
+                }
+                await sftp.upload(item.fullPath, upload[0])
+                await sftp.chmod(item.fullPath, item.mode)
+            })).subscribe()
+            watcher.on('close', () => events.complete())
+            sftp.closed$.subscribe(() => watcher.close())
+        }, 1000)
     }
     }
 }
 }

+ 1 - 1
tabby-settings/src/components/vaultSettingsTab.component.ts

@@ -123,7 +123,7 @@ export class VaultSettingsTabComponent extends BaseComponent {
         }
         }
         await this.vault.updateSecret(secret, {
         await this.vault.updateSecret(secret, {
             ...secret,
             ...secret,
-            value: (await transfers[0].readAll()).toString('base64'),
+            value: Buffer.from(await transfers[0].readAll()).toString('base64'),
         })
         })
         this.loadVault()
         this.loadVault()
     }
     }

+ 4 - 6
tabby-ssh/package.json

@@ -11,22 +11,17 @@
     "build": "webpack --progress --color",
     "build": "webpack --progress --color",
     "watch": "webpack --progress --color --watch",
     "watch": "webpack --progress --color --watch",
     "postinstall": "run-script-os",
     "postinstall": "run-script-os",
-    "postinstall:darwin:linux": "exit",
-    "postinstall:win32": "xcopy /i /y ..\\node_modules\\ssh2\\util\\pagent.exe util\\"
+    "postinstall:darwin:linux": "exit"
   },
   },
   "files": [
   "files": [
     "dist",
     "dist",
-    "util/pagent.exe",
     "typings"
     "typings"
   ],
   ],
   "author": "Eugene Pankov",
   "author": "Eugene Pankov",
   "license": "MIT",
   "license": "MIT",
   "devDependencies": {
   "devDependencies": {
     "@types/node": "20.3.1",
     "@types/node": "20.3.1",
-    "@types/ssh2": "^0.5.46",
     "ansi-colors": "^4.1.1",
     "ansi-colors": "^4.1.1",
-    "diffie-hellman": "^5.0.3",
-    "sshpk": "Eugeny/node-sshpk#c2b71d1243714d2daf0988f84c3323d180817136",
     "strip-ansi": "^7.0.0"
     "strip-ansi": "^7.0.0"
   },
   },
   "dependencies": {
   "dependencies": {
@@ -45,5 +40,8 @@
     "tabby-core": "*",
     "tabby-core": "*",
     "tabby-settings": "*",
     "tabby-settings": "*",
     "tabby-terminal": "*"
     "tabby-terminal": "*"
+  },
+  "resolutions": {
+    "glob": "7.2.3"
   }
   }
 }
 }

+ 42 - 17
tabby-ssh/src/algorithms.ts

@@ -1,20 +1,45 @@
-import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
-import { ALGORITHM_BLACKLIST, SSHAlgorithmType } from './api'
+import * as russh from 'russh'
+import { SSHAlgorithmType } from './api'
 
 
-// Counteracts https://github.com/mscdex/ssh2/commit/f1b5ac3c81734c194740016eab79a699efae83d8
-ALGORITHMS.DEFAULT_CIPHER.push('aes128-gcm')
-ALGORITHMS.DEFAULT_CIPHER.push('aes256-gcm')
-ALGORITHMS.SUPPORTED_CIPHER.push('aes128-gcm')
-ALGORITHMS.SUPPORTED_CIPHER.push('aes256-gcm')
-
-export const supportedAlgorithms: Record<string, string> = {}
+export const supportedAlgorithms = {
+    [SSHAlgorithmType.KEX]: russh.getSupportedKexAlgorithms().filter(x => x !== 'none'),
+    [SSHAlgorithmType.HOSTKEY]: russh.getSupportedKeyTypes().filter(x => x !== 'none'),
+    [SSHAlgorithmType.CIPHER]: russh.getSupportedCiphers().filter(x => x !== 'clear'),
+    [SSHAlgorithmType.HMAC]: russh.getSupportedMACs().filter(x => x !== 'none'),
+}
 
 
-for (const k of Object.values(SSHAlgorithmType)) {
-    const supportedAlg = {
-        [SSHAlgorithmType.KEX]: 'SUPPORTED_KEX',
-        [SSHAlgorithmType.HOSTKEY]: 'SUPPORTED_SERVER_HOST_KEY',
-        [SSHAlgorithmType.CIPHER]: 'SUPPORTED_CIPHER',
-        [SSHAlgorithmType.HMAC]: 'SUPPORTED_MAC',
-    }[k]
-    supportedAlgorithms[k] = ALGORITHMS[supportedAlg].filter(x => !ALGORITHM_BLACKLIST.includes(x)).sort()
+export const defaultAlgorithms = {
+    [SSHAlgorithmType.KEX]: [
+        'curve25519-sha256',
+        '[email protected]',
+        'diffie-hellman-group16-sha512',
+        'diffie-hellman-group14-sha256',
+        'ext-info-c',
+        'ext-info-s',
+        '[email protected]',
+        '[email protected]',
+    ],
+    [SSHAlgorithmType.HOSTKEY]: [
+        'ssh-ed25519',
+        'ecdsa-sha2-nistp256',
+        'ecdsa-sha2-nistp521',
+        'rsa-sha2-256',
+        'rsa-sha2-512',
+        'ssh-rsa',
+    ],
+    [SSHAlgorithmType.CIPHER]: [
+        '[email protected]',
+        '[email protected]',
+        'aes256-ctr',
+        'aes192-ctr',
+        'aes128-ctr',
+    ],
+    [SSHAlgorithmType.HMAC]: [
+        '[email protected]',
+        '[email protected]',
+        'hmac-sha2-512',
+        'hmac-sha2-256',
+        '[email protected]',
+        'hmac-sha1',
+    ],
 }
 }

+ 0 - 1
tabby-ssh/src/api/index.ts

@@ -1,5 +1,4 @@
 export * from './contextMenu'
 export * from './contextMenu'
 export * from './interfaces'
 export * from './interfaces'
 export * from './importer'
 export * from './importer'
-export * from './proxyStream'
 export { SSHMultiplexerService } from '../services/sshMultiplexer.service'
 export { SSHMultiplexerService } from '../services/sshMultiplexer.service'

+ 0 - 10
tabby-ssh/src/api/interfaces.ts

@@ -51,13 +51,3 @@ export interface ForwardedPortConfig {
     targetPort: number
     targetPort: number
     description: string
     description: string
 }
 }
-
-export let ALGORITHM_BLACKLIST = [
-    // cause native crashes in node crypto, use EC instead
-    'diffie-hellman-group-exchange-sha256',
-    'diffie-hellman-group-exchange-sha1',
-]
-
-if (!process.env.TABBY_ENABLE_SSH_ALG_BLACKLIST) {
-    ALGORITHM_BLACKLIST = []
-}

+ 0 - 61
tabby-ssh/src/api/proxyStream.ts

@@ -1,61 +0,0 @@
-import { Observable, Subject } from 'rxjs'
-import { Duplex } from 'stream'
-
-export class SSHProxyStreamSocket extends Duplex {
-    constructor (private parent: SSHProxyStream) {
-        super({
-            allowHalfOpen: false,
-        })
-    }
-
-    _read (size: number): void {
-        this.parent.requestData(size)
-    }
-
-    _write (chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void {
-        this.parent.consumeInput(chunk).then(() => callback(null), e => callback(e))
-    }
-
-    _destroy (error: Error|null, callback: (error: Error|null) => void): void {
-        this.parent.handleStopRequest(error).then(() => callback(null), e => callback(e))
-    }
-}
-
-export abstract class SSHProxyStream {
-    get message$ (): Observable<string> { return this.message }
-    get destroyed$ (): Observable<Error|null> { return this.destroyed }
-    get socket (): SSHProxyStreamSocket|null { return this._socket }
-    private message = new Subject<string>()
-    private destroyed = new Subject<Error|null>()
-    private _socket: SSHProxyStreamSocket|null = null
-
-    async start (): Promise<SSHProxyStreamSocket> {
-        if (!this._socket) {
-            this._socket = new SSHProxyStreamSocket(this)
-        }
-        return this._socket
-    }
-
-    // eslint-disable-next-line @typescript-eslint/no-unused-vars
-    abstract requestData (size: number): void
-
-    abstract consumeInput (data: Buffer): Promise<void>
-
-    protected emitMessage (message: string): void {
-        this.message.next(message)
-    }
-
-    protected emitOutput (data: Buffer): void {
-        this._socket?.push(data)
-    }
-
-    async handleStopRequest (error: Error|null): Promise<void> {
-        this.destroyed.next(error)
-        this.destroyed.complete()
-        this.message.complete()
-    }
-
-    stop (error?: Error): void {
-        this._socket?.destroy(error)
-    }
-}

+ 10 - 2
tabby-ssh/src/components/keyboardInteractiveAuthPanel.component.pug

@@ -14,11 +14,19 @@ input.form-control.mt-2(
 )
 )
 
 
 .d-flex.mt-3
 .d-flex.mt-3
-    button.btn.btn-secondary(
+    checkbox(
+        *ngIf='isPassword()',
+        [(ngModel)]='remember',
+        [text]='"Save password"|translate'
+    )
+
+    .ms-auto
+
+    button.btn.btn-secondary.me-3(
         *ngIf='step > 0',
         *ngIf='step > 0',
         (click)='previous()'
         (click)='previous()'
     )
     )
-    .ms-auto
+
     button.btn.btn-primary(
     button.btn.btn-primary(
         (click)='next()'
         (click)='next()'
     )
     )

+ 11 - 2
tabby-ssh/src/components/keyboardInteractiveAuthPanel.component.ts

@@ -1,6 +1,7 @@
 import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, ChangeDetectionStrategy } from '@angular/core'
 import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, ChangeDetectionStrategy } from '@angular/core'
 import { KeyboardInteractivePrompt } from '../session/ssh'
 import { KeyboardInteractivePrompt } from '../session/ssh'
-
+import { SSHProfile } from '../api'
+import { PasswordStorageService } from '../services/passwordStorage.service'
 
 
 @Component({
 @Component({
     selector: 'keyboard-interactive-auth-panel',
     selector: 'keyboard-interactive-auth-panel',
@@ -9,13 +10,17 @@ import { KeyboardInteractivePrompt } from '../session/ssh'
     changeDetection: ChangeDetectionStrategy.OnPush,
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 })
 export class KeyboardInteractiveAuthComponent {
 export class KeyboardInteractiveAuthComponent {
+    @Input() profile: SSHProfile
     @Input() prompt: KeyboardInteractivePrompt
     @Input() prompt: KeyboardInteractivePrompt
     @Input() step = 0
     @Input() step = 0
     @Output() done = new EventEmitter()
     @Output() done = new EventEmitter()
     @ViewChild('input') input: ElementRef
     @ViewChild('input') input: ElementRef
+    remember = false
+
+    constructor (private passwordStorage: PasswordStorageService) {}
 
 
     isPassword (): boolean {
     isPassword (): boolean {
-        return this.prompt.prompts[this.step].prompt.toLowerCase().includes('password') || !this.prompt.prompts[this.step].echo
+        return this.prompt.isAPasswordPrompt(this.step)
     }
     }
 
 
     previous (): void {
     previous (): void {
@@ -26,6 +31,10 @@ export class KeyboardInteractiveAuthComponent {
     }
     }
 
 
     next (): void {
     next (): void {
+        if (this.isPassword() && this.remember) {
+            this.passwordStorage.savePassword(this.profile, this.prompt.responses[this.step])
+        }
+
         if (this.step === this.prompt.prompts.length - 1) {
         if (this.step === this.prompt.prompts.length - 1) {
             this.prompt.respond()
             this.prompt.respond()
             this.done.emit()
             this.done.emit()

+ 1 - 0
tabby-ssh/src/components/sshTab.component.pug

@@ -51,6 +51,7 @@ sftp-panel.bg-dark(
 keyboard-interactive-auth-panel.bg-dark(
 keyboard-interactive-auth-panel.bg-dark(
     *ngIf='activeKIPrompt',
     *ngIf='activeKIPrompt',
     [prompt]='activeKIPrompt',
     [prompt]='activeKIPrompt',
+    [profile]='profile',
     (click)='$event.stopPropagation()',
     (click)='$event.stopPropagation()',
     (done)='activeKIPrompt = null; frontend?.focus()'
     (done)='activeKIPrompt = null; frontend?.focus()'
 )
 )

+ 17 - 12
tabby-ssh/src/components/sshTab.component.ts

@@ -1,3 +1,4 @@
+import * as russh from 'russh'
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'
 import colors from 'ansi-colors'
 import colors from 'ansi-colors'
 import { Component, Injector, HostListener } from '@angular/core'
 import { Component, Injector, HostListener } from '@angular/core'
@@ -94,17 +95,21 @@ export class SSHTabComponent extends ConnectableTerminalTabComponent<SSHProfile>
                     }
                     }
                 })
                 })
 
 
-                session.jumpStream = await new Promise((resolve, reject) => jumpSession.ssh.forwardOut(
-                    '127.0.0.1', 0, profile.options.host, profile.options.port ?? 22,
-                    (err, stream) => {
-                        if (err) {
-                            jumpSession.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not set up port forward on ${jumpConnection.name}`)
-                            reject(err)
-                            return
-                        }
-                        resolve(stream)
-                    },
-                ))
+                if (!(jumpSession.ssh instanceof russh.AuthenticatedSSHClient)) {
+                    throw new Error('Jump session is not authenticated yet somehow')
+                }
+
+                try {
+                    session.jumpChannel = await jumpSession.ssh.openTCPForwardChannel({
+                        addressToConnectTo: profile.options.host,
+                        portToConnectTo: profile.options.port ?? 22,
+                        originatorAddress: '127.0.0.1',
+                        originatorPort: 0,
+                    })
+                } catch (err) {
+                    jumpSession.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not set up port forward on ${jumpConnection.name}`)
+                    throw err
+                }
             }
             }
         }
         }
 
 
@@ -125,7 +130,7 @@ export class SSHTabComponent extends ConnectableTerminalTabComponent<SSHProfile>
         })
         })
 
 
         if (!session.open) {
         if (!session.open) {
-            this.write('\r\n' + colors.black.bgWhite(' SSH ') + ` Connecting to ${session.profile.options.host}\r\n`)
+            this.write('\r\n' + colors.black.bgWhite(' SSH ') + ` Connecting to ${session.profile.name}\r\n`)
 
 
             this.startSpinner(this.translate.instant(_('Connecting')))
             this.startSpinner(this.translate.instant(_('Connecting')))
 
 

+ 0 - 2
tabby-ssh/src/index.ts

@@ -1,5 +1,3 @@
-import './polyfills'
-
 import { NgModule } from '@angular/core'
 import { NgModule } from '@angular/core'
 import { CommonModule } from '@angular/common'
 import { CommonModule } from '@angular/common'
 import { FormsModule } from '@angular/forms'
 import { FormsModule } from '@angular/forms'

+ 0 - 12
tabby-ssh/src/polyfills.ts

@@ -1,12 +0,0 @@
-import 'ssh2'
-const nodeCrypto = require('crypto')
-const browserDH = require('diffie-hellman/browser')
-nodeCrypto.createDiffieHellmanGroup = browserDH.createDiffieHellmanGroup
-nodeCrypto.createDiffieHellman = browserDH.createDiffieHellman
-
-// Declare function missing from @types
-declare module 'ssh2' {
-    interface Client {
-        setNoDelay: (enable?: boolean) => this
-    }
-}

+ 7 - 13
tabby-ssh/src/profiles.ts

@@ -1,11 +1,11 @@
 import { Injectable, InjectFlags, Injector } from '@angular/core'
 import { Injectable, InjectFlags, Injector } from '@angular/core'
 import { NewTabParameters, PartialProfile, TranslateService, QuickConnectProfileProvider } from 'tabby-core'
 import { NewTabParameters, PartialProfile, TranslateService, QuickConnectProfileProvider } from 'tabby-core'
-import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
 import { SSHProfileSettingsComponent } from './components/sshProfileSettings.component'
 import { SSHProfileSettingsComponent } from './components/sshProfileSettings.component'
 import { SSHTabComponent } from './components/sshTab.component'
 import { SSHTabComponent } from './components/sshTab.component'
 import { PasswordStorageService } from './services/passwordStorage.service'
 import { PasswordStorageService } from './services/passwordStorage.service'
-import { ALGORITHM_BLACKLIST, SSHAlgorithmType, SSHProfile } from './api'
+import { SSHAlgorithmType, SSHProfile } from './api'
 import { SSHProfileImporter } from './api/importer'
 import { SSHProfileImporter } from './api/importer'
+import { defaultAlgorithms } from './algorithms'
 
 
 @Injectable({ providedIn: 'root' })
 @Injectable({ providedIn: 'root' })
 export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile> {
 export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile> {
@@ -29,10 +29,10 @@ export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile>
             agentForward: false,
             agentForward: false,
             warnOnClose: null,
             warnOnClose: null,
             algorithms: {
             algorithms: {
-                hmac: [],
-                kex: [],
-                cipher: [],
-                serverHostKey: [],
+                hmac: [] as string[],
+                kex: [] as string[],
+                cipher: [] as string[],
+                serverHostKey: [] as string[],
             },
             },
             proxyCommand: null,
             proxyCommand: null,
             forwardedPorts: [],
             forwardedPorts: [],
@@ -54,13 +54,7 @@ export class SSHProfilesService extends QuickConnectProfileProvider<SSHProfile>
     ) {
     ) {
         super()
         super()
         for (const k of Object.values(SSHAlgorithmType)) {
         for (const k of Object.values(SSHAlgorithmType)) {
-            const defaultAlg = {
-                [SSHAlgorithmType.KEX]: 'DEFAULT_KEX',
-                [SSHAlgorithmType.HOSTKEY]: 'DEFAULT_SERVER_HOST_KEY',
-                [SSHAlgorithmType.CIPHER]: 'DEFAULT_CIPHER',
-                [SSHAlgorithmType.HMAC]: 'DEFAULT_MAC',
-            }[k]
-            this.configDefaults.options.algorithms[k] = ALGORITHMS[defaultAlg].filter(x => !ALGORITHM_BLACKLIST.includes(x))
+            this.configDefaults.options.algorithms[k] = [...defaultAlgorithms[k]]
             this.configDefaults.options.algorithms[k].sort()
             this.configDefaults.options.algorithms[k].sort()
         }
         }
     }
     }

+ 3 - 177
tabby-ssh/src/services/ssh.service.ts

@@ -1,15 +1,9 @@
-import * as shellQuote from 'shell-quote'
-import * as net from 'net'
-import * as fs from 'fs/promises'
+// import * as fs from 'fs/promises'
 import * as tmp from 'tmp-promise'
 import * as tmp from 'tmp-promise'
-import socksv5 from '@luminati-io/socksv5'
-import { Duplex } from 'stream'
 import { Injectable } from '@angular/core'
 import { Injectable } from '@angular/core'
-import { spawn } from 'child_process'
-import { ChildProcess } from 'node:child_process'
 import { ConfigService, HostAppService, Platform, PlatformService } from 'tabby-core'
 import { ConfigService, HostAppService, Platform, PlatformService } from 'tabby-core'
 import { SSHSession } from '../session/ssh'
 import { SSHSession } from '../session/ssh'
-import { SSHProfile, SSHProxyStream, SSHProxyStreamSocket } from '../api'
+import { SSHProfile } from '../api'
 import { PasswordStorageService } from './passwordStorage.service'
 import { PasswordStorageService } from './passwordStorage.service'
 
 
 @Injectable({ providedIn: 'root' })
 @Injectable({ providedIn: 'root' })
@@ -55,7 +49,7 @@ export class SSHService {
         let tmpFile: tmp.FileResult|null = null
         let tmpFile: tmp.FileResult|null = null
         if (session.activePrivateKey) {
         if (session.activePrivateKey) {
             tmpFile = await tmp.file()
             tmpFile = await tmp.file()
-            await fs.writeFile(tmpFile.path, session.activePrivateKey)
+            // await fs.writeFile(tmpFile.path, session.activePrivateKey)
             const winSCPcom = path.slice(0, -3) + 'com'
             const winSCPcom = path.slice(0, -3) + 'com'
             await this.platform.exec(winSCPcom, ['/keygen', tmpFile.path, `/output=${tmpFile.path}`])
             await this.platform.exec(winSCPcom, ['/keygen', tmpFile.path, `/output=${tmpFile.path}`])
             args.push(`/privatekey=${tmpFile.path}`)
             args.push(`/privatekey=${tmpFile.path}`)
@@ -64,171 +58,3 @@ export class SSHService {
         tmpFile?.cleanup()
         tmpFile?.cleanup()
     }
     }
 }
 }
-
-export class ProxyCommandStream extends SSHProxyStream {
-    private process: ChildProcess|null
-
-    constructor (private command: string) {
-        super()
-    }
-
-    async start (): Promise<SSHProxyStreamSocket> {
-        const argv = shellQuote.parse(this.command)
-        this.process = spawn(argv[0], argv.slice(1), {
-            windowsHide: true,
-            stdio: ['pipe', 'pipe', 'pipe'],
-        })
-        this.process.on('error', error => {
-            this.stop(new Error(`Proxy command has failed to start: ${error.message}`))
-        })
-        this.process.on('exit', code => {
-            this.stop(new Error(`Proxy command has exited with code ${code}`))
-        })
-        this.process.stdout?.on('data', data => {
-            this.emitOutput(data)
-        })
-        this.process.stdout?.on('error', (err) => {
-            this.stop(err)
-        })
-        this.process.stderr?.on('data', data => {
-            this.emitMessage(data.toString())
-        })
-        return super.start()
-    }
-
-    requestData (size: number): void {
-        this.process?.stdout?.read(size)
-    }
-
-    async consumeInput (data: Buffer): Promise<void> {
-        const process = this.process
-        if (process) {
-            await new Promise(resolve => process.stdin?.write(data, resolve))
-        }
-    }
-
-    async stop (error?: Error): Promise<void> {
-        this.process?.kill()
-        super.stop(error)
-    }
-}
-
-export class SocksProxyStream extends SSHProxyStream {
-    private client: Duplex|null
-    private header: Buffer|null
-
-    constructor (private profile: SSHProfile) {
-        super()
-    }
-
-    async start (): Promise<SSHProxyStreamSocket> {
-        this.client = await new Promise((resolve, reject) => {
-            const connector = socksv5.connect({
-                host: this.profile.options.host,
-                port: this.profile.options.port,
-                proxyHost: this.profile.options.socksProxyHost ?? '127.0.0.1',
-                proxyPort: this.profile.options.socksProxyPort ?? 5000,
-                auths: [socksv5.auth.None()],
-                strictLocalDNS: false,
-            }, s => {
-                resolve(s)
-                this.header = s.read()
-                if (this.header) {
-                    this.emitOutput(this.header)
-                }
-            })
-            connector.on('error', (err) => {
-                reject(err)
-                this.stop(new Error(`SOCKS connection failed: ${err.message}`))
-            })
-        })
-        this.client?.on('data', data => {
-            if (!this.header || data !== this.header) {
-                // socksv5 doesn't reliably emit the first data event
-                this.emitOutput(data)
-                this.header = null
-            }
-        })
-        this.client?.on('close', error => {
-            this.stop(error)
-        })
-
-        return super.start()
-    }
-
-    requestData (size: number): void {
-        this.client?.read(size)
-    }
-
-    async consumeInput (data: Buffer): Promise<void> {
-        return new Promise((resolve, reject) => {
-            this.client?.write(data, undefined, err => err ? reject(err) : resolve())
-        })
-    }
-
-    async stop (error?: Error): Promise<void> {
-        this.client?.destroy()
-        super.stop(error)
-    }
-}
-
-export class HTTPProxyStream extends SSHProxyStream {
-    private client: Duplex|null
-    private connected = false
-
-    constructor (private profile: SSHProfile) {
-        super()
-    }
-
-    async start (): Promise<SSHProxyStreamSocket> {
-        this.client = await new Promise((resolve, reject) => {
-            const connector = net.createConnection({
-                host: this.profile.options.httpProxyHost!,
-                port: this.profile.options.httpProxyPort!,
-            }, () => resolve(connector))
-            connector.on('error', error => {
-                reject(error)
-                this.stop(new Error(`Proxy connection failed: ${error.message}`))
-            })
-        })
-        this.client?.write(Buffer.from(`CONNECT ${this.profile.options.host}:${this.profile.options.port} HTTP/1.1\r\n\r\n`))
-        this.client?.on('data', (data: Buffer) => {
-            if (this.connected) {
-                this.emitOutput(data)
-            } else {
-                if (data.slice(0, 5).equals(Buffer.from('HTTP/'))) {
-                    const idx = data.indexOf('\n\n')
-                    const headers = data.slice(0, idx).toString()
-                    const code = parseInt(headers.split(' ')[1])
-                    if (code >= 200 && code < 300) {
-                        this.emitMessage('Connected')
-                        this.emitOutput(data.slice(idx + 2))
-                        this.connected = true
-                    } else {
-                        this.stop(new Error(`Connection failed, code ${code}`))
-                    }
-                }
-            }
-        })
-        this.client?.on('close', error => {
-            this.stop(error)
-        })
-
-        return super.start()
-    }
-
-    requestData (size: number): void {
-        this.client?.read(size)
-    }
-
-    async consumeInput (data: Buffer): Promise<void> {
-        return new Promise((resolve, reject) => {
-            this.client?.write(data, undefined, err => err ? reject(err) : resolve())
-        })
-    }
-
-    async stop (error?: Error): Promise<void> {
-        this.client?.destroy()
-        super.stop(error)
-    }
-}

+ 48 - 82
tabby-ssh/src/session/sftp.ts

@@ -1,12 +1,9 @@
-import * as C from 'constants'
+/* eslint-disable @typescript-eslint/no-unused-vars */
 import { Subject, Observable } from 'rxjs'
 import { Subject, Observable } from 'rxjs'
 import { posix as posixPath } from 'path'
 import { posix as posixPath } from 'path'
-import { Injector, NgZone } from '@angular/core'
-import { FileDownload, FileUpload, Logger, LogService, wrapPromise } from 'tabby-core'
-import { SFTPWrapper } from 'ssh2'
-import { promisify } from 'util'
-
-import type { FileEntry, Stats } from 'ssh2-streams'
+import { Injector } from '@angular/core'
+import { FileDownload, FileUpload, Logger, LogService } from 'tabby-core'
+import * as russh from 'russh'
 
 
 export interface SFTPFile {
 export interface SFTPFile {
     name: string
     name: string
@@ -22,63 +19,37 @@ export class SFTPFileHandle {
     position = 0
     position = 0
 
 
     constructor (
     constructor (
-        private sftp: SFTPWrapper,
-        private handle: Buffer,
-        private zone: NgZone,
+        private inner: russh.SFTPFile|null,
     ) { }
     ) { }
 
 
-    read (): Promise<Buffer> {
-        const buffer = Buffer.alloc(256 * 1024)
-        return wrapPromise(this.zone, new Promise((resolve, reject) => {
-            while (true) {
-                const wait = this.sftp.read(this.handle, buffer, 0, buffer.length, this.position, (err, read) => {
-                    if (err) {
-                        reject(err)
-                        return
-                    }
-                    this.position += read
-                    resolve(buffer.slice(0, read))
-                })
-                if (!wait) {
-                    break
-                }
-            }
-        }))
+    async read (): Promise<Uint8Array> {
+        if (!this.inner) {
+            return Promise.resolve(new Uint8Array(0))
+        }
+        return this.inner.read(256 * 1024)
     }
     }
 
 
-    write (chunk: Buffer): Promise<void> {
-        return wrapPromise(this.zone, new Promise<void>((resolve, reject) => {
-            while (true) {
-                const wait = this.sftp.write(this.handle, chunk, 0, chunk.length, this.position, err => {
-                    if (err) {
-                        reject(err)
-                        return
-                    }
-                    this.position += chunk.length
-                    resolve()
-                })
-                if (!wait) {
-                    break
-                }
-            }
-        }))
+    async write (chunk: Uint8Array): Promise<void> {
+        if (!this.inner) {
+            throw new Error('File handle is closed')
+        }
+        await this.inner.writeAll(chunk)
     }
     }
 
 
-    close (): Promise<void> {
-        return wrapPromise(this.zone, promisify(this.sftp.close.bind(this.sftp))(this.handle))
+    async close (): Promise<void> {
+        await this.inner?.shutdown()
+        this.inner = null
     }
     }
 }
 }
 
 
 export class SFTPSession {
 export class SFTPSession {
     get closed$ (): Observable<void> { return this.closed }
     get closed$ (): Observable<void> { return this.closed }
     private closed = new Subject<void>()
     private closed = new Subject<void>()
-    private zone: NgZone
     private logger: Logger
     private logger: Logger
 
 
-    constructor (private sftp: SFTPWrapper, injector: Injector) {
-        this.zone = injector.get(NgZone)
+    constructor (private sftp: russh.SFTP, injector: Injector) {
         this.logger = injector.get(LogService).create('sftp')
         this.logger = injector.get(LogService).create('sftp')
-        sftp.on('close', () => {
+        sftp.closed$.subscribe(() => {
             this.closed.next()
             this.closed.next()
             this.closed.complete()
             this.closed.complete()
         })
         })
@@ -86,67 +57,64 @@ export class SFTPSession {
 
 
     async readdir (p: string): Promise<SFTPFile[]> {
     async readdir (p: string): Promise<SFTPFile[]> {
         this.logger.debug('readdir', p)
         this.logger.debug('readdir', p)
-        const entries = await wrapPromise(this.zone, promisify<FileEntry[]>(f => this.sftp.readdir(p, f))())
+        const entries = await this.sftp.readDirectory(p)
         return entries.map(entry => this._makeFile(
         return entries.map(entry => this._makeFile(
-            posixPath.join(p, entry.filename), entry,
+            posixPath.join(p, entry.name), entry,
         ))
         ))
     }
     }
 
 
     readlink (p: string): Promise<string> {
     readlink (p: string): Promise<string> {
         this.logger.debug('readlink', p)
         this.logger.debug('readlink', p)
-        return wrapPromise(this.zone, promisify<string>(f => this.sftp.readlink(p, f))())
+        return this.sftp.readlink(p)
     }
     }
 
 
     async stat (p: string): Promise<SFTPFile> {
     async stat (p: string): Promise<SFTPFile> {
         this.logger.debug('stat', p)
         this.logger.debug('stat', p)
-        const stats = await wrapPromise(this.zone, promisify<Stats>(f => this.sftp.stat(p, f))())
+        const stats = await this.sftp.stat(p)
         return {
         return {
             name: posixPath.basename(p),
             name: posixPath.basename(p),
             fullPath: p,
             fullPath: p,
-            isDirectory: stats.isDirectory(),
-            isSymlink: stats.isSymbolicLink(),
-            mode: stats.mode,
+            isDirectory: stats.type === russh.SFTPFileType.Directory,
+            isSymlink: stats.type === russh.SFTPFileType.Symlink,
+            mode: stats.permissions ?? 0,
             size: stats.size,
             size: stats.size,
-            modified: new Date(stats.mtime * 1000),
+            modified: new Date((stats.mtime ?? 0) * 1000),
         }
         }
     }
     }
 
 
-    async open (p: string, mode: string): Promise<SFTPFileHandle> {
-        this.logger.debug('open', p)
-        const handle = await wrapPromise(this.zone, promisify<Buffer>(f => this.sftp.open(p, mode, f))())
-        return new SFTPFileHandle(this.sftp, handle, this.zone)
+    async open (p: string, mode: number): Promise<SFTPFileHandle> {
+        this.logger.debug('open', p, mode)
+        const handle = await this.sftp.open(p, mode)
+        return new SFTPFileHandle(handle)
     }
     }
 
 
     async rmdir (p: string): Promise<void> {
     async rmdir (p: string): Promise<void> {
-        this.logger.debug('rmdir', p)
-        await promisify((f: any) => this.sftp.rmdir(p, f))()
+        await this.sftp.removeDirectory(p)
     }
     }
 
 
     async mkdir (p: string): Promise<void> {
     async mkdir (p: string): Promise<void> {
-        this.logger.debug('mkdir', p)
-        await promisify((f: any) => this.sftp.mkdir(p, f))()
+        await this.sftp.createDirectory(p)
     }
     }
 
 
     async rename (oldPath: string, newPath: string): Promise<void> {
     async rename (oldPath: string, newPath: string): Promise<void> {
         this.logger.debug('rename', oldPath, newPath)
         this.logger.debug('rename', oldPath, newPath)
-        await promisify((f: any) => this.sftp.rename(oldPath, newPath, f))()
+        await this.sftp.rename(oldPath, newPath)
     }
     }
 
 
     async unlink (p: string): Promise<void> {
     async unlink (p: string): Promise<void> {
-        this.logger.debug('unlink', p)
-        await promisify((f: any) => this.sftp.unlink(p, f))()
+        await this.sftp.removeFile(p)
     }
     }
 
 
     async chmod (p: string, mode: string|number): Promise<void> {
     async chmod (p: string, mode: string|number): Promise<void> {
         this.logger.debug('chmod', p, mode)
         this.logger.debug('chmod', p, mode)
-        await promisify((f: any) => this.sftp.chmod(p, mode, f))()
+        await this.sftp.chmod(p, mode)
     }
     }
 
 
     async upload (path: string, transfer: FileUpload): Promise<void> {
     async upload (path: string, transfer: FileUpload): Promise<void> {
         this.logger.info('Uploading into', path)
         this.logger.info('Uploading into', path)
         const tempPath = path + '.tabby-upload'
         const tempPath = path + '.tabby-upload'
         try {
         try {
-            const handle = await this.open(tempPath, 'w')
+            const handle = await this.open(tempPath, russh.OPEN_WRITE | russh.OPEN_CREATE)
             while (true) {
             while (true) {
                 const chunk = await transfer.read()
                 const chunk = await transfer.read()
                 if (!chunk.length) {
                 if (!chunk.length) {
@@ -154,15 +122,13 @@ export class SFTPSession {
                 }
                 }
                 await handle.write(chunk)
                 await handle.write(chunk)
             }
             }
-            handle.close()
-            try {
-                await this.unlink(path)
-            } catch { }
+            await handle.close()
+            await this.unlink(path).catch(() => null)
             await this.rename(tempPath, path)
             await this.rename(tempPath, path)
             transfer.close()
             transfer.close()
         } catch (e) {
         } catch (e) {
             transfer.cancel()
             transfer.cancel()
-            this.unlink(tempPath)
+            this.unlink(tempPath).catch(() => null)
             throw e
             throw e
         }
         }
     }
     }
@@ -170,7 +136,7 @@ export class SFTPSession {
     async download (path: string, transfer: FileDownload): Promise<void> {
     async download (path: string, transfer: FileDownload): Promise<void> {
         this.logger.info('Downloading', path)
         this.logger.info('Downloading', path)
         try {
         try {
-            const handle = await this.open(path, 'r')
+            const handle = await this.open(path, russh.OPEN_READ)
             while (true) {
             while (true) {
                 const chunk = await handle.read()
                 const chunk = await handle.read()
                 if (!chunk.length) {
                 if (!chunk.length) {
@@ -186,15 +152,15 @@ export class SFTPSession {
         }
         }
     }
     }
 
 
-    private _makeFile (p: string, entry: FileEntry): SFTPFile {
+    private _makeFile (p: string, entry: russh.SFTPDirectoryEntry): SFTPFile {
         return {
         return {
             fullPath: p,
             fullPath: p,
             name: posixPath.basename(p),
             name: posixPath.basename(p),
-            isDirectory: (entry.attrs.mode & C.S_IFDIR) === C.S_IFDIR,
-            isSymlink: (entry.attrs.mode & C.S_IFLNK) === C.S_IFLNK,
-            mode: entry.attrs.mode,
-            size: entry.attrs.size,
-            modified: new Date(entry.attrs.mtime * 1000),
+            isDirectory: entry.metadata.type === russh.SFTPFileType.Directory,
+            isSymlink: entry.metadata.type === russh.SFTPFileType.Symlink,
+            mode: entry.metadata.permissions ?? 0,
+            size: entry.metadata.size,
+            modified: new Date((entry.metadata.mtime ?? 0) * 1000),
         }
         }
     }
     }
 }
 }

+ 14 - 19
tabby-ssh/src/session/shell.ts

@@ -1,15 +1,15 @@
 import { Observable, Subject } from 'rxjs'
 import { Observable, Subject } from 'rxjs'
 import stripAnsi from 'strip-ansi'
 import stripAnsi from 'strip-ansi'
-import { ClientChannel } from 'ssh2'
 import { Injector } from '@angular/core'
 import { Injector } from '@angular/core'
 import { LogService } from 'tabby-core'
 import { LogService } from 'tabby-core'
 import { BaseSession, UTF8SplitterMiddleware, InputProcessor } from 'tabby-terminal'
 import { BaseSession, UTF8SplitterMiddleware, InputProcessor } from 'tabby-terminal'
 import { SSHSession } from './ssh'
 import { SSHSession } from './ssh'
 import { SSHProfile } from '../api'
 import { SSHProfile } from '../api'
+import * as russh from 'russh'
 
 
 
 
 export class SSHShellSession extends BaseSession {
 export class SSHShellSession extends BaseSession {
-    shell?: ClientChannel
+    shell?: russh.Channel
     get serviceMessage$ (): Observable<string> { return this.serviceMessage }
     get serviceMessage$ (): Observable<string> { return this.serviceMessage }
     private serviceMessage = new Subject<string>()
     private serviceMessage = new Subject<string>()
     private ssh: SSHSession|null
     private ssh: SSHSession|null
@@ -53,19 +53,11 @@ export class SSHShellSession extends BaseSession {
 
 
         this.loginScriptProcessor?.executeUnconditionalScripts()
         this.loginScriptProcessor?.executeUnconditionalScripts()
 
 
-        this.shell.on('greeting', greeting => {
-            this.emitServiceMessage(`Shell greeting: ${greeting}`)
+        this.shell.data$.subscribe(data => {
+            this.emitOutput(Buffer.from(data))
         })
         })
 
 
-        this.shell.on('banner', banner => {
-            this.emitServiceMessage(`Shell banner: ${banner}`)
-        })
-
-        this.shell.on('data', data => {
-            this.emitOutput(data)
-        })
-
-        this.shell.on('end', () => {
+        this.shell.eof$.subscribe(() => {
             this.logger.info('Shell session ended')
             this.logger.info('Shell session ended')
             if (this.open) {
             if (this.open) {
                 this.destroy()
                 this.destroy()
@@ -79,19 +71,22 @@ export class SSHShellSession extends BaseSession {
     }
     }
 
 
     resize (columns: number, rows: number): void {
     resize (columns: number, rows: number): void {
-        if (this.shell) {
-            this.shell.setWindow(rows, columns, rows, columns)
-        }
+        this.shell?.resizePTY({
+            columns,
+            rows,
+            pixHeight: 0,
+            pixWidth: 0,
+        })
     }
     }
 
 
     write (data: Buffer): void {
     write (data: Buffer): void {
         if (this.shell) {
         if (this.shell) {
-            this.shell.write(data)
+            this.shell.write(new Uint8Array(data))
         }
         }
     }
     }
 
 
-    kill (signal?: string): void {
-        this.shell?.signal(signal ?? 'TERM')
+    kill (_signal?: string): void {
+        // this.shell?.signal(signal ?? 'TERM')
     }
     }
 
 
     async destroy (): Promise<void> {
     async destroy (): Promise<void> {

+ 390 - 311
tabby-ssh/src/session/ssh.ts

@@ -1,24 +1,22 @@
 import * as fs from 'mz/fs'
 import * as fs from 'mz/fs'
 import * as crypto from 'crypto'
 import * as crypto from 'crypto'
-import * as sshpk from 'sshpk'
 import colors from 'ansi-colors'
 import colors from 'ansi-colors'
 import stripAnsi from 'strip-ansi'
 import stripAnsi from 'strip-ansi'
-import { Injector, NgZone } from '@angular/core'
+import * as shellQuote from 'shell-quote'
+import { Injector } from '@angular/core'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
-import { ConfigService, FileProvidersService, HostAppService, NotificationsService, Platform, PlatformService, wrapPromise, PromptModalComponent, LogService, Logger, TranslateService } from 'tabby-core'
+import { ConfigService, FileProvidersService, NotificationsService, PromptModalComponent, LogService, Logger, TranslateService, Platform, HostAppService } from 'tabby-core'
 import { Socket } from 'net'
 import { Socket } from 'net'
-import { Client, ClientChannel, SFTPWrapper } from 'ssh2'
 import { Subject, Observable } from 'rxjs'
 import { Subject, Observable } from 'rxjs'
 import { HostKeyPromptModalComponent } from '../components/hostKeyPromptModal.component'
 import { HostKeyPromptModalComponent } from '../components/hostKeyPromptModal.component'
-import { HTTPProxyStream, ProxyCommandStream, SocksProxyStream } from '../services/ssh.service'
 import { PasswordStorageService } from '../services/passwordStorage.service'
 import { PasswordStorageService } from '../services/passwordStorage.service'
 import { SSHKnownHostsService } from '../services/sshKnownHosts.service'
 import { SSHKnownHostsService } from '../services/sshKnownHosts.service'
-import { promisify } from 'util'
 import { SFTPSession } from './sftp'
 import { SFTPSession } from './sftp'
-import { SSHAlgorithmType, PortForwardType, SSHProfile, SSHProxyStream, AutoPrivateKeyLocator } from '../api'
+import { SSHAlgorithmType, SSHProfile, AutoPrivateKeyLocator, PortForwardType } from '../api'
 import { ForwardedPort } from './forwards'
 import { ForwardedPort } from './forwards'
 import { X11Socket } from './x11'
 import { X11Socket } from './x11'
 import { supportedAlgorithms } from '../algorithms'
 import { supportedAlgorithms } from '../algorithms'
+import * as russh from 'russh'
 
 
 const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
 const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
 
 
@@ -27,48 +25,74 @@ export interface Prompt {
     echo?: boolean
     echo?: boolean
 }
 }
 
 
-interface AuthMethod {
-    type: 'none'|'publickey'|'agent'|'password'|'keyboard-interactive'|'hostbased'
-    name?: string
-    contents?: Buffer
-}
-
-interface Handshake {
-    kex: string
-    serverHostKey: string
+type AuthMethod = {
+    type: 'none'|'prompt-password'|'hostbased'
+} | {
+    type: 'keyboard-interactive',
+    savedPassword?: string
+} | {
+    type: 'saved-password',
+    password: string
+} | {
+    type: 'publickey'
+    name: string
+    contents: Buffer
+} | {
+    type: 'agent',
+    kind: 'unix-socket',
+    path: string
+} | {
+    type: 'agent',
+    kind: 'named-pipe',
+    path: string
+} | {
+    type: 'agent',
+    kind: 'pageant',
 }
 }
 
 
 export class KeyboardInteractivePrompt {
 export class KeyboardInteractivePrompt {
-    responses: string[] = []
+    readonly responses: string[] = []
+
+    private _resolve: (value: string[]) => void
+    private _reject: (reason: any) => void
+    readonly promise = new Promise<string[]>((resolve, reject) => {
+        this._resolve = resolve
+        this._reject = reject
+    })
 
 
     constructor (
     constructor (
         public name: string,
         public name: string,
         public instruction: string,
         public instruction: string,
         public prompts: Prompt[],
         public prompts: Prompt[],
-        private callback: (_: string[]) => void,
     ) {
     ) {
         this.responses = new Array(this.prompts.length).fill('')
         this.responses = new Array(this.prompts.length).fill('')
     }
     }
 
 
+    isAPasswordPrompt (index: number): boolean {
+        return this.prompts[index].prompt.toLowerCase().includes('password') && !this.prompts[index].echo
+    }
+
     respond (): void {
     respond (): void {
-        this.callback(this.responses)
+        this._resolve(this.responses)
+    }
+
+    reject (): void {
+        this._reject(new Error('Keyboard-interactive auth rejected'))
     }
     }
 }
 }
 
 
 export class SSHSession {
 export class SSHSession {
-    shell?: ClientChannel
-    ssh: Client
-    sftp?: SFTPWrapper
+    shell?: russh.Channel
+    ssh: russh.SSHClient|russh.AuthenticatedSSHClient
+    sftp?: russh.SFTP
     forwardedPorts: ForwardedPort[] = []
     forwardedPorts: ForwardedPort[] = []
-    jumpStream: any
-    proxyCommandStream: SSHProxyStream|null = null
+    jumpChannel: russh.Channel|null = null
     savedPassword?: string
     savedPassword?: string
     get serviceMessage$ (): Observable<string> { return this.serviceMessage }
     get serviceMessage$ (): Observable<string> { return this.serviceMessage }
     get keyboardInteractivePrompt$ (): Observable<KeyboardInteractivePrompt> { return this.keyboardInteractivePrompt }
     get keyboardInteractivePrompt$ (): Observable<KeyboardInteractivePrompt> { return this.keyboardInteractivePrompt }
     get willDestroy$ (): Observable<void> { return this.willDestroy }
     get willDestroy$ (): Observable<void> { return this.willDestroy }
 
 
-    agentPath?: string
-    activePrivateKey: string|null = null
+    activePrivateKey: russh.KeyPair|null = null
     authUsername: string|null = null
     authUsername: string|null = null
 
 
     open = false
     open = false
@@ -79,15 +103,11 @@ export class SSHSession {
     private serviceMessage = new Subject<string>()
     private serviceMessage = new Subject<string>()
     private keyboardInteractivePrompt = new Subject<KeyboardInteractivePrompt>()
     private keyboardInteractivePrompt = new Subject<KeyboardInteractivePrompt>()
     private willDestroy = new Subject<void>()
     private willDestroy = new Subject<void>()
-    private keychainPasswordUsed = false
-    private hostKeyDigest = ''
 
 
     private passwordStorage: PasswordStorageService
     private passwordStorage: PasswordStorageService
     private ngbModal: NgbModal
     private ngbModal: NgbModal
     private hostApp: HostAppService
     private hostApp: HostAppService
-    private platform: PlatformService
     private notifications: NotificationsService
     private notifications: NotificationsService
-    private zone: NgZone
     private fileProviders: FileProvidersService
     private fileProviders: FileProvidersService
     private config: ConfigService
     private config: ConfigService
     private translate: TranslateService
     private translate: TranslateService
@@ -103,9 +123,7 @@ export class SSHSession {
         this.passwordStorage = injector.get(PasswordStorageService)
         this.passwordStorage = injector.get(PasswordStorageService)
         this.ngbModal = injector.get(NgbModal)
         this.ngbModal = injector.get(NgbModal)
         this.hostApp = injector.get(HostAppService)
         this.hostApp = injector.get(HostAppService)
-        this.platform = injector.get(PlatformService)
         this.notifications = injector.get(NotificationsService)
         this.notifications = injector.get(NotificationsService)
-        this.zone = injector.get(NgZone)
         this.fileProviders = injector.get(FileProvidersService)
         this.fileProviders = injector.get(FileProvidersService)
         this.config = injector.get(ConfigService)
         this.config = injector.get(ConfigService)
         this.translate = injector.get(TranslateService)
         this.translate = injector.get(TranslateService)
@@ -120,27 +138,6 @@ export class SSHSession {
     }
     }
 
 
     async init (): Promise<void> {
     async init (): Promise<void> {
-        if (this.hostApp.platform === Platform.Windows) {
-            if (this.config.store.ssh.agentType === 'auto') {
-                if (await fs.exists(WINDOWS_OPENSSH_AGENT_PIPE)) {
-                    this.agentPath = WINDOWS_OPENSSH_AGENT_PIPE
-                } else {
-                    if (
-                        await this.platform.isProcessRunning('pageant.exe') ||
-                        await this.platform.isProcessRunning('gpg-agent.exe')
-                    ) {
-                        this.agentPath = 'pageant'
-                    }
-                }
-            } else if (this.config.store.ssh.agentType === 'pageant') {
-                this.agentPath = 'pageant'
-            } else {
-                this.agentPath = this.config.store.ssh.agentPath || WINDOWS_OPENSSH_AGENT_PIPE
-            }
-        } else {
-            this.agentPath = process.env.SSH_AUTH_SOCK!
-        }
-
         this.remainingAuthMethods = [{ type: 'none' }]
         this.remainingAuthMethods = [{ type: 'none' }]
         if (!this.profile.options.auth || this.profile.options.auth === 'publicKey') {
         if (!this.profile.options.auth || this.profile.options.auth === 'publicKey') {
             if (this.profile.options.privateKeys?.length) {
             if (this.profile.options.privateKeys?.length) {
@@ -167,184 +164,192 @@ export class SSHSession {
                 }
                 }
             }
             }
         }
         }
+
         if (!this.profile.options.auth || this.profile.options.auth === 'agent') {
         if (!this.profile.options.auth || this.profile.options.auth === 'agent') {
-            if (!this.agentPath) {
-                this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Agent auth selected, but no running agent is detected`)
+            const spec = await this.getAgentConnectionSpec()
+            if (!spec) {
+                this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Agent auth selected, but no running Agent process is found`)
             } else {
             } else {
-                this.remainingAuthMethods.push({ type: 'agent' })
+                this.remainingAuthMethods.push({
+                    type: 'agent',
+                    ...spec,
+                })
             }
             }
         }
         }
         if (!this.profile.options.auth || this.profile.options.auth === 'password') {
         if (!this.profile.options.auth || this.profile.options.auth === 'password') {
-            this.remainingAuthMethods.push({ type: 'password' })
+            if (this.profile.options.password) {
+                this.remainingAuthMethods.push({ type: 'saved-password', password: this.profile.options.password })
+            }
+            const password = await this.passwordStorage.loadPassword(this.profile)
+            if (password) {
+                this.remainingAuthMethods.push({ type: 'saved-password', password })
+            }
+            this.remainingAuthMethods.push({ type: 'prompt-password' })
         }
         }
         if (!this.profile.options.auth || this.profile.options.auth === 'keyboardInteractive') {
         if (!this.profile.options.auth || this.profile.options.auth === 'keyboardInteractive') {
+            const savedPassword = this.profile.options.password ?? await this.passwordStorage.loadPassword(this.profile)
+            if (savedPassword) {
+                this.remainingAuthMethods.push({ type: 'keyboard-interactive', savedPassword })
+            }
             this.remainingAuthMethods.push({ type: 'keyboard-interactive' })
             this.remainingAuthMethods.push({ type: 'keyboard-interactive' })
         }
         }
         this.remainingAuthMethods.push({ type: 'hostbased' })
         this.remainingAuthMethods.push({ type: 'hostbased' })
     }
     }
 
 
+    private async getAgentConnectionSpec (): Promise<russh.AgentConnectionSpec|null> {
+        if (this.hostApp.platform === Platform.Windows) {
+            if (this.config.store.ssh.agentType === 'auto') {
+                if (await fs.exists(WINDOWS_OPENSSH_AGENT_PIPE)) {
+                    return {
+                        kind: 'named-pipe',
+                        path: WINDOWS_OPENSSH_AGENT_PIPE,
+                    }
+                } else if (russh.isPageantRunning()) {
+                    return {
+                        kind: 'pageant',
+                    }
+                } else {
+                    this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Agent auth selected, but no running Agent process is found`)
+                }
+            } else if (this.config.store.ssh.agentType === 'pageant') {
+                return {
+                    kind: 'pageant',
+                }
+            } else {
+                return {
+                    kind: 'named-pipe',
+                    path: this.config.store.ssh.agentPath || WINDOWS_OPENSSH_AGENT_PIPE,
+                }
+            }
+        } else {
+            return {
+                kind: 'unix-socket',
+                path: process.env.SSH_AUTH_SOCK!,
+            }
+        }
+        return null
+    }
+
     async openSFTP (): Promise<SFTPSession> {
     async openSFTP (): Promise<SFTPSession> {
+        if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
+            throw new Error('Cannot open SFTP session before auth')
+        }
         if (!this.sftp) {
         if (!this.sftp) {
-            this.sftp = await wrapPromise(this.zone, promisify<SFTPWrapper>(f => this.ssh.sftp(f))())
+            this.sftp = await this.ssh.openSFTPChannel()
         }
         }
         return new SFTPSession(this.sftp, this.injector)
         return new SFTPSession(this.sftp, this.injector)
     }
     }
 
 
-
     async start (): Promise<void> {
     async start (): Promise<void> {
-        const log = (s: any) => this.emitServiceMessage(s)
-
-        const ssh = new Client()
-        this.ssh = ssh
         await this.init()
         await this.init()
 
 
-        let connected = false
         const algorithms = {}
         const algorithms = {}
         for (const key of Object.values(SSHAlgorithmType)) {
         for (const key of Object.values(SSHAlgorithmType)) {
             algorithms[key] = this.profile.options.algorithms![key].filter(x => supportedAlgorithms[key].includes(x))
             algorithms[key] = this.profile.options.algorithms![key].filter(x => supportedAlgorithms[key].includes(x))
         }
         }
 
 
-        const hostVerifiedPromise: Promise<void> = new Promise((resolve, reject) => {
-            ssh.on('handshake', async handshake => {
-                if (!await this.verifyHostKey(handshake)) {
-                    this.ssh.end()
-                    reject(new Error('Host key verification failed'))
-                }
-                this.logger.info('Handshake complete:', handshake)
-                resolve()
-            })
-        })
-
-        const resultPromise: Promise<void> = new Promise(async (resolve, reject) => {
-            ssh.on('ready', () => {
-                connected = true
-                // Fix SSH Lagging
-                ssh.setNoDelay(true)
-                if (this.savedPassword) {
-                    this.passwordStorage.savePassword(this.profile, this.savedPassword)
-                }
-
-                this.zone.run(resolve)
-            })
-            ssh.on('error', error => {
-                if (error.message === 'All configured authentication methods failed') {
-                    this.passwordStorage.deletePassword(this.profile)
-                }
-                this.zone.run(() => {
-                    if (connected) {
-                        // eslint-disable-next-line @typescript-eslint/no-base-to-string
-                        this.notifications.error(error.toString())
-                    } else {
-                        reject(error)
-                    }
-                })
-            })
-            ssh.on('close', () => {
-                if (this.open) {
-                    this.destroy()
-                }
-            })
+        // eslint-disable-next-line @typescript-eslint/init-declarations
+        let transport: russh.SshTransport
+        if (this.profile.options.proxyCommand) {
+            this.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ` Using ${this.profile.options.proxyCommand}`)
+
+            const argv = shellQuote.parse(this.profile.options.proxyCommand)
+            transport = await russh.SshTransport.newCommand(argv[0], argv.slice(1))
+        } else if (this.jumpChannel) {
+            transport = await russh.SshTransport.newSshChannel(await this.jumpChannel.take())
+            this.jumpChannel = null
+        } else if (this.profile.options.socksProxyHost) {
+            this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.socksProxyHost}:${this.profile.options.socksProxyPort}`)
+            transport = await russh.SshTransport.newSocksProxy(
+                this.profile.options.socksProxyHost,
+                this.profile.options.socksProxyPort ?? 1080,
+                this.profile.options.host,
+                this.profile.options.port ?? 22,
+            )
+        } else if (this.profile.options.httpProxyHost) {
+            this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.httpProxyHost}:${this.profile.options.httpProxyPort}`)
+            transport = await russh.SshTransport.newHttpProxy(
+                this.profile.options.httpProxyHost,
+                this.profile.options.httpProxyPort ?? 8080,
+                this.profile.options.host,
+                this.profile.options.port ?? 22,
+            )
+        } else {
+            transport = await russh.SshTransport.newSocket(`${this.profile.options.host.trim()}:${this.profile.options.port ?? 22}`)
+        }
 
 
-            ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
-                this.emitKeyboardInteractivePrompt(new KeyboardInteractivePrompt(
-                    name,
-                    instructions,
-                    prompts,
-                    finish,
-                ))
-            }))
-
-            ssh.on('greeting', greeting => {
-                if (!this.profile.options.skipBanner) {
-                    log('Greeting: ' + greeting)
+        this.ssh = await russh.SSHClient.connect(
+            transport,
+            async key => {
+                if (!await this.verifyHostKey(key)) {
+                    return false
                 }
                 }
-            })
+                this.logger.info('Host key verified')
+                return true
+            },
+            {
+                preferred: {
+                    ciphers: this.profile.options.algorithms?.[SSHAlgorithmType.CIPHER]?.filter(x => supportedAlgorithms[SSHAlgorithmType.CIPHER].includes(x)),
+                    kex: this.profile.options.algorithms?.[SSHAlgorithmType.KEX]?.filter(x => supportedAlgorithms[SSHAlgorithmType.KEX].includes(x)),
+                    mac: this.profile.options.algorithms?.[SSHAlgorithmType.HMAC]?.filter(x => supportedAlgorithms[SSHAlgorithmType.HMAC].includes(x)),
+                    key: this.profile.options.algorithms?.[SSHAlgorithmType.HOSTKEY]?.filter(x => supportedAlgorithms[SSHAlgorithmType.HOSTKEY].includes(x)),
+                },
+                keepaliveIntervalSeconds: Math.round((this.profile.options.keepaliveInterval ?? 15000) / 1000),
+                keepaliveCountMax: this.profile.options.keepaliveCountMax,
+                connectionTimeoutSeconds: this.profile.options.readyTimeout ? Math.round(this.profile.options.readyTimeout / 1000) : undefined,
+            },
+        )
 
 
-            ssh.on('banner', banner => {
-                if (!this.profile.options.skipBanner) {
-                    log(banner)
-                }
-            })
+        this.ssh.banner$.subscribe(banner => {
+            if (!this.profile.options.skipBanner) {
+                this.emitServiceMessage(banner)
+            }
         })
         })
 
 
-        try {
-            if (this.profile.options.socksProxyHost) {
-                this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.socksProxyHost}:${this.profile.options.socksProxyPort}`)
-                this.proxyCommandStream = new SocksProxyStream(this.profile)
-            }
-            if (this.profile.options.httpProxyHost) {
-                this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.httpProxyHost}:${this.profile.options.httpProxyPort}`)
-                this.proxyCommandStream = new HTTPProxyStream(this.profile)
+        this.ssh.disconnect$.subscribe(() => {
+            if (this.open) {
+                this.destroy()
             }
             }
-            if (this.profile.options.proxyCommand) {
-                this.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ` Using ${this.profile.options.proxyCommand}`)
-                this.proxyCommandStream = new ProxyCommandStream(this.profile.options.proxyCommand)
-            }
-            if (this.proxyCommandStream) {
-                this.proxyCommandStream.destroyed$.subscribe(err => {
-                    if (err) {
-                        this.emitServiceMessage(colors.bgRed.black(' X ') + ` ${err.message}`)
-                        this.destroy()
-                    }
-                })
+        })
 
 
-                this.proxyCommandStream.message$.subscribe(message => {
-                    this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ' ' + message.trim())
-                })
+        // Authentication
 
 
-                await this.proxyCommandStream.start()
+        this.authUsername ??= this.profile.options.user
+        if (!this.authUsername) {
+            const modal = this.ngbModal.open(PromptModalComponent)
+            modal.componentInstance.prompt = `Username for ${this.profile.options.host}`
+            try {
+                const result = await modal.result.catch(() => null)
+                this.authUsername = result?.value ?? null
+            } catch {
+                this.authUsername = 'root'
             }
             }
+        }
 
 
-            this.authUsername ??= this.profile.options.user
-            if (!this.authUsername) {
-                const modal = this.ngbModal.open(PromptModalComponent)
-                modal.componentInstance.prompt = `Username for ${this.profile.options.host}`
-                try {
-                    const result = await modal.result.catch(() => null)
-                    this.authUsername = result?.value ?? null
-                } catch {
-                    this.authUsername = 'root'
-                }
-            }
-            if (this.authUsername?.startsWith('$')) {
-                try {
-                    const result = process.env[this.authUsername.slice(1)]
-                    this.authUsername = result ?? this.authUsername
-                } catch {
-                    this.authUsername = 'root'
-                }
+        if (this.authUsername?.startsWith('$')) {
+            try {
+                const result = process.env[this.authUsername.slice(1)]
+                this.authUsername = result ?? this.authUsername
+            } catch {
+                this.authUsername = 'root'
             }
             }
+        }
 
 
-            ssh.connect({
-                host: this.profile.options.host.trim(),
-                port: this.profile.options.port ?? 22,
-                sock: this.proxyCommandStream?.socket ?? this.jumpStream,
-                username: this.authUsername ?? undefined,
-                tryKeyboard: true,
-                agent: this.agentPath,
-                agentForward: this.profile.options.agentForward && !!this.agentPath,
-                keepaliveInterval: this.profile.options.keepaliveInterval ?? 15000,
-                keepaliveCountMax: this.profile.options.keepaliveCountMax,
-                readyTimeout: this.profile.options.readyTimeout,
-                hostVerifier: (key: any) => {
-                    this.hostKeyDigest = crypto.createHash('sha256').update(key).digest('base64')
-                    return true
-                },
-                algorithms,
-                authHandler: (methodsLeft, partialSuccess, callback) => {
-                    this.zone.run(async () => {
-                        await hostVerifiedPromise
-                        callback(await this.handleAuth(methodsLeft))
-                    })
-                },
-            })
-        } catch (e) {
-            this.notifications.error(e.message)
-            throw e
+        const authenticatedClient = await this.handleAuth()
+        if (authenticatedClient) {
+            this.ssh = authenticatedClient
+        } else {
+            this.ssh.disconnect()
+            this.passwordStorage.deletePassword(this.profile)
+            // eslint-disable-next-line @typescript-eslint/no-base-to-string
+            throw new Error('Authentication rejected')
         }
         }
 
 
-        await resultPromise
-        await hostVerifiedPromise
+        // auth success
+
+        if (this.savedPassword) {
+            this.passwordStorage.savePassword(this.profile, this.savedPassword)
+        }
 
 
         for (const fw of this.profile.options.forwardedPorts ?? []) {
         for (const fw of this.profile.options.forwardedPorts ?? []) {
             this.addPortForward(Object.assign(new ForwardedPort(), fw))
             this.addPortForward(Object.assign(new ForwardedPort(), fw))
@@ -352,12 +357,11 @@ export class SSHSession {
 
 
         this.open = true
         this.open = true
 
 
-        this.ssh.on('tcp connection', (details, accept, reject) => {
-            this.logger.info(`Incoming forwarded connection: (remote) ${details.srcIP}:${details.srcPort} -> (local) ${details.destIP}:${details.destPort}`)
-            const forward = this.forwardedPorts.find(x => x.port === details.destPort)
+        this.ssh.tcpChannelOpen$.subscribe(async event => {
+            this.logger.info(`Incoming forwarded connection: ${event.clientAddress}:${event.clientPort} -> ${event.targetAddress}:${event.targetPort}`)
+            const forward = this.forwardedPorts.find(x => x.port === event.targetPort && x.host === event.targetAddress)
             if (!forward) {
             if (!forward) {
-                this.emitServiceMessage(colors.bgRed.black(' X ') + ` Rejected incoming forwarded connection for unrecognized port ${details.destPort}`)
-                reject()
+                this.emitServiceMessage(colors.bgRed.black(' X ') + ` Rejected incoming forwarded connection for unrecognized port ${event.targetAddress}:${event.targetPort}`)
                 return
                 return
             }
             }
             const socket = new Socket()
             const socket = new Socket()
@@ -365,24 +369,19 @@ export class SSHSession {
             socket.on('error', e => {
             socket.on('error', e => {
                 // eslint-disable-next-line @typescript-eslint/no-base-to-string
                 // eslint-disable-next-line @typescript-eslint/no-base-to-string
                 this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not forward the remote connection to ${forward.targetAddress}:${forward.targetPort}: ${e}`)
                 this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not forward the remote connection to ${forward.targetAddress}:${forward.targetPort}: ${e}`)
-                reject()
+                event.channel.close()
             })
             })
+            event.channel.data$.subscribe(data => socket.write(data))
+            socket.on('data', data => event.channel.write(Uint8Array.from(data)))
+            event.channel.closed$.subscribe(() => socket.destroy())
+            socket.on('close', () => event.channel.close())
             socket.on('connect', () => {
             socket.on('connect', () => {
                 this.logger.info('Connection forwarded')
                 this.logger.info('Connection forwarded')
-                const stream = accept()
-                stream.pipe(socket)
-                socket.pipe(stream)
-                stream.on('close', () => {
-                    socket.destroy()
-                })
-                socket.on('close', () => {
-                    stream.close()
-                })
             })
             })
         })
         })
 
 
-        this.ssh.on('x11', async (details, accept, reject) => {
-            this.logger.info(`Incoming X11 connection from ${details.srcIP}:${details.srcPort}`)
+        this.ssh.x11ChannelOpen$.subscribe(async event => {
+            this.logger.info(`Incoming X11 connection from ${event.clientAddress}:${event.clientPort}`)
             const displaySpec = (this.config.store.ssh.x11Display || process.env.DISPLAY) ?? 'localhost:0'
             const displaySpec = (this.config.store.ssh.x11Display || process.env.DISPLAY) ?? 'localhost:0'
             this.logger.debug(`Trying display ${displaySpec}`)
             this.logger.debug(`Trying display ${displaySpec}`)
 
 
@@ -390,14 +389,18 @@ export class SSHSession {
             try {
             try {
                 const x11Stream = await socket.connect(displaySpec)
                 const x11Stream = await socket.connect(displaySpec)
                 this.logger.info('Connection forwarded')
                 this.logger.info('Connection forwarded')
-                const stream = accept()
-                stream.pipe(x11Stream)
-                x11Stream.pipe(stream)
-                stream.on('close', () => {
+
+                event.channel.data$.subscribe(data => {
+                    x11Stream.write(data)
+                })
+                x11Stream.on('data', data => {
+                    event.channel.write(Uint8Array.from(data))
+                })
+                event.channel.closed$.subscribe(() => {
                     socket.destroy()
                     socket.destroy()
                 })
                 })
                 x11Stream.on('close', () => {
                 x11Stream.on('close', () => {
-                    stream.close()
+                    event.channel.close()
                 })
                 })
             } catch (e) {
             } catch (e) {
                 // eslint-disable-next-line @typescript-eslint/no-base-to-string
                 // eslint-disable-next-line @typescript-eslint/no-base-to-string
@@ -408,27 +411,43 @@ export class SSHSession {
                     this.emitServiceMessage('    * VcXsrv: https://sourceforge.net/projects/vcxsrv/')
                     this.emitServiceMessage('    * VcXsrv: https://sourceforge.net/projects/vcxsrv/')
                     this.emitServiceMessage('    * Xming: https://sourceforge.net/projects/xming/')
                     this.emitServiceMessage('    * Xming: https://sourceforge.net/projects/xming/')
                 }
                 }
-                reject()
+                event.channel.close()
             }
             }
         })
         })
+
+        this.ssh.agentChannelOpen$.subscribe(async channel => {
+            const spec = await this.getAgentConnectionSpec()
+            if (!spec) {
+                await channel.close()
+                return
+            }
+
+            const agent = await russh.SSHAgentStream.connect(spec)
+            channel.data$.subscribe(data => agent.write(data))
+            agent.data$.subscribe(data => channel.write(data), undefined, () => channel.close())
+            channel.closed$.subscribe(() => agent.close())
+        })
     }
     }
 
 
-    private async verifyHostKey (handshake: Handshake): Promise<boolean> {
+    private async verifyHostKey (key: russh.SshPublicKey): Promise<boolean> {
         this.emitServiceMessage('Host key fingerprint:')
         this.emitServiceMessage('Host key fingerprint:')
-        this.emitServiceMessage(colors.white.bgBlack(` ${handshake.serverHostKey} `) + colors.bgBlackBright(' ' + this.hostKeyDigest + ' '))
+        this.emitServiceMessage(colors.white.bgBlack(` ${key.algorithm()} `) + colors.bgBlackBright(' ' + key.fingerprint() + ' '))
         if (!this.config.store.ssh.verifyHostKeys) {
         if (!this.config.store.ssh.verifyHostKeys) {
             return true
             return true
         }
         }
         const selector = {
         const selector = {
             host: this.profile.options.host,
             host: this.profile.options.host,
             port: this.profile.options.port ?? 22,
             port: this.profile.options.port ?? 22,
-            type: handshake.serverHostKey,
+            type: key.algorithm(),
         }
         }
+
+        const keyDigest = crypto.createHash('sha256').update(key.bytes()).digest('base64')
+
         const knownHost = this.knownHosts.getFor(selector)
         const knownHost = this.knownHosts.getFor(selector)
-        if (!knownHost || knownHost.digest !== this.hostKeyDigest) {
+        if (!knownHost || knownHost.digest !== keyDigest) {
             const modal = this.ngbModal.open(HostKeyPromptModalComponent)
             const modal = this.ngbModal.open(HostKeyPromptModalComponent)
             modal.componentInstance.selector = selector
             modal.componentInstance.selector = selector
-            modal.componentInstance.digest = this.hostKeyDigest
+            modal.componentInstance.digest = keyDigest
             return modal.result.catch(() => false)
             return modal.result.catch(() => false)
         }
         }
         return true
         return true
@@ -450,57 +469,49 @@ export class SSHSession {
         this.keyboardInteractivePrompt.next(prompt)
         this.keyboardInteractivePrompt.next(prompt)
     }
     }
 
 
-    async handleAuth (methodsLeft?: string[] | null): Promise<any> {
+    async handleAuth (methodsLeft?: string[] | null): Promise<russh.AuthenticatedSSHClient|null> {
         this.activePrivateKey = null
         this.activePrivateKey = null
 
 
+        if (!(this.ssh instanceof russh.SSHClient)) {
+            throw new Error('Wrong state for auth handling')
+        }
+
+        if (!this.authUsername) {
+            throw new Error('No username')
+        }
+
         while (true) {
         while (true) {
             const method = this.remainingAuthMethods.shift()
             const method = this.remainingAuthMethods.shift()
             if (!method) {
             if (!method) {
-                return false
+                return null
             }
             }
             if (methodsLeft && !methodsLeft.includes(method.type) && method.type !== 'agent') {
             if (methodsLeft && !methodsLeft.includes(method.type) && method.type !== 'agent') {
                 // Agent can still be used even if not in methodsLeft
                 // Agent can still be used even if not in methodsLeft
                 this.logger.info('Server does not support auth method', method.type)
                 this.logger.info('Server does not support auth method', method.type)
                 continue
                 continue
             }
             }
-            if (method.type === 'password') {
-                if (this.profile.options.password) {
-                    this.emitServiceMessage(this.translate.instant('Using preset password'))
-                    return {
-                        type: 'password',
-                        username: this.authUsername,
-                        password: this.profile.options.password,
-                    }
-                }
-
-                if (!this.keychainPasswordUsed && this.profile.options.user) {
-                    const password = await this.passwordStorage.loadPassword(this.profile)
-                    if (password) {
-                        this.emitServiceMessage(this.translate.instant('Trying saved password'))
-                        this.keychainPasswordUsed = true
-                        return {
-                            type: 'password',
-                            username: this.authUsername,
-                            password,
-                        }
-                    }
+            if (method.type === 'saved-password') {
+                this.emitServiceMessage(this.translate.instant('Using saved password'))
+                const result = await this.ssh.authenticateWithPassword(this.authUsername, method.password)
+                if (result) {
+                    return result
                 }
                 }
-
+            }
+            if (method.type === 'prompt-password') {
                 const modal = this.ngbModal.open(PromptModalComponent)
                 const modal = this.ngbModal.open(PromptModalComponent)
                 modal.componentInstance.prompt = `Password for ${this.authUsername}@${this.profile.options.host}`
                 modal.componentInstance.prompt = `Password for ${this.authUsername}@${this.profile.options.host}`
                 modal.componentInstance.password = true
                 modal.componentInstance.password = true
                 modal.componentInstance.showRememberCheckbox = true
                 modal.componentInstance.showRememberCheckbox = true
 
 
                 try {
                 try {
-                    const result = await modal.result.catch(() => null)
-                    if (result) {
-                        if (result.remember) {
-                            this.savedPassword = result.value
+                    const promptResult = await modal.result.catch(() => null)
+                    if (promptResult) {
+                        if (promptResult.remember) {
+                            this.savedPassword = promptResult.value
                         }
                         }
-                        return {
-                            type: 'password',
-                            username: this.authUsername,
-                            password: result.value,
+                        const result = await this.ssh.authenticateWithPassword(this.authUsername, promptResult.value)
+                        if (result) {
+                            return result
                         }
                         }
                     } else {
                     } else {
                         continue
                         continue
@@ -509,50 +520,104 @@ export class SSHSession {
                     continue
                     continue
                 }
                 }
             }
             }
-            if (method.type === 'publickey' && method.contents) {
+            if (method.type === 'publickey') {
                 try {
                 try {
-                    const key = await this.loadPrivateKey(method.name!, method.contents)
-                    return {
-                        type: 'publickey',
-                        username: this.authUsername,
-                        key,
+                    const key = await this.loadPrivateKey(method.name, method.contents)
+                    const result = await this.ssh.authenticateWithKeyPair(this.authUsername, key)
+                    if (result) {
+                        return result
                     }
                     }
                 } catch (e) {
                 } catch (e) {
                     this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to load private key ${method.name}: ${e}`)
                     this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to load private key ${method.name}: ${e}`)
                     continue
                     continue
                 }
                 }
             }
             }
-            return method.type
+            if (method.type === 'keyboard-interactive') {
+                let state: russh.AuthenticatedSSHClient|russh.KeyboardInteractiveAuthenticationState = await this.ssh.startKeyboardInteractiveAuthentication(this.authUsername)
+
+                while (true) {
+                    if (state.state === 'failure') {
+                        break
+                    }
+
+                    const prompts = state.prompts()
+
+                    let responses: string[] = []
+                    // OpenSSH can send a k-i request without prompts
+                    // just respond ok to it
+                    if (prompts.length > 0) {
+                        const prompt = new KeyboardInteractivePrompt(
+                            state.name,
+                            state.instructions,
+                            state.prompts(),
+                        )
+
+                        if (method.savedPassword) {
+                            // eslint-disable-next-line max-depth
+                            for (let i = 0; i < prompt.prompts.length; i++) {
+                                // eslint-disable-next-line max-depth
+                                if (prompt.isAPasswordPrompt(i)) {
+                                    prompt.responses[i] = method.savedPassword
+                                }
+                            }
+                        }
+
+                        this.emitKeyboardInteractivePrompt(prompt)
+
+                        try {
+                            // eslint-disable-next-line @typescript-eslint/await-thenable
+                            responses = await prompt.promise
+                        } catch {
+                            break // this loop
+                        }
+                    }
+
+                    state = await this.ssh .continueKeyboardInteractiveAuthentication(responses)
+
+                    if (state instanceof russh.AuthenticatedSSHClient) {
+                        return state
+                    }
+                }
+            }
+            if (method.type === 'agent') {
+                try {
+                    const result = await this.ssh.authenticateWithAgent(this.authUsername, method)
+                    if (result) {
+                        return result
+                    }
+                } catch (e) {
+                    this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to authenticate using agent: ${e}`)
+                    continue
+                }
+            }
         }
         }
+        return null
     }
     }
 
 
     async addPortForward (fw: ForwardedPort): Promise<void> {
     async addPortForward (fw: ForwardedPort): Promise<void> {
         if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {
         if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {
-            await fw.startLocalListener((accept, reject, sourceAddress, sourcePort, targetAddress, targetPort) => {
+            await fw.startLocalListener(async (accept, reject, sourceAddress, sourcePort, targetAddress, targetPort) => {
                 this.logger.info(`New connection on ${fw}`)
                 this.logger.info(`New connection on ${fw}`)
-                this.ssh.forwardOut(
-                    sourceAddress ?? '127.0.0.1',
-                    sourcePort ?? 0,
-                    targetAddress,
-                    targetPort,
-                    (err, stream) => {
-                        if (err) {
-                            // eslint-disable-next-line @typescript-eslint/no-base-to-string
-                            this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwarded connection to ${targetAddress}:${targetPort} via ${fw}: ${err}`)
-                            reject()
-                            return
-                        }
-                        const socket = accept()
-                        stream.pipe(socket)
-                        socket.pipe(stream)
-                        stream.on('close', () => {
-                            socket.destroy()
-                        })
-                        socket.on('close', () => {
-                            stream.close()
-                        })
-                    },
-                )
+                if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
+                    this.logger.error(`Connection while unauthenticated on ${fw}`)
+                    reject()
+                    return
+                }
+                const channel = await this.ssh.openTCPForwardChannel({
+                    addressToConnectTo: targetAddress,
+                    portToConnectTo: targetPort,
+                    originatorAddress: sourceAddress ?? '127.0.0.1',
+                    originatorPort: sourcePort ?? 0,
+                }).catch(err => {
+                    this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwarded connection to ${targetAddress}:${targetPort} via ${fw}: ${err}`)
+                    reject()
+                    throw err
+                })
+                const socket = accept()
+                channel.data$.subscribe(data => socket.write(data))
+                socket.on('data', data => channel.write(Uint8Array.from(data)))
+                channel.closed$.subscribe(() => socket.destroy())
+                socket.on('close', () => channel.close())
             }).then(() => {
             }).then(() => {
                 this.emitServiceMessage(colors.bgGreen.black(' -> ') + ` Forwarded ${fw}`)
                 this.emitServiceMessage(colors.bgGreen.black(' -> ') + ` Forwarded ${fw}`)
                 this.forwardedPorts.push(fw)
                 this.forwardedPorts.push(fw)
@@ -562,17 +627,16 @@ export class SSHSession {
             })
             })
         }
         }
         if (fw.type === PortForwardType.Remote) {
         if (fw.type === PortForwardType.Remote) {
-            await new Promise<void>((resolve, reject) => {
-                this.ssh.forwardIn(fw.host, fw.port, err => {
-                    if (err) {
-                        // eslint-disable-next-line @typescript-eslint/no-base-to-string
-                        this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected port forwarding for ${fw}: ${err}`)
-                        reject(err)
-                        return
-                    }
-                    resolve()
-                })
-            })
+            if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
+                throw new Error('Cannot add remote port forward before auth')
+            }
+            try {
+                await this.ssh.forwardTCPPort(fw.host, fw.port)
+            } catch (err) {
+                // eslint-disable-next-line @typescript-eslint/no-base-to-string
+                this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected port forwarding for ${fw}: ${err}`)
+                return
+            }
             this.emitServiceMessage(colors.bgGreen.black(' <- ') + ` Forwarded ${fw}`)
             this.emitServiceMessage(colors.bgGreen.black(' <- ') + ` Forwarded ${fw}`)
             this.forwardedPorts.push(fw)
             this.forwardedPorts.push(fw)
         }
         }
@@ -584,7 +648,10 @@ export class SSHSession {
             this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
             this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
         }
         }
         if (fw.type === PortForwardType.Remote) {
         if (fw.type === PortForwardType.Remote) {
-            this.ssh.unforwardIn(fw.host, fw.port)
+            if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
+                throw new Error('Cannot remove remote port forward before auth')
+            }
+            this.ssh.stopForwardingTCPPort(fw.host, fw.port)
             this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
             this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
         }
         }
         this.emitServiceMessage(`Stopped forwarding ${fw}`)
         this.emitServiceMessage(`Stopped forwarding ${fw}`)
@@ -595,43 +662,55 @@ export class SSHSession {
         this.willDestroy.next()
         this.willDestroy.next()
         this.willDestroy.complete()
         this.willDestroy.complete()
         this.serviceMessage.complete()
         this.serviceMessage.complete()
-        this.proxyCommandStream?.stop()
-        this.ssh.end()
+        this.ssh.disconnect()
     }
     }
 
 
-    openShellChannel (options: { x11: boolean }): Promise<ClientChannel> {
-        return new Promise<ClientChannel>((resolve, reject) => {
-            this.ssh.shell({ term: 'xterm-256color' }, options, (err, shell) => {
-                if (err) {
-                    reject(err)
-                } else {
-                    resolve(shell)
-                }
-            })
+    async openShellChannel (options: { x11: boolean }): Promise<russh.Channel> {
+        if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
+            throw new Error('Cannot open shell channel before auth')
+        }
+        const ch = await this.ssh.openSessionChannel()
+        await ch.requestPTY('xterm-256color', {
+            columns: 80,
+            rows: 24,
+            pixHeight: 0,
+            pixWidth: 0,
         })
         })
+        if (options.x11) {
+            await ch.requestX11Forwarding({
+                singleConnection: false,
+                authProtocol: 'MIT-MAGIC-COOKIE-1',
+                authCookie: crypto.randomBytes(16).toString('hex'),
+                screenNumber: 0,
+            })
+        }
+        if (this.profile.options.agentForward) {
+            await ch.requestAgentForwarding()
+        }
+        await ch.requestShell()
+        return ch
     }
     }
 
 
-    async loadPrivateKey (name: string, privateKeyContents: Buffer): Promise<string|null> {
+    async loadPrivateKey (name: string, privateKeyContents: Buffer): Promise<russh.KeyPair> {
         this.emitServiceMessage(`Loading private key: ${name}`)
         this.emitServiceMessage(`Loading private key: ${name}`)
-        const parsedKey = await this.parsePrivateKey(privateKeyContents.toString())
-        this.activePrivateKey = parsedKey.toString('openssh')
+        this.activePrivateKey = await this.loadPrivateKeyWithPassphraseMaybe(privateKeyContents.toString())
         return this.activePrivateKey
         return this.activePrivateKey
     }
     }
 
 
-    async parsePrivateKey (privateKey: string): Promise<any> {
+    async loadPrivateKeyWithPassphraseMaybe (privateKey: string): Promise<russh.KeyPair> {
         const keyHash = crypto.createHash('sha512').update(privateKey).digest('hex')
         const keyHash = crypto.createHash('sha512').update(privateKey).digest('hex')
         let triedSavedPassphrase = false
         let triedSavedPassphrase = false
         let passphrase: string|null = null
         let passphrase: string|null = null
         while (true) {
         while (true) {
             try {
             try {
-                return sshpk.parsePrivateKey(privateKey, 'auto', { passphrase })
+                return await russh.KeyPair.parse(privateKey, passphrase ?? undefined)
             } catch (e) {
             } catch (e) {
                 if (!triedSavedPassphrase) {
                 if (!triedSavedPassphrase) {
                     passphrase = await this.passwordStorage.loadPrivateKeyPassword(keyHash)
                     passphrase = await this.passwordStorage.loadPrivateKeyPassword(keyHash)
                     triedSavedPassphrase = true
                     triedSavedPassphrase = true
                     continue
                     continue
                 }
                 }
-                if (e instanceof sshpk.KeyEncryptedError || e instanceof sshpk.KeyParseError) {
+                if (e.toString() === 'Error: Keys(KeyIsEncrypted)' || e.toString() === 'Error: Keys(SshKey(Crypto))') {
                     await this.passwordStorage.deletePrivateKeyPassword(keyHash)
                     await this.passwordStorage.deletePrivateKeyPassword(keyHash)
 
 
                     const modal = this.ngbModal.open(PromptModalComponent)
                     const modal = this.ngbModal.open(PromptModalComponent)

+ 0 - 5
tabby-ssh/webpack.config.mjs

@@ -7,9 +7,4 @@ import config from '../webpack.plugin.config.mjs'
 export default () => config({
 export default () => config({
     name: 'ssh',
     name: 'ssh',
     dirname: __dirname,
     dirname: __dirname,
-    alias: {
-        'cpu-features': false,
-        './crypto/build/Release/sshcrypto.node': false,
-        '../build/Release/cpufeatures.node': false,
-    },
 })
 })

+ 14 - 169
tabby-ssh/yarn.lock

@@ -9,33 +9,11 @@
   dependencies:
   dependencies:
     ipv6 "*"
     ipv6 "*"
 
 
-"@types/node@*":
-  version "22.1.0"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-22.1.0.tgz#6d6adc648b5e03f0e83c78dc788c2b037d0ad94b"
-  integrity sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==
-  dependencies:
-    undici-types "~6.13.0"
-
 "@types/[email protected]":
 "@types/[email protected]":
   version "20.3.1"
   version "20.3.1"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.1.tgz#e8a83f1aa8b649377bb1fb5d7bac5cb90e784dfe"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.1.tgz#e8a83f1aa8b649377bb1fb5d7bac5cb90e784dfe"
   integrity sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==
   integrity sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==
 
 
-"@types/ssh2-streams@*":
-  version "0.1.12"
-  resolved "https://registry.yarnpkg.com/@types/ssh2-streams/-/ssh2-streams-0.1.12.tgz#e68795ba2bf01c76b93f9c9809e1f42f0eaaec5f"
-  integrity sha512-Sy8tpEmCce4Tq0oSOYdfqaBpA3hDM8SoxoFh5vzFsu2oL+znzGz8oVWW7xb4K920yYMUY+PIG31qZnFMfPWNCg==
-  dependencies:
-    "@types/node" "*"
-
-"@types/ssh2@^0.5.46":
-  version "0.5.52"
-  resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-0.5.52.tgz#9dbd8084e2a976e551d5e5e70b978ed8b5965741"
-  integrity sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==
-  dependencies:
-    "@types/node" "*"
-    "@types/ssh2-streams" "*"
-
 ansi-colors@^4.1.1:
 ansi-colors@^4.1.1:
   version "4.1.3"
   version "4.1.3"
   resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b"
   resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b"
@@ -46,40 +24,16 @@ ansi-regex@^6.0.1:
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a"
   integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==
   integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==
 
 
-asn1@~0.2.3:
-  version "0.2.4"
-  resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
-  integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
-  dependencies:
-    safer-buffer "~2.1.0"
-
-assert-plus@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
-  integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
-
 [email protected]:
 [email protected]:
   version "0.2.10"
   version "0.2.10"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
-  integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E=
+  integrity sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==
 
 
 balanced-match@^1.0.0:
 balanced-match@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
   integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
   integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
 
 
-bcrypt-pbkdf@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
-  integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
-  dependencies:
-    tweetnacl "^0.14.3"
-
-bn.js@^4.0.0, bn.js@^4.1.0:
-  version "4.12.0"
-  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
-  integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
-
 brace-expansion@^1.1.7:
 brace-expansion@^1.1.7:
   version "1.1.11"
   version "1.1.11"
   resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
   resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -88,22 +42,17 @@ brace-expansion@^1.1.7:
     balanced-match "^1.0.0"
     balanced-match "^1.0.0"
     concat-map "0.0.1"
     concat-map "0.0.1"
 
 
-brorand@^1.0.1:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
-  integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==
-
 [email protected]:
 [email protected]:
   version "0.4.5"
   version "0.4.5"
   resolved "https://registry.yarnpkg.com/cli/-/cli-0.4.5.tgz#78f9485cd161b566e9a6c72d7170c4270e81db61"
   resolved "https://registry.yarnpkg.com/cli/-/cli-0.4.5.tgz#78f9485cd161b566e9a6c72d7170c4270e81db61"
-  integrity sha1-ePlIXNFhtWbppsctcXDEJw6B22E=
+  integrity sha512-dbn5HyeJWSOU58RwOEiF1VWrl7HRvDsKLpu0uiI/vExH6iNoyUzjB5Mr3IJY5DVUfnbpe9793xw4DFJVzC9nWQ==
   dependencies:
   dependencies:
     glob ">= 3.1.4"
     glob ">= 3.1.4"
 
 
 [email protected]:
 [email protected]:
   version "0.1.10"
   version "0.1.10"
   resolved "https://registry.yarnpkg.com/cliff/-/cliff-0.1.10.tgz#53be33ea9f59bec85609ee300ac4207603e52013"
   resolved "https://registry.yarnpkg.com/cliff/-/cliff-0.1.10.tgz#53be33ea9f59bec85609ee300ac4207603e52013"
-  integrity sha1-U74z6p9ZvshWCe4wCsQgdgPlIBM=
+  integrity sha512-roZWcC2Cxo/kKjRXw7YUpVNtxJccbvcl7VzTjUYgLQk6Ot0R8bm2netbhSZYWWNrKlOO/7HD6GXHl8dtzE6SiQ==
   dependencies:
   dependencies:
     colors "~1.0.3"
     colors "~1.0.3"
     eyes "~0.1.8"
     eyes "~0.1.8"
@@ -112,12 +61,12 @@ [email protected]:
 [email protected]:
 [email protected]:
   version "0.6.2"
   version "0.6.2"
   resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc"
   resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc"
-  integrity sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=
+  integrity sha512-OsSVtHK8Ir8r3+Fxw/b4jS1ZLPXkV6ZxDRJQzeD7qo0SqMXWrHDM71DgYzPMHY8SFJ0Ao+nNU2p1MmwdzKqPrw==
 
 
 colors@~1.0.3:
 colors@~1.0.3:
   version "1.0.3"
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
-  integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=
+  integrity sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==
 
 
 [email protected]:
 [email protected]:
   version "0.0.1"
   version "0.0.1"
@@ -127,62 +76,19 @@ [email protected]:
 [email protected]:
 [email protected]:
   version "1.0.3"
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2"
   resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2"
-  integrity sha1-IegLK+hYD5i0aPN5QwZisEbDStI=
-
-dashdash@^1.12.0:
-  version "1.14.1"
-  resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
-  integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
-  dependencies:
-    assert-plus "^1.0.0"
-
-diffie-hellman@^5.0.3:
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
-  integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==
-  dependencies:
-    bn.js "^4.1.0"
-    miller-rabin "^4.0.0"
-    randombytes "^2.0.0"
-
-ecc-jsbn@~0.1.1:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
-  integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
-  dependencies:
-    jsbn "~0.1.0"
-    safer-buffer "^2.1.0"
+  integrity sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==
 
 
 [email protected], eyes@~0.1.8:
 [email protected], eyes@~0.1.8:
   version "0.1.8"
   version "0.1.8"
   resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
   resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
-  integrity sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=
+  integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==
 
 
 fs.realpath@^1.0.0:
 fs.realpath@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 
 
-getpass@^0.1.1:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
-  integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
-  dependencies:
-    assert-plus "^1.0.0"
-
-"glob@>= 3.1.4":
-  version "7.1.6"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
-  integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
-  dependencies:
-    fs.realpath "^1.0.0"
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "^3.0.4"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
-glob@^7.1.3:
[email protected], "glob@>= 3.1.4", glob@^7.1.3:
   version "7.2.3"
   version "7.2.3"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
   integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
   integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@@ -210,7 +116,7 @@ inherits@2:
 ipv6@*:
 ipv6@*:
   version "3.1.3"
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/ipv6/-/ipv6-3.1.3.tgz#4d9064f9c2dafa0dd10b8b7d76ffca4aad31b3b9"
   resolved "https://registry.yarnpkg.com/ipv6/-/ipv6-3.1.3.tgz#4d9064f9c2dafa0dd10b8b7d76ffca4aad31b3b9"
-  integrity sha1-TZBk+cLa+g3RC4t9dv/KSq0xs7k=
+  integrity sha512-TmLbUIURMAZ161GZDddTtAAb3aceRNLn7PRmP8fANp8xDRCW9oIQva8eenA48bRvw347jBqSREXMI38DybbUiQ==
   dependencies:
   dependencies:
     cli "0.4.x"
     cli "0.4.x"
     cliff "0.1.x"
     cliff "0.1.x"
@@ -219,27 +125,7 @@ ipv6@*:
 [email protected]:
 [email protected]:
   version "0.1.2"
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
-  integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
-
-jsbn@~0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
-  integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
-
-miller-rabin@^4.0.0:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
-  integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==
-  dependencies:
-    bn.js "^4.0.0"
-    brorand "^1.0.1"
-
-minimatch@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
-  integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
-  dependencies:
-    brace-expansion "^1.1.7"
+  integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==
 
 
 minimatch@^3.1.1:
 minimatch@^3.1.1:
   version "3.1.2"
   version "3.1.2"
@@ -263,14 +149,7 @@ path-is-absolute@^1.0.0:
 [email protected]:
 [email protected]:
   version "0.3.1"
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21"
   resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21"
-  integrity sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=
-
-randombytes@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
-  integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
-  dependencies:
-    safe-buffer "^5.1.0"
+  integrity sha512-yO5feByMzAp96LtP58wvPKSbaKAi/1C4kV9XpTctr6EepnP6F33RBNOiVrdz9BrPA98U2BMFsTNHo44TWcbQ2A==
 
 
 rimraf@^3.0.0:
 rimraf@^3.0.0:
   version "3.0.2"
   version "3.0.2"
@@ -284,39 +163,15 @@ run-script-os@^1.1.3:
   resolved "https://registry.yarnpkg.com/run-script-os/-/run-script-os-1.1.6.tgz#8b0177fb1b54c99a670f95c7fdc54f18b9c72347"
   resolved "https://registry.yarnpkg.com/run-script-os/-/run-script-os-1.1.6.tgz#8b0177fb1b54c99a670f95c7fdc54f18b9c72347"
   integrity sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==
   integrity sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==
 
 
-safe-buffer@^5.1.0:
-  version "5.2.1"
-  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
-  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
-
-safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
-  integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
-
 [email protected]:
 [email protected]:
   version "0.1.5"
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/sprintf/-/sprintf-0.1.5.tgz#8f83e39a9317c1a502cb7db8050e51c679f6edcf"
   resolved "https://registry.yarnpkg.com/sprintf/-/sprintf-0.1.5.tgz#8f83e39a9317c1a502cb7db8050e51c679f6edcf"
-  integrity sha1-j4PjmpMXwaUCy324BQ5Rxnn27c8=
-
-sshpk@Eugeny/node-sshpk#c2b71d1243714d2daf0988f84c3323d180817136:
-  version "1.18.0"
-  resolved "https://codeload.github.com/Eugeny/node-sshpk/tar.gz/c2b71d1243714d2daf0988f84c3323d180817136"
-  dependencies:
-    asn1 "~0.2.3"
-    assert-plus "^1.0.0"
-    bcrypt-pbkdf "^1.0.0"
-    dashdash "^1.12.0"
-    ecc-jsbn "~0.1.1"
-    getpass "^0.1.1"
-    jsbn "~0.1.0"
-    safer-buffer "^2.0.2"
-    tweetnacl "~0.14.0"
+  integrity sha512-4X5KsuXFQ7f+d7Y+bi4qSb6eI+YoifDTGr0MQJXRoYO7BO7evfRCjds6kk3z7l5CiJYxgDN1x5Er4WiyCt+zTQ==
 
 
 [email protected]:
 [email protected]:
   version "0.0.10"
   version "0.0.10"
   resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
   resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
-  integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=
+  integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==
 
 
 strip-ansi@^7.0.0:
 strip-ansi@^7.0.0:
   version "7.1.0"
   version "7.1.0"
@@ -339,20 +194,10 @@ tmp@^0.2.0:
   dependencies:
   dependencies:
     rimraf "^3.0.0"
     rimraf "^3.0.0"
 
 
-tweetnacl@^0.14.3, tweetnacl@~0.14.0:
-  version "0.14.5"
-  resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
-  integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
-
-undici-types@~6.13.0:
-  version "6.13.0"
-  resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.13.0.tgz#e3e79220ab8c81ed1496b5812471afd7cf075ea5"
-  integrity sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==
-
 [email protected]:
 [email protected]:
   version "0.8.3"
   version "0.8.3"
   resolved "https://registry.yarnpkg.com/winston/-/winston-0.8.3.tgz#64b6abf4cd01adcaefd5009393b1d8e8bec19db0"
   resolved "https://registry.yarnpkg.com/winston/-/winston-0.8.3.tgz#64b6abf4cd01adcaefd5009393b1d8e8bec19db0"
-  integrity sha1-ZLar9M0Brcrv1QCTk7HY6L7BnbA=
+  integrity sha512-fPoamsHq8leJ62D1M9V/f15mjQ1UHe4+7j1wpAT3fqgA5JqhJkk4aIfPEjfMTI9x6ZTjaLOpMAjluLtmgO5b6g==
   dependencies:
   dependencies:
     async "0.2.x"
     async "0.2.x"
     colors "0.6.x"
     colors "0.6.x"

+ 3 - 3
tabby-web/src/platform.ts

@@ -149,7 +149,7 @@ export class WebPlatformService extends PlatformService {
 }
 }
 
 
 class HTMLFileDownload extends FileDownload {
 class HTMLFileDownload extends FileDownload {
-    private buffers: Buffer[] = []
+    private buffers: Uint8Array[] = []
 
 
     constructor (
     constructor (
         private name: string,
         private name: string,
@@ -171,8 +171,8 @@ class HTMLFileDownload extends FileDownload {
         return this.size
         return this.size
     }
     }
 
 
-    async write (buffer: Buffer): Promise<void> {
-        this.buffers.push(Buffer.from(buffer))
+    async write (buffer: Uint8Array): Promise<void> {
+        this.buffers.push(Uint8Array.from(buffer))
         this.increaseProgress(buffer.length)
         this.increaseProgress(buffer.length)
         if (this.isComplete()) {
         if (this.isComplete()) {
             this.finish()
             this.finish()

+ 1 - 0
webpack.plugin.config.mjs

@@ -157,6 +157,7 @@ export default options => {
             'os',
             'os',
             'path',
             'path',
             'readline',
             'readline',
+            'russh',
             '@luminati-io/socksv5',
             '@luminati-io/socksv5',
             'stream',
             'stream',
             'windows-native-registry',
             'windows-native-registry',

+ 2 - 33
yarn.lock

@@ -1553,13 +1553,6 @@ asn1.js@^5.2.0:
     minimalistic-assert "^1.0.0"
     minimalistic-assert "^1.0.0"
     safer-buffer "^2.1.0"
     safer-buffer "^2.1.0"
 
 
-asn1@^0.2.6:
-  version "0.2.6"
-  resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
-  integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==
-  dependencies:
-    safer-buffer "~2.1.0"
-
 asn1@~0.2.3:
 asn1@~0.2.3:
   version "0.2.4"
   version "0.2.4"
   resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
   resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
@@ -1694,7 +1687,7 @@ base64-js@^1.3.1, base64-js@^1.5.1:
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
   integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
   integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
 
 
-bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2:
+bcrypt-pbkdf@^1.0.0:
   version "1.0.2"
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
   resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
   integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
   integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
@@ -1914,11 +1907,6 @@ buffer@^5.1.0:
     base64-js "^1.3.1"
     base64-js "^1.3.1"
     ieee754 "^1.1.13"
     ieee754 "^1.1.13"
 
 
-buildcheck@~0.0.6:
-  version "0.0.6"
-  resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.6.tgz#89aa6e417cfd1e2196e3f8fe915eb709d2fe4238"
-  integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==
-
 [email protected]:
 [email protected]:
   version "9.2.1"
   version "9.2.1"
   resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz#3184dcdf7ed6c47afb8df733813224ced4f624fd"
   resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz#3184dcdf7ed6c47afb8df733813224ced4f624fd"
@@ -2507,14 +2495,6 @@ [email protected], core-util-is@~1.0.0:
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
   integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
   integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
 
 
-cpu-features@~0.0.9:
-  version "0.0.10"
-  resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.10.tgz#9aae536db2710c7254d7ed67cb3cbc7d29ad79c5"
-  integrity sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==
-  dependencies:
-    buildcheck "~0.0.6"
-    nan "^2.19.0"
-
 crc@^3.8.0:
 crc@^3.8.0:
   version "3.8.0"
   version "3.8.0"
   resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6"
   resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6"
@@ -5985,7 +5965,7 @@ mute-stream@~0.0.4:
   resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz"
   resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz"
   integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
   integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
 
 
[email protected], nan@^2.18.0, nan@^2.19.0:
[email protected]:
   version "2.17.0"
   version "2.17.0"
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
   integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
   integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
@@ -8220,17 +8200,6 @@ sprintf-js@~1.0.2:
   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
   resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
   integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
   integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
 
 
-ssh2@^1.14.0:
-  version "1.15.0"
-  resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.15.0.tgz#2f998455036a7f89e0df5847efb5421748d9871b"
-  integrity sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==
-  dependencies:
-    asn1 "^0.2.6"
-    bcrypt-pbkdf "^1.0.2"
-  optionalDependencies:
-    cpu-features "~0.0.9"
-    nan "^2.18.0"
-
 sshpk@^1.7.0:
 sshpk@^1.7.0:
   version "1.16.1"
   version "1.16.1"
   resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
   resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"