diff --git a/terminus-ssh/package.json b/terminus-ssh/package.json index be5cf666..7fcca0e1 100644 --- a/terminus-ssh/package.json +++ b/terminus-ssh/package.json @@ -19,6 +19,8 @@ "devDependencies": { "@types/node": "12.7.3", "@types/ssh2": "^0.5.35", + "ansi-colors": "^4.1.1", + "cli-spinner": "^0.2.10", "ssh2": "^0.8.2", "ssh2-streams": "^0.4.2", "sshpk": "^1.16.1", diff --git a/terminus-ssh/src/api.ts b/terminus-ssh/src/api.ts index b859c181..83f2d434 100644 --- a/terminus-ssh/src/api.ts +++ b/terminus-ssh/src/api.ts @@ -1,3 +1,4 @@ +import colors from 'ansi-colors' import { BaseSession } from 'terminus-terminal' import { Server, Socket, createServer, createConnection } from 'net' import { Client, ClientChannel } from 'ssh2' @@ -92,7 +93,7 @@ export class SSHSession extends BaseSession { try { this.shell = await this.openShellChannel({ x11: this.connection.x11 }) } catch (err) { - this.emitServiceMessage(`Remote rejected opening a shell channel: ${err}`) + this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected opening a shell channel: ${err}`) } this.shell.on('greeting', greeting => { @@ -159,13 +160,13 @@ export class SSHSession extends BaseSession { this.logger.info(`Incoming forwarded connection: (remote) ${details.srcIP}:${details.srcPort} -> (local) ${details.destIP}:${details.destPort}`) const forward = this.forwardedPorts.find(x => x.port === details.destPort) if (!forward) { - this.emitServiceMessage(`Rejected incoming forwarded connection for unrecognized port ${details.destPort}`) + this.emitServiceMessage(colors.bgRed.black(' X ') + ` Rejected incoming forwarded connection for unrecognized port ${details.destPort}`) return reject() } const socket = new Socket() socket.connect(forward.targetPort, forward.targetAddress) socket.on('error', e => { - this.emitServiceMessage(`Could not forward the remote connection to ${forward.targetAddress}:${forward.targetPort}: ${e}`) + this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not forward the remote connection to ${forward.targetAddress}:${forward.targetPort}: ${e}`) reject() }) socket.on('connect', () => { @@ -195,7 +196,7 @@ export class SSHSession extends BaseSession { socket.connect(xPort, xHost) } socket.on('error', e => { - this.emitServiceMessage(`Could not connect to the X server ${xHost}:${xPort}: ${e}`) + this.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not connect to the X server ${xHost}:${xPort}: ${e}`) reject() }) socket.on('connect', () => { @@ -231,7 +232,7 @@ export class SSHSession extends BaseSession { fw.targetPort, (err, stream) => { if (err) { - this.emitServiceMessage(`Remote has rejected the forwaded connection via ${fw}: ${err}`) + this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote has rejected the forwaded connection via ${fw}: ${err}`) socket.destroy() return } @@ -246,10 +247,10 @@ export class SSHSession extends BaseSession { } ) }).then(() => { - this.emitServiceMessage(`Forwaded ${fw}`) + this.emitServiceMessage(colors.bgGreen.black(' -> ') + ` Forwaded ${fw}`) this.forwardedPorts.push(fw) }).catch(e => { - this.emitServiceMessage(`Failed to forward port ${fw}: ${e}`) + this.emitServiceMessage(colors.bgRed.black(' X ') + ` Failed to forward port ${fw}: ${e}`) throw e }) } @@ -257,13 +258,13 @@ export class SSHSession extends BaseSession { await new Promise((resolve, reject) => { this.ssh.forwardIn(fw.host, fw.port, err => { if (err) { - this.emitServiceMessage(`Remote rejected port forwarding for ${fw}: ${err}`) + this.emitServiceMessage(colors.bgRed.black(' X ') + ` Remote rejected port forwarding for ${fw}: ${err}`) return reject(err) } resolve() }) }) - this.emitServiceMessage(`Forwaded ${fw}`) + this.emitServiceMessage(colors.bgGreen.black(' <- ') + ` Forwaded ${fw}`) this.forwardedPorts.push(fw) } } diff --git a/terminus-ssh/src/components/sshTab.component.ts b/terminus-ssh/src/components/sshTab.component.ts index 8038ca85..378af624 100644 --- a/terminus-ssh/src/components/sshTab.component.ts +++ b/terminus-ssh/src/components/sshTab.component.ts @@ -1,3 +1,5 @@ +import colors from 'ansi-colors' +import { Spinner } from 'cli-spinner' import { Component } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { first } from 'rxjs/operators' @@ -43,23 +45,32 @@ export class SSHTabComponent extends BaseTerminalTabComponent { this.session = this.ssh.createSession(this.connection) this.session.serviceMessage$.subscribe(msg => { - this.write(`\r\n[SSH] ${msg}\r\n`) + this.write('\r\n' + colors.black.bgWhite(' SSH ') + ' ' + msg + '\r\n') this.session.resize(this.size.columns, this.size.rows) }) this.attachSessionHandlers() this.write(`Connecting to ${this.connection.host}`) - const interval = setInterval(() => this.write('.'), 500) + + const spinner = new Spinner({ + text: 'Connecting', + stream: { + write: x => this.write(x), + }, + }) + spinner.setSpinnerString(6) + spinner.start() + try { await this.ssh.connectSession(this.session, (message: string) => { - this.write('\r\n' + message) + spinner.stop(true) + this.write(message + '\r\n') + spinner.start() }) + spinner.stop(true) } catch (e) { - this.write('\r\n') - this.write(e.message) + spinner.stop(true) + this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n') return - } finally { - clearInterval(interval) - this.write('\r\n') } await this.session.start() this.session.resize(this.size.columns, this.size.rows) diff --git a/terminus-ssh/src/services/ssh.service.ts b/terminus-ssh/src/services/ssh.service.ts index d228753d..9ec836a2 100644 --- a/terminus-ssh/src/services/ssh.service.ts +++ b/terminus-ssh/src/services/ssh.service.ts @@ -1,3 +1,4 @@ +import colors from 'ansi-colors' import { Injectable, NgZone } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { Client } from 'ssh2' @@ -65,17 +66,17 @@ export class SSHService { 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}`) + log('Using user\'s default private key') privateKeyPath = userKeyPath } } if (privateKeyPath) { - log(`Loading private key from ${privateKeyPath}`) + log('Loading private key from ' + colors.bgWhite.blackBright(' ' + privateKeyPath + ' ')) try { privateKey = (await fs.readFile(privateKeyPath)).toString() } catch (error) { - log('Could not read the private key file') + log(colors.bgRed.black(' X ') + 'Could not read the private key file') this.toastr.error('Could not read the private key file') } @@ -86,7 +87,7 @@ export class SSHService { } catch (e) { if (e instanceof sshpk.KeyEncryptedError) { const modal = this.ngbModal.open(PromptModalComponent) - log('Key requires passphrase') + log(colors.bgYellow.yellow.black(' ! ') + ' Key requires passphrase') modal.componentInstance.prompt = 'Private key passphrase' modal.componentInstance.password = true let passphrase = '' @@ -133,7 +134,7 @@ export class SSHService { }) }) ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => { - log(`Keyboard-interactive auth requested: ${name}`) + log(colors.bgBlackBright(' ') + ` Keyboard-interactive auth requested: ${name}`) this.logger.info('Keyboard-interactive auth:', name, instructions, instructionsLang) const results: string[] = [] for (const prompt of prompts) { @@ -182,7 +183,8 @@ export class SSHService { keepaliveCountMax: session.connection.keepaliveCountMax, readyTimeout: session.connection.readyTimeout, hostVerifier: digest => { - log('SHA256 fingerprint: ' + digest) + log(colors.bgWhite(' ') + ' Host key fingerprint:') + log(colors.bgWhite(' ') + ' ' + colors.black.bgWhite(' SHA256 ') + colors.bgBlackBright(' ' + digest + ' ')) return true }, hostHash: 'sha256' as any, diff --git a/terminus-ssh/yarn.lock b/terminus-ssh/yarn.lock index 2eea813d..8b6d2d4d 100644 --- a/terminus-ssh/yarn.lock +++ b/terminus-ssh/yarn.lock @@ -27,6 +27,11 @@ "@types/node" "*" "@types/ssh2-streams" "*" +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + asn1@~0.2.0, asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" @@ -46,6 +51,11 @@ bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2: dependencies: tweetnacl "^0.14.3" +cli-spinner@^0.2.10: + version "0.2.10" + resolved "https://registry.yarnpkg.com/cli-spinner/-/cli-spinner-0.2.10.tgz#f7d617a36f5c47a7bc6353c697fc9338ff782a47" + integrity sha512-U0sSQ+JJvSLi1pAYuJykwiA8Dsr15uHEy85iCJ6A+0DjVxivr3d+N2Wjvodeg89uP5K6TswFkKBfAD7B3YSn/Q== + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" diff --git a/terminus-terminal/src/api/baseTerminalTab.component.ts b/terminus-terminal/src/api/baseTerminalTab.component.ts index 9046d134..b7c8cb55 100644 --- a/terminus-terminal/src/api/baseTerminalTab.component.ts +++ b/terminus-terminal/src/api/baseTerminalTab.component.ts @@ -174,6 +174,14 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit this.size = { columns, rows } this.frontendReady.next() + this.config.enabledServices(this.decorators).forEach(decorator => { + try { + decorator.attach(this) + } catch (e) { + this.logger.warn('Decorator attach() throws', e) + } + }) + setTimeout(() => { this.session.resize(columns, rows) }, 1000) @@ -204,14 +212,6 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit this.configure() - this.config.enabledServices(this.decorators).forEach((decorator) => { - try { - decorator.attach(this) - } catch (e) { - this.logger.warn('Decorator attach() throws', e) - } - }) - setTimeout(() => { this.output.subscribe(() => { this.displayActivity()