persistent-shell
Wrapper class for ssh2 client.shell command.
The package allows for user/program/interface to run commands without disconnecting each time.
Installation:
npm install persistent-shell
API
Properties:
this.host
Is the host object passed to the constructor.
this.commands
(Optional) is array of commands.
Commands are set using host.commands = [commands]
or this.runCommand([commands])
.
Functions:
Instance = new persistent-shell(host)
requires the host object defined above.
this.connect(callback)
Connects using host.server properties running the callback when finished. Callback is optional.
this.runCommand(command/s)
takes a command or an array of commands. Runs command or first command in array.
callback = function(sessionText){}
Runs after everything has closed allowing you to process the full session.
Event Handlers: (All optional)
this.on("pipe",function(writeStream){})
Allows you to bind a write stream to the shell stream.
this.on("unpipe", function(writeStream){})
Runs when a pipe is removed.
this.on("data", function(data){})
Runs every time data is received from the host.
this.on("commandProcessing", function(response){})
Runs with each data event before a prompt is detected.
this.on("commandComplete", function(response){})
Runs when a prompt is detected after a command.
this.on("end", function (sessionText){})
Runs when the stream/connection is being closed.
this.on("msg", function(message){})
Output a message but with no carrage return.
this.on("info", function(message){})
Emits msg with carrage return. Host.onInfo
is not available modify msg instead.
this.on("error", function(err, type, close = false, callback){})
Runs when an error occures.
this.on("keyboard-interactive", function(name, instructions, instructionsLang, prompts, finish){})
Keyboard-interactive authentication requires host.server.tryKeyboard to be set.
Host Configuration:
Persistent-shell expects an object with the following structure to be passed to its constructor:
Note: Any property or event handler with a default value does not need to be added
to your host object unless you want to change it.
host = {
server: {
host: "IP Address",
port: "external port number",
userName: "user name",
password: "user password",
passPhrase: "privateKeyPassphrase",
privateKey: require('fs').readFileSync('/path/to/private/key/id_rsa')
},
commands: [],
standardPrompt: ">$%#",
passwordPrompt: ":",
passphrasePrompt: ":",
showBanner: false,
window: false,
enter: "\n",
streamEncoding: "utf8",
asciiFilter: "[^\r\n\x20-\x7e]",
disableColorFilter: false,
textColorFilter: "(\[{1}[0-9;]+m{1})",
msg: function( message ) { process.stdout.write(message)},
verbose: false,
debug: false,
connectedMessage: "Connected",
readyMessage: "Ready",
closedMessage: "Closed",
callback: function( sessionText ){},
onFirstPrompt: function() {},
onData: function( data ) {},
onPipe: function( writable ){},
onUnpipe: function( writable ) {},
onCommandProcessing: function( response ) {},
onCommandComplete: function( response ) {},
onEnd: function( sessionText ) {},
onError: function( err, type, close = false, callback ) {},
onKeyboardInteractive: function(name, instructions, instructionsLang, prompts, finish){}
};
- Host.server will accept current SSH2.client.connect parameters.
- Optional host properties or event handlers do not need to be included if you are not changing them.
- Host event handlers completely replace the default event handler definitions in the class when defined.
- The
this
keyword is available within host event handlers to give access to persistent-shell functions and properties. this.host
or host variable passed into a function provides access to all the host config, some instance
variables.
Example:
Persistent shell for manually entering cammands from a terminal session.
var persistentShell = require('persistent-shell'),
host = {
server: {
host: "192.168.0.1",
port: "22",
userName: "user",
password: "password"
},
stdin: process.openStdin()
};
var session = new persistentShell(host),
callback = function( sessionText ){
this.emit("info", "-----Session text:\n\n" + sessionText + "\n\n-----" );
session.host.stdin.addListener("data", function(input){
var command = input.toString().trim();
if (command == "exit"){
session.host.stdin.end();
session.exit();
}else {
session.runCommand(command);
}
});
session.connect(callback);
Trouble shooting:
Error: Unable to parse private key while generating public key (expected sequence)
is caused by the pass phrase
being incorrect. This confused me because it doesn't indicate the pass phrase was the problem but it does indicate
that it could not decrypt the private key.- Recheck your pass phrase for typos or missing chars.
- Try connecting manually to the host using the exact pass phrase used by the code to confirm it works.
- I did read of people having problems with the pass phrase or password having an \n added when used from an
external file causing it to fail. They had to add .trim() when setting it.
- If your password is incorrect the connection will return an error.
- There is an optional debug setting in the host object that will output process information when set to true.
Verbose and Debug:
Authentication:
- When using key authentication you may require a valid pass phrase if your key was created with one.
- When using fingerprint validation both host.server.hashMethod property and host.server.hostVerifier function must be
set.
- When using keyboard-interactive authentication both
host.server.tryKeyboard
and instance.on ("keayboard-interactive", function...)
or host.onKeyboardInteractive()
must be defined. - Set the default cyphers and keys.
Default Cyphers:
Default Cyphers and Keys used in the initial ssh connection can be redefined by setting the ssh2.connect.algorithms through the host.server.algorithms option.
As with this property all ssh2.connect properties
are set in the host.server
object.
Example:
var host = {
server: {
host: "<host IP>",
port: "22",
userName: "<username>",
password: "<password>",
hashMethod: "md5",
algorithms: {
kex: [
'diffie-hellman-group1-sha1',
'ecdh-sha2-nistp256',
'ecdh-sha2-nistp384',
'ecdh-sha2-nistp521',
'diffie-hellman-group-exchange-sha256',
'diffie-hellman-group14-sha1'],
cipher: [
'aes128-ctr',
'aes192-ctr',
'aes256-ctr',
'aes128-gcm',
'aes128-gcm@openssh.com',
'aes256-gcm',
'aes256-gcm@openssh.com',
'aes256-cbc'
]
}
},
......
}
Fingerprint Validation:
At connection time the hash of the server’s public key can be compared with the hash the client had previously recorded
for that server. This stops "man in the middle" attacks where you are redirected to a different server as you connect
to the server you expected to. This hash only changes with a reinstall of SSH, a key change on the server or a load
balancer is now in place.
Note:
Fingerprint check doesn't work the same way for tunnelling. The first host will validate using this method but the
subsequent connections would have to be handled by your commands. Only the first host uses the SSH2 connection method
that does the validation.
To use fingerprint validation you first need the server hash string which can be obtained using persistent-shell as follows:
- Set host.verbose to true then set host.server.hashKey to any non-empty string (say "1234").
- Validation will be checked and fail causing the connection to terminate.
- A verbose message will return both the server hash and client hash values that failed comparison.
- This is also what will happen if your hash fails the comparison with the server in the normal verification process.
- Turn on verbose in the host object, run your script with hashKey unset and check the very start of the text returned
for the servers hash value.
- The servers hash value can be saved to a variable outside the host or class so you can access it without having to
parse response text.
Example:
var serverHash, host;
var expectedHash
expectedHash = "85:19:8a:fb:60:4b:94:13:5c:ea:fe:3b:99:c7:a5:4e";
host = {
server: {
hashMethod: "md5",
hostVerifier: function(hashedKey) {
var recievedHash,
expectedHash = expectedHash + "".replace(/[:]/g, "").toLowerCase(),
recievedHash = hashedKey + "".replace(/[:]/g, "").toLowerCase();
if (expectedHash === "") {
serverHash = hashedKey;
console.log("Server hash: " + serverHash);
return true;
} else if (recievedHash === expectedHash) {
console.log("Hash values matched");
return true;
}
console.log("Hash values: Server = " + recievedHash + " <> Client = " + expectedHash);
return false;
},
},
};
var persistent-shell = require ('persistent-shell'),
session = new persistent-shell(host);
session.connect();
Note:
host.server.hashMethod only supports md5 or sha1 according to the current SSH2 documentation anything else may produce
undesired results.
Keyboard-interactive
Keyboard-interactive authentication is available when both host.server.tryKeyboard is set to true and the event handler
keyboard-interactive is defined as below.
The keyboard-interactive event handler can only be used on the first connection.
Also see test/keyboard-interactivetest.js for the full example
Defining the event handler:
host.server.tryKeyboard = true;
var persistent-shell = require ('../lib/persistent-shell');
var session = new persistent-shell(host);
session.on ('keyboard-interactive', function(name, instructions, instructionsLang, prompts, finish){
if (this.host.debug) {this.emit('msg', this.host.server.host + ": Keyboard-interactive");}
if (this.host.verbose){
this.emit('msg', "name: " + name);
this.emit('msg', "instructions: " + instructions);
var str = JSON.stringify(prompts, null, 4);
this.emit('msg', "Prompts object: " + str);
}
finish([this.host.server.password] );
});
session.connect();
Or
host = {
...,
onKeyboardInteractive: function(name, instructions, instructionsLang, prompts, finish){
if (this.host.debug) {this.emit('msg', this.host.server.host + ": Keyboard-interactive");}
if (this.host.verbose){
this.emit('msg', "name: " + name);
this.emit('msg', "instructions: " + instructions);
var str = JSON.stringify(prompts, null, 4);
this.emit('msg', "Prompts object: " + str);
}
finish([this.host.server.password] );
},
...
}