1
1
mirror of https://github.com/Eugeny/tabby.git synced 2025-01-03 08:04:02 +03:00

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

This commit is contained in:
Eugene Pankov 2021-11-28 18:21:26 +01:00
parent 246ae9fe77
commit 9856249c88
No known key found for this signature in database
GPG Key ID: 5896FCBBDD1CF4F4
12 changed files with 169 additions and 44 deletions

View File

@ -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",

View File

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

View File

@ -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)

View File

@ -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]
}
}

View File

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

View File

@ -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

View File

@ -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'

View File

@ -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))
})
},
})

View File

@ -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/node@16.0.1":
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 "*"
sprintf@0.1.x:
version "0.1.5"
resolved "https://registry.yarnpkg.com/sprintf/-/sprintf-0.1.5.tgz#8f83e39a9317c1a502cb7db8050e51c679f6edcf"

View File

@ -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) {

View File

@ -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,

View File

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