tabby/terminus-ssh/src/services/ssh.service.ts
2020-01-01 01:04:41 +01:00

232 lines
9.0 KiB
TypeScript

import { Injectable, NgZone } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Client } from 'ssh2'
import * as fs from 'mz/fs'
import * as path from 'path'
import { ToastrService } from 'ngx-toastr'
import { AppService, HostAppService, Platform, Logger, LogService } from 'terminus-core'
import { SSHConnection, SSHSession } from '../api'
import { PromptModalComponent } from '../components/promptModal.component'
import { SSHTabComponent } from '../components/sshTab.component'
import { PasswordStorageService } from './passwordStorage.service'
import { SSH2Stream } from 'ssh2-streams'
try {
var windowsProcessTreeNative = require('windows-process-tree/build/Release/windows_process_tree.node') // eslint-disable-line @typescript-eslint/no-var-requires
} catch { }
@Injectable({ providedIn: 'root' })
export class SSHService {
private logger: Logger
private constructor (
private log: LogService,
private app: AppService,
private zone: NgZone,
private ngbModal: NgbModal,
private hostApp: HostAppService,
private passwordStorage: PasswordStorageService,
private toastr: ToastrService,
) {
this.logger = log.create('ssh')
}
async openTab (connection: SSHConnection): Promise<SSHTabComponent> {
const tab = this.zone.run(() => this.app.openNewTab(
SSHTabComponent,
{ connection }
) as SSHTabComponent)
if (connection.color) {
(this.app.getParentTab(tab) || tab).color = connection.color
}
return tab
}
createSession (connection: SSHConnection): SSHSession {
const session = new SSHSession(connection)
session.logger = this.log.create(`ssh-${connection.host}-${connection.port}`)
return session
}
async connectSession (session: SSHSession, logCallback?: (s: any) => void): Promise<void> {
let privateKey: string|null = null
let privateKeyPassphrase: string|null = null
let privateKeyPath = session.connection.privateKey
if (!logCallback) {
logCallback = () => null
}
const log = (s: any) => {
logCallback!(s)
this.logger.info(s)
}
if (!privateKeyPath) {
const userKeyPath = path.join(process.env.HOME as string, '.ssh', 'id_rsa')
if (await fs.exists(userKeyPath)) {
log(`Using user's default private key: ${userKeyPath}`)
privateKeyPath = userKeyPath
}
}
if (privateKeyPath) {
try {
privateKey = (await fs.readFile(privateKeyPath)).toString()
} catch (error) {
log('Could not read the private key file')
this.toastr.error('Could not read the private key file')
}
if (privateKey) {
log(`Loading private key from ${privateKeyPath}`)
let encrypted = privateKey.includes('ENCRYPTED')
if (privateKeyPath.toLowerCase().endsWith('.ppk')) {
encrypted = encrypted || privateKey.includes('Encryption:') && !privateKey.includes('Encryption: none')
}
if (encrypted) {
const modal = this.ngbModal.open(PromptModalComponent)
log('Key requires passphrase')
modal.componentInstance.prompt = 'Private key passphrase'
modal.componentInstance.password = true
try {
const result = await modal.result
if (result) {
privateKeyPassphrase = result.value
}
} catch (e) { }
}
}
}
const ssh = new Client()
session.ssh = ssh
let connected = false
let savedPassword: string|null = null
await new Promise(async (resolve, reject) => {
ssh.on('ready', () => {
connected = true
if (savedPassword) {
this.passwordStorage.savePassword(session.connection, savedPassword)
}
this.zone.run(resolve)
})
ssh.on('error', error => {
if (error.message === 'All configured authentication methods failed') {
this.passwordStorage.deletePassword(session.connection)
}
this.zone.run(() => {
if (connected) {
this.toastr.error(error.toString())
} else {
reject(error)
}
})
})
ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
log(`Keyboard-interactive auth requested: ${name}`)
this.logger.info('Keyboard-interactive auth:', name, instructions, instructionsLang)
const results: string[] = []
for (const prompt of prompts) {
const modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = prompt.prompt
modal.componentInstance.password = !prompt.echo
const result = await modal.result
results.push(result ? result.value : '')
}
finish(results)
}))
ssh.on('greeting', greeting => {
log('Greeting: ' + greeting)
})
ssh.on('banner', banner => {
log('Banner: \n' + banner)
})
let agent: string|null = null
if (this.hostApp.platform === Platform.Windows) {
const pageantRunning = new Promise<boolean>(resolve => {
windowsProcessTreeNative.getProcessList(list => { // eslint-disable-line block-scoped-var
resolve(list.some(x => x.name === 'pageant.exe'))
}, 0)
})
if (await pageantRunning) {
agent = 'pageant'
}
} else {
agent = process.env.SSH_AUTH_SOCK as string
}
try {
ssh.connect({
host: session.connection.host,
port: session.connection.port || 22,
username: session.connection.user,
password: session.connection.privateKey ? undefined : '',
privateKey: privateKey || undefined,
passphrase: privateKeyPassphrase || undefined,
tryKeyboard: true,
agent: agent || undefined,
agentForward: !!agent,
keepaliveInterval: session.connection.keepaliveInterval,
keepaliveCountMax: session.connection.keepaliveCountMax,
readyTimeout: session.connection.readyTimeout,
hostVerifier: digest => {
log('SHA256 fingerprint: ' + digest)
return true
},
hostHash: 'sha256' as any,
algorithms: session.connection.algorithms,
})
} catch (e) {
this.toastr.error(e.message)
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 ''
}
})
})
}
}
/* eslint-disable */
const _authPassword = SSH2Stream.prototype.authPassword
SSH2Stream.prototype.authPassword = async function (username, passwordFn: any) {
_authPassword.bind(this)(username, await passwordFn())
} as any