import { EventEmitter } from 'events' ; import * as net from 'net' ; import * as fs from 'fs' ; import SSHConstants from './sshConstants' ; import SSHUtils , { Deferred } from './sshUtils' ; import SSH2Promise from './index' ; import TunnelConfig from './tunnelConfig' ; import { Client , ClientChannel , ClientErrorExtensions , ExecOptions , SFTPWrapper , ShellOptions , } from 'ssh2' ; import { Server } from "net" ; import SSHConfig from './sshConfig' ; const socks = require ( '@heroku/socksv5' ) ; const defaultOptions = { reconnect : true , port : 22 , reconnectTries : 10 , reconnectDelay : 5000 } ; export default class SSHConnection extends EventEmitter { __sshTunnels : Array < SSH2Promise > ; config : SSHConfig ; private activeTunnels : { [ index ] : TunnelConfig & { server : Server ; } ; } = { } ; private __$connectPromise : Promise < SSHConnection > = null ; private __retries : number = 0 ; private __err : Error & ClientErrorExtensions & { code ? : string ; } = null ; private __sftp : Promise < SFTPWrapper > = null ; private __x11 : Deferred < void > = null ; private sshConnection : Client = null ; constructor ( options ) { super ( ) ; this . config = Object . assign ( { } , defaultOptions , options ) ; this . config . uniqueId = this . config . uniqueId || `${ this . config . username }@${ this . config . host }` ; } param { * } channel param { * } status param { * } payload emit ( channel , status , payload ? ) { super . emit ( channel , status , this , payload ) ; return super . emit ( `${ channel }:${ status }` , this , payload ) ; } /**
     * Get shell socket
     */ shell ( options = { } ) { return this . connect ( ) . then ( ( ) => { return new Promise < ClientChannel > ( ( resolve , reject ) => { this . sshConnection . shell ( options , ( err , stream ) => err ? reject ( err ) : resolve ( stream ) ) ; } ) ; } ) ; } /**
     * Get a sftp session
     */ sftp ( createNew ) { return this . connect ( ) . then ( ( ) => { if ( createNew || ! this . __sftp ) { this . __sftp = new Promise ( ( resolve , reject ) => { this . sshConnection . sftp ( ( err , sftp ) => err ? reject ( err ) : resolve ( sftp ) ) ; } ) ; } return this . __sftp ; } ) ; } /**
     * Get a subsys
     */ subsys ( cmd ) { return this . connect ( ) . then ( ( ) => { return new Promise ( ( resolve , reject ) => { this . sshConnection . subsys ( cmd , ( err , stream ) => err ? reject ( err ) : resolve ( stream ) ) ; } ) ; } ) ; } /**
     * Spawn a command
     */ spawn ( cmd , params ? , options = { } ) { cmd += ( Array . isArray ( params ) ? ( " " + params . join ( " " ) ) : "" ) ; return this . connect ( ) . then ( ( ) => { return new Promise ( ( resolve , reject ) => { this . sshConnection . exec ( cmd , options , ( err , stream ) => { if ( err ) return reject ( err ) ; stream . on ( 'close' , function ( ) { } ) . on ( 'finish' , function ( ) { } ) ; resolve ( stream ) ; } ) ; } ) ; } ) ; } /**
     * Exec a command
     */ exec ( cmd , params ? , options = { } ) { cmd += ( Array . isArray ( params ) ? ( " " + params . join ( " " ) ) : "" ) ; return this . connect ( ) . then ( ( ) => { return new Promise ( ( resolve , reject ) => { this . sshConnection . exec ( cmd , options , ( err , stream ) => { if ( err ) return reject ( err ) ; var buffer = "" ; stream . on ( 'close' , function ( ) { resolve ( buffer ) ; } ) . on ( 'data' , function ( data ) { buffer += data ; } ) . stderr . on ( 'data' , function ( data ) { reject ( data ) ; } ) ; } ) ; } ) ; } ) ; } /**
     * Forward out
     */ forwardOut ( srcIP , srcPort , destIP , destPort ) { return this . connect ( ) . then ( ( ) => { return new Promise ( ( resolve , reject ) => { this . sshConnection . forwardOut ( srcIP , srcPort , destIP , destPort , ( err , stream ) => { if ( err ) return reject ( err ) ; resolve ( stream ) ; } ) ; } ) ; } ) ; } /**
     * Get a Socks Port
     */ getSocksPort ( localPort ) { return this . addTunnel ( { name : "__socksServer" , socks : true , localPort : localPort } ) . then ( ( tunnel ) => { return tunnel . localPort ; } ) ; } /**
     * Get a X11 port
     */ x11 ( cmd ) { return this . spawn ( cmd , null , { x11 : true } ) . then ( ( stream ) => { this . __x11 = SSHUtils . createDeferredPromise ( ) ; var code = 0 ; stream . on ( 'finish' , ( err ) => { if ( code !== 0 ) { this . __x11 . reject ( "X11 forwading not enabled on server" ) ; this . emit ( SSHConstants . CHANNEL . X11 , SSHConstants . STATUS . DISCONNECT , { err : err , msg : "X11 forwading not enabled on server" } ) ; } } ) . on ( 'exit' , ( exitcode ) => { code = exitcode ; } ) ; this . __x11 . promise . catch ( ( ) => { stream . close ( ) ; } ) ; return this . __x11 . promise ; } ) ; } /**
     * Close SSH Connection
     */ close ( ) { this . emit ( SSHConstants . CHANNEL . SSH , SSHConstants . STATUS . BEFOREDISCONNECT ) ; return this . closeTunnel ( ) . then ( ( ) => { if ( this . sshConnection ) { this . sshConnection . end ( ) ; this . emit ( SSHConstants . CHANNEL . SSH , SSHConstants . STATUS . DISCONNECT ) ; } } ) ; } /**
     * Connect the SSH Connection
     */ connect ( c ? ) { this . config = Object . assign ( this . config , c ) ; ++ this . __retries ; if ( this . __$connectPromise != null ) return this . __$connectPromise ; this . __$connectPromise = new Promise ( ( resolve , reject ) => { this . emit ( SSHConstants . CHANNEL . SSH , SSHConstants . STATUS . BEFORECONNECT ) ; if ( ! this . config || typeof this . config === 'function' || ! ( this . config . host || this . config . sock ) || ! this . config . username ) { reject ( "Invalid SSH connection configuration host/username can't be empty" ) ; this . __$connectPromise = null ; return ; } if ( this . config . tryKeyboard && ! this . config . password && typeof this . config !== 'undefined' ) { delete this . config . password ; } if ( this . config . identity ) { if ( fs . existsSync ( this . config . identity ) ) { this . config . privateKey = fs . readFileSync ( this . config . identity ) ; } delete this . config . identity ; } this . sshConnection = new Client ( ) ; this . sshConnection . on ( 'keyboard-interactive' , ( name , instructions , lang , prompts , finish ) => { prompts . forEach ( ( prompt ) => { SSHUtils . prompt ( prompt . prompt , ( password ) => { finish ( [ password ] ) ; } ) ; } ) ; } ) . on ( 'ready' , ( err ) => { if ( err ) { this . emit ( SSHConstants . CHANNEL . SSH , SSHConstants . STATUS . DISCONNECT , { err : err } ) ; this . __$connectPromise = null ; return reject ( err ) ; } this . emit ( SSHConstants . CHANNEL . SSH , SSHConstants . STATUS . CONNECT ) ; this . __retries = 0 ; this . __err = null ; resolve ( this ) ; } ) . on ( 'x11' , ( info , accept ) => { var xserversock = new net . Socket ( ) ; xserversock . on ( 'connect' , ( ) => { var xclientsock = accept ( ) ; xclientsock . pipe ( xserversock ) . pipe ( xclientsock ) ; this . __x11 . resolve ( ) ; this . emit ( SSHConstants . CHANNEL . X11 , SSHConstants . STATUS . CONNECT ) ; } ) . on ( 'error' , ( err ) => { this . __x11 . reject ( "X Server not running locally." ) ; this . emit ( SSHConstants . CHANNEL . X11 , SSHConstants . STATUS . DISCONNECT , { err : err , msg : "X Server not running locally." } ) ; } ) ; if ( this . config . x11 ) { if ( typeof this . config . x11 === 'string' ) { xserversock . connect ( this . config . x11 ) ; } else { xserversock . connect ( this . config . x11 . srcPort , this . config . x11 . srcIP ) ; } } else { xserversock . connect ( 6000 , 'localhost' ) ; } } ) . on ( 'error' , ( err ) => { this . emit ( SSHConstants . CHANNEL . SSH , SSHConstants . STATUS . DISCONNECT , { err : err } ) ; this . __err = err ; } ) . on ( 'close' , ( ) => { this . emit ( SSHConstants . CHANNEL . SSH , SSHConstants . STATUS . DISCONNECT , { err : this . __err } ) ; if ( this . config . reconnect && this . __retries <= this . config . reconnectTries && this . __err != null && this . __err . level != "client-authentication" && this . __err . code != 'ENOTFOUND' ) { setTimeout ( ( ) => { this . __$connectPromise = null ; resolve ( this . connect ( ) ) ; } , this . config . reconnectDelay ) ; } else { reject ( this . __err ) ; } } ) . connect ( this . config ) ; } ) ; return this . __$connectPromise ; } /**
     * Get existing tunnel by name
     */ getTunnel ( name ) { return this . activeTunnels [ name ] ; } /**
     * Add new tunnel if not exist
     */ addTunnel ( tunnelConfig ) { tunnelConfig . name = tunnelConfig . name || `${ tunnelConfig . remoteAddr }@${ tunnelConfig . remotePort }` ; this . emit ( SSHConstants . CHANNEL . TUNNEL , SSHConstants . STATUS . BEFORECONNECT , { tunnelConfig : tunnelConfig } ) ; if ( this . getTunnel ( tunnelConfig . name ) ) { this . emit ( SSHConstants . CHANNEL . TUNNEL , SSHConstants . STATUS . CONNECT , { tunnelConfig : tunnelConfig } ) ; return Promise . resolve ( this . getTunnel ( tunnelConfig . name ) ) ; } else { return new Promise ( ( resolve , reject ) => { var server ; if ( tunnelConfig . socks ) { server = socks . createServer ( ( info , accept , deny ) => { this . connect ( ) . then ( ( ) => { this . sshConnection . forwardOut ( info . srcAddr , info . srcPort , info . dstAddr , info . dstPort , ( err , stream ) => { if ( err ) { this . emit ( SSHConstants . CHANNEL . TUNNEL , SSHConstants . STATUS . DISCONNECT , { tunnelConfig : tunnelConfig , err : err } ) ; return deny ( ) ; } const clientSocket = accept ( true ) ; if ( clientSocket ) { stream . pipe ( clientSocket ) . pipe ( stream ) . on ( 'close' , ( ) => { stream . end ( ) ; } ) ; } else if ( stream ) { stream . end ( ) ; } } ) ; } ) ; } ) . useAuth ( socks . auth . None ( ) ) ; } else { server = net . createServer ( ) . on ( 'connection' , ( socket ) => { this . connect ( ) . then ( ( ) => { this . sshConnection . forwardOut ( '' , 0 , tunnelConfig . remoteAddr , tunnelConfig . remotePort , ( err , stream ) => { if ( err ) { this . emit ( SSHConstants . CHANNEL . TUNNEL , SSHConstants . STATUS . DISCONNECT , { tunnelConfig : tunnelConfig , err : err } ) ; return ; } stream . pipe ( socket ) . pipe ( stream ) . on ( 'close' , ( ) => { stream . end ( ) ; } ) ; } ) ; } ) ; } ) ; } tunnelConfig . localPort = tunnelConfig . localPort || 0 ; server . on ( 'listening' , ( ) => { tunnelConfig . localPort = server . address ( ) . port ; this . activeTunnels [ tunnelConfig . name ] = Object . assign ( { } , { server : server } , tunnelConfig ) ; this . emit ( SSHConstants . CHANNEL . TUNNEL , SSHConstants . STATUS . CONNECT , { tunnelConfig : tunnelConfig } ) ; resolve ( this . activeTunnels [ tunnelConfig . name ] ) ; } ) . on ( 'error' , ( err ) => { this . emit ( SSHConstants . CHANNEL . TUNNEL , SSHConstants . STATUS . DISCONNECT , { tunnelConfig : tunnelConfig , err : err } ) ; server . close ( ) ; reject ( err ) ; delete this . activeTunnels [ tunnelConfig . name ] ; } ) . listen ( tunnelConfig . localPort ) ; } ) ; } } /**
     * Close the tunnel
     */ closeTunnel ( name ? ) { if ( name && this . activeTunnels [ name ] ) { return new Promise ( ( resolve ) => { var tunnel = this . activeTunnels [ name ] ; this . emit ( SSHConstants . CHANNEL . TUNNEL , SSHConstants . STATUS . BEFOREDISCONNECT , { tunnelConfig : tunnel } ) ; tunnel . server . close ( ( ) => { this . emit ( SSHConstants . CHANNEL . TUNNEL , SSHConstants . STATUS . DISCONNECT , { tunnelConfig : this . activeTunnels [ name ] } ) ; delete this . activeTunnels [ name ] ; resolve ( ) ; } ) ; } ) ; } else if ( ! name ) { var tunnels = Object . keys ( this . activeTunnels ) . map ( ( key ) => this . closeTunnel ( key ) ) ; return Promise . all ( tunnels ) . then ( ( ) => { } ) ; } } }