mirror of
https://github.com/Eugeny/tabby.git
synced 2025-06-22 20:40:08 +00:00
an option to remember private key passphrases - fixes #3689
This commit is contained in:
parent
220ae6ccaa
commit
f87efcf5bd
@ -25,6 +25,13 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||||||
private homeEndSubscription: Subscription
|
private homeEndSubscription: Subscription
|
||||||
private recentInputs = ''
|
private recentInputs = ''
|
||||||
private reconnectOffered = false
|
private reconnectOffered = false
|
||||||
|
private spinner = new Spinner({
|
||||||
|
text: 'Connecting',
|
||||||
|
stream: {
|
||||||
|
write: x => this.write(x),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
private spinnerActive = false
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
injector: Injector,
|
injector: Injector,
|
||||||
@ -113,32 +120,22 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||||||
this.sessionStack.push(session)
|
this.sessionStack.push(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.attachSessionHandler(session.serviceMessage$.subscribe(msg => {
|
|
||||||
this.write(`\r\n${colors.black.bgWhite(' SSH ')} ${msg}\r\n`)
|
|
||||||
session.resize(this.size.columns, this.size.rows)
|
|
||||||
}))
|
|
||||||
|
|
||||||
|
|
||||||
this.write('\r\n' + colors.black.bgWhite(' 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({
|
this.startSpinner()
|
||||||
text: 'Connecting',
|
|
||||||
stream: {
|
this.attachSessionHandler(session.serviceMessage$.subscribe(msg => {
|
||||||
write: x => this.write(x),
|
this.pauseSpinner(() => {
|
||||||
},
|
this.write(`\r${colors.black.bgWhite(' SSH ')} ${msg}\r\n`)
|
||||||
})
|
session.resize(this.size.columns, this.size.rows)
|
||||||
spinner.setSpinnerString(6)
|
})
|
||||||
spinner.start()
|
}))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.ssh.connectSession(session, (message: string) => {
|
await this.ssh.connectSession(session)
|
||||||
spinner.stop(true)
|
this.stopSpinner()
|
||||||
this.write(message + '\r\n')
|
|
||||||
spinner.start()
|
|
||||||
})
|
|
||||||
spinner.stop(true)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
spinner.stop(true)
|
this.stopSpinner()
|
||||||
this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n')
|
this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -232,4 +229,24 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
|
|||||||
this.homeEndSubscription.unsubscribe()
|
this.homeEndSubscription.unsubscribe()
|
||||||
super.ngOnDestroy()
|
super.ngOnDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private startSpinner () {
|
||||||
|
this.spinner.setSpinnerString(6)
|
||||||
|
this.spinner.start()
|
||||||
|
this.spinnerActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopSpinner () {
|
||||||
|
this.spinner.stop(true)
|
||||||
|
this.spinnerActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private pauseSpinner (work: () => void) {
|
||||||
|
const wasActive = this.spinnerActive
|
||||||
|
this.stopSpinner()
|
||||||
|
work()
|
||||||
|
if (wasActive) {
|
||||||
|
this.startSpinner()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,26 +5,44 @@ import * as keytar from 'keytar'
|
|||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class PasswordStorageService {
|
export class PasswordStorageService {
|
||||||
async savePassword (connection: SSHConnection, password: string): Promise<void> {
|
async savePassword (connection: SSHConnection, password: string): Promise<void> {
|
||||||
let key = `ssh@${connection.host}`
|
const key = this.getKeyForConnection(connection)
|
||||||
if (connection.port) {
|
|
||||||
key = `ssh@${connection.host}:${connection.port}`
|
|
||||||
}
|
|
||||||
return keytar.setPassword(key, connection.user, password)
|
return keytar.setPassword(key, connection.user, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
async deletePassword (connection: SSHConnection): Promise<void> {
|
async deletePassword (connection: SSHConnection): Promise<void> {
|
||||||
let key = `ssh@${connection.host}`
|
const key = this.getKeyForConnection(connection)
|
||||||
if (connection.port) {
|
|
||||||
key = `ssh@${connection.host}:${connection.port}`
|
|
||||||
}
|
|
||||||
await keytar.deletePassword(key, connection.user)
|
await keytar.deletePassword(key, connection.user)
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPassword (connection: SSHConnection): Promise<string|null> {
|
async loadPassword (connection: SSHConnection): Promise<string|null> {
|
||||||
|
const key = this.getKeyForConnection(connection)
|
||||||
|
return keytar.getPassword(key, connection.user)
|
||||||
|
}
|
||||||
|
|
||||||
|
async savePrivateKeyPassword (id: string, password: string): Promise<void> {
|
||||||
|
const key = this.getKeyForPrivateKey(id)
|
||||||
|
return keytar.setPassword(key, 'user', password)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deletePrivateKeyPassword (id: string): Promise<void> {
|
||||||
|
const key = this.getKeyForPrivateKey(id)
|
||||||
|
await keytar.deletePassword(key, 'user')
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadPrivateKeyPassword (id: string): Promise<string|null> {
|
||||||
|
const key = this.getKeyForPrivateKey(id)
|
||||||
|
return keytar.getPassword(key, 'user')
|
||||||
|
}
|
||||||
|
|
||||||
|
private getKeyForConnection (connection: SSHConnection): string {
|
||||||
let key = `ssh@${connection.host}`
|
let key = `ssh@${connection.host}`
|
||||||
if (connection.port) {
|
if (connection.port) {
|
||||||
key = `ssh@${connection.host}:${connection.port}`
|
key = `ssh@${connection.host}:${connection.port}`
|
||||||
}
|
}
|
||||||
return keytar.getPassword(key, connection.user)
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
private getKeyForPrivateKey (id: string): string {
|
||||||
|
return `ssh-private-key:${id}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import colors from 'ansi-colors'
|
import colors from 'ansi-colors'
|
||||||
import { Duplex } from 'stream'
|
import { Duplex } from 'stream'
|
||||||
import stripAnsi from 'strip-ansi'
|
import * as crypto from 'crypto'
|
||||||
import { open as openTemp } from 'temp'
|
import { open as openTemp } from 'temp'
|
||||||
import { Injectable, NgZone } from '@angular/core'
|
import { Injectable, NgZone } from '@angular/core'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
@ -55,51 +55,29 @@ export class SSHService {
|
|||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPrivateKeyForSession (session: SSHSession, logCallback?: SSHLogCallback): Promise<string|null> {
|
async loadPrivateKeyForSession (session: SSHSession): Promise<string|null> {
|
||||||
let privateKey: string|null = null
|
let privateKey: string|null = null
|
||||||
let privateKeyPath = session.connection.privateKey
|
let privateKeyPath = session.connection.privateKey
|
||||||
|
|
||||||
if (!privateKeyPath) {
|
if (!privateKeyPath) {
|
||||||
const userKeyPath = path.join(process.env.HOME!, '.ssh', 'id_rsa')
|
const userKeyPath = path.join(process.env.HOME!, '.ssh', 'id_rsa')
|
||||||
if (await fs.exists(userKeyPath)) {
|
if (await fs.exists(userKeyPath)) {
|
||||||
logCallback?.('Using user\'s default private key')
|
session.emitServiceMessage('Using user\'s default private key')
|
||||||
privateKeyPath = userKeyPath
|
privateKeyPath = userKeyPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (privateKeyPath) {
|
if (privateKeyPath) {
|
||||||
logCallback?.('Loading private key from ' + colors.bgWhite.blackBright(' ' + privateKeyPath + ' '))
|
session.emitServiceMessage('Loading private key from ' + colors.bgWhite.blackBright(' ' + privateKeyPath + ' '))
|
||||||
try {
|
try {
|
||||||
privateKey = (await fs.readFile(privateKeyPath)).toString()
|
privateKey = (await fs.readFile(privateKeyPath)).toString()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logCallback?.(colors.bgRed.black(' X ') + 'Could not read the private key file')
|
session.emitServiceMessage(colors.bgRed.black(' X ') + 'Could not read the private key file')
|
||||||
this.notifications.error('Could not read the private key file')
|
this.notifications.error('Could not read the private key file')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (privateKey) {
|
if (privateKey) {
|
||||||
let parsedKey: any = null
|
const parsedKey = await this.parsePrivateKey(privateKey)
|
||||||
try {
|
|
||||||
parsedKey = sshpk.parsePrivateKey(privateKey, 'auto')
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof sshpk.KeyEncryptedError) {
|
|
||||||
const modal = this.ngbModal.open(PromptModalComponent)
|
|
||||||
logCallback?.(colors.bgYellow.yellow.black(' ! ') + ' Key requires passphrase')
|
|
||||||
modal.componentInstance.prompt = 'Private key passphrase'
|
|
||||||
modal.componentInstance.password = true
|
|
||||||
let passphrase = ''
|
|
||||||
try {
|
|
||||||
const result = await modal.result
|
|
||||||
passphrase = result?.value
|
|
||||||
} catch { }
|
|
||||||
parsedKey = sshpk.parsePrivateKey(
|
|
||||||
privateKey,
|
|
||||||
'auto',
|
|
||||||
{ passphrase: passphrase }
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sshFormatKey = parsedKey.toString('openssh')
|
const sshFormatKey = parsedKey.toString('openssh')
|
||||||
const temp = await openTemp()
|
const temp = await openTemp()
|
||||||
@ -134,15 +112,40 @@ export class SSHService {
|
|||||||
return privateKey
|
return privateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
async connectSession (session: SSHSession, logCallback?: SSHLogCallback): Promise<void> {
|
async parsePrivateKey (privateKey: string): Promise<any> {
|
||||||
if (!logCallback) {
|
const keyHash = crypto.createHash('sha512').update(privateKey).digest('hex')
|
||||||
logCallback = () => null
|
let passphrase: string|null = await this.passwordStorage.loadPrivateKeyPassword(keyHash)
|
||||||
}
|
while (true) {
|
||||||
|
try {
|
||||||
|
return sshpk.parsePrivateKey(privateKey, 'auto', { passphrase })
|
||||||
|
} catch (e) {
|
||||||
|
this.notifications.error('Could not read the private key', e.toString())
|
||||||
|
if (e instanceof sshpk.KeyEncryptedError || e instanceof sshpk.KeyParseError) {
|
||||||
|
await this.passwordStorage.deletePrivateKeyPassword(keyHash)
|
||||||
|
|
||||||
const log = (s: any) => {
|
const modal = this.ngbModal.open(PromptModalComponent)
|
||||||
logCallback!(s)
|
modal.componentInstance.prompt = 'Private key passphrase'
|
||||||
this.logger.info(stripAnsi(s))
|
modal.componentInstance.password = true
|
||||||
|
modal.componentInstance.showRememberCheckbox = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await modal.result
|
||||||
|
passphrase = result?.value
|
||||||
|
if (passphrase && result.remember) {
|
||||||
|
this.passwordStorage.savePrivateKeyPassword(keyHash, passphrase)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectSession (session: SSHSession): Promise<void> {
|
||||||
|
const log = (s: any) => session.emitServiceMessage(s)
|
||||||
|
|
||||||
let privateKey: string|null = null
|
let privateKey: string|null = null
|
||||||
|
|
||||||
@ -154,7 +157,8 @@ export class SSHService {
|
|||||||
for (const key of Object.keys(session.connection.algorithms ?? {})) {
|
for (const key of Object.keys(session.connection.algorithms ?? {})) {
|
||||||
algorithms[key] = session.connection.algorithms![key].filter(x => !ALGORITHM_BLACKLIST.includes(x))
|
algorithms[key] = session.connection.algorithms![key].filter(x => !ALGORITHM_BLACKLIST.includes(x))
|
||||||
}
|
}
|
||||||
await new Promise(async (resolve, reject) => {
|
|
||||||
|
const resultPromise: Promise<void> = new Promise(async (resolve, reject) => {
|
||||||
ssh.on('ready', () => {
|
ssh.on('ready', () => {
|
||||||
connected = true
|
connected = true
|
||||||
if (savedPassword) {
|
if (savedPassword) {
|
||||||
@ -211,137 +215,143 @@ export class SSHService {
|
|||||||
log(banner)
|
log(banner)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let agent: string|null = null
|
|
||||||
if (this.hostApp.platform === Platform.Windows) {
|
|
||||||
if (await fs.exists(WINDOWS_OPENSSH_AGENT_PIPE)) {
|
|
||||||
agent = WINDOWS_OPENSSH_AGENT_PIPE
|
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
||||||
const pageantRunning = await new Promise<boolean>(resolve => {
|
|
||||||
windowsProcessTreeNative.getProcessList(list => { // eslint-disable-line block-scoped-var
|
|
||||||
resolve(list.some(x => x.name === 'pageant.exe'))
|
|
||||||
}, 0)
|
|
||||||
})
|
|
||||||
if (pageantRunning) {
|
|
||||||
agent = 'pageant'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
agent = process.env.SSH_AUTH_SOCK!
|
|
||||||
}
|
|
||||||
|
|
||||||
const authMethodsLeft = ['none']
|
|
||||||
if (!session.connection.auth || session.connection.auth === 'publicKey') {
|
|
||||||
privateKey = await this.loadPrivateKeyForSession(session, log)
|
|
||||||
if (!privateKey) {
|
|
||||||
log('\r\nPrivate key auth selected, but no key is loaded\r\n')
|
|
||||||
} else {
|
|
||||||
authMethodsLeft.push('publickey')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!session.connection.auth || session.connection.auth === 'agent') {
|
|
||||||
if (!agent) {
|
|
||||||
log('\r\nAgent auth selected, but no running agent is detected\r\n')
|
|
||||||
} else {
|
|
||||||
authMethodsLeft.push('agent')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!session.connection.auth || session.connection.auth === 'password') {
|
|
||||||
authMethodsLeft.push('password')
|
|
||||||
}
|
|
||||||
if (!session.connection.auth || session.connection.auth === 'keyboardInteractive') {
|
|
||||||
authMethodsLeft.push('keyboard-interactive')
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
tryKeyboard: true,
|
|
||||||
agent: agent ?? undefined,
|
|
||||||
agentForward: session.connection.agentForward && !!agent,
|
|
||||||
keepaliveInterval: session.connection.keepaliveInterval ?? 15000,
|
|
||||||
keepaliveCountMax: session.connection.keepaliveCountMax,
|
|
||||||
readyTimeout: session.connection.readyTimeout,
|
|
||||||
hostVerifier: (digest: string) => {
|
|
||||||
log(colors.bgWhite(' ') + ' Host key fingerprint:')
|
|
||||||
log(colors.bgWhite(' ') + ' ' + colors.black.bgWhite(' SHA256 ') + colors.bgBlackBright(' ' + digest + ' '))
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
} as any)
|
|
||||||
} catch (e) {
|
|
||||||
this.notifications.error(e.message)
|
|
||||||
return reject(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 ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let agent: string|null = null
|
||||||
|
if (this.hostApp.platform === Platform.Windows) {
|
||||||
|
if (await fs.exists(WINDOWS_OPENSSH_AGENT_PIPE)) {
|
||||||
|
agent = WINDOWS_OPENSSH_AGENT_PIPE
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
|
const pageantRunning = await new Promise<boolean>(resolve => {
|
||||||
|
windowsProcessTreeNative.getProcessList(list => { // eslint-disable-line block-scoped-var
|
||||||
|
resolve(list.some(x => x.name === 'pageant.exe'))
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
if (pageantRunning) {
|
||||||
|
agent = 'pageant'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
agent = process.env.SSH_AUTH_SOCK!
|
||||||
|
}
|
||||||
|
|
||||||
|
const authMethodsLeft = ['none']
|
||||||
|
if (!session.connection.auth || session.connection.auth === 'publicKey') {
|
||||||
|
try {
|
||||||
|
privateKey = await this.loadPrivateKeyForSession(session)
|
||||||
|
} catch (e) {
|
||||||
|
session.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Failed to load private key: ${e}`)
|
||||||
|
}
|
||||||
|
if (!privateKey) {
|
||||||
|
session.emitServiceMessage(colors.bgYellow.yellow.black(' ! ') + ` Private key auth selected, but no key is loaded`)
|
||||||
|
} else {
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!session.connection.auth || session.connection.auth === 'password') {
|
||||||
|
authMethodsLeft.push('password')
|
||||||
|
}
|
||||||
|
if (!session.connection.auth || session.connection.auth === 'keyboardInteractive') {
|
||||||
|
authMethodsLeft.push('keyboard-interactive')
|
||||||
|
}
|
||||||
|
authMethodsLeft.push('hostbased')
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (session.connection.proxyCommand) {
|
||||||
|
session.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ` Using ${session.connection.proxyCommand}`)
|
||||||
|
session.proxyCommandStream = new ProxyCommandStream(session.connection.proxyCommand)
|
||||||
|
|
||||||
|
session.proxyCommandStream.output$.subscribe((message: string) => {
|
||||||
|
session.emitServiceMessage(colors.bgBlue.black(' Proxy command ') + ' ' + message.trim())
|
||||||
|
})
|
||||||
|
|
||||||
|
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,
|
||||||
|
tryKeyboard: true,
|
||||||
|
agent: agent ?? undefined,
|
||||||
|
agentForward: session.connection.agentForward && !!agent,
|
||||||
|
keepaliveInterval: session.connection.keepaliveInterval ?? 15000,
|
||||||
|
keepaliveCountMax: session.connection.keepaliveCountMax,
|
||||||
|
readyTimeout: session.connection.readyTimeout,
|
||||||
|
hostVerifier: (digest: string) => {
|
||||||
|
log('Host key fingerprint:')
|
||||||
|
log(colors.white.bgBlack(' SHA256 ') + colors.bgBlackBright(' ' + digest + ' '))
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} as any)
|
||||||
|
} catch (e) {
|
||||||
|
this.notifications.error(e.message)
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
async showConnectionSelector (): Promise<void> {
|
async showConnectionSelector (): Promise<void> {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user