This commit is contained in:
Eugene 2024-07-10 23:01:05 +02:00
parent d265b5f8ab
commit c8d5b7ab61
No known key found for this signature in database
GPG Key ID: 5896FCBBDD1CF4F4
3 changed files with 118 additions and 151 deletions

View File

@ -125,7 +125,7 @@ export class SSHTabComponent extends ConnectableTerminalTabComponent<SSHProfile>
}) })
if (!session.open) { if (!session.open) {
this.write('\r\n' + colors.black.bgWhite(' SSH ') + ` Connecting to ${session.profile.options.host}\r\n`) this.write('\r\n' + colors.black.bgWhite(' SSH ') + ` Connecting to ${session.profile.name}\r\n`)
this.startSpinner(this.translate.instant(_('Connecting'))) this.startSpinner(this.translate.instant(_('Connecting')))

View File

@ -53,14 +53,6 @@ export class SSHShellSession extends BaseSession {
this.loginScriptProcessor?.executeUnconditionalScripts() this.loginScriptProcessor?.executeUnconditionalScripts()
// this.shell.on('greeting', greeting => {
// this.emitServiceMessage(`Shell greeting: ${greeting}`)
// })
// this.shell.on('banner', banner => {
// this.emitServiceMessage(`Shell banner: ${banner}`)
// })
this.shell.data$.subscribe(data => { this.shell.data$.subscribe(data => {
this.emitOutput(Buffer.from(data)) this.emitOutput(Buffer.from(data))
}) })

View File

@ -3,10 +3,11 @@ import * as crypto from 'crypto'
import * as sshpk from 'sshpk' import * as sshpk from 'sshpk'
import colors from 'ansi-colors' import colors from 'ansi-colors'
import stripAnsi from 'strip-ansi' import stripAnsi from 'strip-ansi'
import * as shellQuote from 'shell-quote'
import { Injector } from '@angular/core' import { Injector } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { ConfigService, FileProvidersService, HostAppService, NotificationsService, Platform, PlatformService, PromptModalComponent, LogService, Logger, TranslateService } from 'tabby-core' import { ConfigService, FileProvidersService, HostAppService, NotificationsService, Platform, PlatformService, PromptModalComponent, LogService, Logger, TranslateService } from 'tabby-core'
// import { Socket } from 'net' 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'
@ -14,7 +15,7 @@ import { HostKeyPromptModalComponent } from '../components/hostKeyPromptModal.co
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 { SFTPSession } from './sftp' import { SFTPSession } from './sftp'
import { SSHAlgorithmType, SSHProfile, SSHProxyStream, AutoPrivateKeyLocator } from '../api' import { SSHAlgorithmType, SSHProfile, SSHProxyStream, AutoPrivateKeyLocator, PortForwardType } from '../api'
import { ForwardedPort } from './forwards' import { ForwardedPort } from './forwards'
import { X11Socket } from './x11' import { X11Socket } from './x11'
import { supportedAlgorithms } from '../algorithms' import { supportedAlgorithms } from '../algorithms'
@ -33,11 +34,6 @@ interface AuthMethod {
contents?: Buffer contents?: Buffer
} }
// interface Handshake {
// kex: string
// serverHostKey: string
// }
export class KeyboardInteractivePrompt { export class KeyboardInteractivePrompt {
readonly responses: string[] = [] readonly responses: string[] = []
@ -200,7 +196,6 @@ export class SSHSession {
// return new SFTPSession(this.sftp, this.injector) // return new SFTPSession(this.sftp, this.injector)
} }
async start (): Promise<void> { async start (): Promise<void> {
// const log = (s: any) => this.emitServiceMessage(s) // const log = (s: any) => this.emitServiceMessage(s)
@ -212,8 +207,30 @@ export class SSHSession {
} }
// todo migrate connection opts // todo migrate connection opts
// eslint-disable-next-line @typescript-eslint/init-declarations
let transport: russh.SshTransport
if (this.profile.options.proxyCommand) {
this.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ` Using ${this.profile.options.proxyCommand}`)
const argv = shellQuote.parse(this.profile.options.proxyCommand)
transport = await russh.SshTransport.newCommand(argv[0], argv.slice(1))
// TODO stderr service messages
// this.proxyCommandStream.destroyed$.subscribe(err => {
// if (err) {
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` ${err.message}`)
// this.destroy()
// }
// })
// this.proxyCommandStream.message$.subscribe(message => {
// this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ' ' + message.trim())
// })
} else {
transport = await russh.SshTransport.newSocket(`${this.profile.options.host.trim()}:${this.profile.options.port ?? 22}`)
}
this.ssh = await russh.SSHClient.connect( this.ssh = await russh.SSHClient.connect(
`${this.profile.options.host.trim()}:${this.profile.options.port ?? 22}`, transport,
async key => { async key => {
if (!await this.verifyHostKey(key)) { if (!await this.verifyHostKey(key)) {
return false return false
@ -231,13 +248,19 @@ export class SSHSession {
}, },
) )
this.ssh.banner$.subscribe(banner => {
if (!this.profile.options.skipBanner) {
this.emitServiceMessage(banner)
}
})
this.ssh.disconnect$.subscribe(() => { this.ssh.disconnect$.subscribe(() => {
if (this.open) { if (this.open) {
this.destroy() this.destroy()
} }
}) })
// auth // Authentication
this.authUsername ??= this.profile.options.user this.authUsername ??= this.profile.options.user
if (!this.authUsername) { if (!this.authUsername) {
@ -276,24 +299,6 @@ export class SSHSession {
this.passwordStorage.savePassword(this.profile, this.savedPassword) this.passwordStorage.savePassword(this.profile, this.savedPassword)
} }
//zone ?
// const resultPromise: Promise<void> = new Promise(async (resolve, reject) => {
// ssh.on('greeting', greeting => {
// if (!this.profile.options.skipBanner) {
// log('Greeting: ' + greeting)
// }
// })
// ssh.on('banner', banner => {
// if (!this.profile.options.skipBanner) {
// log(banner)
// }
// })
// })
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}`)
@ -303,21 +308,6 @@ export class SSHSession {
// this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.httpProxyHost}:${this.profile.options.httpProxyPort}`) // this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.httpProxyHost}:${this.profile.options.httpProxyPort}`)
// this.proxyCommandStream = new HTTPProxyStream(this.profile) // this.proxyCommandStream = new HTTPProxyStream(this.profile)
// } // }
// 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)
// }
// if (this.proxyCommandStream) {
// this.proxyCommandStream.destroyed$.subscribe(err => {
// if (err) {
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` ${err.message}`)
// this.destroy()
// }
// })
// this.proxyCommandStream.message$.subscribe(message => {
// this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ' ' + message.trim())
// })
// await this.proxyCommandStream.start() // await this.proxyCommandStream.start()
// } // }
@ -327,59 +317,45 @@ export class SSHSession {
// 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?.socket ?? this.jumpStream, // sock: this.proxyCommandStream?.socket ?? this.jumpStream,
// username: this.authUsername ?? undefined,
// tryKeyboard: true,
// agent: this.agentPath, // agent: this.agentPath,
// agentForward: this.profile.options.agentForward && !!this.agentPath, // agentForward: this.profile.options.agentForward && !!this.agentPath,
// keepaliveInterval: this.profile.options.keepaliveInterval ?? 15000, // keepaliveInterval: this.profile.options.keepaliveInterval ?? 15000,
// keepaliveCountMax: this.profile.options.keepaliveCountMax, // keepaliveCountMax: this.profile.options.keepaliveCountMax,
// readyTimeout: this.profile.options.readyTimeout, // readyTimeout: this.profile.options.readyTimeout,
// algorithms,
// authHandler: (methodsLeft, partialSuccess, callback) => {
// this.zone.run(async () => {
// callback(await this.handleAuth(methodsLeft))
// })
// },
// }) // })
} catch (e) { } catch (e) {
this.notifications.error(e.message) this.notifications.error(e.message)
throw e throw e
} }
// for (const fw of this.profile.options.forwardedPorts ?? []) { for (const fw of this.profile.options.forwardedPorts ?? []) {
// this.addPortForward(Object.assign(new ForwardedPort(), fw)) this.addPortForward(Object.assign(new ForwardedPort(), fw))
// } }
this.open = true this.open = true
// this.ssh.on('tcp connection', (details, accept, reject) => { this.ssh.tcpChannelOpen$.subscribe(async event => {
// this.logger.info(`Incoming forwarded connection: (remote) ${details.srcIP}:${details.srcPort} -> (local) ${details.destIP}:${details.destPort}`) this.logger.info(`Incoming forwarded connection: ${event.clientAddress}:${event.clientPort} -> ${event.targetAddress}:${event.targetPort}`)
// const forward = this.forwardedPorts.find(x => x.port === details.destPort) const forward = this.forwardedPorts.find(x => x.port === event.targetPort && x.host === event.targetAddress)
// if (!forward) { if (!forward) {
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` Rejected incoming forwarded connection for unrecognized port ${details.destPort}`) this.emitServiceMessage(colors.bgRed.black(' X ') + ` Rejected incoming forwarded connection for unrecognized port ${event.targetAddress}:${event.targetPort}`)
// reject() return
// return }
// } const socket = new Socket()
// const socket = new Socket() socket.connect(forward.targetPort, forward.targetAddress)
// socket.connect(forward.targetPort, forward.targetAddress) socket.on('error', e => {
// socket.on('error', e => { // eslint-disable-next-line @typescript-eslint/no-base-to-string
// // eslint-disable-next-line @typescript-eslint/no-base-to-string this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not forward the remote connection to ${forward.targetAddress}:${forward.targetPort}: ${e}`)
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not forward the remote connection to ${forward.targetAddress}:${forward.targetPort}: ${e}`) event.channel.close()
// reject() })
// }) event.channel.data$.subscribe(data => socket.write(data))
// socket.on('connect', () => { socket.on('data', data => event.channel.write(Uint8Array.from(data)))
// this.logger.info('Connection forwarded') event.channel.closed$.subscribe(() => socket.destroy())
// const stream = accept() socket.on('close', () => event.channel.close())
// stream.pipe(socket) socket.on('connect', () => {
// socket.pipe(stream) this.logger.info('Connection forwarded')
// stream.on('close', () => { })
// socket.destroy() })
// })
// socket.on('close', () => {
// stream.close()
// })
// })
// })
this.ssh.x11ChannelOpen$.subscribe(async event => { this.ssh.x11ChannelOpen$.subscribe(async event => {
this.logger.info(`Incoming X11 connection from ${event.clientAddress}:${event.clientPort}`) this.logger.info(`Incoming X11 connection from ${event.clientAddress}:${event.clientPort}`)
@ -541,7 +517,7 @@ export class SSHSession {
break break
} }
const prompts = await state.prompts() const prompts = state.prompts()
let responses: string[] = [] let responses: string[] = []
// OpenSSH can send a k-i request without prompts // OpenSSH can send a k-i request without prompts
@ -550,7 +526,7 @@ export class SSHSession {
const prompt = new KeyboardInteractivePrompt( const prompt = new KeyboardInteractivePrompt(
state.name, state.name,
state.instructions, state.instructions,
await state.prompts(), state.prompts(),
) )
this.emitKeyboardInteractivePrompt(prompt) this.emitKeyboardInteractivePrompt(prompt)
@ -573,67 +549,66 @@ export class SSHSession {
return null return null
} }
async addPortForward (_fw: ForwardedPort): Promise<void> { async addPortForward (fw: ForwardedPort): Promise<void> {
// if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) { if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {
// await fw.startLocalListener((accept, reject, sourceAddress, sourcePort, targetAddress, targetPort) => { await fw.startLocalListener(async (accept, reject, sourceAddress, sourcePort, targetAddress, targetPort) => {
// this.logger.info(`New connection on ${fw}`) this.logger.info(`New connection on ${fw}`)
// this.ssh.forwardOut( if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
// sourceAddress ?? '127.0.0.1', this.logger.error(`Connection while unauthenticated on ${fw}`)
// sourcePort ?? 0, reject()
// targetAddress, return
// targetPort, }
// (err, stream) => { const channel = await this.ssh.openTCPForwardChannel({
// if (err) { addressToConnectTo: targetAddress,
// // eslint-disable-next-line @typescript-eslint/no-base-to-string portToConnectTo: targetPort,
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwarded connection to ${targetAddress}:${targetPort} via ${fw}: ${err}`) originatorAddress: sourceAddress ?? '127.0.0.1',
// reject() originatorPort: sourcePort ?? 0,
// return }).catch(err => {
// } this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwarded connection to ${targetAddress}:${targetPort} via ${fw}: ${err}`)
// const socket = accept() reject()
// stream.pipe(socket) throw err
// socket.pipe(stream) })
// stream.on('close', () => { const socket = accept()
// socket.destroy() channel.data$.subscribe(data => socket.write(data))
// }) socket.on('data', data => channel.write(Uint8Array.from(data)))
// socket.on('close', () => { channel.closed$.subscribe(() => socket.destroy())
// stream.close() socket.on('close', () => channel.close())
// }) }).then(() => {
// }, this.emitServiceMessage(colors.bgGreen.black(' -> ') + ` Forwarded ${fw}`)
// ) this.forwardedPorts.push(fw)
// }).then(() => { }).catch(e => {
// this.emitServiceMessage(colors.bgGreen.black(' -> ') + ` Forwarded ${fw}`) this.emitServiceMessage(colors.bgRed.black(' X ') + ` Failed to forward port ${fw}: ${e}`)
// this.forwardedPorts.push(fw) throw e
// }).catch(e => { })
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` Failed to forward port ${fw}: ${e}`) }
// throw e if (fw.type === PortForwardType.Remote) {
// }) if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
// } throw new Error('Cannot add remote port forward before auth')
// if (fw.type === PortForwardType.Remote) { }
// await new Promise<void>((resolve, reject) => { try {
// this.ssh.forwardIn(fw.host, fw.port, err => { await this.ssh.forwardTCPPort(fw.host, fw.port)
// if (err) { } catch (err) {
// // eslint-disable-next-line @typescript-eslint/no-base-to-string // eslint-disable-next-line @typescript-eslint/no-base-to-string
// this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected port forwarding for ${fw}: ${err}`) this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected port forwarding for ${fw}: ${err}`)
// reject(err) return
// return }
// } this.emitServiceMessage(colors.bgGreen.black(' <- ') + ` Forwarded ${fw}`)
// resolve() this.forwardedPorts.push(fw)
// }) }
// })
// this.emitServiceMessage(colors.bgGreen.black(' <- ') + ` Forwarded ${fw}`)
// this.forwardedPorts.push(fw)
// }
} }
async removePortForward (fw: ForwardedPort): Promise<void> { async removePortForward (fw: ForwardedPort): Promise<void> {
// if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) { if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {
// fw.stopLocalListener() fw.stopLocalListener()
// this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw) this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
// } }
// if (fw.type === PortForwardType.Remote) { if (fw.type === PortForwardType.Remote) {
// this.ssh.unforwardIn(fw.host, fw.port) if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
// this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw) throw new Error('Cannot remove remote port forward before auth')
// } }
this.ssh.stopForwardingTCPPort(fw.host, fw.port)
this.forwardedPorts = this.forwardedPorts.filter(x => x !== fw)
}
this.emitServiceMessage(`Stopped forwarding ${fw}`) this.emitServiceMessage(`Stopped forwarding ${fw}`)
} }