Explorar o código

fixed #5000 - native socksv5 connection support in ssh and connection mode UI overhaul

Eugene Pankov %!s(int64=4) %!d(string=hai) anos
pai
achega
9856249c88

+ 1 - 1
tabby-ssh/package.json

@@ -29,7 +29,7 @@
   },
   "dependencies": {
     "run-script-os": "^1.1.3",
-    "socksv5": "^0.0.6"
+    "@luminati-io/socksv5": "^0.0.7"
   },
   "peerDependencies": {
     "@angular/animations": "^9.1.9",

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

@@ -30,6 +30,8 @@ export interface SSHProfileOptions extends LoginScriptsOptions {
     algorithms?: Record<string, string[]>
     proxyCommand?: string
     forwardedPorts?: ForwardedPortConfig[]
+    socksProxyHost?: string
+    socksProxyPort?: number
 }
 
 export enum PortForwardType {

+ 58 - 25
tabby-ssh/src/components/sshProfileSettings.component.pug

@@ -2,15 +2,48 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
     li(ngbNavItem)
         a(ngbNavLink) General
         ng-template(ngbNavContent)
-            .d-flex.w-100(*ngIf='!useProxyCommand')
-                .form-group.w-100.mr-4
+            .d-flex.w-100.mt-3
+                .form-group.mr-2(
+                    ngbDropdown
+                )
+                    label Connection
+                    button.btn.btn-secondary.d-block(ngbDropdownToggle) {{getConnectionDropdownTitle()}}
+                    div(ngbDropdownMenu)
+                        button.dropdown-item(
+                            (click)='connectionMode = "direct"',
+                        ) Direct
+                        button.dropdown-item(
+                            *ngIf='hostApp.platform !== Platform.Web',
+                            (click)='connectionMode = "proxyCommand"',
+                        )
+                            div Proxy command
+                            .text-muted Command's stdin/stdout is used instead of a network connection
+                        button.dropdown-item(
+                            (click)='connectionMode = "jumpHost"',
+                        )
+                            div Jump host
+                            .text-muted Connect to a different host first and use it as a proxy
+                        button.dropdown-item(
+                            (click)='connectionMode = "socksProxy"',
+                        )
+                            div SOCKS proxy
+                            .text-muted Connect through a proxy server
+
+                .form-group.w-100(*ngIf='connectionMode === "proxyCommand"')
+                    label Proxy command
+                    input.form-control(
+                        type='text',
+                        [(ngModel)]='profile.options.proxyCommand',
+                    )
+
+                .form-group.w-100.mr-2(*ngIf='connectionMode !== "proxyCommand"')
                     label Host
                     input.form-control(
                         type='text',
                         [(ngModel)]='profile.options.host',
                     )
 
-                .form-group
+                .form-group(*ngIf='connectionMode !== "proxyCommand"')
                     label Port
                     input.form-control(
                         type='number',
@@ -18,8 +51,28 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
                         [(ngModel)]='profile.options.port',
                     )
 
-            .alert.alert-info(*ngIf='useProxyCommand')
-                .mr-auto Using a proxy command instead of a network connection
+            .form-group(*ngIf='connectionMode === "jumpHost"')
+                label Jump host
+                select.form-control([(ngModel)]='profile.options.jumpHost')
+                    option([ngValue]='null') Select
+                    option([ngValue]='x.id', *ngFor='let x of jumpHosts') {{x.name}}
+
+
+            .d-flex.w-100(*ngIf='connectionMode === "socksProxy"')
+                .form-group.w-100.mr-2
+                    label SOCKS proxy host
+                    input.form-control(
+                        type='text',
+                        [(ngModel)]='profile.options.socksProxyHost',
+                    )
+
+                .form-group
+                    label SOCKS proxy port
+                    input.form-control(
+                        type='number',
+                        placeholder='5000',
+                        [(ngModel)]='profile.options.socksProxyPort',
+                    )
 
             .form-group
                 label Username
@@ -93,13 +146,6 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
     li(ngbNavItem)
         a(ngbNavLink) Advanced
         ng-template(ngbNavContent)
-            .form-line(*ngIf='!useProxyCommand')
-                .header
-                    .title Jump host
-                select.form-control([(ngModel)]='profile.options.jumpHost')
-                    option(value='') None
-                    option([ngValue]='x.id', *ngFor='let x of jumpHosts') {{x.name}}
-
             .form-line(ng:if='hostApp.platform !== Platform.Web')
                 .header
                     .title X11 forwarding
@@ -143,19 +189,6 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
                     [(ngModel)]='profile.options.readyTimeout',
                 )
 
-            .form-line(*ngIf='!profile.options.jumpHost && hostApp.platform !== Platform.Web')
-                .header
-                    .title Use a proxy command
-                    .description Command's stdin/stdout is used instead of a network connection
-                toggle([(ngModel)]='useProxyCommand')
-
-            .form-group(*ngIf='useProxyCommand && !profile.options.jumpHost')
-                label Proxy command
-                input.form-control(
-                    type='text',
-                    [(ngModel)]='profile.options.proxyCommand',
-                )
-
     li(ngbNavItem)
         a(ngbNavLink) Ciphers
         ng-template(ngbNavContent)

+ 29 - 3
tabby-ssh/src/components/sshProfileSettings.component.ts

@@ -16,7 +16,8 @@ export class SSHProfileSettingsComponent {
     Platform = Platform
     profile: SSHProfile
     hasSavedPassword: boolean
-    useProxyCommand: boolean
+
+    connectionMode: 'direct'|'proxyCommand'|'jumpHost'|'socksProxy' = 'direct'
 
     supportedAlgorithms = supportedAlgorithms
     algorithms: Record<string, Record<string, boolean>> = {}
@@ -43,7 +44,14 @@ export class SSHProfileSettingsComponent {
         this.profile.options.auth = this.profile.options.auth ?? null
         this.profile.options.privateKeys ??= []
 
-        this.useProxyCommand = !!this.profile.options.proxyCommand
+        if (this.profile.options.proxyCommand) {
+            this.connectionMode = 'proxyCommand'
+        } else if (this.profile.options.jumpHost) {
+            this.connectionMode = 'jumpHost'
+        } else if (this.profile.options.socksProxyHost) {
+            this.connectionMode = 'socksProxy'
+        }
+
         if (this.profile.options.user) {
             try {
                 this.hasSavedPassword = !!await this.passwordStorage.loadPassword(this.profile)
@@ -90,9 +98,18 @@ export class SSHProfileSettingsComponent {
                 .map(([key, _]) => key)
             this.profile.options.algorithms![k].sort()
         }
-        if (!this.useProxyCommand) {
+
+        if (this.connectionMode !== 'jumpHost') {
+            this.profile.options.jumpHost = undefined
+        }
+        if (this.connectionMode !== 'proxyCommand') {
             this.profile.options.proxyCommand = undefined
         }
+        if (this.connectionMode !== 'socksProxy') {
+            this.profile.options.socksProxyHost = undefined
+            this.profile.options.socksProxyPort = undefined
+        }
+
         this.loginScriptsSettings?.save()
     }
 
@@ -104,4 +121,13 @@ export class SSHProfileSettingsComponent {
     onForwardRemoved (fw: ForwardedPortConfig) {
         this.profile.options.forwardedPorts = this.profile.options.forwardedPorts?.filter(x => x !== fw)
     }
+
+    getConnectionDropdownTitle () {
+        return {
+            direct: 'Direct',
+            proxyCommand: 'Proxy command',
+            jumpHost: 'Jump host',
+            socksProxy: 'SOCKS proxy',
+        }[this.connectionMode]
+    }
 }

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

@@ -37,6 +37,8 @@ export class SSHProfilesService extends ProfileProvider<SSHProfile> {
             proxyCommand: null,
             forwardedPorts: [],
             scripts: [],
+            socksProxyHost: null,
+            socksProxyPort: null,
         },
     }
 

+ 56 - 0
tabby-ssh/src/services/ssh.service.ts

@@ -1,4 +1,5 @@
 import * as shellQuote from 'shell-quote'
+import socksv5 from '@luminati-io/socksv5'
 import { Duplex } from 'stream'
 import { Injectable } from '@angular/core'
 import { spawn } from 'child_process'
@@ -52,6 +53,61 @@ export class SSHService {
     }
 }
 
+export class SocksProxyStream extends Duplex {
+    private client: Duplex|null
+    private header: Buffer|null
+
+    constructor (private profile: SSHProfile) {
+        super({
+            allowHalfOpen: false,
+        })
+    }
+
+    async start (): Promise<void> {
+        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()],
+            }, s => {
+                resolve(s)
+                this.header = s.read()
+                this.push(this.header)
+            })
+            connector.on('error', (err) => {
+                reject(err)
+                this.destroy(err)
+            })
+        })
+        this.client?.on('data', data => {
+            if (data !== this.header) {
+                // socksv5 doesn't reliably emit the first data event
+                this.push(data)
+                this.header = null
+            }
+        })
+        this.client?.on('close', (err) => {
+            this.destroy(err)
+        })
+    }
+
+    _read (size: number): void {
+        this.client?.read(size)
+    }
+
+    _write (chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void {
+        this.client?.write(chunk, callback)
+    }
+
+    _destroy (error: Error|null, callback: (error: Error|null) => void): void {
+        this.client?.destroy()
+        callback(error)
+    }
+}
+
+
 export class ProxyCommandStream extends Duplex {
     private process: ChildProcess
 

+ 1 - 1
tabby-ssh/src/session/forwards.ts

@@ -1,4 +1,4 @@
-import socksv5 from 'socksv5'
+import socksv5 from '@luminati-io/socksv5'
 import { Server, Socket, createServer } from 'net'
 
 import { ForwardedPortConfig, PortForwardType } from '../api'

+ 9 - 5
tabby-ssh/src/session/ssh.ts

@@ -12,7 +12,7 @@ import { BaseSession } from 'tabby-terminal'
 import { Socket } from 'net'
 import { Client, ClientChannel, SFTPWrapper } from 'ssh2'
 import { Subject, Observable } from 'rxjs'
-import { ProxyCommandStream } from '../services/ssh.service'
+import { ProxyCommandStream, SocksProxyStream } from '../services/ssh.service'
 import { PasswordStorageService } from '../services/passwordStorage.service'
 import { promisify } from 'util'
 import { SFTPSession } from './sftp'
@@ -50,6 +50,7 @@ export class SSHSession extends BaseSession {
     forwardedPorts: ForwardedPort[] = []
     jumpStream: any
     proxyCommandStream: ProxyCommandStream|null = null
+    socksProxyStream: SocksProxyStream|null = null
     savedPassword?: string
     get serviceMessage$ (): Observable<string> { return this.serviceMessage }
     get keyboardInteractivePrompt$ (): Observable<KeyboardInteractivePrompt> { return this.keyboardInteractivePrompt }
@@ -231,6 +232,11 @@ export class SSHSession extends BaseSession {
         })
 
         try {
+            if (this.profile.options.socksProxyHost) {
+                this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.socksProxyHost}:${this.profile.options.socksProxyPort}`)
+                this.socksProxyStream = new SocksProxyStream(this.profile)
+                await this.socksProxyStream.start()
+            }
             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)
@@ -262,7 +268,7 @@ export class SSHSession extends BaseSession {
             ssh.connect({
                 host: this.profile.options.host.trim(),
                 port: this.profile.options.port ?? 22,
-                sock: this.proxyCommandStream ?? this.jumpStream,
+                sock: this.proxyCommandStream ?? this.jumpStream ?? this.socksProxyStream,
                 username: this.authUsername ?? undefined,
                 tryKeyboard: true,
                 agent: this.agentPath,
@@ -279,9 +285,7 @@ export class SSHSession extends BaseSession {
                 algorithms,
                 authHandler: (methodsLeft, partialSuccess, callback) => {
                     this.zone.run(async () => {
-                        const a = await this.handleAuth(methodsLeft)
-                        console.warn(a)
-                        callback(a)
+                        callback(await this.handleAuth(methodsLeft))
                     })
                 },
             })

+ 7 - 7
tabby-ssh/yarn.lock

@@ -2,6 +2,13 @@
 # yarn lockfile v1
 
 
+"@luminati-io/socksv5@^0.0.7":
+  version "0.0.7"
+  resolved "https://registry.yarnpkg.com/@luminati-io/socksv5/-/socksv5-0.0.7.tgz#87414177d473c97aaefa907a3fe454d62d2fceca"
+  integrity sha512-paEEbcstjMZb2SvFHsSUOzimkx80/pFmMG5T3XR6Keb4NeBfYWEAtlVeiF39OrHRf9AjpNxahhwzdCAlLXZ4Hw==
+  dependencies:
+    ipv6 "*"
+
 "@types/node@*", "@types/[email protected]":
   version "16.0.1"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-16.0.1.tgz#70cedfda26af7a2ca073fdcc9beb2fff4aa693f8"
@@ -215,13 +222,6 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
-socksv5@^0.0.6:
-  version "0.0.6"
-  resolved "https://registry.yarnpkg.com/socksv5/-/socksv5-0.0.6.tgz#1327235ff7e8de21ac434a0a579dc69c3f071061"
-  integrity sha1-EycjX/fo3iGsQ0oKV53GnD8HEGE=
-  dependencies:
-    ipv6 "*"
-
 [email protected]:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/sprintf/-/sprintf-0.1.5.tgz#8f83e39a9317c1a502cb7db8050e51c679f6edcf"

+ 2 - 0
tabby-terminal/src/api/baseTerminalTab.component.ts

@@ -298,6 +298,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
         this.frontend.resize$.pipe(first()).subscribe(async ({ columns, rows }) => {
             this.size = { columns, rows }
             this.frontendReady.next()
+            this.frontendReady.complete()
 
             this.config.enabledServices(this.decorators).forEach(decorator => {
                 try {
@@ -554,6 +555,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
             }
         })
         this.output.complete()
+        this.frontendReady.complete()
 
         super.destroy()
         if (this.session?.open) {

+ 1 - 1
web/polyfills.ts

@@ -84,7 +84,7 @@ Tabby.registerModule('crypto', {
     },
 })
 Tabby.registerMock('dns', {})
-Tabby.registerMock('socksv5', {})
+Tabby.registerMock('@luminati-io/socksv5', {})
 Tabby.registerMock('util', require('util/'))
 Tabby.registerMock('keytar', {
     getPassword: () => null,

+ 1 - 1
webpack.plugin.config.js

@@ -109,7 +109,7 @@ module.exports = options => {
             'os',
             'path',
             'readline',
-            'socksv5',
+            '@luminati-io/socksv5',
             'stream',
             'windows-native-registry',
             'windows-process-tree',