control-freak-ide/server/nodejs/nxapp/protocols/SSH.js
plastic-hub-dev-node-saturn 538369cff7 latest
2021-05-12 18:35:18 +02:00

484 lines
19 KiB
JavaScript

//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;
});