|
|
|
@@ -37,7 +37,6 @@ type AuthMethod = {
|
|
|
|
|
type: 'publickey'
|
|
|
|
|
name: string
|
|
|
|
|
contents: Buffer
|
|
|
|
|
hashAlg: 'sha256'|'sha512'|'sha1'|null
|
|
|
|
|
} | {
|
|
|
|
|
type: 'agent',
|
|
|
|
|
kind: 'unix-socket',
|
|
|
|
@@ -87,7 +86,7 @@ export class SSHSession {
|
|
|
|
|
ssh: russh.SSHClient|russh.AuthenticatedSSHClient
|
|
|
|
|
sftp?: russh.SFTP
|
|
|
|
|
forwardedPorts: ForwardedPort[] = []
|
|
|
|
|
jumpChannel: russh.Channel|null = null
|
|
|
|
|
jumpChannel: russh.NewChannel|null = null
|
|
|
|
|
savedPassword?: string
|
|
|
|
|
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
|
|
|
|
|
get keyboardInteractivePrompt$ (): Observable<KeyboardInteractivePrompt> { return this.keyboardInteractivePrompt }
|
|
|
|
@@ -139,14 +138,11 @@ export class SSHSession {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private addPublicKeyAuthMethod (name: string, contents: Buffer) {
|
|
|
|
|
for (const hashAlg of ['sha512', 'sha256', 'sha1', null] as const) {
|
|
|
|
|
this.remainingAuthMethods.push({
|
|
|
|
|
type: 'publickey',
|
|
|
|
|
name,
|
|
|
|
|
contents,
|
|
|
|
|
hashAlg,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
this.remainingAuthMethods.push({
|
|
|
|
|
type: 'publickey',
|
|
|
|
|
name,
|
|
|
|
|
contents,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async init (): Promise<void> {
|
|
|
|
@@ -244,7 +240,7 @@ export class SSHSession {
|
|
|
|
|
throw new Error('Cannot open SFTP session before auth')
|
|
|
|
|
}
|
|
|
|
|
if (!this.sftp) {
|
|
|
|
|
this.sftp = await this.ssh.openSFTPChannel()
|
|
|
|
|
this.sftp = await this.ssh.activateSFTP(await this.ssh.openSessionChannel())
|
|
|
|
|
}
|
|
|
|
|
return new SFTPSession(this.sftp, this.injector)
|
|
|
|
|
}
|
|
|
|
@@ -265,7 +261,7 @@ export class SSHSession {
|
|
|
|
|
const argv = shellQuote.parse(this.profile.options.proxyCommand)
|
|
|
|
|
transport = await russh.SshTransport.newCommand(argv[0], argv.slice(1))
|
|
|
|
|
} else if (this.jumpChannel) {
|
|
|
|
|
transport = await russh.SshTransport.newSshChannel(await this.jumpChannel.take())
|
|
|
|
|
transport = await russh.SshTransport.newSshChannel(this.jumpChannel.take())
|
|
|
|
|
this.jumpChannel = null
|
|
|
|
|
} else if (this.profile.options.socksProxyHost) {
|
|
|
|
|
this.emitServiceMessage(colors.bgBlue.black(' Proxy ') + ` Using ${this.profile.options.socksProxyHost}:${this.profile.options.socksProxyPort}`)
|
|
|
|
@@ -368,22 +364,31 @@ export class SSHSession {
|
|
|
|
|
|
|
|
|
|
this.ssh.tcpChannelOpen$.subscribe(async event => {
|
|
|
|
|
this.logger.info(`Incoming forwarded connection: ${event.clientAddress}:${event.clientPort} -> ${event.targetAddress}:${event.targetPort}`)
|
|
|
|
|
|
|
|
|
|
if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
|
|
|
|
|
throw new Error('Cannot open agent channel before auth')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const channel = await this.ssh.activateChannel(event.channel)
|
|
|
|
|
|
|
|
|
|
const forward = this.forwardedPorts.find(x => x.port === event.targetPort && x.host === event.targetAddress)
|
|
|
|
|
if (!forward) {
|
|
|
|
|
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Rejected incoming forwarded connection for unrecognized port ${event.targetAddress}:${event.targetPort}`)
|
|
|
|
|
channel.close()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const socket = new Socket()
|
|
|
|
|
socket.connect(forward.targetPort, forward.targetAddress)
|
|
|
|
|
socket.on('error', e => {
|
|
|
|
|
// 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}`)
|
|
|
|
|
event.channel.close()
|
|
|
|
|
channel.close()
|
|
|
|
|
})
|
|
|
|
|
event.channel.data$.subscribe(data => socket.write(data))
|
|
|
|
|
socket.on('data', data => event.channel.write(Uint8Array.from(data)))
|
|
|
|
|
event.channel.closed$.subscribe(() => socket.destroy())
|
|
|
|
|
socket.on('close', () => event.channel.close())
|
|
|
|
|
channel.data$.subscribe(data => socket.write(data))
|
|
|
|
|
socket.on('data', data => channel.write(Uint8Array.from(data)))
|
|
|
|
|
channel.closed$.subscribe(() => socket.destroy())
|
|
|
|
|
socket.on('close', () => channel.close())
|
|
|
|
|
socket.on('connect', () => {
|
|
|
|
|
this.logger.info('Connection forwarded')
|
|
|
|
|
})
|
|
|
|
@@ -394,22 +399,28 @@ export class SSHSession {
|
|
|
|
|
const displaySpec = (this.config.store.ssh.x11Display || process.env.DISPLAY) ?? 'localhost:0'
|
|
|
|
|
this.logger.debug(`Trying display ${displaySpec}`)
|
|
|
|
|
|
|
|
|
|
if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
|
|
|
|
|
throw new Error('Cannot open agent channel before auth')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const channel = await this.ssh.activateChannel(event.channel)
|
|
|
|
|
|
|
|
|
|
const socket = new X11Socket()
|
|
|
|
|
try {
|
|
|
|
|
const x11Stream = await socket.connect(displaySpec)
|
|
|
|
|
this.logger.info('Connection forwarded')
|
|
|
|
|
|
|
|
|
|
event.channel.data$.subscribe(data => {
|
|
|
|
|
channel.data$.subscribe(data => {
|
|
|
|
|
x11Stream.write(data)
|
|
|
|
|
})
|
|
|
|
|
x11Stream.on('data', data => {
|
|
|
|
|
event.channel.write(Uint8Array.from(data))
|
|
|
|
|
channel.write(Uint8Array.from(data))
|
|
|
|
|
})
|
|
|
|
|
event.channel.closed$.subscribe(() => {
|
|
|
|
|
channel.closed$.subscribe(() => {
|
|
|
|
|
socket.destroy()
|
|
|
|
|
})
|
|
|
|
|
x11Stream.on('close', () => {
|
|
|
|
|
event.channel.close()
|
|
|
|
|
channel.close()
|
|
|
|
|
})
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
|
|
@@ -420,11 +431,17 @@ export class SSHSession {
|
|
|
|
|
this.emitServiceMessage(' * VcXsrv: https://sourceforge.net/projects/vcxsrv/')
|
|
|
|
|
this.emitServiceMessage(' * Xming: https://sourceforge.net/projects/xming/')
|
|
|
|
|
}
|
|
|
|
|
event.channel.close()
|
|
|
|
|
channel.close()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.ssh.agentChannelOpen$.subscribe(async channel => {
|
|
|
|
|
this.ssh.agentChannelOpen$.subscribe(async newChannel => {
|
|
|
|
|
if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
|
|
|
|
|
throw new Error('Cannot open agent channel before auth')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const channel = await this.ssh.activateChannel(newChannel)
|
|
|
|
|
|
|
|
|
|
const spec = await this.getAgentConnectionSpec()
|
|
|
|
|
if (!spec) {
|
|
|
|
|
await channel.close()
|
|
|
|
@@ -532,19 +549,13 @@ export class SSHSession {
|
|
|
|
|
if (method.type === 'publickey') {
|
|
|
|
|
try {
|
|
|
|
|
const key = await this.loadPrivateKey(method.name, method.contents)
|
|
|
|
|
const possibleHashAlgs = (['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512'].includes(key.algorithm) ? ['sha256', 'sha512', 'sha1'] : [null]) as (string|null)[]
|
|
|
|
|
if (!possibleHashAlgs.includes(method.hashAlg)) {
|
|
|
|
|
// skip incompatible hash algs
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
let msg = `Using private key: ${method.name}`
|
|
|
|
|
if (method.hashAlg) {
|
|
|
|
|
msg += ` (${method.hashAlg})`
|
|
|
|
|
}
|
|
|
|
|
this.emitServiceMessage(msg)
|
|
|
|
|
const result = await this.ssh.authenticateWithKeyPair(this.authUsername, key, method.hashAlg)
|
|
|
|
|
if (result instanceof russh.AuthenticatedSSHClient) {
|
|
|
|
|
return result
|
|
|
|
|
const possibleHashAlgs = ['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512'].includes(key.algorithm) ? ['sha256', 'sha512', 'sha1'] as const : [null] as const
|
|
|
|
|
this.emitServiceMessage(`Trying private key: ${method.name}`)
|
|
|
|
|
for (const alg of possibleHashAlgs) {
|
|
|
|
|
const result = await this.ssh.authenticateWithKeyPair(this.authUsername, key, alg)
|
|
|
|
|
if (result instanceof russh.AuthenticatedSSHClient) {
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
this.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to load private key ${method.name}: ${e}`)
|
|
|
|
@@ -622,7 +633,7 @@ export class SSHSession {
|
|
|
|
|
reject()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const channel = await this.ssh.openTCPForwardChannel({
|
|
|
|
|
const channel = await this.ssh.activateChannel(await this.ssh.openTCPForwardChannel({
|
|
|
|
|
addressToConnectTo: targetAddress,
|
|
|
|
|
portToConnectTo: targetPort,
|
|
|
|
|
originatorAddress: sourceAddress ?? '127.0.0.1',
|
|
|
|
@@ -631,7 +642,7 @@ export class SSHSession {
|
|
|
|
|
this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwarded connection to ${targetAddress}:${targetPort} via ${fw}: ${err}`)
|
|
|
|
|
reject()
|
|
|
|
|
throw err
|
|
|
|
|
})
|
|
|
|
|
}))
|
|
|
|
|
const socket = accept()
|
|
|
|
|
channel.data$.subscribe(data => socket.write(data))
|
|
|
|
|
socket.on('data', data => channel.write(Uint8Array.from(data)))
|
|
|
|
@@ -688,7 +699,7 @@ export class SSHSession {
|
|
|
|
|
if (!(this.ssh instanceof russh.AuthenticatedSSHClient)) {
|
|
|
|
|
throw new Error('Cannot open shell channel before auth')
|
|
|
|
|
}
|
|
|
|
|
const ch = await this.ssh.openSessionChannel()
|
|
|
|
|
const ch = await this.ssh.activateChannel(await this.ssh.openSessionChannel())
|
|
|
|
|
await ch.requestPTY('xterm-256color', {
|
|
|
|
|
columns: 80,
|
|
|
|
|
rows: 24,
|
|
|
|
@@ -731,7 +742,12 @@ export class SSHSession {
|
|
|
|
|
triedSavedPassphrase = true
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if (e.toString() === 'Error: Keys(KeyIsEncrypted)' || e.toString() === 'Error: Keys(SshKey(Crypto))') {
|
|
|
|
|
if ([
|
|
|
|
|
'Error: Keys(KeyIsEncrypted)',
|
|
|
|
|
'Error: Keys(SshKey(Ppk(Encrypted)))',
|
|
|
|
|
'Error: Keys(SshKey(Ppk(IncorrectMac)))',
|
|
|
|
|
'Error: Keys(SshKey(Crypto))',
|
|
|
|
|
].includes(e.toString())) {
|
|
|
|
|
await this.passwordStorage.deletePrivateKeyPassword(keyHash)
|
|
|
|
|
|
|
|
|
|
const modal = this.ngbModal.open(PromptModalComponent)
|
|
|
|
|