mirror of
https://github.com/Eugeny/tabby.git
synced 2025-06-09 14:00:03 +00:00
SSH proxy support, unified proxy streams interface - fixed #5698
This commit is contained in:
parent
3eb4bd53a9
commit
81e1757ae9
@ -1,3 +1,4 @@
|
|||||||
export * from './contextMenu'
|
export * from './contextMenu'
|
||||||
export * from './interfaces'
|
export * from './interfaces'
|
||||||
export * from './importer'
|
export * from './importer'
|
||||||
|
export * from './proxyStream'
|
||||||
|
@ -32,6 +32,8 @@ export interface SSHProfileOptions extends LoginScriptsOptions {
|
|||||||
forwardedPorts?: ForwardedPortConfig[]
|
forwardedPorts?: ForwardedPortConfig[]
|
||||||
socksProxyHost?: string
|
socksProxyHost?: string
|
||||||
socksProxyPort?: number
|
socksProxyPort?: number
|
||||||
|
httpProxyHost?: string
|
||||||
|
httpProxyPort?: number
|
||||||
reuseSession?: boolean
|
reuseSession?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
61
tabby-ssh/src/api/proxyStream.ts
Normal file
61
tabby-ssh/src/api/proxyStream.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -29,6 +29,11 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
|||||||
)
|
)
|
||||||
div(translate) SOCKS proxy
|
div(translate) SOCKS proxy
|
||||||
.text-muted(translate) Connect through a proxy server
|
.text-muted(translate) Connect through a proxy server
|
||||||
|
button.dropdown-item(
|
||||||
|
(click)='connectionMode = "httpProxy"',
|
||||||
|
)
|
||||||
|
div(translate) HTTP proxy
|
||||||
|
.text-muted(translate) Using CONNECT method
|
||||||
|
|
||||||
.form-group.w-100(*ngIf='connectionMode === "proxyCommand"')
|
.form-group.w-100(*ngIf='connectionMode === "proxyCommand"')
|
||||||
label(translate) Proxy command
|
label(translate) Proxy command
|
||||||
@ -75,6 +80,22 @@ ul.nav-tabs(ngbNav, #nav='ngbNav')
|
|||||||
[(ngModel)]='profile.options.socksProxyPort',
|
[(ngModel)]='profile.options.socksProxyPort',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
.d-flex.w-100(*ngIf='connectionMode === "httpProxy"')
|
||||||
|
.form-group.w-100.mr-2
|
||||||
|
label(translate) HTTP proxy host
|
||||||
|
input.form-control(
|
||||||
|
type='text',
|
||||||
|
[(ngModel)]='profile.options.httpProxyHost',
|
||||||
|
)
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label(translate) HTTP proxy port
|
||||||
|
input.form-control(
|
||||||
|
type='number',
|
||||||
|
placeholder='5000',
|
||||||
|
[(ngModel)]='profile.options.httpProxyPort',
|
||||||
|
)
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
label(translate) Username
|
label(translate) Username
|
||||||
input.form-control(
|
input.form-control(
|
||||||
|
@ -17,7 +17,7 @@ export class SSHProfileSettingsComponent {
|
|||||||
profile: SSHProfile
|
profile: SSHProfile
|
||||||
hasSavedPassword: boolean
|
hasSavedPassword: boolean
|
||||||
|
|
||||||
connectionMode: 'direct'|'proxyCommand'|'jumpHost'|'socksProxy' = 'direct'
|
connectionMode: 'direct'|'proxyCommand'|'jumpHost'|'socksProxy'|'httpProxy' = 'direct'
|
||||||
|
|
||||||
supportedAlgorithms = supportedAlgorithms
|
supportedAlgorithms = supportedAlgorithms
|
||||||
algorithms: Record<string, Record<string, boolean>> = {}
|
algorithms: Record<string, Record<string, boolean>> = {}
|
||||||
@ -50,6 +50,8 @@ export class SSHProfileSettingsComponent {
|
|||||||
this.connectionMode = 'jumpHost'
|
this.connectionMode = 'jumpHost'
|
||||||
} else if (this.profile.options.socksProxyHost) {
|
} else if (this.profile.options.socksProxyHost) {
|
||||||
this.connectionMode = 'socksProxy'
|
this.connectionMode = 'socksProxy'
|
||||||
|
} else if (this.profile.options.httpProxyHost) {
|
||||||
|
this.connectionMode = 'httpProxy'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.profile.options.user) {
|
if (this.profile.options.user) {
|
||||||
@ -109,6 +111,10 @@ export class SSHProfileSettingsComponent {
|
|||||||
this.profile.options.socksProxyHost = undefined
|
this.profile.options.socksProxyHost = undefined
|
||||||
this.profile.options.socksProxyPort = undefined
|
this.profile.options.socksProxyPort = undefined
|
||||||
}
|
}
|
||||||
|
if (this.connectionMode !== 'httpProxy') {
|
||||||
|
this.profile.options.httpProxyHost = undefined
|
||||||
|
this.profile.options.httpProxyPort = undefined
|
||||||
|
}
|
||||||
|
|
||||||
this.loginScriptsSettings?.save()
|
this.loginScriptsSettings?.save()
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,8 @@ export class SSHProfilesService extends ProfileProvider<SSHProfile> {
|
|||||||
scripts: [],
|
scripts: [],
|
||||||
socksProxyHost: null,
|
socksProxyHost: null,
|
||||||
socksProxyPort: null,
|
socksProxyPort: null,
|
||||||
|
httpProxyHost: null,
|
||||||
|
httpProxyPort: null,
|
||||||
reuseSession: true,
|
reuseSession: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import * as shellQuote from 'shell-quote'
|
import * as shellQuote from 'shell-quote'
|
||||||
|
import * as net from 'net'
|
||||||
import socksv5 from '@luminati-io/socksv5'
|
import socksv5 from '@luminati-io/socksv5'
|
||||||
import { Duplex } from 'stream'
|
import { Duplex } from 'stream'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { spawn } from 'child_process'
|
import { spawn } from 'child_process'
|
||||||
import { ChildProcess } from 'node:child_process'
|
import { ChildProcess } from 'node:child_process'
|
||||||
import { Subject, Observable } from 'rxjs'
|
|
||||||
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 } from '../api'
|
import { SSHProfile, SSHProxyStream, SSHProxyStreamSocket } from '../api'
|
||||||
import { PasswordStorageService } from './passwordStorage.service'
|
import { PasswordStorageService } from './passwordStorage.service'
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
@ -53,17 +53,63 @@ export class SSHService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SocksProxyStream extends Duplex {
|
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 client: Duplex|null
|
||||||
private header: Buffer|null
|
private header: Buffer|null
|
||||||
|
|
||||||
constructor (private profile: SSHProfile) {
|
constructor (private profile: SSHProfile) {
|
||||||
super({
|
super()
|
||||||
allowHalfOpen: false,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async start (): Promise<void> {
|
async start (): Promise<SSHProxyStreamSocket> {
|
||||||
this.client = await new Promise((resolve, reject) => {
|
this.client = await new Promise((resolve, reject) => {
|
||||||
const connector = socksv5.connect({
|
const connector = socksv5.connect({
|
||||||
host: this.profile.options.host,
|
host: this.profile.options.host,
|
||||||
@ -75,87 +121,101 @@ export class SocksProxyStream extends Duplex {
|
|||||||
resolve(s)
|
resolve(s)
|
||||||
this.header = s.read()
|
this.header = s.read()
|
||||||
if (this.header) {
|
if (this.header) {
|
||||||
this.push(this.header)
|
this.emitOutput(this.header)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
connector.on('error', (err) => {
|
connector.on('error', (err) => {
|
||||||
reject(err)
|
reject(err)
|
||||||
this.destroy(err)
|
this.stop(new Error(`SOCKS connection failed: ${err.message}`))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
this.client?.on('data', data => {
|
this.client?.on('data', data => {
|
||||||
if (!this.header || data !== this.header) {
|
if (!this.header || data !== this.header) {
|
||||||
// socksv5 doesn't reliably emit the first data event
|
// socksv5 doesn't reliably emit the first data event
|
||||||
this.push(data)
|
this.emitOutput(data)
|
||||||
this.header = null
|
this.header = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.client?.on('close', (err) => {
|
this.client?.on('close', error => {
|
||||||
this.destroy(err)
|
this.stop(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return super.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
_read (size: number): void {
|
requestData (size: number): void {
|
||||||
this.client?.read(size)
|
this.client?.read(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
_write (chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void {
|
async consumeInput (data: Buffer): Promise<void> {
|
||||||
this.client?.write(chunk, callback)
|
return new Promise((resolve, reject) => {
|
||||||
|
this.client?.write(data, undefined, err => err ? reject(err) : resolve())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
_destroy (error: Error|null, callback: (error: Error|null) => void): void {
|
async stop (error?: Error): Promise<void> {
|
||||||
this.client?.destroy()
|
this.client?.destroy()
|
||||||
callback(error)
|
super.stop(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class HTTPProxyStream extends SSHProxyStream {
|
||||||
|
private client: Duplex|null
|
||||||
|
private connected = false
|
||||||
|
|
||||||
export class ProxyCommandStream extends Duplex {
|
constructor (private profile: SSHProfile) {
|
||||||
private process: ChildProcess
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
get output$ (): Observable<string> { return this.output }
|
async start (): Promise<SSHProxyStreamSocket> {
|
||||||
private output = new Subject<string>()
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
constructor (private command: string) {
|
return super.start()
|
||||||
super({
|
}
|
||||||
allowHalfOpen: false,
|
|
||||||
|
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 start (): Promise<void> {
|
async stop (error?: Error): Promise<void> {
|
||||||
const argv = shellQuote.parse(this.command)
|
this.client?.destroy()
|
||||||
this.process = spawn(argv[0], argv.slice(1), {
|
super.stop(error)
|
||||||
windowsHide: true,
|
|
||||||
stdio: ['pipe', 'pipe', 'ignore'],
|
|
||||||
})
|
|
||||||
this.process.on('error', error => {
|
|
||||||
this.destroy(new Error(`Proxy command has failed to start: ${error.message}`))
|
|
||||||
})
|
|
||||||
this.process.on('exit', code => {
|
|
||||||
this.destroy(new Error(`Proxy command has exited with code ${code}`))
|
|
||||||
})
|
|
||||||
this.process.stdout?.on('data', data => {
|
|
||||||
this.push(data)
|
|
||||||
})
|
|
||||||
this.process.stdout?.on('error', (err) => {
|
|
||||||
this.destroy(err)
|
|
||||||
})
|
|
||||||
this.process.stderr?.on('data', data => {
|
|
||||||
this.output.next(data.toString())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
_read (size: number): void {
|
|
||||||
this.process.stdout?.read(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
_write (chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void {
|
|
||||||
this.process.stdin?.write(chunk, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
_destroy (error: Error|null, callback: (error: Error|null) => void): void {
|
|
||||||
this.process.kill()
|
|
||||||
this.output.complete()
|
|
||||||
callback(error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,12 @@ import { Socket } from 'net'
|
|||||||
import { Client, ClientChannel, SFTPWrapper } from 'ssh2'
|
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 { ProxyCommandStream, SocksProxyStream } from '../services/ssh.service'
|
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 { promisify } from 'util'
|
||||||
import { SFTPSession } from './sftp'
|
import { SFTPSession } from './sftp'
|
||||||
import { ALGORITHM_BLACKLIST, SSHAlgorithmType, PortForwardType, SSHProfile } from '../api'
|
import { ALGORITHM_BLACKLIST, SSHAlgorithmType, PortForwardType, SSHProfile, SSHProxyStream } from '../api'
|
||||||
import { ForwardedPort } from './forwards'
|
import { ForwardedPort } from './forwards'
|
||||||
import { X11Socket } from './x11'
|
import { X11Socket } from './x11'
|
||||||
|
|
||||||
@ -62,8 +62,7 @@ export class SSHSession {
|
|||||||
sftp?: SFTPWrapper
|
sftp?: SFTPWrapper
|
||||||
forwardedPorts: ForwardedPort[] = []
|
forwardedPorts: ForwardedPort[] = []
|
||||||
jumpStream: any
|
jumpStream: any
|
||||||
proxyCommandStream: ProxyCommandStream|null = null
|
proxyCommandStream: SSHProxyStream|null = null
|
||||||
socksProxyStream: SocksProxyStream|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 }
|
||||||
@ -264,20 +263,26 @@ export class SSHSession {
|
|||||||
try {
|
try {
|
||||||
if (this.profile.options.socksProxyHost) {
|
if (this.profile.options.socksProxyHost) {
|
||||||
this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.socksProxyHost}:${this.profile.options.socksProxyPort}`)
|
this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.socksProxyHost}:${this.profile.options.socksProxyPort}`)
|
||||||
this.socksProxyStream = new SocksProxyStream(this.profile)
|
this.proxyCommandStream = new SocksProxyStream(this.profile)
|
||||||
await this.socksProxyStream.start()
|
}
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
if (this.profile.options.proxyCommand) {
|
if (this.profile.options.proxyCommand) {
|
||||||
this.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ` Using ${this.profile.options.proxyCommand}`)
|
this.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ` Using ${this.profile.options.proxyCommand}`)
|
||||||
this.proxyCommandStream = new ProxyCommandStream(this.profile.options.proxyCommand)
|
this.proxyCommandStream = new ProxyCommandStream(this.profile.options.proxyCommand)
|
||||||
|
}
|
||||||
this.proxyCommandStream.on('error', err => {
|
if (this.proxyCommandStream) {
|
||||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` ${err.message}`)
|
this.proxyCommandStream.destroyed$.subscribe(err => {
|
||||||
this.destroy()
|
if (err) {
|
||||||
|
this.emitServiceMessage(colors.bgRed.black(' X ') + ` ${err.message}`)
|
||||||
|
this.destroy()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.proxyCommandStream.output$.subscribe((message: string) => {
|
this.proxyCommandStream.message$.subscribe(message => {
|
||||||
this.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ' ' + message.trim())
|
this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ' ' + message.trim())
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.proxyCommandStream.start()
|
await this.proxyCommandStream.start()
|
||||||
@ -298,7 +303,7 @@ export class SSHSession {
|
|||||||
ssh.connect({
|
ssh.connect({
|
||||||
host: this.profile.options.host.trim(),
|
host: this.profile.options.host.trim(),
|
||||||
port: this.profile.options.port ?? 22,
|
port: this.profile.options.port ?? 22,
|
||||||
sock: this.proxyCommandStream ?? this.jumpStream ?? this.socksProxyStream,
|
sock: this.proxyCommandStream?.socket ?? this.jumpStream,
|
||||||
username: this.authUsername ?? undefined,
|
username: this.authUsername ?? undefined,
|
||||||
tryKeyboard: true,
|
tryKeyboard: true,
|
||||||
agent: this.agentPath,
|
agent: this.agentPath,
|
||||||
@ -578,7 +583,7 @@ export class SSHSession {
|
|||||||
this.willDestroy.next()
|
this.willDestroy.next()
|
||||||
this.willDestroy.complete()
|
this.willDestroy.complete()
|
||||||
this.serviceMessage.complete()
|
this.serviceMessage.complete()
|
||||||
this.proxyCommandStream?.destroy()
|
this.proxyCommandStream?.stop()
|
||||||
this.ssh.end()
|
this.ssh.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ div
|
|||||||
)
|
)
|
||||||
|
|
||||||
div.mt-4
|
div.mt-4
|
||||||
h3 Keyboard
|
h3(translate) Keyboard
|
||||||
|
|
||||||
.form-line
|
.form-line
|
||||||
.header
|
.header
|
||||||
@ -48,7 +48,7 @@ div.mt-4
|
|||||||
)
|
)
|
||||||
|
|
||||||
div.mt-4
|
div.mt-4
|
||||||
h3 Mouse
|
h3(translate) Mouse
|
||||||
|
|
||||||
.form-line
|
.form-line
|
||||||
.header
|
.header
|
||||||
@ -146,7 +146,7 @@ div.mt-4
|
|||||||
)
|
)
|
||||||
|
|
||||||
div.mt-4
|
div.mt-4
|
||||||
h3 Sound
|
h3(translate) Sound
|
||||||
|
|
||||||
.form-line
|
.form-line
|
||||||
.header
|
.header
|
||||||
@ -204,7 +204,7 @@ div.mt-4
|
|||||||
)
|
)
|
||||||
|
|
||||||
div.mt-4(*ngIf='hostApp.platform === Platform.Windows')
|
div.mt-4(*ngIf='hostApp.platform === Platform.Windows')
|
||||||
h3 Windows
|
h3(translate) Windows
|
||||||
|
|
||||||
.form-line
|
.form-line
|
||||||
.header
|
.header
|
||||||
|
Loading…
x
Reference in New Issue
Block a user