Browse Source

ssh jump hosts - fixes #737

Eugene Pankov 5 years ago
parent
commit
7f55d6f1e2

+ 2 - 0
terminus-ssh/src/api.ts

@@ -35,6 +35,7 @@ export interface SSHConnection {
     x11?: boolean
     skipBanner?: boolean
     disableDynamicTitle?: boolean
+    jumpHost?: string
 
     algorithms?: {[t: string]: string[]}
 }
@@ -80,6 +81,7 @@ export class SSHSession extends BaseSession {
     ssh: Client
     forwardedPorts: ForwardedPort[] = []
     logger: Logger
+    jumpStream: any
 
     get serviceMessage$ (): Observable<string> { return this.serviceMessage }
     private serviceMessage = new Subject<string>()

+ 6 - 0
terminus-ssh/src/components/editConnectionModal.component.pug

@@ -70,6 +70,12 @@
         ngb-tab(id='advanced')
             ng-template(ngbTabTitle) Advanced
             ng-template(ngbTabContent)
+                .form-line
+                    .header
+                        .title Jump host
+                    select.form-control([(ngModel)]='connection.jumpHost')
+                        option([value]='x.name', *ngFor='let x of config.store.ssh.connections') {{x.name}}
+
                 .form-line
                     .header
                         .title X11 forwarding

+ 2 - 1
terminus-ssh/src/components/editConnectionModal.component.ts

@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
 import { Component } from '@angular/core'
 import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
-import { ElectronService, HostAppService } from 'terminus-core'
+import { ElectronService, HostAppService, ConfigService } from 'terminus-core'
 import { PasswordStorageService } from '../services/passwordStorage.service'
 import { SSHConnection, LoginScript, SSHAlgorithmType } from '../api'
 import { PromptModalComponent } from './promptModal.component'
@@ -20,6 +20,7 @@ export class EditConnectionModalComponent {
     algorithms: {[id: string]: {[a: string]: boolean}} = {}
 
     constructor (
+        public config: ConfigService,
         private modalInstance: NgbActiveModal,
         private electron: ElectronService,
         private hostApp: HostAppService,

+ 47 - 10
terminus-ssh/src/components/sshTab.component.ts

@@ -10,6 +10,7 @@ import { SSHConnection, SSHSession } from '../api'
 import { SSHPortForwardingModalComponent } from './sshPortForwardingModal.component'
 import { Subscription } from 'rxjs'
 
+
 /** @hidden */
 @Component({
     selector: 'ssh-tab',
@@ -20,6 +21,7 @@ import { Subscription } from 'rxjs'
 export class SSHTabComponent extends BaseTerminalTabComponent {
     connection: SSHConnection
     session: SSHSession
+    private sessionStack: SSHSession[] = []
     private homeEndSubscription: Subscription
 
     constructor (
@@ -60,19 +62,40 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
         })
     }
 
-    async initializeSession (): Promise<void> {
-        if (!this.connection) {
-            this.logger.error('No SSH connection info supplied')
-            return
+    async setupOneSession (session: SSHSession): Promise<void> {
+        if (session.connection.jumpHost) {
+            const jumpConnection = this.config.store.ssh.connections.find(x => x.name === session.connection.jumpHost)
+            const jumpSession = this.ssh.createSession(jumpConnection)
+
+            await this.setupOneSession(jumpSession)
+
+            jumpSession.destroyed$.subscribe(() => session.destroy())
+
+            session.jumpStream = await new Promise((resolve, reject) => jumpSession.ssh.forwardOut(
+                '127.0.0.1', 0, session.connection.host, session.connection.port,
+                (err, stream) => {
+                    if (err) {
+                        jumpSession.emitServiceMessage(colors.bgRed.black(' X ') + ` Could not set up port forward on ${jumpConnection.name}`)
+                        return reject(err)
+                    }
+                    resolve(stream)
+                }
+            ))
+
+            session.jumpStream.on('close', () => {
+                jumpSession.destroy()
+            })
+
+            this.sessionStack.push(session)
         }
 
-        this.session = this.ssh.createSession(this.connection)
-        this.session.serviceMessage$.subscribe(msg => {
+
+        session.serviceMessage$.subscribe(msg => {
             this.write('\r\n' + colors.black.bgWhite(' SSH ') + ' ' + msg + '\r\n')
-            this.session.resize(this.size.columns, this.size.rows)
+            session.resize(this.size.columns, this.size.rows)
         })
-        this.attachSessionHandlers()
-        this.write(`Connecting to ${this.connection.host}`)
+
+        this.write('\r\n' + colors.black.bgCyan(' SSH ') + ` Connecting to ${session.connection.host}\r\n`)
 
         const spinner = new Spinner({
             text: 'Connecting',
@@ -84,7 +107,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
         spinner.start()
 
         try {
-            await this.ssh.connectSession(this.session, (message: string) => {
+            await this.ssh.connectSession(session, (message: string) => {
                 spinner.stop(true)
                 this.write(message + '\r\n')
                 spinner.start()
@@ -95,6 +118,20 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
             this.write(colors.black.bgRed(' X ') + ' ' + colors.red(e.message) + '\r\n')
             return
         }
+    }
+
+    async initializeSession (): Promise<void> {
+        if (!this.connection) {
+            this.logger.error('No SSH connection info supplied')
+            return
+        }
+
+        this.session = this.ssh.createSession(this.connection)
+
+        await this.setupOneSession(this.session)
+
+        this.attachSessionHandlers()
+
         await this.session.start()
         this.session.resize(this.size.columns, this.size.rows)
     }

+ 7 - 0
terminus-ssh/src/services/ssh.service.ts

@@ -151,6 +151,12 @@ export class SSHService {
                     }
                 })
             })
+            ssh.on('close', () => {
+                if (session.open) {
+                    session.destroy()
+                }
+            })
+
             ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => this.zone.run(async () => {
                 log(colors.bgBlackBright(' ') + ` Keyboard-interactive auth requested: ${name}`)
                 this.logger.info('Keyboard-interactive auth:', name, instructions, instructionsLang)
@@ -211,6 +217,7 @@ export class SSHService {
                     },
                     hostHash: 'sha256' as any,
                     algorithms: session.connection.algorithms,
+                    sock: session.jumpStream,
                 })
             } catch (e) {
                 this.toastr.error(e.message)