484 lines
19 KiB
JavaScript
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;
|
|
|
|
}); |