diff --git a/terminus-ssh/src/api.ts b/terminus-ssh/src/api.ts index f2650340..d0521d6e 100644 --- a/terminus-ssh/src/api.ts +++ b/terminus-ssh/src/api.ts @@ -23,10 +23,11 @@ export interface SSHConnection { export class SSHSession extends BaseSession { scripts?: LoginScript[] + shell: any - constructor (private shell: any, conn: SSHConnection) { + constructor (public connection: SSHConnection) { super() - this.scripts = conn.scripts || [] + this.scripts = connection.scripts || [] } start () { @@ -87,15 +88,21 @@ export class SSHSession extends BaseSession { } resize (columns, rows) { - this.shell.setWindow(rows, columns) + if (this.shell) { + this.shell.setWindow(rows, columns) + } } write (data) { - this.shell.write(data) + if (this.shell) { + this.shell.write(data) + } } kill (signal?: string) { - this.shell.signal(signal || 'TERM') + if (this.shell) { + this.shell.signal(signal || 'TERM') + } } async getChildProcesses (): Promise { diff --git a/terminus-ssh/src/components/sshModal.component.ts b/terminus-ssh/src/components/sshModal.component.ts index dbc79d08..735e8bec 100644 --- a/terminus-ssh/src/components/sshModal.component.ts +++ b/terminus-ssh/src/components/sshModal.component.ts @@ -61,7 +61,7 @@ export class SSHModalComponent { connect (connection: SSHConnection) { this.close() - this.ssh.connect(connection).catch(error => { + this.ssh.openTab(connection).catch(error => { this.toastr.error(`Could not connect: ${error}`) }).then(() => { setTimeout(() => { diff --git a/terminus-ssh/src/components/sshTab.component.ts b/terminus-ssh/src/components/sshTab.component.ts index 634ecc65..ea609087 100644 --- a/terminus-ssh/src/components/sshTab.component.ts +++ b/terminus-ssh/src/components/sshTab.component.ts @@ -1,5 +1,8 @@ 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({ template: ` @@ -10,5 +13,44 @@ import { TerminalTabComponent } from 'terminus-terminal' `, 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() + } } diff --git a/terminus-ssh/src/services/ssh.service.ts b/terminus-ssh/src/services/ssh.service.ts index 437afae2..715e6b66 100644 --- a/terminus-ssh/src/services/ssh.service.ts +++ b/terminus-ssh/src/services/ssh.service.ts @@ -33,14 +33,31 @@ export class SSHService { this.logger = log.create('ssh') } - async connect (connection: SSHConnection): Promise { + async openTab (connection: SSHConnection): Promise { + return this.zone.run(() => this.app.openNewTab( + SSHTabComponent, + { connection } + ) as SSHTabComponent) + } + + async connectSession (session: SSHSession, logCallback?: (s: string) => void): Promise { let privateKey: 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) { let userKeyPath = path.join(process.env.HOME, '.ssh', 'id_rsa') 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 } } @@ -49,11 +66,12 @@ export class SSHService { try { privateKey = (await fs.readFile(privateKeyPath)).toString() } catch (error) { + log('Could not read the private key file') this.toastr.warning('Could not read the private key file') } if (privateKey) { - this.logger.info('Loaded private key from', privateKeyPath) + log(`Loading private key from ${privateKeyPath}`) let encrypted = privateKey.includes('ENCRYPTED') if (privateKeyPath.toLowerCase().endsWith('.ppk')) { @@ -61,6 +79,7 @@ export class SSHService { } if (encrypted) { let modal = this.ngbModal.open(PromptModalComponent) + log('Key requires passphrase') modal.componentInstance.prompt = 'Private key passphrase' modal.componentInstance.password = true try { @@ -77,12 +96,12 @@ export class SSHService { ssh.on('ready', () => { connected = true if (savedPassword) { - this.passwordStorage.savePassword(connection, savedPassword) + this.passwordStorage.savePassword(session.connection, savedPassword) } this.zone.run(resolve) }) ssh.on('error', error => { - this.passwordStorage.deletePassword(connection) + this.passwordStorage.deletePassword(session.connection) this.zone.run(() => { if (connected) { 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 () => { - console.log(name, instructions, instructionsLang) + log(`Keyboard-interactive auth requested: ${name}`) + this.logger.info('Keyboard-interactive auth:', name, instructions, instructionsLang) let results = [] for (let prompt of prompts) { let modal = this.ngbModal.open(PromptModalComponent) @@ -103,6 +123,14 @@ export class SSHService { finish(results) })) + ssh.on('greeting', greeting => { + log('Greeting: ' + greeting) + }) + + ssh.on('banner', banner => { + log('Banner: ' + banner) + }) + let agent: string = null if (this.hostApp.platform === Platform.Windows) { let pageantRunning = new Promise(resolve => { @@ -119,50 +147,61 @@ export class SSHService { try { ssh.connect({ - host: connection.host, - port: connection.port || 22, - username: connection.user, - password: connection.privateKey ? undefined : '', + host: session.connection.host, + port: session.connection.port || 22, + username: session.connection.user, + password: session.connection.privateKey ? undefined : '', privateKey, passphrase: privateKeyPassphrase, tryKeyboard: true, agent, agentForward: !!agent, - keepaliveInterval: connection.keepaliveInterval, - keepaliveCountMax: connection.keepaliveCountMax, - readyTimeout: connection.readyTimeout, + keepaliveInterval: session.connection.keepaliveInterval, + keepaliveCountMax: session.connection.keepaliveCountMax, + readyTimeout: session.connection.readyTimeout, + debug: (...x) => console.log(...x), + hostVerifier: digest => { + log('SHA256 fingerprint: ' + digest) + return true + }, + hostHash: 'sha256' as any, }) } catch (e) { this.toastr.error(e.message) + reject(e) } let keychainPasswordUsed = false ;(ssh as any).config.password = () => this.zone.run(async () => { - if (connection.password) { - this.logger.info('Using preset password') - return connection.password + if (session.connection.password) { + log('Using preset password') + return session.connection.password } if (!keychainPasswordUsed) { - let password = await this.passwordStorage.loadPassword(connection) + let password = await this.passwordStorage.loadPassword(session.connection) if (password) { - this.logger.info('Using saved password') + log('Trying saved password') keychainPasswordUsed = true return password } } 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 - savedPassword = await modal.result + try { + savedPassword = await modal.result + } catch (_) { + return '' + } return savedPassword }) }) try { - let shell = await new Promise((resolve, reject) => { + let shell: any = await new Promise((resolve, reject) => { ssh.shell({ term: 'xterm-256color' }, (err, shell) => { if (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( - SSHTabComponent, - { session, sessionOptions: {} } - ) as SSHTabComponent) + shell.on('greeting', greeting => { + log('Shell Greeting: ' + greeting) + }) + + shell.on('banner', banner => { + log('Shell Banner: ' + banner) + }) } catch (error) { - console.log(error) + this.toastr.error(error.message) throw error } } diff --git a/terminus-terminal/src/components/baseTerminalTab.component.ts b/terminus-terminal/src/components/baseTerminalTab.component.ts index 62ff955b..14c58a3c 100644 --- a/terminus-terminal/src/components/baseTerminalTab.component.ts +++ b/terminus-terminal/src/components/baseTerminalTab.component.ts @@ -1,10 +1,10 @@ import { Observable, Subject, Subscription } from 'rxjs' import { first } from 'rxjs/operators' 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 { Session, SessionsService } from '../services/sessions.service' +import { BaseSession, SessionsService } from '../services/sessions.service' import { TerminalFrontendService } from '../services/terminalFrontend.service' import { TerminalDecorator, ResizeEvent, TerminalContextMenuItemProvider } from '../api' @@ -20,7 +20,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit ` static styles = [require('./terminalTab.component.scss')] - session: Session + session: BaseSession @Input() zoom = 0 @ViewChild('content') content @HostBinding('style.background-color') backgroundColor: string @@ -43,6 +43,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit constructor ( public config: ConfigService, + protected injector: Injector, protected zone: NgZone, protected app: AppService, protected hostApp: HostAppService, @@ -60,8 +61,6 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit this.decorators = this.decorators || [] this.setTitle('Terminal') - this.session = new Session(this.config) - this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => { if (!this.hasFocus) { return @@ -241,7 +240,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit this.logger.info(`Resizing to ${columns}x${rows}`) this.size = { columns, rows } this.zone.run(() => { - if (this.session.open) { + if (this.session && this.session.open) { this.session.resize(columns, rows) } }) @@ -333,4 +332,19 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit 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) + }) + } } diff --git a/terminus-terminal/src/components/terminalTab.component.ts b/terminus-terminal/src/components/terminalTab.component.ts index 5789574f..58f20d81 100644 --- a/terminus-terminal/src/components/terminalTab.component.ts +++ b/terminus-terminal/src/components/terminalTab.component.ts @@ -3,6 +3,7 @@ import { first } from 'rxjs/operators' import { BaseTabProcess } from 'terminus-core' import { BaseTerminalTabComponent } from './baseTerminalTab.component' import { SessionOptions } from '../api' +import { Session } from '../services/sessions.service' @Component({ selector: 'terminalTab', @@ -14,6 +15,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent { ngOnInit () { this.logger = this.log.create('terminalTab') + this.session = new Session(this.config) this.frontendReady$.pipe(first()).subscribe(() => { 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.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) - }) + this.attachSessionHandlers() } async getRecoveryToken (): Promise { diff --git a/terminus-terminal/src/services/terminalFrontend.service.ts b/terminus-terminal/src/services/terminalFrontend.service.ts index 63426fc3..b02b778e 100644 --- a/terminus-terminal/src/services/terminalFrontend.service.ts +++ b/terminus-terminal/src/services/terminalFrontend.service.ts @@ -11,13 +11,16 @@ export class TerminalFrontendService { 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)) { this.containers.set( session, - (this.config.store.terminal.frontend === 'xterm') - ? new XTermFrontend() - : new HTermFrontend() + this.getFrontend(), ) } return this.containers.get(session)