Browse Source

ssh - show connection log while connecting

Eugene Pankov 7 years ago
parent
commit
d03430fb2e

+ 12 - 5
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<any[]> {

+ 1 - 1
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(() => {

+ 44 - 2
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()
+    }
 }

+ 70 - 28
terminus-ssh/src/services/ssh.service.ts

@@ -33,14 +33,31 @@ export class SSHService {
         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 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<boolean>(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<any>((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
         }
     }

+ 20 - 6
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)
+        })
+    }
 }

+ 3 - 12
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<any> {

+ 7 - 4
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)