//noinspection JSAnnotator /** @module nxapp/protocols/SSH */ define([ 'dcl/dcl', "xide/utils", "nxapp/types/Types", 'nxapp/protocols/ProtocolBase', "dojo/node!net", "dojo/node!fs", "dojo/node!path", "dojo/node!ssh2", "dojo/node!strip-ansi", "dojo/node!child_process", "dojo/node!util", "dojo/node!deferred", "dojo/node!ansi-to-html", "dojo/node!ansi_up", "nxapp/utils/_console", 'xide/encoding/MD5', "dojo/Deferred" ],function(dcl,utils,types,ProtocolBase,net,fs,path,ssh2,stripAnsi,child,nUtil,deferred,AnsiToHtml,ansi_up,_console,MD5,Deferred){ var debug = false; var debugData = false; var debugRun = false; var console = _console; /** * SSH protocol client * @class module:nxapp/protocols/SSH * @extends module:nxapp/protocols/ProtocolBase */ var Module = dcl(ProtocolBase,{ declaredClass:"nxapp.protocols.SSH", _socket:null, protocolName:'ssh', instance:null, sshOptions:null, running:null, /** * Creates a proper SSH2 option object * @param host * @param port * @param deviceOptions * @returns {Object} */ getOptions:function(host,port,deviceOptions){ var privateKey = null; if(deviceOptions.privateKeyPath){ var _path = path.resolve(deviceOptions.privateKeyPath); if(_path){ privateKey = fs.readFileSync(_path).toString(); }else{ debug || this.isDebug() && console.warn('Sorry, cant resolve file '+deviceOptions.privateKeyPath); } } var options = { config: false, host: host, username: deviceOptions.username, password: deviceOptions.password, agent: "", agentForward: false, port: port, proxy: { port: port }, ignoreErrors: false, minimatch: {}, debug:debug || this.isDebug() ? function(str){ console.log("SSH _ DEBUG : "+str); } : null, suppressRemoteErrors: false, callback: function() {} }; if(privateKey){ options['privateKey'] = privateKey; } return utils.mixin(options,deviceOptions); }, onConnected:function(){ debug || this.isDebug() && console.log('# ' +this.protocolName + ' - Protocol:: onConnected ' + this.options.host + ' : ' + this.options.port + ' @ ' + this.protocolName); this.connection.connected=true; this.delegate.onConnect2(this.connection); }, onError:function(error,options) { this.delegate.onError(this.connection,utils.mixin({ code:error },options),this.options); }, onClose:function() { this.delegate.onClose(this.connection,this.options); }, connect:function(){ var _options = this.options; var self = this; if(!_options || !_options.driver){ debug || self.isDebug() && console.error('no driver in options',_options); return this; } var host = _options.host; var port = _options.port; var deviceOptions = utils.getJson(_options.options); if(deviceOptions.debug===true){ debug = true; } var options = this.getOptions(host,port,deviceOptions); var connectionOptions = parseConnectionOptions(options); this.sshOptions = options; this.host = host; this.port = port; this.protocol = this.protocolName; function parseConnectionOptions(options) { var connectionOptions = { host: options.host, port: options.port, username: options.username, readyTimeout: options.readyTimeout, agentForward: options.agentForward }; if (options.privateKey) { connectionOptions.privateKey = options.privateKey.trim(); if (options.passphrase) { connectionOptions.passphrase = options.passphrase.trim(); } } else if (options.password) { connectionOptions.password = options.password; if (options.agent) { connectionOptions.agent = options.agent; } } else { connectionOptions.agent = options.agent; } return connectionOptions; } debug || self.isDebug() && console.log('SSH->connecting SSH with'); var c = new ssh2(); c.on('keyboard-interactive', function(){ var prompts = arguments[3]; var reply = arguments[4]; prompts.forEach(function(question){ var msg = question.prompt.toLowerCase(); if (msg.indexOf('password') !== -1){ reply([options.password]); } }); }); c.on('connect', function () { debug || self.isDebug() && console.log('SSH->Connection :: connect'); }); c.on('ready', function () { debug || self.isDebug() && console.log('SSH->Connection :: ready'); var rows, cols, term; c.once('session', function (accept, reject) { accept().once('pty', function (accept, reject, info) { rows = info.rows; cols = info.cols; term = info.term; accept && accept(); debug && console.log('SSH-> pty '+rows + ' ' + cols + ' ' + term); }).once('shell', function(accept, reject) { debug || self.isDebug() && console.log('SSH->shell'); }); }); //console.log('sock : ' , c._sock); self.onConnected(); }); c.on('error', function (err) { debug || self.isDebug() && console.warn('SSH->Connection - Error :: ' + err); var errorString = err.level +":" + err.message; self.onError(errorString); self.lastError = errorString; }); c.on('debug', function (message) { debug || self.isDebug() && console.log('SSH->Connection :: debug :: ' + message); }); c.on('end', function () { debug || self.isDebug() && console.log('SSH->Connection :: end'); self.onClose(); }); c.on('close', function (had_error) { if(had_error){ debug || self.isDebug() && console.log('SSH - Connection :: Close, had Error '); }else { self.onClose(); } }); try { c.connect(connectionOptions); }catch(err){ debug || self.isDebug() && console.warn('SSH->Connection - Error :: ' + err); var errorString = 'fatal' +":" + err.message; self.onError(errorString); return; } this._socket = c; this._socket.writable=true; return this; }, onData:function(evt,buffer){ debugData || this.isDebug() && console.log('SSH->onData',evt); evt = evt || "" this.delegate.onData(this.connection,evt,buffer); }, onProgress:function(cmd,options,buffer){ this.isDebug() && console.info('SSH->onProgress ' + cmd + ' id ' + options.params.id + ' src ' + options.params.src); try { this.delegate.onData(this.connection, utils.mixin({ cmd: cmd, event: types.EVENTS.ON_COMMAND_PROGRESS, result:buffer.toString() },options),buffer); }catch(e){ console.error('onFinish-Progress:',e); } }, onCommandError:function(cmd,options,buffer){ debug || this.isDebug() && console.log('SSH->onCommandError ' + cmd + ' id ' + options.params.id + ' src ' + options.params.src); try { this.delegate.onData(this.connection, utils.mixin({ cmd: cmd, event: types.EVENTS.ON_COMMAND_ERROR },options),buffer); }catch(e){ console.error('---',e); } }, onFinish:function(cmd,options,buffer){ debug || this.isDebug() && console.info('SSH->onFinish ' + cmd + ' id ' + options.params.id + ' src ' + options.params.src); try { this.delegate.onData(this.connection, utils.mixin({ cmd: cmd, event: types.EVENTS.ON_COMMAND_FINISH },options),buffer); }catch(e){ console.error('onFinish-Error:',e); } }, _addCommand:function(commands){ var hash = MD5(JSON.stringify(commands.join(',')), 1); //this.running[hash] }, execCommand:function(commands,sendOptions){ var c = this._socket; var self = this, options = self.sshOptions, dfd = deferred(), params = sendOptions.params, id = params.id, wait = params.wait, ansiOptions = options.ansiOptions || {}, convert = new AnsiToHtml(ansiOptions), waitForEOF = options.waitForEOF == true, allData = ''; if (commands.length === 0) { debugData || self.isDebug() && console.log('SSH->no more SSH commands, call Defered::resolve'); } else { var command = commands.shift(); debugData || self.isDebug() && console.log('SSH->Executing :: ' + command,sendOptions.params); c.exec(command, options, function (err, stream) { if (err) { debug || self.isDebug() && console.log('---error in SSH ',err); return; } var out; stream.on('data', function (data, extended) { out = String(data); if(options.ansiUp!==true && options.html == true ) { out = convert.toHtml(out); }else if(options.ansiUp==true && options.html == true){ out = ansi_up.ansi_to_html(out); } debugData || self.isDebug() && console.log('SSH DATA : ' + 'signal = ' +extended + " : " + out); if (extended === 'stderr') { if (!options.suppressRemoteErrors) { //debug || self.isDebug() && console.warn('SSH->stdErr' + out); } else { //debugData || self.isDebug() && console.warn('SSH->data - 1' + out); } } else { //debugData || self.isDebug() && console.log('SSH->data - 2 ' + out); if(waitForEOF){ allData +=out; stream.lastData = allData; }else{ if(wait) { self.onProgress(command,sendOptions,data); }else{ self.onData(out, data); } stream.lastData = out; } } }); stream.on('end', function (args) { debug || self.isDebug() && console.log('SSH->Stream :: EOF, had Error ' + (stream.hadError==true)); //console.log('SSH->Stream :: EOF, had Error ' + (stream.hadError==true),args); if (out && typeof options.callback === "function") { options.callback(out.trim()); } if(stream.hadError!==true) { if(waitForEOF){ debug || self.isDebug() && console.error('--------------release data on EOF'); self.onData(stream.lastData); } dfd.resolve(id); }else{ dfd.reject({ id:id, error:stream.lastData }); } }); stream.on('exit', function () { debug || self.isDebug() && console.log('SSH->Stream :: exit'); }); stream.on('close', function (code, signal) { debug || self.isDebug() && console.log('SSH->Stream :: close :: code: ' + code + ', signal: ' + signal); if (code == 127) { stream.hadError=true; dfd.reject({ id:id, error:stream.lastData }); } if (!options.ignoreErrors && code !== 0) { debug || self.isDebug() && console.error('SSH->Error executing task ' + command + ' _ code ' +code); self.onCommandError(command,sendOptions); } else { self.execCommand(commands,sendOptions); } }); }); } return dfd.promise; }, send:function(cmd,options) { debug || this.isDebug() && console.log('send : ' + cmd,options.params); if(cmd==null){ console.error('SSH: invalid command'); return; } options = options || { id:utils.createUUID() }; cmd = this.toString(cmd); debug || this.isDebug() && console.log('SSH->send : ' + cmd); var dfd = this.execCommand([cmd],options), self = this; dfd.then(function(id){ self.onFinish(cmd,options); },function(error){ options.error=error.error; self.onCommandError(cmd,options); }); return dfd; }, close:function() { if(this._socket){ this._socket.end(); } }, init:function(){ this.running = {}; } }); Module.options = function (query) { try { var dfd = new Deferred(); var ECIType = types.ECIType; var NetworkGroup = 'Network'; function createOption(label, value) { return { label: label, value: label || value } } var cis = [ utils.createCI('username', ECIType.STRING,'', { group: NetworkGroup, title:'User name' }), utils.createCI('password', ECIType.STRING,'', { group: NetworkGroup, title:'Password', password:true }), utils.createCI('localhostName', ECIType.STRING,'', { group: NetworkGroup, title:'Local host name', description:'Along with localUsername and privateKey, set this to a non-empty string for hostbased user authentication' }), utils.createCI('tryKeyboard', ECIType.BOOL,false, { group: NetworkGroup, title:"Try keyboard", description:"ry keyboard-interactive user authentication if primary user authentication method fails. If you set this to true" }), utils.createCI('keepaliveInterval', ECIType.INTEGER,0, { group: NetworkGroup, title:" Keep alive interval", description:"How often (in milliseconds) to send SSH-level keepalive packets to the server (in a similar way as OpenSSH's ServerAliveInterval config option). Set to 0 to disable." }), utils.createCI('keepaliveCountMax', ECIType.INTEGER,3, { group: NetworkGroup, title:"Keep alive count maximum", description:" How many consecutive, unanswered SSH-level keepalive packets that can be sent to the server before disconnection (similar to OpenSSH's ServerAliveCountMax config option)." }), utils.createCI('readyTimeout', ECIType.INTEGER,20000, { group: NetworkGroup, title:"Ready timeout", description:"How long (in milliseconds) to wait for the SSH handshake to complete." }), utils.createCI('strictVendor', ECIType.BOOL,true, { group: NetworkGroup, title:"Strict vendor", description:"Performs a strict server vendor check before sending vendor-specific requests, etc. (e.g. check for OpenSSH server when using openssh_noMoreSessions()" }) ] dfd.resolve(cis); return dfd; } catch (e) { console.error('error', e); } return dfd; } return Module; });