updated ssh2

This commit is contained in:
Eugene Pankov
2021-06-04 22:54:12 +02:00
parent d435abd944
commit 779eb235f3
15 changed files with 138 additions and 179 deletions

View File

@@ -22,12 +22,9 @@
"license": "MIT",
"devDependencies": {
"@types/node": "14.14.31",
"@types/ssh2": "^0.5.35",
"ansi-colors": "^4.1.1",
"cli-spinner": "^0.2.10",
"clone-deep": "^4.0.1",
"ssh2": "^0.8.9",
"ssh2-streams": "Eugeny/ssh2-streams#75f6d3425d071ac73a18fd46e2f5e738bfe897c5",
"sshpk": "Eugeny/node-sshpk#89ed17dfae425a8b629873c8337e77d26838c04f",
"strip-ansi": "^7.0.0"
},

View File

@@ -1,12 +1,16 @@
import colors from 'ansi-colors'
import stripAnsi from 'strip-ansi'
import socksv5 from 'socksv5'
import { Injector } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { BaseSession } from 'terminus-terminal'
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'
import { PasswordStorageService } from './services/passwordStorage.service'
import { PromptModalComponent } from './components/promptModal.component'
export interface LoginScript {
expect: string
@@ -129,12 +133,22 @@ export class SSHSession extends BaseSession {
logger: Logger
jumpStream: any
proxyCommandStream: ProxyCommandStream|null = null
authMethodsLeft: string[] = []
savedPassword: string|undefined
get serviceMessage$ (): Observable<string> { return this.serviceMessage }
private serviceMessage = new Subject<string>()
private keychainPasswordUsed = false
private passwordStorage: PasswordStorageService
private ngbModal: NgbModal
constructor (public connection: SSHConnection) {
constructor (
injector: Injector,
public connection: SSHConnection
) {
super()
this.passwordStorage = injector.get(PasswordStorageService)
this.ngbModal = injector.get(NgbModal)
this.scripts = connection.scripts ?? []
this.destroyed$.subscribe(() => {
for (const port of this.forwardedPorts) {
@@ -197,7 +211,7 @@ export class SSHSession extends BaseSession {
if (match) {
this.logger.info('Executing script: "' + cmd + '"')
this.shell!.write(cmd + '\n')
this.shell.write(cmd + '\n')
this.scripts = this.scripts.filter(x => x !== script)
} else {
if (script.optional) {
@@ -296,6 +310,67 @@ export class SSHSession extends BaseSession {
this.logger.info(stripAnsi(msg))
}
async handleAuth (methodsLeft?: string[]): Promise<any> {
while (true) {
const method = this.authMethodsLeft.shift()
if (!method) {
return false
}
if (methodsLeft && !methodsLeft.includes(method) && method !== 'agent') {
// Agent can still be used even if not in methodsLeft
this.logger.info('Server does not support auth method', method)
continue
}
if (method === 'password') {
if (this.connection.password) {
this.emitServiceMessage('Using preset password')
return {
type: 'password',
username: this.connection.user,
password: this.connection.password,
}
}
if (!this.keychainPasswordUsed) {
const password = await this.passwordStorage.loadPassword(this.connection)
if (password) {
this.emitServiceMessage('Trying saved password')
this.keychainPasswordUsed = true
return {
type: 'password',
username: this.connection.user,
password,
}
}
}
const modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = `Password for ${this.connection.user}@${this.connection.host}`
modal.componentInstance.password = true
modal.componentInstance.showRememberCheckbox = true
try {
const result = await modal.result
if (result) {
if (result.remember) {
this.savedPassword = result.value
}
return {
type: 'password',
username: this.connection.user,
password: result.value,
}
} else {
continue
}
} catch {
continue
}
}
return method
}
}
async addPortForward (fw: ForwardedPort): Promise<void> {
if (fw.type === PortForwardType.Local || fw.type === PortForwardType.Dynamic) {
await fw.startLocalListener((accept, reject, sourceAddress, sourcePort, targetAddress, targetPort) => {
@@ -417,7 +492,7 @@ export class SSHSession extends BaseSession {
for (const script of this.scripts) {
if (!script.expect) {
console.log('Executing script:', script.send)
this.shell!.write(script.send + '\n')
this.shell.write(script.send + '\n')
this.scripts = this.scripts.filter(x => x !== script)
} else {
break

View File

@@ -197,28 +197,28 @@
.form-line.align-items-start
.header
.title Ciphers
.w-50
.w-75
div(*ngFor='let alg of supportedAlgorithms.cipher')
checkbox([text]='alg', [(ngModel)]='algorithms.cipher[alg]')
.form-line.align-items-start
.header
.title Key exchange
.w-50
.w-75
div(*ngFor='let alg of supportedAlgorithms.kex')
checkbox([text]='alg', [(ngModel)]='algorithms.kex[alg]')
.form-line.align-items-start
.header
.title HMAC
.w-50
.w-75
div(*ngFor='let alg of supportedAlgorithms.hmac')
checkbox([text]='alg', [(ngModel)]='algorithms.hmac[alg]')
.form-line.align-items-start
.header
.title Host key
.w-50
.w-75
div(*ngFor='let alg of supportedAlgorithms.serverHostKey')
checkbox([text]='alg', [(ngModel)]='algorithms.serverHostKey[alg]')

View File

@@ -8,7 +8,7 @@ import { ElectronService, HostAppService, ConfigService, PlatformService } from
import { PasswordStorageService } from '../services/passwordStorage.service'
import { SSHConnection, LoginScript, ForwardedPortConfig, SSHAlgorithmType, ALGORITHM_BLACKLIST } from '../api'
import { PromptModalComponent } from './promptModal.component'
import { ALGORITHMS } from 'ssh2-streams/lib/constants'
import * as ALGORITHMS from 'ssh2/lib/protocol/constants'
/** @hidden */
@Component({
@@ -39,15 +39,15 @@ export class EditConnectionModalComponent {
[SSHAlgorithmType.KEX]: 'SUPPORTED_KEX',
[SSHAlgorithmType.HOSTKEY]: 'SUPPORTED_SERVER_HOST_KEY',
[SSHAlgorithmType.CIPHER]: 'SUPPORTED_CIPHER',
[SSHAlgorithmType.HMAC]: 'SUPPORTED_HMAC',
[SSHAlgorithmType.HMAC]: 'SUPPORTED_MAC',
}[k]
const defaultAlg = {
[SSHAlgorithmType.KEX]: 'KEX',
[SSHAlgorithmType.HOSTKEY]: 'SERVER_HOST_KEY',
[SSHAlgorithmType.CIPHER]: 'CIPHER',
[SSHAlgorithmType.HMAC]: 'HMAC',
[SSHAlgorithmType.KEX]: 'DEFAULT_KEX',
[SSHAlgorithmType.HOSTKEY]: 'DEFAULT_SERVER_HOST_KEY',
[SSHAlgorithmType.CIPHER]: 'DEFAULT_CIPHER',
[SSHAlgorithmType.HMAC]: 'DEFAULT_MAC',
}[k]
this.supportedAlgorithms[k] = ALGORITHMS[supportedAlg].filter(x => !ALGORITHM_BLACKLIST.includes(x))
this.supportedAlgorithms[k] = ALGORITHMS[supportedAlg].filter(x => !ALGORITHM_BLACKLIST.includes(x)).sort()
this.defaultAlgorithms[k] = ALGORITHMS[defaultAlg].filter(x => !ALGORITHM_BLACKLIST.includes(x))
}

View File

@@ -154,7 +154,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
this.reconnectOffered = true
this.write('Press any key to reconnect\r\n')
this.input$.pipe(first()).subscribe(() => {
if (!this.session?.open) {
if (!this.session?.open && this.reconnectOffered) {
this.reconnect()
}
})

View File

@@ -1,10 +1,9 @@
import colors from 'ansi-colors'
import { Duplex } from 'stream'
import * as crypto from 'crypto'
import { Injectable, NgZone } from '@angular/core'
import { Injectable, Injector, NgZone } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Client } from 'ssh2'
import { SSH2Stream } from 'ssh2-streams'
import * as fs from 'mz/fs'
import { exec } from 'child_process'
import * as path from 'path'
@@ -25,6 +24,7 @@ export class SSHService {
private logger: Logger
private constructor (
private injector: Injector,
private log: LogService,
private zone: NgZone,
private ngbModal: NgbModal,
@@ -39,7 +39,7 @@ export class SSHService {
}
createSession (connection: SSHConnection): SSHSession {
const session = new SSHSession(connection)
const session = new SSHSession(this.injector, connection)
session.logger = this.log.create(`ssh-${connection.host}-${connection.port}`)
return session
}
@@ -114,7 +114,6 @@ export class SSHService {
const ssh = new Client()
session.ssh = ssh
let connected = false
let savedPassword: string|null = null
const algorithms = {}
for (const key of Object.keys(session.connection.algorithms ?? {})) {
algorithms[key] = session.connection.algorithms![key].filter(x => !ALGORITHM_BLACKLIST.includes(x))
@@ -123,8 +122,8 @@ export class SSHService {
const resultPromise: Promise<void> = new Promise(async (resolve, reject) => {
ssh.on('ready', () => {
connected = true
if (savedPassword) {
this.passwordStorage.savePassword(session.connection, savedPassword)
if (session.savedPassword) {
this.passwordStorage.savePassword(session.connection, session.savedPassword)
}
for (const fw of session.connection.forwardedPorts ?? []) {
@@ -133,6 +132,9 @@ export class SSHService {
this.zone.run(resolve)
})
ssh.on('handshake', negotiated => {
this.logger.info('Handshake complete:', negotiated)
})
ssh.on('error', error => {
if (error.message === 'All configured authentication methods failed') {
this.passwordStorage.deletePassword(session.connection)
@@ -197,7 +199,7 @@ export class SSHService {
agent = process.env.SSH_AUTH_SOCK!
}
const authMethodsLeft = ['none']
session.authMethodsLeft = ['none']
if (!session.connection.auth || session.connection.auth === 'publicKey') {
try {
privateKey = await this.loadPrivateKeyForSession(session)
@@ -207,23 +209,23 @@ export class SSHService {
if (!privateKey) {
session.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Private key auth selected, but no key is loaded`)
} else {
authMethodsLeft.push('publickey')
session.authMethodsLeft.push('publickey')
}
}
if (!session.connection.auth || session.connection.auth === 'agent') {
if (!agent) {
session.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Agent auth selected, but no running agent is detected`)
} else {
authMethodsLeft.push('agent')
session.authMethodsLeft.push('agent')
}
}
if (!session.connection.auth || session.connection.auth === 'password') {
authMethodsLeft.push('password')
session.authMethodsLeft.push('password')
}
if (!session.connection.auth || session.connection.auth === 'keyboardInteractive') {
authMethodsLeft.push('keyboard-interactive')
session.authMethodsLeft.push('keyboard-interactive')
}
authMethodsLeft.push('hostbased')
session.authMethodsLeft.push('hostbased')
try {
if (session.connection.proxyCommand) {
@@ -257,19 +259,10 @@ export class SSHService {
},
hostHash: 'sha256' as any,
algorithms,
authHandler: methodsLeft => {
while (true) {
const method = authMethodsLeft.shift()
if (!method) {
return false
}
if (methodsLeft && !methodsLeft.includes(method) && method !== 'agent') {
// Agent can still be used even if not in methodsLeft
this.logger.info('Server does not support auth method', method)
continue
}
return method
}
authHandler: (methodsLeft, partialSuccess, callback) => {
this.zone.run(async () => {
callback(await session.handleAuth(methodsLeft))
})
},
} as any)
} catch (e) {
@@ -277,41 +270,6 @@ export class SSHService {
throw e
}
let keychainPasswordUsed = false
;(ssh as any).config.password = () => this.zone.run(async () => {
if (session.connection.password) {
log('Using preset password')
return session.connection.password
}
if (!keychainPasswordUsed) {
const password = await this.passwordStorage.loadPassword(session.connection)
if (password) {
log('Trying saved password')
keychainPasswordUsed = true
return password
}
}
const modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = `Password for ${session.connection.user}@${session.connection.host}`
modal.componentInstance.password = true
modal.componentInstance.showRememberCheckbox = true
try {
const result = await modal.result
if (result) {
if (result.remember) {
savedPassword = result.value
}
return result.value
}
return ''
} catch {
return ''
}
})
return resultPromise
}
@@ -479,9 +437,3 @@ export class ProxyCommandStream extends Duplex {
callback(error)
}
}
/* eslint-disable */
const _authPassword = SSH2Stream.prototype.authPassword
SSH2Stream.prototype.authPassword = async function (username, passwordFn: any) {
_authPassword.bind(this)(username, await passwordFn())
} as any

View File

@@ -2,26 +2,11 @@
# yarn lockfile v1
"@types/node@*", "@types/node@14.14.31":
"@types/node@14.14.31":
version "14.14.31"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055"
integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==
"@types/ssh2-streams@*":
version "0.1.7"
resolved "https://registry.yarnpkg.com/@types/ssh2-streams/-/ssh2-streams-0.1.7.tgz#bf79349ec85a4dfb5b3fd9f4a05af729121f07e6"
integrity sha512-cQNV72C+BOG7G8WNGarTQdB2Ii37cJlWatSpx5zTYxtI2ZvUt2lbq6Nc2XZ4kbge28V7Xe5KYYr82d96/rDMnQ==
dependencies:
"@types/node" "*"
"@types/ssh2@^0.5.35":
version "0.5.46"
resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-0.5.46.tgz#e12341a242aea0e98ac2dec89e039bf421fd3584"
integrity sha512-1pC8FHrMPYdkLoUOwTYYifnSEPzAFZRsp3JFC/vokQ+dRrVI+hDBwz0SNmQ3pL6h39OSZlPs0uCG7wKJkftnaA==
dependencies:
"@types/node" "*"
"@types/ssh2-streams" "*"
ansi-colors@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
@@ -32,7 +17,7 @@ ansi-regex@^6.0.0:
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.0.tgz#ecc7f5933cbe5ac7b33e209a5ff409ab1669c6b2"
integrity sha512-tAaOSrWCHF+1Ear1Z4wnJCXA9GGox4K6Ic85a5qalES2aeEwQGr7UC93mwef49536PkCYjzkp0zIxfFvexJ6zQ==
asn1@~0.2.0, asn1@~0.2.3:
asn1@~0.2.3:
version "0.2.4"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
@@ -54,7 +39,7 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2:
bcrypt-pbkdf@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
@@ -265,30 +250,6 @@ sprintf@0.1.x:
resolved "https://registry.yarnpkg.com/sprintf/-/sprintf-0.1.5.tgz#8f83e39a9317c1a502cb7db8050e51c679f6edcf"
integrity sha1-j4PjmpMXwaUCy324BQ5Rxnn27c8=
ssh2-streams@Eugeny/ssh2-streams#75f6d3425d071ac73a18fd46e2f5e738bfe897c5:
version "0.4.10"
resolved "https://codeload.github.com/Eugeny/ssh2-streams/tar.gz/75f6d3425d071ac73a18fd46e2f5e738bfe897c5"
dependencies:
asn1 "~0.2.0"
bcrypt-pbkdf "^1.0.2"
streamsearch "~0.1.2"
ssh2-streams@~0.4.10:
version "0.4.10"
resolved "https://registry.yarnpkg.com/ssh2-streams/-/ssh2-streams-0.4.10.tgz#48ef7e8a0e39d8f2921c30521d56dacb31d23a34"
integrity sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==
dependencies:
asn1 "~0.2.0"
bcrypt-pbkdf "^1.0.2"
streamsearch "~0.1.2"
ssh2@^0.8.9:
version "0.8.9"
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.8.9.tgz#54da3a6c4ba3daf0d8477a538a481326091815f3"
integrity sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==
dependencies:
ssh2-streams "~0.4.10"
sshpk@Eugeny/node-sshpk#89ed17dfae425a8b629873c8337e77d26838c04f:
version "1.16.1"
resolved "https://codeload.github.com/Eugeny/node-sshpk/tar.gz/89ed17dfae425a8b629873c8337e77d26838c04f"
@@ -308,11 +269,6 @@ stack-trace@0.0.x:
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=
streamsearch@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
strip-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.0.tgz#1dc49b980c3a4100366617adac59327eefdefcb0"