mirror of
https://github.com/Eugeny/tabby.git
synced 2025-04-12 16:10:26 +08:00
ssh - added proxy command support - fixes #3722
This commit is contained in:
parent
ff0cd36b6a
commit
7cf8f8d58e
@ -6,6 +6,7 @@ import { Server, Socket, createServer, createConnection } from 'net'
|
||||
import { Client, ClientChannel } from 'ssh2'
|
||||
import { Logger } from 'terminus-core'
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { ProxyCommandStream } from './services/ssh.service'
|
||||
|
||||
export interface LoginScript {
|
||||
expect: string
|
||||
@ -42,6 +43,7 @@ export interface SSHConnection {
|
||||
agentForward?: boolean
|
||||
warnOnClose?: boolean
|
||||
algorithms?: Record<string, string[]>
|
||||
proxyCommand?: string
|
||||
}
|
||||
|
||||
export enum PortForwardType {
|
||||
@ -117,6 +119,7 @@ export class SSHSession extends BaseSession {
|
||||
forwardedPorts: ForwardedPort[] = []
|
||||
logger: Logger
|
||||
jumpStream: any
|
||||
proxyCommandStream: ProxyCommandStream|null = null
|
||||
|
||||
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
|
||||
private serviceMessage = new Subject<string>()
|
||||
@ -136,6 +139,11 @@ export class SSHSession extends BaseSession {
|
||||
async start (): Promise<void> {
|
||||
this.open = true
|
||||
|
||||
this.proxyCommandStream?.on('error', err => {
|
||||
this.emitServiceMessage(colors.bgRed.black(' X ') + ` ${err.message}`)
|
||||
this.destroy()
|
||||
})
|
||||
|
||||
try {
|
||||
this.shell = await this.openShellChannel({ x11: this.connection.x11 })
|
||||
} catch (err) {
|
||||
@ -361,6 +369,7 @@ export class SSHSession extends BaseSession {
|
||||
|
||||
async destroy (): Promise<void> {
|
||||
this.serviceMessage.complete()
|
||||
this.proxyCommandStream?.destroy()
|
||||
await super.destroy()
|
||||
}
|
||||
|
||||
|
@ -19,8 +19,8 @@
|
||||
[(ngModel)]='connection.group',
|
||||
)
|
||||
|
||||
.d-flex
|
||||
.form-group
|
||||
.d-flex.w-100(*ngIf='!useProxyCommand')
|
||||
.form-group.w-100.mr-4
|
||||
label Host
|
||||
input.form-control(
|
||||
type='text',
|
||||
@ -35,6 +35,9 @@
|
||||
[(ngModel)]='connection.port',
|
||||
)
|
||||
|
||||
.alert.alert-info(*ngIf='useProxyCommand')
|
||||
.mr-auto Using a proxy command instead of a network connection
|
||||
|
||||
.form-group
|
||||
label Username
|
||||
input.form-control(
|
||||
@ -42,9 +45,9 @@
|
||||
[(ngModel)]='connection.user',
|
||||
)
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Authentication
|
||||
.form-group
|
||||
label Authentication method
|
||||
|
||||
.btn-group.w-100(
|
||||
[(ngModel)]='connection.auth',
|
||||
ngbRadioGroup
|
||||
@ -99,7 +102,7 @@
|
||||
li(ngbNavItem)
|
||||
a(ngbNavLink) Advanced
|
||||
ng-template(ngbNavContent)
|
||||
.form-line
|
||||
.form-line(*ngIf='!useProxyCommand')
|
||||
.header
|
||||
.title Jump host
|
||||
select.form-control([(ngModel)]='connection.jumpHost')
|
||||
@ -165,6 +168,19 @@
|
||||
[(ngModel)]='connection.readyTimeout',
|
||||
)
|
||||
|
||||
.form-line(*ngIf='!connection.jumpHost')
|
||||
.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 && !connection.jumpHost')
|
||||
label Proxy command
|
||||
input.form-control(
|
||||
type='text',
|
||||
[(ngModel)]='connection.proxyCommand',
|
||||
)
|
||||
|
||||
li(ngbNavItem)
|
||||
a(ngbNavLink) Ciphers
|
||||
ng-template(ngbNavContent)
|
||||
|
@ -14,6 +14,7 @@ import { ALGORITHMS } from 'ssh2-streams/lib/constants'
|
||||
export class EditConnectionModalComponent {
|
||||
connection: SSHConnection
|
||||
hasSavedPassword: boolean
|
||||
useProxyCommand: boolean
|
||||
|
||||
supportedAlgorithms: Record<string, string> = {}
|
||||
defaultAlgorithms: Record<string, string[]> = {}
|
||||
@ -51,6 +52,8 @@ export class EditConnectionModalComponent {
|
||||
this.connection.scripts = this.connection.scripts ?? []
|
||||
this.connection.auth = this.connection.auth ?? null
|
||||
|
||||
this.useProxyCommand = !!this.connection.proxyCommand
|
||||
|
||||
for (const k of Object.values(SSHAlgorithmType)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!this.connection.algorithms[k]) {
|
||||
@ -102,6 +105,9 @@ export class EditConnectionModalComponent {
|
||||
.filter(([_, v]) => !!v)
|
||||
.map(([key, _]) => key)
|
||||
}
|
||||
if (!this.useProxyCommand) {
|
||||
this.connection.proxyCommand = undefined
|
||||
}
|
||||
this.modalInstance.close(this.connection)
|
||||
}
|
||||
|
||||
|
@ -119,7 +119,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
}))
|
||||
|
||||
|
||||
this.write('\r\n' + colors.black.bgCyan(' SSH ') + ` Connecting to ${session.connection.host}\r\n`)
|
||||
this.write('\r\n' + colors.black.bgWhite(' SSH ') + ` Connecting to ${session.connection.host}\r\n`)
|
||||
|
||||
const spinner = new Spinner({
|
||||
text: 'Connecting',
|
||||
@ -156,7 +156,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
||||
this.destroy()
|
||||
} else {
|
||||
// Session was closed abruptly
|
||||
this.write('\r\n' + colors.black.bgCyan(' SSH ') + ` ${session.connection.host}: session closed\r\n`)
|
||||
this.write('\r\n' + colors.black.bgWhite(' SSH ') + ` ${session.connection.host}: session closed\r\n`)
|
||||
if (!this.reconnectOffered) {
|
||||
this.reconnectOffered = true
|
||||
this.write('Press any key to reconnect\r\n')
|
||||
|
@ -1,4 +1,5 @@
|
||||
import colors from 'ansi-colors'
|
||||
import { Duplex } from 'stream'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
import { open as openTemp } from 'temp'
|
||||
import { Injectable, NgZone } from '@angular/core'
|
||||
@ -7,14 +8,17 @@ import { Client } from 'ssh2'
|
||||
import { SSH2Stream } from 'ssh2-streams'
|
||||
import * as fs from 'mz/fs'
|
||||
import { execFile } from 'mz/child_process'
|
||||
import { exec } from 'child_process'
|
||||
import * as path from 'path'
|
||||
import * as sshpk from 'sshpk'
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { HostAppService, Platform, Logger, LogService, ElectronService, AppService, SelectorOption, ConfigService, NotificationsService } from 'terminus-core'
|
||||
import { SettingsTabComponent } from 'terminus-settings'
|
||||
import { ALGORITHM_BLACKLIST, SSHConnection, SSHSession } from '../api'
|
||||
import { PromptModalComponent } from '../components/promptModal.component'
|
||||
import { PasswordStorageService } from './passwordStorage.service'
|
||||
import { SSHTabComponent } from '../components/sshTab.component'
|
||||
import { ChildProcess } from 'node:child_process'
|
||||
|
||||
const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
|
||||
|
||||
@ -252,9 +256,21 @@ export class SSHService {
|
||||
authMethodsLeft.push('hostbased')
|
||||
|
||||
try {
|
||||
if (session.connection.proxyCommand) {
|
||||
log(`Using proxy command: ${session.connection.proxyCommand}`)
|
||||
session.proxyCommandStream = new ProxyCommandStream(session.connection.proxyCommand)
|
||||
|
||||
session.proxyCommandStream.output$.subscribe((message: string) => {
|
||||
session.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ' ' + message)
|
||||
})
|
||||
|
||||
await session.proxyCommandStream.start()
|
||||
}
|
||||
|
||||
ssh.connect({
|
||||
host: session.connection.host,
|
||||
port: session.connection.port ?? 22,
|
||||
sock: session.proxyCommandStream ?? session.jumpStream,
|
||||
username: session.connection.user,
|
||||
password: session.connection.privateKey ? undefined : '',
|
||||
privateKey: privateKey ?? undefined,
|
||||
@ -271,7 +287,6 @@ export class SSHService {
|
||||
},
|
||||
hostHash: 'sha256' as any,
|
||||
algorithms,
|
||||
sock: session.jumpStream,
|
||||
authHandler: methodsLeft => {
|
||||
while (true) {
|
||||
const method = authMethodsLeft.shift()
|
||||
@ -448,6 +463,52 @@ export class SSHService {
|
||||
}
|
||||
}
|
||||
|
||||
export class ProxyCommandStream extends Duplex {
|
||||
private process: ChildProcess
|
||||
|
||||
get output$ (): Observable<string> { return this.output }
|
||||
private output = new Subject<string>()
|
||||
|
||||
constructor (private command: string) {
|
||||
super({
|
||||
allowHalfOpen: false,
|
||||
})
|
||||
}
|
||||
|
||||
async start (): Promise<void> {
|
||||
this.process = exec(this.command, {
|
||||
windowsHide: true,
|
||||
encoding: 'buffer',
|
||||
})
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
const _authPassword = SSH2Stream.prototype.authPassword
|
||||
SSH2Stream.prototype.authPassword = async function (username, passwordFn: any) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user