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:
parent
246ae9fe77
commit
9856249c88
@ -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",
|
||||
|
@ -30,6 +30,8 @@ export interface SSHProfileOptions extends LoginScriptsOptions {
|
||||
algorithms?: Record<string, string[]>
|
||||
proxyCommand?: string
|
||||
forwardedPorts?: ForwardedPortConfig[]
|
||||
socksProxyHost?: string
|
||||
socksProxyPort?: number
|
||||
}
|
||||
|
||||
export enum PortForwardType {
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,8 @@ export class SSHProfilesService extends ProfileProvider<SSHProfile> {
|
||||
proxyCommand: null,
|
||||
forwardedPorts: [],
|
||||
scripts: [],
|
||||
socksProxyHost: null,
|
||||
socksProxyPort: null,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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,4 +1,4 @@
|
||||
import socksv5 from 'socksv5'
|
||||
import socksv5 from '@luminati-io/socksv5'
|
||||
import { Server, Socket, createServer } from 'net'
|
||||
|
||||
import { ForwardedPortConfig, PortForwardType } from '../api'
|
||||
|
@ -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))
|
||||
})
|
||||
},
|
||||
})
|
||||
|
@ -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"
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -109,7 +109,7 @@ module.exports = options => {
|
||||
'os',
|
||||
'path',
|
||||
'readline',
|
||||
'socksv5',
|
||||
'@luminati-io/socksv5',
|
||||
'stream',
|
||||
'windows-native-registry',
|
||||
'windows-process-tree',
|
||||
|
Loading…
Reference in New Issue
Block a user