ssh - show connection log while connecting

This commit is contained in:
Eugene Pankov
2019-01-06 11:14:13 +01:00
parent caacc01aea
commit d03430fb2e
7 changed files with 157 additions and 58 deletions

View File

@@ -23,10 +23,11 @@ export interface SSHConnection {
export class SSHSession extends BaseSession { export class SSHSession extends BaseSession {
scripts?: LoginScript[] scripts?: LoginScript[]
shell: any
constructor (private shell: any, conn: SSHConnection) { constructor (public connection: SSHConnection) {
super() super()
this.scripts = conn.scripts || [] this.scripts = connection.scripts || []
} }
start () { start () {
@@ -87,15 +88,21 @@ export class SSHSession extends BaseSession {
} }
resize (columns, rows) { resize (columns, rows) {
this.shell.setWindow(rows, columns) if (this.shell) {
this.shell.setWindow(rows, columns)
}
} }
write (data) { write (data) {
this.shell.write(data) if (this.shell) {
this.shell.write(data)
}
} }
kill (signal?: string) { kill (signal?: string) {
this.shell.signal(signal || 'TERM') if (this.shell) {
this.shell.signal(signal || 'TERM')
}
} }
async getChildProcesses (): Promise<any[]> { async getChildProcesses (): Promise<any[]> {

View File

@@ -61,7 +61,7 @@ export class SSHModalComponent {
connect (connection: SSHConnection) { connect (connection: SSHConnection) {
this.close() this.close()
this.ssh.connect(connection).catch(error => { this.ssh.openTab(connection).catch(error => {
this.toastr.error(`Could not connect: ${error}`) this.toastr.error(`Could not connect: ${error}`)
}).then(() => { }).then(() => {
setTimeout(() => { setTimeout(() => {

View File

@@ -1,5 +1,8 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { TerminalTabComponent } from 'terminus-terminal' import { first } from 'rxjs/operators'
import { BaseTerminalTabComponent } from 'terminus-terminal'
import { SSHService } from '../services/ssh.service'
import { SSHConnection, SSHSession } from '../api'
@Component({ @Component({
template: ` template: `
@@ -10,5 +13,44 @@ import { TerminalTabComponent } from 'terminus-terminal'
`, `,
styles: [require('./sshTab.component.scss')], styles: [require('./sshTab.component.scss')],
}) })
export class SSHTabComponent extends TerminalTabComponent { export class SSHTabComponent extends BaseTerminalTabComponent {
connection: SSHConnection
ssh: SSHService
session: SSHSession
ngOnInit () {
this.logger = this.log.create('terminalTab')
this.ssh = this.injector.get(SSHService)
this.frontendReady$.pipe(first()).subscribe(() => {
this.initializeSession()
})
super.ngOnInit()
}
async initializeSession () {
if (!this.connection) {
this.logger.error('No SSH connection info supplied')
return
}
this.session = new SSHSession(this.connection)
this.attachSessionHandlers()
this.write(`Connecting to ${this.connection.host}`)
let interval = setInterval(() => this.write('.'), 500)
try {
await this.ssh.connectSession(this.session, message => {
this.write('\r\n' + message)
})
} catch (e) {
this.write('\r\n')
this.write(e.message)
return
} finally {
clearInterval(interval)
this.write('\r\n')
}
this.session.resize(this.size.columns, this.size.rows)
this.session.start()
}
} }

View File

@@ -33,14 +33,31 @@ export class SSHService {
this.logger = log.create('ssh') this.logger = log.create('ssh')
} }
async connect (connection: SSHConnection): Promise<SSHTabComponent> { async openTab (connection: SSHConnection): Promise<SSHTabComponent> {
return this.zone.run(() => this.app.openNewTab(
SSHTabComponent,
{ connection }
) as SSHTabComponent)
}
async connectSession (session: SSHSession, logCallback?: (s: string) => void): Promise<void> {
let privateKey: string = null let privateKey: string = null
let privateKeyPassphrase: string = null let privateKeyPassphrase: string = null
let privateKeyPath = connection.privateKey let privateKeyPath = session.connection.privateKey
if (!logCallback) {
logCallback = (s) => null
}
const log = s => {
logCallback(s)
this.logger.info(s)
}
if (!privateKeyPath) { if (!privateKeyPath) {
let userKeyPath = path.join(process.env.HOME, '.ssh', 'id_rsa') let userKeyPath = path.join(process.env.HOME, '.ssh', 'id_rsa')
if (await fs.exists(userKeyPath)) { if (await fs.exists(userKeyPath)) {
this.logger.info('Using user\'s default private key:', userKeyPath) log(`Using user's default private key: ${userKeyPath}`)
privateKeyPath = userKeyPath privateKeyPath = userKeyPath
} }
} }
@@ -49,11 +66,12 @@ export class SSHService {
try { try {
privateKey = (await fs.readFile(privateKeyPath)).toString() privateKey = (await fs.readFile(privateKeyPath)).toString()
} catch (error) { } catch (error) {
log('Could not read the private key file')
this.toastr.warning('Could not read the private key file') this.toastr.warning('Could not read the private key file')
} }
if (privateKey) { if (privateKey) {
this.logger.info('Loaded private key from', privateKeyPath) log(`Loading private key from ${privateKeyPath}`)
let encrypted = privateKey.includes('ENCRYPTED') let encrypted = privateKey.includes('ENCRYPTED')
if (privateKeyPath.toLowerCase().endsWith('.ppk')) { if (privateKeyPath.toLowerCase().endsWith('.ppk')) {
@@ -61,6 +79,7 @@ export class SSHService {
} }
if (encrypted) { if (encrypted) {
let modal = this.ngbModal.open(PromptModalComponent) let modal = this.ngbModal.open(PromptModalComponent)
log('Key requires passphrase')
modal.componentInstance.prompt = 'Private key passphrase' modal.componentInstance.prompt = 'Private key passphrase'
modal.componentInstance.password = true modal.componentInstance.password = true
try { try {
@@ -77,12 +96,12 @@ export class SSHService {
ssh.on('ready', () => { ssh.on('ready', () => {
connected = true connected = true
if (savedPassword) { if (savedPassword) {
this.passwordStorage.savePassword(connection, savedPassword) this.passwordStorage.savePassword(session.connection, savedPassword)
} }
this.zone.run(resolve) this.zone.run(resolve)
}) })
ssh.on('error', error => { ssh.on('error', error => {
this.passwordStorage.deletePassword(connection) this.passwordStorage.deletePassword(session.connection)
this.zone.run(() => { this.zone.run(() => {
if (connected) { if (connected) {
this.toastr.error(error.toString()) this.toastr.error(error.toString())
@@ -92,7 +111,8 @@ export class SSHService {
}) })
}) })
ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => { ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
console.log(name, instructions, instructionsLang) log(`Keyboard-interactive auth requested: ${name}`)
this.logger.info('Keyboard-interactive auth:', name, instructions, instructionsLang)
let results = [] let results = []
for (let prompt of prompts) { for (let prompt of prompts) {
let modal = this.ngbModal.open(PromptModalComponent) let modal = this.ngbModal.open(PromptModalComponent)
@@ -103,6 +123,14 @@ export class SSHService {
finish(results) finish(results)
})) }))
ssh.on('greeting', greeting => {
log('Greeting: ' + greeting)
})
ssh.on('banner', banner => {
log('Banner: ' + banner)
})
let agent: string = null let agent: string = null
if (this.hostApp.platform === Platform.Windows) { if (this.hostApp.platform === Platform.Windows) {
let pageantRunning = new Promise<boolean>(resolve => { let pageantRunning = new Promise<boolean>(resolve => {
@@ -119,50 +147,61 @@ export class SSHService {
try { try {
ssh.connect({ ssh.connect({
host: connection.host, host: session.connection.host,
port: connection.port || 22, port: session.connection.port || 22,
username: connection.user, username: session.connection.user,
password: connection.privateKey ? undefined : '', password: session.connection.privateKey ? undefined : '',
privateKey, privateKey,
passphrase: privateKeyPassphrase, passphrase: privateKeyPassphrase,
tryKeyboard: true, tryKeyboard: true,
agent, agent,
agentForward: !!agent, agentForward: !!agent,
keepaliveInterval: connection.keepaliveInterval, keepaliveInterval: session.connection.keepaliveInterval,
keepaliveCountMax: connection.keepaliveCountMax, keepaliveCountMax: session.connection.keepaliveCountMax,
readyTimeout: connection.readyTimeout, readyTimeout: session.connection.readyTimeout,
debug: (...x) => console.log(...x),
hostVerifier: digest => {
log('SHA256 fingerprint: ' + digest)
return true
},
hostHash: 'sha256' as any,
}) })
} catch (e) { } catch (e) {
this.toastr.error(e.message) this.toastr.error(e.message)
reject(e)
} }
let keychainPasswordUsed = false let keychainPasswordUsed = false
;(ssh as any).config.password = () => this.zone.run(async () => { ;(ssh as any).config.password = () => this.zone.run(async () => {
if (connection.password) { if (session.connection.password) {
this.logger.info('Using preset password') log('Using preset password')
return connection.password return session.connection.password
} }
if (!keychainPasswordUsed) { if (!keychainPasswordUsed) {
let password = await this.passwordStorage.loadPassword(connection) let password = await this.passwordStorage.loadPassword(session.connection)
if (password) { if (password) {
this.logger.info('Using saved password') log('Trying saved password')
keychainPasswordUsed = true keychainPasswordUsed = true
return password return password
} }
} }
let modal = this.ngbModal.open(PromptModalComponent) let modal = this.ngbModal.open(PromptModalComponent)
modal.componentInstance.prompt = `Password for ${connection.user}@${connection.host}` modal.componentInstance.prompt = `Password for ${session.connection.user}@${session.connection.host}`
modal.componentInstance.password = true modal.componentInstance.password = true
savedPassword = await modal.result try {
savedPassword = await modal.result
} catch (_) {
return ''
}
return savedPassword return savedPassword
}) })
}) })
try { try {
let shell = await new Promise((resolve, reject) => { let shell: any = await new Promise<any>((resolve, reject) => {
ssh.shell({ term: 'xterm-256color' }, (err, shell) => { ssh.shell({ term: 'xterm-256color' }, (err, shell) => {
if (err) { if (err) {
reject(err) reject(err)
@@ -172,14 +211,17 @@ export class SSHService {
}) })
}) })
let session = new SSHSession(shell, connection) session.shell = shell
return this.zone.run(() => this.app.openNewTab( shell.on('greeting', greeting => {
SSHTabComponent, log('Shell Greeting: ' + greeting)
{ session, sessionOptions: {} } })
) as SSHTabComponent)
shell.on('banner', banner => {
log('Shell Banner: ' + banner)
})
} catch (error) { } catch (error) {
console.log(error) this.toastr.error(error.message)
throw error throw error
} }
} }

View File

@@ -1,10 +1,10 @@
import { Observable, Subject, Subscription } from 'rxjs' import { Observable, Subject, Subscription } from 'rxjs'
import { first } from 'rxjs/operators' import { first } from 'rxjs/operators'
import { ToastrService } from 'ngx-toastr' import { ToastrService } from 'ngx-toastr'
import { NgZone, OnInit, OnDestroy, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core' import { NgZone, OnInit, OnDestroy, Inject, Injector, Optional, ViewChild, HostBinding, Input } from '@angular/core'
import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, Platform, LogService, Logger } from 'terminus-core' import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, Platform, LogService, Logger } from 'terminus-core'
import { Session, SessionsService } from '../services/sessions.service' import { BaseSession, SessionsService } from '../services/sessions.service'
import { TerminalFrontendService } from '../services/terminalFrontend.service' import { TerminalFrontendService } from '../services/terminalFrontend.service'
import { TerminalDecorator, ResizeEvent, TerminalContextMenuItemProvider } from '../api' import { TerminalDecorator, ResizeEvent, TerminalContextMenuItemProvider } from '../api'
@@ -20,7 +20,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
` `
static styles = [require('./terminalTab.component.scss')] static styles = [require('./terminalTab.component.scss')]
session: Session session: BaseSession
@Input() zoom = 0 @Input() zoom = 0
@ViewChild('content') content @ViewChild('content') content
@HostBinding('style.background-color') backgroundColor: string @HostBinding('style.background-color') backgroundColor: string
@@ -43,6 +43,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
constructor ( constructor (
public config: ConfigService, public config: ConfigService,
protected injector: Injector,
protected zone: NgZone, protected zone: NgZone,
protected app: AppService, protected app: AppService,
protected hostApp: HostAppService, protected hostApp: HostAppService,
@@ -60,8 +61,6 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
this.decorators = this.decorators || [] this.decorators = this.decorators || []
this.setTitle('Terminal') this.setTitle('Terminal')
this.session = new Session(this.config)
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => { this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
if (!this.hasFocus) { if (!this.hasFocus) {
return return
@@ -241,7 +240,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
this.logger.info(`Resizing to ${columns}x${rows}`) this.logger.info(`Resizing to ${columns}x${rows}`)
this.size = { columns, rows } this.size = { columns, rows }
this.zone.run(() => { this.zone.run(() => {
if (this.session.open) { if (this.session && this.session.open) {
this.session.resize(columns, rows) this.session.resize(columns, rows)
} }
}) })
@@ -333,4 +332,19 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
await this.session.destroy() await this.session.destroy()
} }
} }
protected attachSessionHandlers () {
// this.session.output$.bufferTime(10).subscribe((datas) => {
this.session.output$.subscribe(data => {
this.zone.run(() => {
this.output.next(data)
this.write(data)
})
})
this.sessionCloseSubscription = this.session.closed$.subscribe(() => {
this.frontend.destroy()
this.app.closeTab(this)
})
}
} }

View File

@@ -3,6 +3,7 @@ import { first } from 'rxjs/operators'
import { BaseTabProcess } from 'terminus-core' import { BaseTabProcess } from 'terminus-core'
import { BaseTerminalTabComponent } from './baseTerminalTab.component' import { BaseTerminalTabComponent } from './baseTerminalTab.component'
import { SessionOptions } from '../api' import { SessionOptions } from '../api'
import { Session } from '../services/sessions.service'
@Component({ @Component({
selector: 'terminalTab', selector: 'terminalTab',
@@ -14,6 +15,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
ngOnInit () { ngOnInit () {
this.logger = this.log.create('terminalTab') this.logger = this.log.create('terminalTab')
this.session = new Session(this.config)
this.frontendReady$.pipe(first()).subscribe(() => { this.frontendReady$.pipe(first()).subscribe(() => {
this.initializeSession(this.size.columns, this.size.rows) this.initializeSession(this.size.columns, this.size.rows)
@@ -31,18 +33,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
}) })
) )
// this.session.output$.bufferTime(10).subscribe((datas) => { this.attachSessionHandlers()
this.session.output$.subscribe(data => {
this.zone.run(() => {
this.output.next(data)
this.write(data)
})
})
this.sessionCloseSubscription = this.session.closed$.subscribe(() => {
this.frontend.destroy()
this.app.closeTab(this)
})
} }
async getRecoveryToken (): Promise<any> { async getRecoveryToken (): Promise<any> {

View File

@@ -11,13 +11,16 @@ export class TerminalFrontendService {
constructor (private config: ConfigService) { } constructor (private config: ConfigService) { }
getFrontend (session: BaseSession): Frontend { getFrontend (session?: BaseSession): Frontend {
if (!session) {
return (this.config.store.terminal.frontend === 'xterm')
? new XTermFrontend()
: new HTermFrontend()
}
if (!this.containers.has(session)) { if (!this.containers.has(session)) {
this.containers.set( this.containers.set(
session, session,
(this.config.store.terminal.frontend === 'xterm') this.getFrontend(),
? new XTermFrontend()
: new HTermFrontend()
) )
} }
return this.containers.get(session) return this.containers.get(session)