tabby/terminus-ssh/src/services/ssh.service.ts
2017-12-07 20:47:25 +01:00

195 lines
6.4 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 { AppService, HostAppService, Platform } from 'terminus-core'
import { TerminalTabComponent } from 'terminus-terminal'
import { SSHConnection, SSHSession } from '../api'
import { PromptModalComponent } from '../components/promptModal.component'
const { SSH2Stream } = require('ssh2-streams')
let xkeychain
let wincredmgr
try {
xkeychain = require('xkeychain')
} catch (error) {
try {
wincredmgr = require('wincredmgr')
} catch (error2) {
console.warn('No keychain manager available')
}
}
@Injectable()
export class SSHService {
constructor (
private app: AppService,
private zone: NgZone,
private ngbModal: NgbModal,
private hostApp: HostAppService,
) {
}
savePassword (connection: SSHConnection, password: string) {
if (xkeychain) {
xkeychain.setPassword({
account: connection.user,
service: `ssh@${connection.host}`,
password
}, () => null)
} else {
wincredmgr.WriteCredentials(
'user',
password,
`ssh:${connection.user}@${connection.host}`,
)
}
}
deletePassword (connection: SSHConnection) {
if (xkeychain) {
xkeychain.deletePassword({
account: connection.user,
service: `ssh@${connection.host}`,
}, () => null)
} else {
wincredmgr.DeleteCredentials(
`ssh:${connection.user}@${connection.host}`,
)
}
}
loadPassword (connection: SSHConnection): Promise<string> {
return new Promise(resolve => {
if (xkeychain) {
xkeychain.getPassword({
account: connection.user,
service: `ssh@${connection.host}`,
}, (_, result) => resolve(result))
} else {
try {
resolve(wincredmgr.ReadCredentials(`ssh:${connection.user}@${connection.host}`).password)
} catch (error) {
resolve(null)
}
}
})
}
async connect (connection: SSHConnection): Promise<TerminalTabComponent> {
let privateKey: string = null
let keyPath = path.join(process.env.HOME, '.ssh', 'id_rsa')
if (!connection.privateKey && await fs.exists(keyPath)) {
connection.privateKey = keyPath
}
if (connection.privateKey) {
try {
privateKey = (await fs.readFile(connection.privateKey)).toString()
} catch (error) { }
}
let ssh = new Client()
let connected = false
let savedPassword: string = null
await new Promise((resolve, reject) => {
ssh.on('ready', () => {
connected = true
if (savedPassword) {
this.savePassword(connection, savedPassword)
}
this.zone.run(resolve)
})
ssh.on('error', error => {
this.deletePassword(connection)
this.zone.run(() => {
if (connected) {
alert(`SSH error: ${error}`)
} else {
reject(error)
}
})
})
ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
console.log(name, instructions, instructionsLang)
let results = []
for (let prompt of prompts) {
let modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = prompt.prompt
modal.componentInstance.password = !prompt.echo
results.push(await modal.result)
}
finish(results)
}))
let agent: string = null
if (this.hostApp.platform === Platform.Windows) {
agent = 'pageant'
} else {
agent = process.env.SSH_AUTH_SOCK
}
ssh.connect({
host: connection.host,
username: connection.user,
password: privateKey ? undefined : '',
privateKey,
tryKeyboard: true,
agent,
agentForward: !!agent,
})
let keychainPasswordUsed = false
;(ssh as any).config.password = () => this.zone.run(async () => {
if (connection.password) {
return connection.password
}
if (!keychainPasswordUsed && (wincredmgr || xkeychain.isSupported())) {
let password = await this.loadPassword(connection)
if (password) {
keychainPasswordUsed = true
return password
}
}
let modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = `Password for ${connection.user}@${connection.host}`
modal.componentInstance.password = true
savedPassword = await modal.result
return savedPassword
})
})
try {
let shell = await new Promise((resolve, reject) => {
ssh.shell({ term: 'xterm-256color' }, (err, shell) => {
if (err) {
reject(err)
} else {
resolve(shell)
}
})
})
let session = new SSHSession(shell)
return this.zone.run(() => this.app.openNewTab(
TerminalTabComponent,
{ session, sessionOptions: {} }
) as TerminalTabComponent)
} catch (error) {
console.log(error)
throw error
}
}
}
const _authPassword = SSH2Stream.prototype.authPassword
SSH2Stream.prototype.authPassword = async function (username, passwordFn) {
_authPassword.bind(this)(username, await passwordFn())
}