├── .env-example ├── .gitignore ├── .npmignore ├── README.md ├── lib └── ssh2shell.js ├── package-lock.json ├── package.json ├── src └── ssh2shell.coffee └── test ├── .ignore ├── chaining.js ├── devtest.js ├── issue_69.js ├── keyboard-interactive.js ├── lots_of_commands.js ├── multiple_primary_hosts.js ├── notifications.js ├── pipe.js ├── simple.js ├── ssh_user_command.js ├── sudo_no_pass.js ├── sudo_su.js ├── timeout.js ├── tunnel.js └── tunnel_ssh_user_commands.js /.env-example: -------------------------------------------------------------------------------- 1 | HOST=10.0.0.1 2 | PORT=22 3 | USER_NAME=username 4 | PASSWORD=password 5 | SUDO_PASSWORD=secondpassword 6 | PRIV_KEY_PATH=~/.ssh/id_rsa 7 | PASS_PHRASE=passphrase 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pm-debug.log 2 | node_modules 3 | .env 4 | *.log 5 | /test/devtest.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | .env 4 | /test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ssh2shell 2 | ========= 3 | [![NPM](https://nodei.co/npm/ssh2shell.png?downloads=true&stars=true)](https://nodei.co/npm/ssh2shell/) 4 | 5 | Ssh2shell uses [ssh2](https://www.npmjs.org/package/ssh2) to open a ssh shell session to a host/s to run multiple commands and process the responses. 6 | 7 | Index: 8 | ------------ 9 | * [Installation](#installation-) 10 | * [Minimal Example](#minimal-example-) 11 | * [Host Configuration](#host-configuration-) 12 | * [ssh2shell API](#ssh2shell-api-) 13 | * [Usage](#usage-) 14 | * [Authentication](#authentication-) 15 | * [Default Algorithms](#default-algorithms-) 16 | * [Fingerprint Validation](#fingerprint-validation-) 17 | * [Keyboard-interactive](#keyboard-interactive-) 18 | * [Troubleshooting](#troubleshooting-) 19 | * [Verbose and Debug](#verbose-and-debug-) 20 | * [Notification commands](#notification-commands-) 21 | * [Sudo and su Commands](#sudo-and-su-commands-) 22 | * [Prompt detection override](#prompt-detection-override-) 23 | * [Text regular expression filters](#text-regular-expression-filters-) 24 | * [Responding to non-standard command prompts](#responding-to-non-standard-command-prompts-) 25 | * [Command Time-out event](#command-time-out-event-) 26 | * [Multiple Primary Hosts](#multiple-primary-hosts-) 27 | * [Tunnelling nested host objects](#tunnelling-nested-host-objects-) 28 | * [Event Handlers](#event-handlers-) 29 | * [Bash scripts on the fly](#bash-scripts-on-the-fly-) 30 | 31 | 32 | [Installation: ^](#) 33 | ------------ 34 | ``` 35 | npm install ssh2shell 36 | ``` 37 | 38 | 39 | [Minimal Example: ^](#) 40 | ------------ 41 | ```javascript 42 | //host configuration with connection settings and commands 43 | var host = { 44 | server: { 45 | host: "127.0.0.1", 46 | userName: "test", 47 | password: "password", 48 | }, 49 | commands: [ "echo $(pwd)", "ls -l" ] 50 | }; 51 | 52 | var SSH2Shell = require ('ssh2shell'), 53 | SSH = new SSH2Shell(host), 54 | callback = function(sessionText){ 55 | console.log(sessionText) 56 | } 57 | 58 | //Start the process 59 | SSH.connect(callback); 60 | ``` 61 | 62 | 63 | [Host Configuration: ^](#) 64 | ------------ 65 | SSH2Shell requires an object with the following structure: 66 | ```javascript 67 | //Host cobfiguration 68 | host = { 69 | server: { 70 | //host connection settings 71 | host: "192.168.0.2", 72 | port: "22", //optional 73 | userName: "username", 74 | //optional or required settings subject to authentication method 75 | password: "password", 76 | passPhrase: "privateKeyPassphrase", 77 | privateKey: require('fs').readFileSync('/path/to/private/key/id_rsa'), 78 | 79 | //Optional: See https://github.com/mscdex/ssh2#client-methods for other ssh2.client.connect(config) options. 80 | //like 81 | algorithms: { 82 | cipher: {}, 83 | compression: {}, 84 | hmac: {}, 85 | kex: {}, 86 | serverhostkey:{} 87 | }, 88 | 89 | //Optional: Ssh options are only for host to host ssh connections. 90 | //See http://man.openbsd.org/ssh for definitions of options. 91 | ssh: { //Options } 92 | }, 93 | //Optional: Array of host objects for multiple host connections 94 | hosts: [], 95 | 96 | //Optional: Characters used in prompt detection 97 | //defaults 98 | standardPrompt: ">$%#", 99 | passwordPrompt: ":", 100 | passphrasePrompt: ":", 101 | passPromptText: "Password", 102 | 103 | //Optional: exclude or include host banner after connection 104 | showBanner: false, 105 | //https://github.com/mscdex/ssh2#pseudo-tty-settings 106 | window: false, 107 | 108 | //Optional: Enter key character to send as end of line. 109 | enter: "\n", 110 | 111 | //Optional: stream encoding 112 | streamEncoding: "utf8", 113 | 114 | //Optional: Regular expressions to simplify response 115 | //defaults: 116 | asciiFilter: "[^\r\n\x20-\x7e]", //removes non-standard ASCII 117 | disableColorFilter: false, //turns colour filtering on and off 118 | textColorFilter: "(\[{1}[0-9;]+m{1})", //removes colour formatting codes 119 | 120 | //Required: array of commands 121 | commands: ["cd /var/logs", "ls -al"], 122 | 123 | //Optional: Used by this.emit("msg", "my message") 124 | msg: function( message ) { 125 | console.log(message); 126 | } 127 | }, 128 | 129 | //Optional: Troubleshooting options 130 | verbose: false, //outputs all received content 131 | debug: false, //outputs information about each process step 132 | 133 | //Optional: Command time-out to stop a command hanging and not returning to a command prompt 134 | idleTimeOut: 5000, //integer 135 | 136 | //Optional: timeout interval when waiting for a host response. 137 | dataIdleTimeOut: 500, //integer 138 | 139 | //Optional: Messages returned on each connection event. 140 | //default: 141 | connectedMessage: "Connected", 142 | readyMessage: "Ready", 143 | closedMessage: "Closed", 144 | 145 | //Optional: Host event handlers. 146 | //Host defined event handlers are cleared with each change of host. 147 | //Default and instance event handlers are global to all hosts and are not cleared. 148 | 149 | //Optional: 150 | onKeyboardInteractive: function(name, instructions, instructionsLang, prompts, finish){ 151 | //See https://github.com/mscdex/ssh2#client-events 152 | }, 153 | 154 | //Optional: 155 | onData: function( data ) { 156 | //data is the raw response from the host 157 | }, 158 | 159 | //Optional: 160 | onPipe: function( stream ) { 161 | //a read stream that will receive raw resonse data 162 | }, 163 | 164 | //Optional: 165 | onUnpipe: function( steam ) { 166 | //removes the steam added in onPipe. 167 | }, 168 | 169 | //Optional: 170 | onCommandProcessing: function( command, response, sshObj, stream ) { 171 | //Event rasised when data is received from the host before the command prompt is detected. 172 | //command: is the last command run. 173 | //response: is the buffer that is being loaded with each data event. 174 | //sshObj: is the current host object. 175 | //stream: gives stream.write() access if a response is required outside the normal command flow. 176 | }, 177 | 178 | //Optional: 179 | onCommandComplete: function( command, response, sshObj ) { 180 | //Event raised when a standard prompt is detected after a command is run. 181 | //response: is the full response from the host for the last command. 182 | //sshObj: is the current host object. 183 | }, 184 | 185 | //Optional: 186 | onCommandTimeout: function( command, response, stream, connection ) { 187 | //Event is raised when a standard prompt is not detected and no data is 188 | //received from the host within the host.idleTimeout value. 189 | //command: is the last command run or "" when first connected. 190 | //response: is the text received up to the time out. 191 | //stream: gives stream.write() access if a response is required outside the normal command flow. 192 | //connection: gives access to close the connection if all else fails 193 | //The timer can be reset from within this event in case a stream write gets no response. 194 | }, 195 | 196 | //Optional: 197 | onEnd: function( sessionText, sshObj ) { 198 | //is raised when the stream.on ("finish") event is triggered 199 | //SessionText is the full text for this hosts session 200 | //sshObj is the host object 201 | }, 202 | 203 | //Optional: 204 | onError: function( err, type, close = false, callback ) { 205 | //Run when an error event is raised. 206 | //err: is either an Error object or a string containing the error message 207 | //type: is a string containing the error type 208 | //close: is a Boolean value indicating if the connection should be closed or not 209 | //callback: is the function to run when handling the error defined as function(err,type){} 210 | //if using this remember to handle closing the connection based on the close parameter. 211 | //To close the connection us this.connection.close() 212 | }, 213 | 214 | //Optional: 215 | callback: function( sessionText ){ 216 | //sessionText: is the full session response 217 | //Is overridden by SSH2shell.connect(callback) 218 | } 219 | }; 220 | ``` 221 | * Host.server will accept current [SSH2.client.connect parameters](https://github.com/mscdex/ssh2#client-methods). 222 | * Optional host properties or event handlers do not need to be included if you are not changing them. 223 | * Host event handlers completely replace the default event handler definitions in the class when defined. 224 | * `this.sshObj` or sshObj variable passed into a function provides access to all the host config, some instance 225 | variables and the current array of commands for the host. The sshObj.commands array changes with each host connection. 226 | 227 | 228 | [ssh2shell API: ^](#) 229 | ------------- 230 | SSH2Shell extends events.EventEmitter 231 | 232 | *Methods* 233 | 234 | * .connect(callback(sessionText)) Is the main function to establish the connection and handle data events from the server. 235 | It takes in an optional callback function for processing the full session text. 236 | 237 | * .emit("eventName", function, parms,... ). Raises the event based on the name in the first string and takes input 238 | parameters based on the handler function definition. 239 | 240 | * .pipe(stream) binds a writable stream to the output of the read stream but only after the connection is established. 241 | 242 | * .unpipe(stream) removes a piped stream but can only be called after a connection has been made. 243 | 244 | *Variables* 245 | 246 | * .sshObj is the host object as defined above along with some instance variables. 247 | 248 | * .command is the current command being run until a new prompt is detected and the next command replaces it. 249 | 250 | 251 | [Usage: ^](#) 252 | -------- 253 | *Connecting to a single host* 254 | 255 | *app.js* 256 | ```javascript 257 | var host = { 258 | server: { 259 | host: "192.168.0.1", 260 | userName: "myuser", 261 | passPhrase: "myPassPhrase", 262 | privateKey: require('fs').readFileSync("~/.ssh/id_rsa") 263 | }, 264 | commands: [ 265 | "`This is a message that will be added to the full sessionText`", 266 | "msg:This is a message that will be displayed during the process", 267 | "cd ~/", 268 | "ls -l" 269 | ] 270 | }; 271 | 272 | var SSH2Shell = require ('ssh2shell'), 273 | SSH = new SSH2Shell(host); 274 | 275 | var callback = function (sessionText){ 276 | console.log (sessionText); 277 | } 278 | 279 | SSH.connect(callback); 280 | ``` 281 | 282 | 283 | [Authentication: ^](#) 284 | --------------- 285 | * Each host authenticates with its own host.server parameters. 286 | * When using key authentication you may require a valid pass phrase if your key was created with one. 287 | * When using fingerprint validation both host.server.hashMethod property and host.server.hostVerifier function must be 288 | set. 289 | * When using keyboard-interactive authentication both host.server.tryKeyboard and instance.on ("keayboard-interactive", 290 | function...) or host.onKeyboardInteractive() must be defined. 291 | * Set the default cyphers and keys. 292 | 293 | [Default Algorithms: ^](#) 294 | --------------- 295 | See [`ssh2.client.connect(config)`](https://github.com/mscdex/ssh2#client-methods) for a list of the supported algorithms. 296 | * Ciphers. 297 | * Compression algorithms. 298 | * (H)MAC algorithms. 299 | * Key exchange algorithms. 300 | * Server host key formats. 301 | 302 | [Fingerprint Validation: ^](#) 303 | --------------- 304 | At connection time the hash of the server’s public key can be compared with the hash the client had previously recorded 305 | for that server. This stops "man in the middle" attacks where you are redirected to a different server as you connect 306 | to the server you expected to. This hash only changes with a reinstall of SSH, a key change on the server or a load 307 | balancer is now in place. 308 | 309 | __*Note:* 310 | Fingerprint check doesn't work the same way for tunnelling. The first host will validate using this method but the 311 | subsequent connections would have to be handled by your commands. Only the first host uses the SSH2 connection method 312 | that does the validation.__ 313 | 314 | To use fingerprint validation you first need the server hash string which can be obtained using ssh2shell as follows: 315 | * Set host.verbose to true then set host.server.hashKey to any non-empty string (say "1234"). 316 | * Validation will be checked and fail causing the connection to terminate. 317 | * A verbose message will return both the server hash and client hash values that failed comparison. 318 | * This is also what will happen if your hash fails the comparison with the server in the normal verification process. 319 | * Turn on verbose in the host object, run your script with hashKey unset and check the very start of the text returned 320 | for the servers hash value. 321 | * The servers hash value can be saved to a variable outside the host or class so you can access it without having to 322 | parse response text. 323 | 324 | *Example:* 325 | ```javascript 326 | //Define the hostValidation function in the host.server config. 327 | //hashKey needs to be defined at the top level if you want to access the server hash at run time 328 | var serverHash, host; 329 | //don't set expectedHash if you want to know the server hash 330 | var expectedHash 331 | expectedHash = "85:19:8a:fb:60:4b:94:13:5c:ea:fe:3b:99:c7:a5:4e"; 332 | 333 | host = { 334 | server: { 335 | //other normal connection params, 336 | hashMethod: "md5", //"md5" or "sha1" 337 | //hostVerifier function must be defined and return true for match of false for failure. 338 | hostVerifier: function(hashedKey) { 339 | var recievedHash; 340 | 341 | expectedHash = expectedHash + "".replace(/[:]/g, "").toLowerCase(); 342 | recievedHash = hashedKey + "".replace(/[:]/g, "").toLowerCase(); 343 | if (expectedHash === "") { 344 | //No expected hash so save save what was received from the host (hashedKey) 345 | //serverHash needs to be defined before host object 346 | serverHash = hashedKey; 347 | console.log("Server hash: " + serverHash); 348 | return true; 349 | } else if (recievedHash === expectedHash) { 350 | console.log("Hash values matched"); 351 | return true; 352 | } 353 | //Output the failed comparison to the console if you want to see what went wrong 354 | console.log("Hash values: Server = " + recievedHash + " <> Client = " + expectedHash); 355 | return false; 356 | }, 357 | }, 358 | //Other settings 359 | }; 360 | 361 | var SSH2Shell = require ('ssh2shell'), 362 | SSH = new SSH2Shell(host); 363 | SSH.connect(); 364 | ``` 365 | __*Note:* 366 | host.server.hashMethod only supports md5 or sha1 according to the current SSH2 documentation anything else may produce 367 | undesired results.__ 368 | 369 | 370 | [Keyboard-interactive: ^](#) 371 | ---------------------- 372 | Keyboard-interactive authentication is available when both host.server.tryKeyboard is set to true and the event handler 373 | keyboard-interactive is defined as below. The keyboard-interactive event handler can only be used on the first connection. 374 | 375 | Also see [test/keyboard-interactivetest.js](https://github.com/cmp-202/ssh2shell/blob/master/test/keyboard-interactivetest.js) for the full example 376 | 377 | *Defining the event handler* 378 | ```javascript 379 | //this is required 380 | host.server.tryKeyboard = true; 381 | 382 | var SSH2Shell = require ('../lib/ssh2shell'); 383 | var SSH = new SSH2Shell(host); 384 | 385 | //Add the keyboard-interactive handler 386 | //The event function must call finish() with an array of responses in the same order as prompts received 387 | // in the prompts array 388 | SSH.on ('keyboard-interactive', function(name, instructions, instructionsLang, prompts, finish){ 389 | if (this.sshObj.debug) {this.emit('msg', this.sshObj.server.host + ": Keyboard-interactive");} 390 | if (this.sshObj.verbose){ 391 | this.emit('msg', "name: " + name); 392 | this.emit('msg', "instructions: " + instructions); 393 | var str = JSON.stringify(prompts, null, 4); 394 | this.emit('msg', "Prompts object: " + str); 395 | } 396 | //The example presumes only the password is required 397 | finish([this.sshObj.server.password] ); 398 | }); 399 | 400 | SSH.connect(); 401 | ``` 402 | 403 | Or 404 | 405 | ```javascript 406 | host = { 407 | ..., 408 | onKeyboardInteractive: function(name, instructions, instructionsLang, prompts, finish){ 409 | if (this.sshObj.debug) {this.emit('msg', this.sshObj.server.host + ": Keyboard-interactive");} 410 | if (this.sshObj.verbose){ 411 | this.emit('msg', "name: " + name); 412 | this.emit('msg', "instructions: " + instructions); 413 | var str = JSON.stringify(prompts, null, 4); 414 | this.emit('msg', "Prompts object: " + str); 415 | } 416 | //The example presumes only the password is required 417 | finish([this.sshObj.server.password] ); 418 | }, 419 | ... 420 | } 421 | ``` 422 | 423 | 424 | [Troubleshooting: ^](#) 425 | ----------------- 426 | 427 | * Adding msg command `"msg:Doing something"` to your commands array at key points will help you track the sequence of 428 | what has been done as the process runs. (See examples) 429 | * `Error: Unable to parse private key while generating public key (expected sequence)` is caused by the pass phrase 430 | being incorrect. This confused me because it doesn't indicate the pass phrase was the problem but it does indicate 431 | that it could not decrypt the private key. 432 | * Recheck your pass phrase for typos or missing chars. 433 | * Try connecting manually to the host using the exact pass phrase used by the code to confirm it works. 434 | * I did read of people having problems with the pass phrase or password having an \n added when used from an 435 | external file causing it to fail. They had to add .trim() when setting it. 436 | * If your password is incorrect the connection will return an error. 437 | * There is an optional debug setting in the host object that will output process information when set to true and 438 | passwords for failed authentication of sudo commands and tunnelling. `host.debug = true` 439 | * The class now has an idle time out timer (default:5000ms) to stop unexpected command prompts from causing the process 440 | hang without error. The default time out can be changed by setting the host.idleTimeOut with a value in milliseconds. 441 | (1000 = 1 sec) 442 | 443 | 444 | [Verbose and Debug: ^](#) 445 | ------------------ 446 | * When host.verbose is set to true each command complete raises a msg event outputting the response text. 447 | * When host.debug is set to true each process step raises a msg event to help identify what the internal processes were. 448 | 449 | __*Note:* 450 | Do not add debug or verbose to the onCommandProccessing event which is called every time a response is received from the host.__ 451 | 452 | Add your own verbose messages as follows: 453 | `if(this.sshObj.verbose){this.emit("msg", this.sshObj.server.host + ": response: " + response);} //response might need to be changed to this._buffer` 454 | 455 | Add your own debug messages as follows: 456 | `if(this.sshObj.debug){this.emit("msg", this.sshObj.server.host + ": eventName");} //where eventName is the text identifying what happened` 457 | 458 | [Notification commands: ^](#) 459 | ---------------------- 460 | There are two notification commands that are added to the host.commands array but are not run as a command on the host. 461 | 462 | 1. `"msg:This is a message"`. outputs to the console. 463 | 2. "\`SessionText notification\`" will take the text between "\` \`" and add it to the sessionText only. 464 | * The reason for not using echo or printf commands as a normal command is that you see both the command and the message in the sessionText which is pointless when all you want is the message. 465 | 466 | 467 | [Sudo and su Commands: ^](#) 468 | -------------- 469 | It is possible to use `sudo [command]`, `sudo su`, `su [username]` and `sudo -u [username] -i`. 470 | Sudo commands uses the password for the current session and is processed by ssh2shell. Su on the other hand uses the password 471 | of root or an other user (`su seconduser`) and requires you detect the password prompt in `host.onCommandProcessing` and run `this.runCommand(password)`. 472 | 473 | See: [su VS sudo su VS sudo -u -i](http://johnkpaul.tumblr.com/post/19841381351/su-vs-sudo-su-vs-sudo-u-i) for 474 | clarification about the difference between the commands. 475 | 476 | See: [test/sudosutest.js](https://github.com/cmp-202/ssh2shell/blob/master/test/sudosutest.js) for a working code example. 477 | 478 | 479 | [Prompt detection override: ^](#) 480 | ------------------------- 481 | Used to detect different prompts. 482 | 483 | These are optional settings. 484 | ``` 485 | host.standardPrompt = ">$%#"; 486 | host.passwordPrompt = ":"; 487 | host.passphrasePrompt = ":"; 488 | ``` 489 | 490 | 491 | [Text regular expression filters: ^](#) 492 | ------------------------------- 493 | There are two regular expression filters that remove unwanted text from response data. 494 | 495 | The first removes non-standard ascii and the second removes ANSI text formatting codes. Both of these can be modified in 496 | your host object to override defaults. It is also possible to output the ANSI codes by setting disableColorFilter to true. 497 | 498 | Defaults: 499 | ```javascript 500 | host.asciiFilter = "[^\r\n\x20-\x7e]" 501 | host.disableColorFilter = false //or true 502 | host.textColorFilter = "(\[{1}[0-9;]+m{1})" 503 | ``` 504 | 505 | 506 | [Responding to non-standard command prompts: ^](#) 507 | ---------------------- 508 | In host.onCommandProcessing event handler the prompt can be detected and a response can be sent via `stream.write("y\n")`. 509 | 510 | `host.onCommandProcessing` definition replaces the default handler and runs only for the current host connection 511 | ```javascript 512 | host.onCommandProcessing = function( command, response, sshObj, stream ) { 513 | //Check the command and prompt exits and respond with a 'y' but only does it once 514 | if (command == "apt-get install nano" && response.indexOf("[y/N]?") != -1 && sshObj.firstRun != true) { 515 | //This debug message will only run when conditions are met not on every data event so is ok here 516 | sshObj.firstRun = true 517 | stream.write('y\n'); 518 | } 519 | }; 520 | \\sshObj.firstRun could be reset to false in host.onCommandComplete to allow for detect another non-standard prompt 521 | ``` 522 | 523 | Instance definition that runs in parallel with every other commandProcessing for every host connection 524 | To handle all hosts the same add an event handler to the class instance 525 | Don't define an event handler in the host object with the same code, it will do it twice! 526 | ```javascript 527 | var SSH2Shell = require ('ssh2shell'); 528 | var SSH = new SSH2Shell(host); 529 | 530 | SSH.on ('commandProcessing', function onCommandProcessing( command, response, sshObj, stream ) { 531 | 532 | //Check the command and prompt exits and respond with a 'y' 533 | if (command == "apt-get install nano" && response.indexOf("[y/N]?") != -1 && sshObj.firstRun != true ) { 534 | //This debug message will only run when conditions are met not on every data event so is ok here 535 | if (sshObj.debug) {this.emit('msg', this.sshObj.server.host + ": Responding to nano install");} 536 | sshObj.firstRun = true 537 | stream.write('y\n'); 538 | } 539 | }; 540 | ``` 541 | 542 | __*Note:* 543 | If no prompt is detected `commandTimeout` will be raised after the idleTimeOut period.__ 544 | 545 | 546 | [Command Time-out event: ^](#) 547 | --------------- 548 | When onCommandTimeout event triggers after the host.idleTimeOut is exceeded. 549 | This is because there is no new data and no prompt is detected to run the next command. 550 | It is recommended to close the connection as a default action if all else fails so you are not left with a hanging script. 551 | The default action is to add the last response text to the session text and disconnect. 552 | Enabling host.debug would also provide the process path leading up to disconnection which in conjunction with 553 | the session text would clarify what command and output triggered the timeout event. 554 | 555 | **Note: If you receive garble back before the clear response you may need to save the previous response text to the 556 | sessionText and clear the buffer before using stream.write() in commandTimeout. 557 | `this.sshObj.sessionText = response` and `this._buffer = ""`** 558 | 559 | 560 | ```javascript 561 | host.onCommandTimeout = function( command, response, stream, connection ) { 562 | if (command === "atp-get install node" 563 | && response.indexOf("[Y/n]?") != -1 564 | && this.sshObj.nodePrompt != true) { 565 | //Setting this.sshObj.nodePrompt stops a response loop 566 | this.sshObj.nodePrompt = true 567 | stream.write('y\n') 568 | }else{ 569 | //emit an error that passes true for the close parameter and callback that loads the last response 570 | //into sessionText 571 | this.emit ("error", this.sshObj.server.host 572 | + ": Command timed out after #{this._idleTime/1000} seconds. command: " 573 | + command, "Timeout", true, function(err, type){ 574 | this.sshObj.sessionText += response 575 | }) 576 | } 577 | } 578 | ``` 579 | 580 | or 581 | 582 | ```javascript 583 | //reset the default handler to do nothing so it doesn't close the connection 584 | host.onCommandTimeout = function( command, response, stream, connection ) {}; 585 | 586 | //Create the new instance 587 | var SSH2Shell = require ('ssh2shell'), 588 | SSH = new SSH2Shell(host) 589 | 590 | //And do it all in the instance event handler 591 | SSH.on ('commandTimeout',function( command, response, stream, connection ){ 592 | //first test should only pass once to stop a response loop 593 | if (command === "atp-get install node" 594 | && response.indexOf("[Y/n]?") != -1 595 | && this.sshObj.nodePrompt != true) { 596 | this.sshObj.nodePrompt = true; 597 | stream.write('y\n'); 598 | 599 | return true; 600 | } 601 | this.sshObj.sessionText += response; 602 | //emit an error that closes the connection 603 | this.emit("error", this.sshObj.server.host + ": Command \`" + command + "\` timed out after " 604 | + (this._idleTime / 1000) + " seconds. command: " + command, "Command Timeout", true); 605 | }); 606 | 607 | SSH.on ('end', function( sessionText, sshObj ) { 608 | this.emit("msg","\nSession text for " + sshObj.server.host + ":\n" + sessionText); 609 | }); 610 | 611 | SSH.connect(); 612 | ``` 613 | 614 | 615 | [Multiple Primary Hosts: ^](#) 616 | ----------------------- 617 | Multiple hosts can be connected to one after the other as you would connecting to a single host. 618 | Each host config is independant of each other but can share thingsa like commands and handlers. 619 | Each host config is added to an array of hosts that is passed to the constructor. 620 | The final `sessionText` contains the output from all the hosts. 621 | 622 | ```javascript 623 | var dotenv = require('dotenv').config(), 624 | debug = false, 625 | verbose = false 626 | 627 | 628 | //Host objects: 629 | var host1 = { 630 | server: { 631 | host: process.env.HOST, 632 | port: process.env.PORT, 633 | userName: process.env.USER_NAME, 634 | password: process.env.PASSWORD 635 | }, 636 | commands: ["echo Host_1"], 637 | connectedMessage: "Connected to host1", 638 | debug: debug, 639 | verbose: verbose 640 | }, 641 | 642 | host2 = { 643 | server: { 644 | host: process.env.SERVER2_HOST, 645 | port: process.env.PORT, 646 | userName: process.env.SERVER2_USER_NAME, 647 | password: process.env.SERVER2_PASSWORD 648 | }, 649 | commands: ["echo Host_2"], 650 | connectedMessage: "Connected to host2", 651 | debug: debug, 652 | verbose: verbose 653 | }, 654 | 655 | host3 = { 656 | server: { 657 | host: process.env.SERVER3_HOST, 658 | port: process.env.PORT, 659 | userName: process.env.SERVER3_USER_NAME, 660 | password: process.env.SERVER3_PASSWORD 661 | }, 662 | commands: ["echo Host_3"], 663 | connectedMessage: "Connected to host3", 664 | debug: debug, 665 | verbose: verbose, 666 | //Event handler only used by this host 667 | onCommandComplete: function( command, response, sshObj ) { 668 | this.emit("msg", sshObj.server.host + ": commandComplete only used on this host"); 669 | } 670 | } 671 | 672 | var SSH2Shell = require ('ssh2shell'), 673 | SSH = new SSH2Shell([host1,host2,host3]), 674 | callback = function( sessionText ){ 675 | console.log ( "-----Callback session text:\n" + sessionText); 676 | console.log ( "-----Callback end" ); 677 | } 678 | 679 | SSH.on ('end', function( sessionText, sshObj ) { 680 | this.emit("msg", sshObj.server.host + ": onEnd every host"); 681 | }) 682 | 683 | SSH.connect(callback); 684 | ``` 685 | 686 | 687 | [Tunnelling nested host objects: ^](#) 688 | --------------------------------- 689 | `hosts: [ host1, host2]` setting can make multiple host connections possible. 690 | Each host config object has its own server settings, commands, command handlers and 691 | event handlers. Each host object also has its own hosts array that other host objects can be added to. 692 | This provides for different host connection sequences to multiple depth of connections. 693 | 694 | **SSH connections:** 695 | Once the primary host connection is made all other connections are made using an ssh command from the 696 | current host to the next. A number of ssh command options are set using an optional `host.server.ssh` object. 697 | `host.server.ssh.options` allows setting any ssh config options. 698 | 699 | ``` 700 | var host = { 701 | server: { ..., 702 | ssh: { 703 | forceProtocolVersion: true/false, 704 | forceAddressType: true/false, 705 | disablePseudoTTY: true/false, 706 | forcePseudoTTY: true/false, 707 | verbose: true/false, 708 | cipherSpec: "", 709 | escape: "", 710 | logFile: "", 711 | configFile: "", 712 | identityFile: "", 713 | loginName: "", 714 | macSpec: "", 715 | options: {} 716 | } 717 | } 718 | } 719 | ``` 720 | 721 | 722 | **Tunnelling Example:** 723 | This example shows two hosts (server2, server3) that are connected to via server1. 724 | The two host configs are added to server1.hosts array. 725 | 726 | ``` 727 | server1.hosts = [server2, server3] 728 | server2.hosts = [] 729 | server3.hosts = [] 730 | ``` 731 | *The following would also be valid:* 732 | ``` 733 | server1.hosts = [server2] 734 | server2.hosts = [server3] 735 | server3.hosts = [] 736 | ``` 737 | 738 | *The nested process:* 739 | 740 | 1. The primary host (server1) is connected and all its commands completed. 741 | 2. Server1.hosts array is checked for other hosts. 742 | 3. Server1 is stored for use later and server2's host object is loaded as the current host. 743 | 4. A connection to the next host is made using its server parameters via running the ssh command in the current session. 744 | 5. That hosts commands are completed and it's hosts array is checked for other hosts. 745 | 6. If no hosts are found the connection is closed triggering an end event for the current host if defined. 746 | 5. Server1 host object is reloaded as the current host object and server2 host object discarded. 747 | 6. Server1.hosts array is checked for other hosts and the next host popped off the array. 748 | 7. Server1's host object is restored and the process repeats for an all other hosts. 749 | 8. Server1 is loaded for the last time and all hosts sessionText's are appended to `this.callback(sessionText)`. 750 | 10. As all sessions are closed, the process ends. 751 | 752 | 753 | **_Note:_** 754 | * A host object needs to be defined before it is added to another host.hosts array. 755 | * Only the primary host objects connected, ready and closed messages will be used by ssh2shell. 756 | * Connection events only apply to the first host. Keyboard-interactive event handler is one of these. 757 | 758 | 759 | *How to:* 760 | * Define nested hosts 761 | * Use unique host connection settings for each host 762 | * Defining different commands and command event handlers for each host 763 | * Sharing duplicate functions between host objects 764 | * What host object attributes you can leave out of primary and secondary host objects 765 | * Unique event handlers set in host objects, common event handler set on class instance 766 | 767 | **_Note:_** 768 | * Change debug to true to see the full process for each host. 769 | 770 | ```javascript 771 | var dotenv = require('dotenv'); 772 | dotenv.load(); 773 | 774 | //Host connection and authentication parameters 775 | var conParamsHost1 = { 776 | host: process.env.SERVER1_HOST, 777 | port: process.env.SERVER1_PORT, 778 | userName: process.env.SERVER1_USER_NAME, 779 | password: process.env.SERVER1_PASSWORD, 780 | passPhrase: process.env.SERVER1_PASS_PHRASE, 781 | privateKey: require('fs').readFileSync(process.env.SERVER1_PRIV_KEY_PATH) 782 | }, 783 | conParamsHost2 = { 784 | host: process.env.SERVER2_HOST, 785 | port: process.env.SERVER2_PORT, 786 | userName: process.env.SERVER2_USER_NAME, 787 | password: process.env.SERVER2_PASSWORD 788 | }, 789 | conParamsHost3 = { 790 | host: process.env.SERVER3_HOST, 791 | port: process.env.SERVER3_PORT, 792 | userName: process.env.SERVER3_USER_NAME, 793 | password: process.env.SERVER3_PASSWORD 794 | } 795 | 796 | 797 | //Host objects: 798 | var host1 = { 799 | server: conParamsHost1, 800 | commands: [ 801 | "msg:connected to host: passed. Listing dir.", 802 | "ls -l" 803 | ], 804 | debug: false, 805 | onCommandComplete: function( command, response, sshObj ) { 806 | //we are listing the dir so output it to the msg handler 807 | if(sshObj.debug){ 808 | this.emit("msg", this.sshObj.server.host + ": host.onCommandComplete host1, command: " + command); 809 | } 810 | }, 811 | onEnd: function( sessionText, sshObj ) { 812 | //show the full session output for all hosts. 813 | if(sshObj.debug){this.emit("msg", this.sshObj.server.host + ": primary host.onEnd all sessiontText");} 814 | this.emit ("msg", "\nAll Hosts SessiontText ---------------------------------------\n"); 815 | this.emit ("msg", sshObj.server.host + ":\n" + sessionText); 816 | this.emit ("msg", "\nEnd sessiontText ---------------------------------------------\n"); 817 | }) 818 | }, 819 | 820 | host2 = { 821 | server: conParamsHost2, 822 | commands: [ 823 | "msg:connected to host: passed", 824 | "msg:Changing to root dir", 825 | "cd ~/", 826 | "msg:Listing dir", 827 | "ls -l" 828 | ], 829 | debug: false, 830 | connectedMessage: "Connected to host2", 831 | onCommandComplete: function( command, response, sshObj ) { 832 | //we are listing the dir so output it to the msg handler 833 | if(sshObj.debug){ 834 | this.emit("msg", this.sshObj.server.host + ": host.onCommandComplete host2, command: " + command); 835 | } 836 | if (command.indexOf("cd") != -1){ 837 | this.emit("msg", this.sshObj.server.host + ": Just ran a cd command:\n"); 838 | this.emit("msg", response); 839 | } 840 | } 841 | }, 842 | 843 | host3 = { 844 | server: conParamsHost3, 845 | commands: [ 846 | "msg:connected to host: passed", 847 | "hostname" 848 | ], 849 | debug: false, 850 | connectedMessage: "Connected to host3", 851 | onCommandComplete: function( command, response, sshObj) { 852 | //we are listing the dir so output it to the msg handler 853 | if(sshObj.debug){ 854 | this.emit("msg", this.sshObj.server.host + ": host.onCommandComplete host3, command: " + command); 855 | } 856 | if (command.indexOf("cd") != -1){ 857 | this.emit("msg", this.sshObj.server.host + ": Just ran hostname command:\n"); 858 | this.emit("msg", response); 859 | } 860 | } 861 | } 862 | 863 | //Set the two hosts you are tunnelling to through host1 864 | host1.hosts = [ host2, host3 ]; 865 | 866 | //or the alternative nested tunnelling method outlined above: 867 | //host2.hosts = [ host3 ]; 868 | //host1.hosts = [ host2 ]; 869 | 870 | //Create the new instance 871 | var SSH2Shell = require ('ssh2shell'), 872 | SSH = new SSH2Shell(host1); 873 | 874 | //Add a commandComplete handler used by all hosts 875 | SSH.on ('commandComplete', function onCommandComplete( command, response, sshObj ) { 876 | //we are listing the dir so output it to the msg handler 877 | if (command == "ls -l"){ 878 | this.emit("msg", this.sshObj.server.host + ":\n" + response); 879 | } 880 | } 881 | 882 | //Add an on end event handler used by all hosts 883 | SSH.on ('end', function( sessionText, sshObj ) { 884 | //show the full session output. This could be emailed or saved to a log file. 885 | this.emit ("msg", "\nSessiontText -------------------------------------------------\n"); 886 | this.emit ("msg", sshObj.server.host + ":\n" + sessionText); 887 | this.emit ("msg", "\nEnd sessiontText ---------------------------------------------\n"); 888 | }); 889 | 890 | //Start the process 891 | SSH.connect(); 892 | 893 | ``` 894 | 895 | 896 | [Event Handlers: ^](#) 897 | --------------- 898 | There are a number of event handlers that enable you to add your own code to be run when those events are triggered. 899 | Most of these you have already encountered in the host object. You do not have to add event handlers unless you want 900 | to add your own functionality as the class already has default handlers defined. 901 | 902 | There are two ways to add event handlers: 903 | 904 | 1. Add handler functions to the host object (See requirements at start of readme). 905 | * These event handlers will only be run for the currently connected host.Important to understand in a multi host setup. 906 | * Within the host event functions `this` is always referencing the ssh2shell instance at run time. 907 | * Instance variables and functions are available through `this` including the Emitter functions like 908 | this.emit("myEvent", properties). 909 | * Connect, ready, error and close events are not available for definition in the host object. 910 | * Defining a host event handler replaces the default coded event handler. 911 | 912 | 2. Add an event handler, as defined below, to the class instance. 913 | * Handlers added to the class instance will be triggered every time the event is raised in parallel with any other 914 | handlers of the same name. 915 | * It will not replace the internal event handler of the class set by the class default or host definition. 916 | 917 | An event can be raised using `this.emit('eventName', parameters)`. 918 | 919 | *Further reading:* 920 | [node.js event emitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) 921 | 922 | **Class Instance Event Definitions:** 923 | 924 | ```javascript 925 | ssh2shell.on ("connect", function onConnect() { 926 | //default: outputs primaryHost.connectedMessage 927 | }); 928 | 929 | ssh2shell.on ("ready", function onReady() { 930 | //default: outputs primaryHost.readyMessage 931 | }); 932 | 933 | ssh2shell.on ("msg", function onMsg( message ) { 934 | //default: outputs the message to the host.msg.send function. If undefined output is to console.log 935 | //message is the text to output. 936 | }); 937 | 938 | ssh2shell.on ("commandProcessing", function onCommandProcessing( command, response, sshObj, stream ) { 939 | //Allows for the handling of a commands response as each character is loaded into the buffer 940 | //default: no action 941 | //default is replaced by host.onCommandProcessing function if defined 942 | //command is the command that is being processed 943 | //response is the text buffer that is being loaded with each data event from stream.ready 944 | //sshObj is the host object 945 | //stream is the connection stream 946 | }); 947 | 948 | ssh2shell.on ("commandComplete", function onCommandComplete( command, response, sshObj ) { 949 | //Allows for the handling of a commands response before the next command is run 950 | //default: returns a debug message if host.debug = true 951 | //default is replaced by host.onCommandComplete function if defined 952 | //command is the completed command 953 | //response is the full buffer response from the command 954 | //sshObj is the host object 955 | }); 956 | 957 | ssh2shell.on ("commandTimeout", function onCommandTimeout( command, response, stream ,connection ) { 958 | //Allows for handling a command timeout that would normally cause the script to hang in a wait state 959 | //default: an error is raised adding response to this.sshObj.sessionText and the connection is closed 960 | //default is replaced by host.onCommandTimeout function if defined 961 | //command is the command that timed out 962 | //response is the text buffer up to the time out period 963 | //stream is the session stream 964 | //connection is the main connection object 965 | }); 966 | 967 | ssh2shell.on ("end", function onEnd( sessionText, sshObj ) { 968 | //Allows access to sessionText when stream.finish triggers 969 | //default: returns a debug message if host.debug = true 970 | //default is replaced by host.onEnd function if defined 971 | //sessionText is the full text response from the session 972 | //sshObj is the host object 973 | }); 974 | 975 | ssh2shell.on ("close", function onClose(had_error = void(0)) { 976 | //default: outputs primaryHost.closeMessage or error if one was received 977 | //default: returns a debug message if host.debug = true 978 | //had_error indicates an error was received on close 979 | }); 980 | 981 | ssh2shell.on ("error", function onError(err, type, close = false, callback(err, type) = undefined) { 982 | //default: raises a msg with the error message, runs the callback if defined and closes the connection 983 | //default is replaced by host.onEnd function if defined 984 | //err is the error received it maybe an Error object or a string containing the error message. 985 | //type is a string identifying the source of the error 986 | //close is a Boolean value indicating if the event should close the connection. 987 | //callback a function that will be run by the handler 988 | }); 989 | 990 | ssh2shell.on ("keyboard-interactive", 991 | function onKeyboardInteractive(name, instructions, instructionsLang, prompts, finish){ 992 | //See https://github.com/mscdex/ssh2#client-events 993 | //name, instructions, instructionsLang don't seem to be of interest for authenticating 994 | //prompts is an object of expected prompts and if they are to be shown to the user 995 | //finish is the function to be called with an array of responses in the same order as 996 | //the prompts parameter defined them. 997 | //See [Client events](https://github.com/mscdex/ssh2#client-events) for more information 998 | //if a non standard prompt results from a successful connection then handle its detection and response in 999 | //onCommandProcessing or commandTimeout. 1000 | //see text/keyboard-interactivetest.js 1001 | }); 1002 | 1003 | ssh2shell.on ("data", function onData(data){ 1004 | //data is a string chunk received from the stream.data event 1005 | }); 1006 | 1007 | ssh2shell.on ("pipe", function onPipe(source){ 1008 | //Source is the read stream to output data from 1009 | }); 1010 | 1011 | ssh2shell.on ("Unpipe", function onUnpipe(source){ 1012 | //Source is the read stream to remove from outputting data 1013 | }); 1014 | ``` 1015 | 1016 | [Bash scripts on the fly: ^](#) 1017 | ------------------------ 1018 | If the commands you need to run would be better suited to a bash script as part of the process it is possible to generate 1019 | or get the script on the fly. You can echo/printf the script content into a file as a command, ensure it is executable, 1020 | run it and then delete it. The other option is to curl or wget the script from a remote location but 1021 | this has some risks associated with it. I like to know what is in the script I am running. 1022 | 1023 | **Note** # and > in the following commands with conflict with the host.standardPrompt definition ">$%#" change it to "$%" 1024 | 1025 | ``` 1026 | host.commands = [ "some commands here", 1027 | "if [ ! -f myscript.sh ]; then printf '#!/bin/bash\n" + 1028 | " #\n" + 1029 | " current=$(pwd);\n" + 1030 | "cd ..;\n" + 1031 | "if [ -f myfile ]; then" + 1032 | "sed \"/^[ \\t]*$/d\" ${current}/myfile | while read line; do\n" + 1033 | "printf \"Doing some stuff\";\n" + 1034 | "printf $line;\n" + 1035 | "done\n" + 1036 | "fi\n' > myscript.sh;" + 1037 | "fi", 1038 | "sudo chmod 700 myscript.sh", 1039 | "./myscript.sh", 1040 | "rm myscript.sh" 1041 | ], 1042 | ``` 1043 | 1044 | from [issue #103](https://github.com/cmp-202/ssh2shell/issues/103) 1045 | "I read the local script file & encoded into Base64 to avoid any special characters. 1046 | Then with it I formed a command at the runtime to execute at the target: 1047 | 1048 | `echo "<>" | base64 -d | bash` 1049 | 1050 | and if you have command line inputs: 1051 | `echo "<>" | base64 -d | bash -s` 1052 | 1053 | This worked for me without physically copying the script on target as well as running into any special character issues with echo or printf statement. 1054 | Hopefully it can help others who are looking to solve the same problem. 1055 | " 1056 | 1057 | from [issue #104](https://github.com/cmp-202/ssh2shell/issues/104) 1058 | ```javascript 1059 | host.commands: [ bash -s ${fs.readFileSync('./script1.sh', 'utf-8')}] 1060 | 1061 | //remove the output of the command content from the script result 1062 | host.onCommandComplete: function( command, response, sshObj) { 1063 | if (command.indexOf("bash ") > -1){ 1064 | //end match would most likely fail if there was a problem 1065 | sshObj.sessonText = response.replace(command.substring(0, 4)+"*"+command.substring(command.length - 2),'') 1066 | } 1067 | } 1068 | ``` 1069 | -------------------------------------------------------------------------------- /lib/ssh2shell.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.12.7 2 | (function() { 3 | var SSH2Shell, Stream, typeIsArray, 4 | bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 5 | extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, 6 | hasProp = {}.hasOwnProperty; 7 | 8 | typeIsArray = Array.isArray || function(value) { 9 | return {}.toString.call(value) === '[object Array]'; 10 | }; 11 | 12 | Stream = require('stream'); 13 | 14 | SSH2Shell = (function(superClass) { 15 | extend(SSH2Shell, superClass); 16 | 17 | SSH2Shell.prototype.sshObj = {}; 18 | 19 | SSH2Shell.prototype.command = ""; 20 | 21 | SSH2Shell.prototype._hosts = []; 22 | 23 | SSH2Shell.prototype._sshToNextHost = false; 24 | 25 | SSH2Shell.prototype._primaryhostSessionText = ""; 26 | 27 | SSH2Shell.prototype._allSessions = ""; 28 | 29 | SSH2Shell.prototype._connections = []; 30 | 31 | SSH2Shell.prototype._stream = {}; 32 | 33 | SSH2Shell.prototype._buffer = ""; 34 | 35 | SSH2Shell.prototype.idleTime = 5000; 36 | 37 | SSH2Shell.prototype.asciiFilter = ""; 38 | 39 | SSH2Shell.prototype.textColorFilter = ""; 40 | 41 | SSH2Shell.prototype.passwordPrompt = ""; 42 | 43 | SSH2Shell.prototype.passphrasePrompt = ""; 44 | 45 | SSH2Shell.prototype.standardPrompt = ""; 46 | 47 | SSH2Shell.prototype._callback = function() {}; 48 | 49 | SSH2Shell.prototype.onCommandProcessing = function() {}; 50 | 51 | SSH2Shell.prototype.onCommandComplete = function() {}; 52 | 53 | SSH2Shell.prototype.onCommandTimeout = function() {}; 54 | 55 | SSH2Shell.prototype.onEnd = function() {}; 56 | 57 | SSH2Shell.prototype._onData = function(data) { 58 | this._buffer += data; 59 | if (this.command.length > 0 && !this.standardPrompt.test(this._sanitizeResponse())) { 60 | this.emit('commandProcessing', this.command, this._buffer, this.sshObj, this._stream); 61 | if (this.idleTimer) { 62 | clearTimeout(this.idleTimer); 63 | } 64 | this.idleTimer = setTimeout((function(_this) { 65 | return function() { 66 | return _this.emit('commandTimeout', _this.command, _this._buffer, _this._stream, _this.connection); 67 | }; 68 | })(this), this.idleTime); 69 | } else if (this.command.length < 1 && !this.standardPrompt.test(this._buffer)) { 70 | this.emit('commandProcessing', this.command, this._buffer, this.sshObj, this._stream); 71 | } 72 | if (this.dataReceivedTimer) { 73 | clearTimeout(this.dataReceivedTimer); 74 | } 75 | return this.dataReceivedTimer = setTimeout((function(_this) { 76 | return function() { 77 | if (_this.idleTimer) { 78 | clearTimeout(_this.idleTimer); 79 | } 80 | if (!_this.sshObj.disableColorFilter) { 81 | _this._buffer = _this._buffer.replace(_this.textColorFilter, ""); 82 | } 83 | if (!_this.sshObj.disableASCIIFilter) { 84 | _this._buffer = _this._buffer.replace(_this.asciiFilter, ""); 85 | } 86 | switch (true) { 87 | case _this.command.length > 0 && _this.command.indexOf("sudo ") !== -1: 88 | if (_this.sshObj.debug) { 89 | _this.emit('msg', _this.sshObj.server.host + ": Sudo command data"); 90 | } 91 | return _this._processPasswordPrompt(); 92 | case _this.sshObj.sshToNextHost && _this.command.length > 0 && _this.command.indexOf("ssh ") === 0: 93 | if (_this.sshObj.debug) { 94 | _this.emit('msg', _this.sshObj.server.host + ": SSH command data"); 95 | } 96 | return _this._processSSHPrompt(); 97 | case _this.command.length > 0 && _this.standardPrompt.test(_this._sanitizeResponse()): 98 | if (_this.sshObj.debug) { 99 | _this.emit('msg', _this.sshObj.server.host + ": Normal prompt detected"); 100 | } 101 | _this.sshObj.pwSent = false; 102 | return _this._commandComplete(); 103 | case _this.command.length < 1 && _this.standardPrompt.test(_this._buffer): 104 | if (_this.sshObj.debug) { 105 | _this.emit('msg', _this.sshObj.server.host + ": First prompt detected"); 106 | } 107 | if (_this.sshObj.showBanner) { 108 | _this.sshObj.sessionText += _this._buffer; 109 | } 110 | return _this._nextCommand(); 111 | default: 112 | if (_this.sshObj.debug) { 113 | _this.emit('msg', "Data processing: data received timeout"); 114 | } 115 | return _this.idleTimer = setTimeout(function() { 116 | return _this.emit('commandTimeout', _this.command, _this._buffer, _this._stream, _this.connection); 117 | }, _this.idleTime); 118 | } 119 | }; 120 | })(this), this.dataIdleTime); 121 | }; 122 | 123 | SSH2Shell.prototype._sanitizeResponse = function() { 124 | return this._buffer.replace(this.command.substr(0, this._buffer.length), ""); 125 | }; 126 | 127 | SSH2Shell.prototype._processPasswordPrompt = function() { 128 | var passwordPrompt, response, standardPrompt; 129 | response = this._sanitizeResponse().trim(); 130 | passwordPrompt = this.passwordPrompt.test(response); 131 | standardPrompt = this.standardPrompt.test(response); 132 | if (this.sshObj.debug) { 133 | this.emit('msg', this.sshObj.server.host + ": Sudo Password prompt detected: " + passwordPrompt); 134 | } 135 | if (this.sshObj.debug) { 136 | this.emit('msg', this.sshObj.server.host + ": Sudo Password prompt: Password sent: " + this.sshObj.pwSent); 137 | } 138 | if (this.sshObj.debug) { 139 | this.emit('msg', this.sshObj.server.host + ": Sudo Password: " + this.sshObj.server.password); 140 | } 141 | if (this.sshObj.verbose) { 142 | this.emit('msg', this.sshObj.server.host + ": Sudo Response: " + response); 143 | } 144 | switch (true) { 145 | case passwordPrompt && !this.sshObj.server.password: 146 | if (this.sshObj.debug) { 147 | this.emit('msg', this.sshObj.server.host + ": Sudo password prompt no password set"); 148 | } 149 | return this._resetFromSudoError(); 150 | case passwordPrompt && !this.sshObj.pwSent: 151 | if (this.sshObj.verbose) { 152 | this.emit('msg', this.sshObj.server.host + ": Sudo password prompt: Buffer: " + response); 153 | } 154 | if (this.sshObj.debug) { 155 | this.emit('msg', this.sshObj.server.host + ": Sudo password prompt: Send password "); 156 | } 157 | if (this.sshObj.verbose) { 158 | this.emit('msg', this.sshObj.server.host + ": Sudo sent password: " + this.sshObj.server.password); 159 | } 160 | this.sshObj.pwSent = true; 161 | return this._runCommand("" + this.sshObj.server.password); 162 | case passwordPrompt && this.sshObj.pwSent: 163 | if (this.sshObj.verbose) { 164 | this.emit('msg', this.sshObj.server.host + ": Sudo password faied: response: " + response); 165 | } 166 | if (this.sshObj.debug) { 167 | this.emit('error', this.sshObj.server.host + ": Sudo password was incorrect for " + this.sshObj.server.userName + ", Sudo authentication"); 168 | } 169 | if (this.sshObj.debug) { 170 | this.emit('msg', this.sshObj.server.host + ": Sudo failed password prompt: Password: [" + this.sshObj.server.password + "]"); 171 | } 172 | this.sshObj.pwSent = false; 173 | return this._resetFromSudoError(); 174 | case standardPrompt: 175 | if (this.sshObj.debug) { 176 | this.emit('msg', this.sshObj.server.host + ": Sudo password accepted"); 177 | } 178 | this.sshObj.pwSent = false; 179 | if (this.sshObj.verbose) { 180 | this.emit('msg', this.sshObj.server.host + ": Sudo Standard Response: " + response); 181 | } 182 | return this._commandComplete(); 183 | default: 184 | return this.idleTimer = setTimeout((function(_this) { 185 | return function() { 186 | return _this.emit('commandTimeout', _this.command, response, _this._stream, _this._connection); 187 | }; 188 | })(this), this.idleTime); 189 | } 190 | }; 191 | 192 | SSH2Shell.prototype._resetFromSudoError = function() { 193 | this.sshObj.pwSent = false; 194 | this.sshObj.sessionText += "" + this._buffer; 195 | this._buffer = ""; 196 | this.command = ""; 197 | return this._stream.write('\x03'); 198 | }; 199 | 200 | SSH2Shell.prototype._processSSHPrompt = function() { 201 | var passphrasePrompt, passwordPrompt, response, standardPrompt, using; 202 | response = this._sanitizeResponse().trim(); 203 | if (this.idleTimer) { 204 | clearTimeout(this.idleTimer); 205 | } 206 | passwordPrompt = this.passwordPrompt.test(response) && !this.sshObj.server.hasOwnProperty("passPhrase"); 207 | passphrasePrompt = this.passphrasePrompt.test(response) && this.sshObj.server.hasOwnProperty("passPhrase"); 208 | standardPrompt = this.standardPrompt.test(response); 209 | if (this.sshObj.verbose) { 210 | this.emit('msg', this.sshObj.server.host + ": SSH: Password previously sent: " + this.sshObj.sshAuth); 211 | } 212 | if (!this.sshObj.sshAuth) { 213 | if (this.sshObj.debug) { 214 | this.emit('msg', this.sshObj.server.host + ": First SSH prompt detection"); 215 | } 216 | if (passwordPrompt) { 217 | if (this.sshObj.debug) { 218 | this.emit('msg', this.sshObj.server.host + ": SSH send password"); 219 | } 220 | this.sshObj.sshAuth = true; 221 | this._buffer = ""; 222 | return this._runCommand("" + this.sshObj.server.password); 223 | } else if (passphrasePrompt) { 224 | if (this.sshObj.debug) { 225 | this.emit('msg', this.sshObj.server.host + ": SSH send passphrase"); 226 | } 227 | this.sshObj.sshAuth = true; 228 | this._buffer = ""; 229 | return this._runCommand("" + this.sshObj.server.passPhrase); 230 | } else if (standardPrompt) { 231 | if (this.sshObj.debug) { 232 | this.emit('msg', this.sshObj.server.host + ": SSH standard prompt: connection failed"); 233 | } 234 | this.emit('msg', this.sshObj.server.host + ": SSH connection failed"); 235 | this.emit('msg', this.sshObj.server.host + ": SSH failed response: " + response); 236 | this.sshObj.sessionText += this.sshObj.server.host + ": SSH failed: response: " + response; 237 | return this._runExit(); 238 | } else { 239 | this.emit('msg', this.sshObj.server.host + ": SSH no prompt was not detected"); 240 | if (this.sshObj.verbose) { 241 | this.emit('msg', this.sshObj.server.host + ": SSH response: " + response); 242 | } 243 | if (this.sshObj.verbose && !this.sshObj.server.hasOwnProperty("password")) { 244 | this.emit('msg', this.sshObj.server.host + ": SSH password prompt: " + this.sshObj.passwordPrompt); 245 | } 246 | if (this.sshObj.verbose && this.sshObj.server.hasOwnProperty("passPhrase")) { 247 | this.emit('msg', this.sshObj.server.host + ": SSH passphrase prompt: " + this.sshObj.passphrasePrompt); 248 | } 249 | if (this.sshObj.verbose) { 250 | this.emit('msg', this.sshObj.server.host + ": SSH standard prompt: " + this.sshObj.standardPrompt); 251 | } 252 | return this._runExit(); 253 | } 254 | } else { 255 | if (this.sshObj.debug) { 256 | this.emit('msg', this.sshObj.server.host + ": SSH post authentication prompt detection"); 257 | } 258 | if (standardPrompt) { 259 | if (this.sshObj.debug) { 260 | this.emit('msg', this.sshObj.server.host + ": SSH complete: normal prompt"); 261 | } 262 | if (this.sshObj.showBanner) { 263 | this.sshObj.sessionText += "\n " + response; 264 | } 265 | this.sshObj.exitCommands.push("exit"); 266 | this.emit('msg', "" + this.sshObj.connectedMessage); 267 | this.emit('msg', "" + this.sshObj.readyMessage); 268 | if (this.sshObj.debug) { 269 | this.emit('msg', this.sshObj.server.host + ": SSH complete: next command"); 270 | } 271 | this.sshObj.sshToNextHost = false; 272 | return this._nextCommand(); 273 | } else if (passwordPrompt || passphrasePrompt) { 274 | this.emit('msg', this.sshObj.server.host + ": SSH authentication failed"); 275 | if (this.sshObj.debug) { 276 | this.emit('msg', this.sshObj.server.host + ": SSH auth failed"); 277 | } 278 | if (this.sshObj.verbose) { 279 | this.emit('msg', this.sshObj.server.host + ": SSH: failed response: " + response); 280 | } 281 | this.sshObj.sshAuth = false; 282 | using = (function() { 283 | switch (false) { 284 | case !passwordPrompt: 285 | return "SSH password: " + this.sshObj.server.password; 286 | case !passphrasePrompt: 287 | return "SSH passphrase: " + this.sshObj.server.passPhrase; 288 | } 289 | }).call(this); 290 | this.emit('error', this.sshObj.server.host + ": SSH authentication failed for " + this.sshObj.server.userName + "@" + this.sshObj.server.host, "Nested host authentication"); 291 | if (this.sshObj.debug) { 292 | this.emit('msg', (this.sshObj.server.host + ": SSH auth failed: Using ") + using); 293 | } 294 | if (this.sshObj.verbose) { 295 | this.emit('msg', this.sshObj.server.host + ": SSH response: " + response); 296 | } 297 | if (this._connections.length > 0) { 298 | return this._previousHost(); 299 | } 300 | return this._runExit(); 301 | } 302 | } 303 | }; 304 | 305 | SSH2Shell.prototype._processNotifications = function() { 306 | var msgNote, sessionNote; 307 | if (this.command) { 308 | if ((sessionNote = this.command.match(/^`(.*)`$/))) { 309 | if (this.sshObj.debug) { 310 | this.emit('msg', this.sshObj.server.host + ": Notifications: sessionText output"); 311 | } 312 | if (this._connections.length > 0) { 313 | this.sshObj.sessionText += this.sshObj.server.host + ": Note: " + sessionNote[1] + this.sshObj.enter; 314 | } else { 315 | this.sshObj.sessionText += "Note: " + sessionNote[1] + this.sshObj.enter; 316 | } 317 | if (this.sshObj.verbose) { 318 | this.emit('msg', sessionNote[1]); 319 | } 320 | return this._nextCommand(); 321 | } else if ((msgNote = this.command.match(/^msg:(.*)$/))) { 322 | if (this.sshObj.debug) { 323 | this.emit('msg', this.sshObj.server.host + ": Notifications: msg to output"); 324 | } 325 | this.emit('msg', this.sshObj.server.host + ": Note: " + msgNote[1]); 326 | return this._nextCommand(); 327 | } else { 328 | if (this.sshObj.debug) { 329 | this.emit('msg', this.sshObj.server.host + ": Notifications: Normal Command to run"); 330 | } 331 | return this._checkCommand(); 332 | } 333 | } 334 | }; 335 | 336 | SSH2Shell.prototype._commandComplete = function() { 337 | var response; 338 | response = this._buffer.trim(); 339 | if (this.command !== "" && this.command !== "exit" && this.sshObj.sshToNextHost !== true) { 340 | if (this.sshObj.verbose) { 341 | this.emit('msg', this.sshObj.server.host + ": Command complete:\nCommand:\n " + this.command + "\nResponse: " + response); 342 | } 343 | this.sshObj.sessionText += response; 344 | if (this.sshObj.debug) { 345 | this.emit('msg', this.sshObj.server.host + ": Raising commandComplete event"); 346 | } 347 | this.emit('commandComplete', this.command, this._buffer, this.sshObj); 348 | } 349 | return this._nextCommand(); 350 | }; 351 | 352 | SSH2Shell.prototype._nextCommand = function() { 353 | this._buffer = ""; 354 | if (this.sshObj.commands.length > 0) { 355 | if (this.sshObj.verbose) { 356 | this.emit('msg', this.sshObj.server.host + ": Host.commands: " + this.sshObj.commands); 357 | } 358 | this.command = this.sshObj.commands.shift(); 359 | if (this.sshObj.verbose) { 360 | this.emit('msg', this.sshObj.server.host + ": Next command from host.commands: " + this.command); 361 | } 362 | return this._processNotifications(); 363 | } else { 364 | if (this.sshObj.debug) { 365 | this.emit('msg', this.sshObj.server.host + ": No commands so exit"); 366 | } 367 | return this._runExit(); 368 | } 369 | }; 370 | 371 | SSH2Shell.prototype._checkCommand = function() { 372 | if (this.command !== "") { 373 | return this._runCommand(this.command); 374 | } else { 375 | if (this.sshObj.debug) { 376 | this.emit('msg', this.sshObj.server.host + ": No command so exit"); 377 | } 378 | return this._runExit(); 379 | } 380 | }; 381 | 382 | SSH2Shell.prototype._runCommand = function(command) { 383 | if (this.sshObj.verbose) { 384 | this.emit('msg', this.sshObj.server.host + ": sending: " + command); 385 | } 386 | if (this.sshObj.debug) { 387 | this.emit('msg', this.sshObj.server.host + ": run command"); 388 | } 389 | return this._stream.write("" + command + this.sshObj.enter); 390 | }; 391 | 392 | SSH2Shell.prototype._previousHost = function() { 393 | if (this.sshObj.debug) { 394 | this.emit('msg', this.sshObj.server.host + ": Load previous host config"); 395 | } 396 | this.emit('end', this.sshObj.server.host + ": \n" + this.sshObj.sessionText, this.sshObj); 397 | if (this.sshObj.debug) { 398 | this.emit('msg', this.sshObj.server.host + ": Previous hosts: " + this._connections.length); 399 | } 400 | if (this._connections.length > 0) { 401 | this.sshObj = this._connections.pop(); 402 | if (this.sshObj.debug) { 403 | this.emit('msg', this.sshObj.server.host + ": Reload previous host"); 404 | } 405 | return this._runExit(); 406 | } else { 407 | return this._runExit(); 408 | } 409 | }; 410 | 411 | SSH2Shell.prototype._nextHost = function() { 412 | var nextHost; 413 | nextHost = this.sshObj.hosts.shift(); 414 | if (this.sshObj.debug) { 415 | this.emit('msg', this.sshObj.server.host + ": SSH to " + nextHost.server.host); 416 | } 417 | if (this.sshObj.debug) { 418 | this.emit('msg', this.sshObj.server.host + ": Clearing previous event handlers"); 419 | } 420 | if (this.sshObj.debug) { 421 | this.emit('msg', this.sshObj.server.host + ": Remove previous event handlers"); 422 | } 423 | this._removeListeners(); 424 | this._connections.push(this.sshObj); 425 | this.sshObj = nextHost; 426 | return this._initiate(this._sshConnect); 427 | }; 428 | 429 | SSH2Shell.prototype._nextPrimaryHost = function(connect) { 430 | if (this._hosts.length > 0) { 431 | if (this.sshObj.server) { 432 | if (this.sshObj.debug) { 433 | this.emit('msg', this.sshObj.server.host + ": Current primary host"); 434 | } 435 | if (this.sshObj.debug) { 436 | this.emit('msg', this.sshObj.server.host + ": Remove previous event handlers"); 437 | } 438 | this.emit('end', this._primaryhostSessionText, this.sshObj); 439 | this._removeListeners(); 440 | } 441 | this.sshObj = this._hosts.shift(); 442 | this._primaryhostSessionText = this.sshObj.server.host + ": "; 443 | if (this.sshObj.debug) { 444 | this.emit('msg', this.sshObj.server.host + ": Next primary host"); 445 | } 446 | return this._initiate(connect); 447 | } else { 448 | if (this.sshObj.debug) { 449 | this.emit('msg', this.sshObj.server.host + ": No more primary hosts"); 450 | } 451 | return this._runExit; 452 | } 453 | }; 454 | 455 | SSH2Shell.prototype._sshConnect = function() { 456 | var option, ref, sshFlags, sshOptions, value; 457 | sshFlags = "-x"; 458 | sshOptions = ""; 459 | if (this.sshObj.server.ssh) { 460 | if (this.sshObj.server.ssh.forceProtocolVersion) { 461 | sshFlags += this.sshObj.server.ssh.forceProtocolVersion; 462 | } 463 | if (this.sshObj.server.ssh.forceAddressType) { 464 | sshFlags += this.sshObj.server.ssh.forceAddressType; 465 | } 466 | if (this.sshObj.server.ssh.disablePseudoTTY) { 467 | sshFlags += "T"; 468 | } 469 | if (this.sshObj.server.ssh.forcePseudoTTY) { 470 | sshFlags += "t"; 471 | } 472 | if (this.sshObj.server.ssh.verbose) { 473 | sshFlags += "v"; 474 | } 475 | if (this.sshObj.server.ssh.cipherSpec) { 476 | sshOptions += " -c " + this.sshObj.server.ssh.cipherSpec; 477 | } 478 | if (this.sshObj.server.ssh.escape) { 479 | sshOptions += " -e " + this.sshObj.server.ssh.escape; 480 | } 481 | if (this.sshObj.server.ssh.logFile) { 482 | sshOptions += " -E " + this.sshObj.server.ssh.logFile; 483 | } 484 | if (this.sshObj.server.ssh.configFile) { 485 | sshOptions += " -F " + this.sshObj.server.ssh.configFile; 486 | } 487 | if (this.sshObj.server.ssh.identityFile) { 488 | sshOptions += " -i " + this.sshObj.server.ssh.identityFile; 489 | } 490 | if (this.sshObj.server.ssh.loginName) { 491 | sshOptions += " -l " + this.sshObj.server.ssh.loginName; 492 | } 493 | if (this.sshObj.server.ssh.macSpec) { 494 | sshOptions += " -m " + this.sshObj.server.ssh.macSpec; 495 | } 496 | ref = this.sshObj.server.ssh.Options; 497 | for (option in ref) { 498 | value = ref[option]; 499 | sshOptions += ' -o "#{option}={#value}"'; 500 | } 501 | } 502 | sshOptions += ' -o "StrictHostKeyChecking=no"'; 503 | sshOptions += " -p " + this.sshObj.server.port; 504 | this.sshObj.sshAuth = false; 505 | this.sshObj.sshToNextHost = true; 506 | this.command = "ssh " + sshFlags + " " + sshOptions + " " + this.sshObj.server.userName + "@" + this.sshObj.server.host; 507 | if (this.sshObj.debug) { 508 | this.emit('msg', this.sshObj.server.host + ": SSH command: connect"); 509 | } 510 | return this._runCommand(this.command); 511 | }; 512 | 513 | SSH2Shell.prototype._runExit = function() { 514 | if (this.sshObj.debug) { 515 | this.emit('msg', this.sshObj.server.host + ": Process an exit"); 516 | } 517 | if (this.sshObj.exitCommands && this.sshObj.exitCommands.length > 0) { 518 | if (this.sshObj.debug) { 519 | this.emit('msg', this.sshObj.server.host + ": Queued exit commands: " + this.sshObj.exitCommands.length); 520 | } 521 | this.command = this.sshObj.exitCommands.pop(); 522 | if (this._connections && this._connections.length > 0) { 523 | if (this.sshObj.verbose) { 524 | this.emit('msg', this.sshObj.server.host + ": Primary host: " + this._connections[0].server.host); 525 | } 526 | this._connections[0].sessionText += "\n" + this.sshObj.server.host + ": " + this.sshObj.sessionText; 527 | } 528 | return this._runCommand(this.command); 529 | } else if (this.sshObj.hosts && this.sshObj.hosts.length > 0) { 530 | if (this.sshObj.debug) { 531 | this.emit('msg', this.sshObj.server.host + ": Next host from this host"); 532 | } 533 | return this._nextHost(); 534 | } else if (this._connections && this._connections.length > 0) { 535 | if (this.sshObj.debug) { 536 | this.emit('msg', this.sshObj.server.host + ": load previous host"); 537 | } 538 | this.emit('msg', this.sshObj.server.host + ": " + this.sshObj.closedMessage); 539 | return this._previousHost(); 540 | } else if (this.command === "exit") { 541 | if (this.sshObj.debug) { 542 | this.emit('msg', this.sshObj.server.host + ": Manual exit command"); 543 | } 544 | return this._runCommand("exit"); 545 | } else { 546 | return this.close(); 547 | } 548 | }; 549 | 550 | SSH2Shell.prototype.close = function() { 551 | if (this.sshObj.debug) { 552 | this.emit('msg', this.sshObj.server.host + ": Exit command: Stream: close"); 553 | } 554 | return this._stream.close(); 555 | }; 556 | 557 | SSH2Shell.prototype._removeListeners = function() { 558 | 559 | /* 560 | if @sshObj.debug 561 | @.emit 'msg', "#{@sshObj.server.host}: Event handler count:" 562 | @.emit 'msg', "keyboard-interactive: " + (@.listenerCount 'keyboard-interactive') 563 | @.emit 'msg', "error: " + (@.listenerCount "error") 564 | @.emit 'msg', "data: " + (@.listenerCount "data") 565 | @.emit 'msg', "stderrData: " + (@.listenerCount "stderrData") 566 | @.emit 'msg', "end: " + (@.listenerCount 'end') 567 | @.emit 'msg', "commandProcessing: " + (@.listenerCount 'commandProcessing') 568 | @.emit 'msg', "commandComplete: " + (@.listenerCount 'commandComplete') 569 | @.emit 'msg', "commandTimeout: " + (@.listenerCount 'commandTimeout') 570 | @.emit 'msg', "msg: " + (@.listenerCount 'msg') 571 | */ 572 | if (typeof this.sshObj.onKeyboardInteractive === 'function') { 573 | this.removeListener("keyboard-interactive", this.sshObj.onKeyboardInteractive); 574 | } 575 | if (typeof this.sshObj.onStderrData === 'function') { 576 | this.removeListener("stderrData", this.sshObj.onStderrData); 577 | } 578 | if (typeof this.sshObj.onData === 'function') { 579 | this.removeListener("data", this.sshObj.onData); 580 | } 581 | if (typeof this.sshObj.onError === 'function') { 582 | this.removeListener("error", this.sshObj.onError); 583 | } 584 | if (typeof this.sshObj.onStderrData === 'function') { 585 | this.removeListener("stderrData", this.sshObj.onStderrData); 586 | } 587 | if (typeof this.sshObj.onEnd === 'function') { 588 | this.removeListener("end", this.sshObj.onEnd); 589 | } 590 | if (typeof this.sshObj.onCommandProcessing === 'function') { 591 | this.removeListener("commandProcessing", this.sshObj.onCommandProcessing); 592 | } 593 | if (typeof this.sshObj.onCommandComplete === 'function') { 594 | this.removeListener("commandComplete", this.sshObj.onCommandComplete); 595 | } 596 | if (typeof this.sshObj.onCommandTimeout === 'function') { 597 | this.removeListener("commandTimeout", this.sshObj.onCommandTimeout); 598 | } 599 | if (typeof this.sshObj.msg === 'function') { 600 | this.removeListener("msg", this.sshObj.msg); 601 | } 602 | if (this.idleTimer) { 603 | clearTimeout(this.idleTimer); 604 | } 605 | if (this.dataReceivedTimer) { 606 | return clearTimeout(this.dataReceivedTimer); 607 | } 608 | }; 609 | 610 | function SSH2Shell(hosts) { 611 | this._connect = bind(this._connect, this); 612 | this.connect = bind(this.connect, this); 613 | this._loadDefaults = bind(this._loadDefaults, this); 614 | this._initiate = bind(this._initiate, this); 615 | this._removeListeners = bind(this._removeListeners, this); 616 | this.close = bind(this.close, this); 617 | this._runExit = bind(this._runExit, this); 618 | this._sshConnect = bind(this._sshConnect, this); 619 | this._nextPrimaryHost = bind(this._nextPrimaryHost, this); 620 | this._nextHost = bind(this._nextHost, this); 621 | this._previousHost = bind(this._previousHost, this); 622 | this._runCommand = bind(this._runCommand, this); 623 | this._checkCommand = bind(this._checkCommand, this); 624 | this._nextCommand = bind(this._nextCommand, this); 625 | this._commandComplete = bind(this._commandComplete, this); 626 | this._processNotifications = bind(this._processNotifications, this); 627 | this._processSSHPrompt = bind(this._processSSHPrompt, this); 628 | this._resetFromSudoError = bind(this._resetFromSudoError, this); 629 | this._processPasswordPrompt = bind(this._processPasswordPrompt, this); 630 | this._sanitizeResponse = bind(this._sanitizeResponse, this); 631 | this._onData = bind(this._onData, this); 632 | this.onEnd = bind(this.onEnd, this); 633 | this.onCommandTimeout = bind(this.onCommandTimeout, this); 634 | this.onCommandComplete = bind(this.onCommandComplete, this); 635 | this.onCommandProcessing = bind(this.onCommandProcessing, this); 636 | this._callback = bind(this._callback, this); 637 | SSH2Shell.__super__.constructor.call(this, hosts); 638 | if (typeIsArray(hosts) && hosts.length > 0) { 639 | this._hosts = hosts; 640 | } else { 641 | this._hosts = [hosts]; 642 | } 643 | this.ssh2Client = require('ssh2').Client; 644 | this.on("msg", (function(_this) { 645 | return function(message) { 646 | return console.log(message); 647 | }; 648 | })(this)); 649 | this.on("newPrimmaryHost", this._nextPrimaryHost); 650 | this.on("data", (function(_this) { 651 | return function(data) { 652 | return _this._onData(data); 653 | }; 654 | })(this)); 655 | this.on("stderrData", (function(_this) { 656 | return function(data) { 657 | return console.error(data); 658 | }; 659 | })(this)); 660 | this.on("error", (function(_this) { 661 | return function(err, type, close, callback) { 662 | if (close == null) { 663 | close = false; 664 | } 665 | if (_this.sshObj.debug) { 666 | _this.emit('msg', "Class.error: " + err + ", " + type); 667 | } 668 | if (err instanceof Error) { 669 | _this.emit('msg', "Error: " + err.message + ", Level: " + err.level); 670 | } else { 671 | _this.emit('msg', (type + " error: ") + err); 672 | } 673 | if (close) { 674 | return _this._stream.close(); 675 | } 676 | }; 677 | })(this)); 678 | if (this._hosts[0].server.tryKeyboard) { 679 | this.on("keyboard-interactive", (function(_this) { 680 | return function(name, instructions, instructionsLang, prompts, finish) { 681 | var str; 682 | if (_this.sshObj.debug) { 683 | _this.emit('msg', _this.sshObj.server.host + ": Class.keyboard-interactive"); 684 | } 685 | if (_this.sshObj.verbose) { 686 | _this.emit('msg', "name: " + name); 687 | _this.emit('msg', "instructions: " + instructions); 688 | str = JSON.stringify(prompts, null, 4); 689 | return _this.emit('msg', "Prompts object: " + str); 690 | } 691 | }; 692 | })(this)); 693 | } 694 | this._allSessions = ""; 695 | } 696 | 697 | SSH2Shell.prototype._initiate = function(action) { 698 | if (this.sshObj.debug) { 699 | this.emit('msg', this.sshObj.server.host + ": initiate"); 700 | } 701 | this._loadDefaults(); 702 | if (typeof action === 'function') { 703 | return action(); 704 | } 705 | }; 706 | 707 | SSH2Shell.prototype._loadDefaults = function() { 708 | var ref, ref1, ref2, ref3, ref4, ref5, ref6; 709 | if (this.sshObj.msg && this.sshObj.msg.send && typeof this.sshObj.msg.send === 'function') { 710 | this.sshObj.msg = this.sshObj.msg.send; 711 | } 712 | if (this.listenerCount("msg") > 0) { 713 | this.removeAllListeners("msg"); 714 | } 715 | if (typeof this.sshObj.msg === 'function') { 716 | this.on("msg", this.sshObj.msg); 717 | } else { 718 | this.on("msg", (function(_this) { 719 | return function(message) { 720 | return console.log(message); 721 | }; 722 | })(this)); 723 | } 724 | if (this.sshObj.debug) { 725 | this.emit('msg', this.sshObj.server.host + ": Load Defaults"); 726 | } 727 | this.command = ""; 728 | this._buffer = ""; 729 | this.sshObj.sshToNextHost = false; 730 | if (!this.sshObj.connectedMessage) { 731 | this.sshObj.connectedMessage = "Connected"; 732 | } 733 | if (!this.sshObj.readyMessage) { 734 | this.sshObj.readyMessage = "Ready"; 735 | } 736 | if (!this.sshObj.closedMessage) { 737 | this.sshObj.closedMessage = "Closed"; 738 | } 739 | if (!this.sshObj.showBanner) { 740 | this.sshObj.showBanner = false; 741 | } 742 | if (!this.sshObj.verbose) { 743 | this.sshObj.verbose = false; 744 | } 745 | if (!this.sshObj.debug) { 746 | this.sshObj.debug = false; 747 | } 748 | if (!this.sshObj.hosts) { 749 | this.sshObj.hosts = []; 750 | } 751 | if (!this.sshObj.commands) { 752 | this.sshObj.commands = []; 753 | } 754 | if (!this.sshObj.standardPrompt) { 755 | this.sshObj.standardPrompt = ">$%#"; 756 | } 757 | if (!this.sshObj.passwordPrompt) { 758 | this.sshObj.passwordPrompt = this.sshObj.passwordPromt; 759 | } 760 | if (!this.sshObj.passphrasePrompt) { 761 | this.sshObj.passphrasePrompt = this.sshObj.passphrasePromt; 762 | } 763 | if (!this.sshObj.passwordPrompt) { 764 | this.sshObj.passwordPrompt = ":"; 765 | } 766 | if (!this.sshObj.passphrasePrompt) { 767 | this.sshObj.passphrasePrompt = ":"; 768 | } 769 | if (!this.sshObj.passPromptText) { 770 | this.sshObj.passPromptText = "Password"; 771 | } 772 | if (!this.sshObj.enter) { 773 | this.sshObj.enter = "\n"; 774 | } 775 | if (!this.sshObj.asciiFilter) { 776 | this.sshObj.asciiFilter = "[^\r\n\x20-\x7e]"; 777 | } 778 | if (this.sshObj.disableColorFilter !== true) { 779 | this.sshObj.disableColorFilter = false; 780 | } 781 | if (this.sshObj.disableASCIIFilter !== true) { 782 | this.sshObj.disableASCIIFilter = false; 783 | } 784 | if (!this.sshObj.textColorFilter) { 785 | this.sshObj.textColorFilter = "(\[{1}[0-9;]+m{1})"; 786 | } 787 | if (!this.sshObj.exitCommands) { 788 | this.sshObj.exitCommands = []; 789 | } 790 | this.sshObj.pwSent = false; 791 | this.sshObj.sshAuth = false; 792 | this.sshObj.server.hashKey = (ref = this.sshObj.server.hashKey) != null ? ref : ""; 793 | if (!this.sshObj.sessionText) { 794 | this.sshObj.sessionText = ""; 795 | } 796 | this.sshObj.streamEncoding = (ref1 = this.sshObj.streamEncoding) != null ? ref1 : "utf8"; 797 | if (!this.sshObj.window) { 798 | this.sshObj.window = true; 799 | } 800 | if (!this.sshObj.pty) { 801 | this.sshObj.pty = true; 802 | } 803 | this.idleTime = (ref2 = this.sshObj.idleTimeOut) != null ? ref2 : 5000; 804 | this.dataIdleTime = (ref3 = this.sshObj.dataIdleTime) != null ? ref3 : 500; 805 | if (!this.asciiFilter) { 806 | this.asciiFilter = new RegExp(this.sshObj.asciiFilter, "g"); 807 | } 808 | if (!this.textColorFilter) { 809 | this.textColorFilter = new RegExp(this.sshObj.textColorFilter, "g"); 810 | } 811 | if (!this.passwordPrompt) { 812 | this.passwordPrompt = new RegExp(this.sshObj.passPromptText + ".*" + this.sshObj.passwordPrompt + "\\s?$", "i"); 813 | } 814 | if (!this.passphrasePrompt) { 815 | this.passphrasePrompt = new RegExp(this.sshObj.passPromptText + ".*" + this.sshObj.passphrasePrompt + "\\s?$", "i"); 816 | } 817 | if (!this.standardPrompt) { 818 | this.standardPrompt = new RegExp("[" + this.sshObj.standardPrompt + "]\\s?$"); 819 | } 820 | this.sshObj.onCommandProcessing = (ref4 = this.sshObj.onCommandProcessing) != null ? ref4 : (function(_this) { 821 | return function(command, response, sshObj, stream) {}; 822 | })(this); 823 | this.sshObj.onCommandComplete = (ref5 = this.sshObj.onCommandComplete) != null ? ref5 : (function(_this) { 824 | return function(command, response, sshObj) { 825 | if (_this.sshObj.debug) { 826 | return _this.emit('msg', _this.sshObj.server.host + ": Class.commandComplete"); 827 | } 828 | }; 829 | })(this); 830 | this.sshObj.onCommandTimeout = (ref6 = this.sshObj.onCommandTimeout) != null ? ref6 : (function(_this) { 831 | return function(command, response, stream, connection) { 832 | response = response.replace(_this.command, ""); 833 | if (_this.sshObj.debug) { 834 | _this.emit('msg', _this.sshObj.server.host + ": Class.commandTimeout"); 835 | } 836 | if (_this.sshObj.verbose) { 837 | _this.emit('msg', _this.sshObj.server.host + ": Timeout command: " + command + " response: " + response); 838 | } 839 | _this.emit("error", _this.sshObj.server.host + ": Command timed out after " + (_this.idleTime / 1000) + " seconds", "Timeout", true, function(err, type) {}); 840 | return _this.sshObj.sessionText += _this._buffer; 841 | }; 842 | })(this); 843 | if (typeof this.sshObj.onKeyboardInteractive === 'function') { 844 | this.on("keyboard-interactive", this.sshObj.onKeyboardInteractive); 845 | } 846 | if (typeof this.sshObj.onError === 'function') { 847 | this.on("error", this.sshObj.onError); 848 | } 849 | if (typeof this.sshObj.onData === 'function') { 850 | this.on("data", this.sshObj.onData); 851 | } 852 | if (typeof this.sshObj.onStderrData === 'function') { 853 | this.on("stderrData", this.sshObj.onStderrData); 854 | } 855 | this.on("commandProcessing", this.sshObj.onCommandProcessing); 856 | this.on("commandComplete", this.sshObj.onCommandComplete); 857 | this.on("commandTimeout", this.sshObj.onCommandTimeout); 858 | if (typeof this.sshObj.onEnd === 'function') { 859 | this.on("end", this.sshObj.onEnd); 860 | } 861 | if (this.sshObj.debug) { 862 | this.emit('msg', this.sshObj.server.host + ": Host loaded"); 863 | } 864 | if (this.sshObj.verbose) { 865 | return this.emit('msg', this.sshObj); 866 | } 867 | }; 868 | 869 | SSH2Shell.prototype.connect = function(callback) { 870 | if (typeof callback === 'function') { 871 | this._callback = callback; 872 | } 873 | return this.emit("newPrimaryHost", this._nextPrimaryHost(this._connect)); 874 | }; 875 | 876 | SSH2Shell.prototype._connect = function() { 877 | var e; 878 | this.connection = new this.ssh2Client(); 879 | this.connection.on("keyboard-interactive", (function(_this) { 880 | return function(name, instructions, instructionsLang, prompts, finish) { 881 | if (_this.sshObj.debug) { 882 | _this.emit('msg', _this.sshObj.server.host + ": Connection.keyboard-interactive"); 883 | } 884 | return _this.emit("keyboard-interactive", name, instructions, instructionsLang, prompts, finish); 885 | }; 886 | })(this)); 887 | this.connection.on("connect", (function(_this) { 888 | return function() { 889 | if (_this.sshObj.debug) { 890 | _this.emit('msg', _this.sshObj.server.host + ": Connection.connect"); 891 | } 892 | return _this.emit('msg', _this.sshObj.connectedMessage); 893 | }; 894 | })(this)); 895 | this.connection.on("ready", (function(_this) { 896 | return function() { 897 | if (_this.sshObj.debug) { 898 | _this.emit('msg', _this.sshObj.server.host + ": Connection.ready"); 899 | } 900 | _this.emit('msg', _this.sshObj.readyMessage); 901 | return _this.connection.shell(_this.sshObj.window, { 902 | pty: _this.sshObj.pty 903 | }, function(err, _stream) { 904 | _this._stream = _stream; 905 | if (err instanceof Error) { 906 | _this.emit('error', err, "Shell", true); 907 | return; 908 | } 909 | if (_this.sshObj.debug) { 910 | _this.emit('msg', _this.sshObj.server.host + ": Connection.shell"); 911 | } 912 | _this._stream.setEncoding(_this.sshObj.streamEncoding); 913 | _this._stream.on("error", function(err) { 914 | if (_this.sshObj.debug) { 915 | _this.emit('msg', _this.sshObj.server.host + ": Stream.error"); 916 | } 917 | return _this.emit('error', err, "Stream"); 918 | }); 919 | _this._stream.stderr.on('data', function(data) { 920 | if (_this.sshObj.debug) { 921 | _this.emit('msg', _this.sshObj.server.host + ": Stream.stderr.data"); 922 | } 923 | return _this.emit('stderrData', data); 924 | }); 925 | _this._stream.on("data", function(data) { 926 | var e; 927 | try { 928 | return _this.emit('data', data); 929 | } catch (error) { 930 | e = error; 931 | err = new Error(e + " " + e.stack); 932 | err.level = "Data handling"; 933 | return _this.emit('error', err, "Stream.read", true); 934 | } 935 | }); 936 | _this._stream.on("finish", function() { 937 | if (_this.sshObj.debug) { 938 | _this.emit('msg', _this.sshObj.server.host + ": Stream.finish"); 939 | } 940 | _this._primaryhostSessionText += _this.sshObj.sessionText + "\n"; 941 | _this._allSessions += _this._primaryhostSessionText; 942 | if (_this._hosts.length === 0) { 943 | return _this.emit('end', _this._allSessions, _this.sshObj); 944 | } 945 | }); 946 | return _this._stream.on("close", function(code, signal) { 947 | if (_this.sshObj.debug) { 948 | _this.emit('msg', _this.sshObj.server.host + ": Stream.close"); 949 | } 950 | return _this.connection.end(); 951 | }); 952 | }); 953 | }; 954 | })(this)); 955 | this.connection.on("error", (function(_this) { 956 | return function(err) { 957 | if (_this.sshObj.debug) { 958 | _this.emit('msg', _this.sshObj.server.host + ": Connection.error"); 959 | } 960 | return _this.emit("error", err, "Connection"); 961 | }; 962 | })(this)); 963 | this.connection.on("close", (function(_this) { 964 | return function(had_error) { 965 | if (_this.sshObj.debug) { 966 | _this.emit('msg', _this.sshObj.server.host + ": Connection.close"); 967 | } 968 | if (_this.idleTimer) { 969 | clearTimeout(_this.idleTimer); 970 | } 971 | if (_this.dataReceivedTimer) { 972 | clearTimeout(_this.dataReceivedTimer); 973 | } 974 | if (had_error) { 975 | _this.emit("error", had_error, "Connection close"); 976 | } else { 977 | _this.emit('msg', _this.sshObj.closedMessage); 978 | } 979 | if (_this._hosts.length === 0) { 980 | if (typeof _this._callback === 'function') { 981 | _this._callback(_this._allSessions); 982 | _this._removeListeners(); 983 | return _this._allSessions; 984 | } 985 | } else { 986 | return _this.emit("newPrimaryHost", _this._nextPrimaryHost(_this._connect)); 987 | } 988 | }; 989 | })(this)); 990 | if (this.sshObj.server && this.sshObj.commands) { 991 | try { 992 | if (!this.sshObj.server.username) { 993 | this.sshObj.server.username = this.sshObj.server.userName; 994 | } 995 | if (!this.sshObj.server.hostHash) { 996 | this.sshObj.server.hostHash = this.sshObj.server.hashMethod; 997 | } 998 | if (!this.sshObj.server.passphrase) { 999 | this.sshObj.server.passphrase = this.sshObj.server.passPhrase; 1000 | } 1001 | this.connection.connect(this.sshObj.server); 1002 | } catch (error) { 1003 | e = error; 1004 | this.emit('error', e + " " + e.stack, "Connection.connect", true); 1005 | } 1006 | } else { 1007 | this.emit('error', "Missing connection parameters", "Parameters", false, function(err, type, close) { 1008 | this.emit('msg', this.sshObj.server); 1009 | return this.emit('msg', this.sshObj.commands); 1010 | }); 1011 | } 1012 | return this._stream; 1013 | }; 1014 | 1015 | return SSH2Shell; 1016 | 1017 | })(Stream); 1018 | 1019 | module.exports = SSH2Shell; 1020 | 1021 | }).call(this); 1022 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssh2shell", 3 | "version": "2.0.9", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "asn1": { 8 | "version": "0.2.4", 9 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 10 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 11 | "requires": { 12 | "safer-buffer": "~2.1.0" 13 | } 14 | }, 15 | "bcrypt-pbkdf": { 16 | "version": "1.0.2", 17 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 18 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 19 | "requires": { 20 | "tweetnacl": "^0.14.3" 21 | } 22 | }, 23 | "coffee-script": { 24 | "version": "1.12.7", 25 | "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", 26 | "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", 27 | "dev": true 28 | }, 29 | "cpu-features": { 30 | "version": "0.0.2", 31 | "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.2.tgz", 32 | "integrity": "sha512-/2yieBqvMcRj8McNzkycjW2v3OIUOibBfd2dLEJ0nWts8NobAxwiyw9phVNS6oDL8x8tz9F7uNVFEVpJncQpeA==", 33 | "optional": true, 34 | "requires": { 35 | "nan": "^2.14.1" 36 | } 37 | }, 38 | "dotenv": { 39 | "version": "8.6.0", 40 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", 41 | "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", 42 | "dev": true 43 | }, 44 | "nan": { 45 | "version": "2.15.0", 46 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", 47 | "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", 48 | "optional": true 49 | }, 50 | "safer-buffer": { 51 | "version": "2.1.2", 52 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 53 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 54 | }, 55 | "ssh2": { 56 | "version": "1.4.0", 57 | "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.4.0.tgz", 58 | "integrity": "sha512-XvXwcXKvS452DyQvCa6Ct+chpucwc/UyxgliYz+rWXJ3jDHdtBb9xgmxJdMmnIn5bpgGAEV3KaEsH98ZGPHqwg==", 59 | "requires": { 60 | "asn1": "^0.2.4", 61 | "bcrypt-pbkdf": "^1.0.2", 62 | "cpu-features": "0.0.2", 63 | "nan": "^2.15.0" 64 | } 65 | }, 66 | "tweetnacl": { 67 | "version": "0.14.5", 68 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 69 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssh2shell", 3 | "version": "2.0.9", 4 | "author": "cmp-202", 5 | "description": "A wrapper class for ssh2 to run multiple sequential commands in an SSH shell session, handle command responses, and tunnel through to other hosts using nested host objects", 6 | "main": "./lib/ssh2shell.js", 7 | "engines": { 8 | "node": ">=4.5.0" 9 | }, 10 | "dependencies": { 11 | "ssh2": "^1.4.0" 12 | }, 13 | "devDependencies": { 14 | "coffee-script": "^1.12.7", 15 | "dotenv": "^8.6.0" 16 | }, 17 | "scripts": {}, 18 | "keywords": [ 19 | "SSH", 20 | "ssh2", 21 | "shell", 22 | "server", 23 | "client", 24 | "multiple", 25 | "command", 26 | "commands", 27 | "tunnel", 28 | "deploy", 29 | "host", 30 | "hosts", 31 | "automation", 32 | "fingerprint", 33 | "validation", 34 | "hash", 35 | "cipher", 36 | "persistent", 37 | "sequential", 38 | "console", 39 | "sudo", 40 | "su", 41 | "prompt", 42 | "session", 43 | "connection" 44 | ], 45 | "repository": { 46 | "type": "git", 47 | "url": "http://github.com/cmp-202/ssh2shell" 48 | }, 49 | "bugs": { 50 | "url": "https://github.com/cmp-202/ssh2shell/issues" 51 | }, 52 | "homepage": "https://github.com/cmp-202/ssh2shell/wiki", 53 | "directories": { 54 | "test": "test" 55 | }, 56 | "license": "ISC" 57 | } 58 | -------------------------------------------------------------------------------- /src/ssh2shell.coffee: -------------------------------------------------------------------------------- 1 | #================================ 2 | # SSH2Shel 3 | #================================ 4 | # Description 5 | # SSH2 wrapper for creating a SSH shell connection and running multiple commands sequentially. 6 | typeIsArray = Array.isArray || ( value ) -> return {}.toString.call( value ) is '[object Array]' 7 | Stream = require('stream'); 8 | 9 | class SSH2Shell extends Stream 10 | sshObj: {} 11 | command: "" 12 | _hosts: [] 13 | _sshToNextHost: false 14 | _primaryhostSessionText: "" 15 | _allSessions: "" 16 | _connections: [] 17 | _stream: {} 18 | _buffer: "" 19 | idleTime: 5000 20 | asciiFilter: "" 21 | textColorFilter: "" 22 | passwordPrompt: "" 23 | passphrasePrompt: "" 24 | standardPrompt: "" 25 | _callback: => 26 | onCommandProcessing:=> 27 | onCommandComplete: => 28 | onCommandTimeout: => 29 | onEnd: => 30 | 31 | _onData: ( data )=> 32 | #add host response data to buffer 33 | 34 | @_buffer += data 35 | if @command.length > 0 and not @standardPrompt.test(@_sanitizeResponse()) 36 | #continue loading the buffer and set/reset a timeout 37 | @.emit 'commandProcessing' , @command, @_buffer, @sshObj, @_stream 38 | clearTimeout @idleTimer if @idleTimer 39 | @idleTimer = setTimeout( => 40 | @.emit 'commandTimeout', @.command, @._buffer, @._stream, @.connection 41 | , @idleTime) 42 | else if @command.length < 1 and not @standardPrompt.test(@_buffer) 43 | @.emit 'commandProcessing' , @command, @_buffer, @sshObj, @_stream 44 | 45 | #Set a timer to fire when no more data events are received 46 | #and then process the buffer based on command and prompt combinations 47 | clearTimeout @dataReceivedTimer if @dataReceivedTimer 48 | @dataReceivedTimer = setTimeout( => 49 | #clear the command and SSH timeout timer 50 | clearTimeout @idleTimer if @idleTimer 51 | 52 | #remove test coloring from responses like [32m[31m 53 | unless @.sshObj.disableColorFilter 54 | @_buffer = @_buffer.replace(@textColorFilter, "") 55 | 56 | #remove non-standard ascii from terminal responses 57 | unless @.sshObj.disableASCIIFilter 58 | @_buffer = @_buffer.replace(@asciiFilter, "") 59 | 60 | switch (true) 61 | #check if sudo password is needed 62 | when @command.length > 0 and @command.indexOf("sudo ") isnt -1 63 | @emit 'msg', "#{@sshObj.server.host}: Sudo command data" if @sshObj.debug 64 | @_processPasswordPrompt() 65 | #check if ssh authentication needs to be handled 66 | when @sshObj.sshToNextHost and @command.length > 0 and @command.indexOf("ssh ") is 0 67 | @emit 'msg', "#{@sshObj.server.host}: SSH command data" if @sshObj.debug 68 | @_processSSHPrompt() 69 | #check for standard prompt from a command 70 | when @command.length > 0 and @standardPrompt.test(@_sanitizeResponse()) 71 | @emit 'msg', "#{@sshObj.server.host}: Normal prompt detected" if @sshObj.debug 72 | @sshObj.pwSent = false #reset sudo prompt checkable 73 | @_commandComplete() 74 | #check for no command but first prompt detected 75 | when @command.length < 1 and @standardPrompt.test(@_buffer) 76 | @emit 'msg', "#{@sshObj.server.host}: First prompt detected" if @sshObj.debug 77 | @sshObj.sessionText += @_buffer if @sshObj.showBanner 78 | @_nextCommand() 79 | else 80 | @emit 'msg', "Data processing: data received timeout" if @sshObj.debug 81 | @idleTimer = setTimeout( => 82 | @.emit 'commandTimeout', @.command, @._buffer, @._stream, @.connection 83 | , @idleTime) 84 | , @dataIdleTime) 85 | 86 | _sanitizeResponse: => 87 | return @_buffer.replace(@command.substr(0, @_buffer.length), "") 88 | 89 | _processPasswordPrompt: => 90 | #First test for password prompt 91 | response = @_sanitizeResponse().trim() 92 | passwordPrompt = @passwordPrompt.test(response) 93 | standardPrompt = @standardPrompt.test(response) 94 | @.emit 'msg', "#{@sshObj.server.host}: Sudo Password prompt detected: #{passwordPrompt}" if @sshObj.debug 95 | @.emit 'msg', "#{@sshObj.server.host}: Sudo Password prompt: Password sent: #{@sshObj.pwSent}" if @sshObj.debug 96 | @.emit 'msg', "#{@sshObj.server.host}: Sudo Password: #{@sshObj.server.password}" if @sshObj.debug 97 | @.emit 'msg', "#{@sshObj.server.host}: Sudo Response: #{response}" if @sshObj.verbose 98 | 99 | #Prompt detection 100 | #no password 101 | switch (true) 102 | when passwordPrompt and not @sshObj.server.password 103 | @.emit 'msg', "#{@sshObj.server.host}: Sudo password prompt no password set" if @sshObj.debug 104 | @_resetFromSudoError() 105 | when passwordPrompt and not @sshObj.pwSent 106 | @.emit 'msg', "#{@sshObj.server.host}: Sudo password prompt: Buffer: #{response}" if @sshObj.verbose 107 | @.emit 'msg', "#{@sshObj.server.host}: Sudo password prompt: Send password " if @sshObj.debug 108 | @.emit 'msg', "#{@sshObj.server.host}: Sudo sent password: #{@sshObj.server.password}" if @sshObj.verbose 109 | #send password 110 | @sshObj.pwSent = true 111 | @_runCommand("#{@sshObj.server.password}") 112 | when passwordPrompt and @sshObj.pwSent 113 | @.emit 'msg', "#{@sshObj.server.host}: Sudo password faied: response: #{response}" if @sshObj.verbose 114 | @.emit 'error', "#{@sshObj.server.host}: Sudo password was incorrect for #{@sshObj.server.userName}, Sudo authentication" if @sshObj.debug 115 | @.emit 'msg', "#{@sshObj.server.host}: Sudo failed password prompt: Password: [#{@sshObj.server.password}]" if @sshObj.debug 116 | @sshObj.pwSent = false 117 | #add buffer to sessionText so the sudo response can be seen 118 | @_resetFromSudoError() 119 | when standardPrompt 120 | @.emit 'msg', "#{@sshObj.server.host}: Sudo password accepted" if @sshObj.debug 121 | @sshObj.pwSent = false 122 | @.emit 'msg', "#{@sshObj.server.host}: Sudo Standard Response: #{response}" if @sshObj.verbose 123 | @_commandComplete() 124 | else 125 | @idleTimer = setTimeout( => 126 | @.emit 'commandTimeout', @.command, response, @._stream, @._connection 127 | , @idleTime) 128 | 129 | _resetFromSudoError: => 130 | @sshObj.pwSent = false 131 | @sshObj.sessionText += "#{@_buffer}" 132 | @_buffer = "" 133 | @command = "" 134 | #cancal command on host 135 | @_stream.write '\x03' 136 | 137 | _processSSHPrompt: => 138 | #not authenticated yet so detect prompts 139 | response = @_sanitizeResponse().trim() 140 | clearTimeout @idleTimer if @idleTimer 141 | passwordPrompt = @passwordPrompt.test(response) and not @sshObj.server.hasOwnProperty("passPhrase") 142 | passphrasePrompt = @passphrasePrompt.test(response) and @sshObj.server.hasOwnProperty("passPhrase") 143 | standardPrompt = @standardPrompt.test(response) 144 | 145 | @.emit 'msg', "#{@sshObj.server.host}: SSH: Password previously sent: #{@sshObj.sshAuth}" if @sshObj.verbose 146 | 147 | unless @sshObj.sshAuth 148 | @.emit 'msg', "#{@sshObj.server.host}: First SSH prompt detection" if @sshObj.debug 149 | 150 | #provide password 151 | if passwordPrompt 152 | @.emit 'msg', "#{@sshObj.server.host}: SSH send password" if @sshObj.debug 153 | @sshObj.sshAuth = true 154 | @_buffer = "" 155 | 156 | @_runCommand("#{@sshObj.server.password}") 157 | #provide passphrase 158 | else if passphrasePrompt 159 | @.emit 'msg', "#{@sshObj.server.host}: SSH send passphrase" if @sshObj.debug 160 | @sshObj.sshAuth = true 161 | @_buffer = "" 162 | @_runCommand("#{@sshObj.server.passPhrase}") 163 | #normal prompt so continue with next command 164 | else if standardPrompt 165 | @.emit 'msg', "#{@sshObj.server.host}: SSH standard prompt: connection failed" if @sshObj.debug 166 | @.emit 'msg', "#{@sshObj.server.host}: SSH connection failed" 167 | @.emit 'msg', "#{@sshObj.server.host}: SSH failed response: #{response}" 168 | @sshObj.sessionText += "#{@sshObj.server.host}: SSH failed: response: #{response}" 169 | @_runExit() 170 | else 171 | @.emit 'msg', "#{@sshObj.server.host}: SSH no prompt was not detected" 172 | @.emit 'msg', "#{@sshObj.server.host}: SSH response: #{response}" if @sshObj.verbose 173 | @.emit 'msg', "#{@sshObj.server.host}: SSH password prompt: #{@sshObj.passwordPrompt}" if @sshObj.verbose and not @sshObj.server.hasOwnProperty("password") 174 | @.emit 'msg', "#{@sshObj.server.host}: SSH passphrase prompt: #{@sshObj.passphrasePrompt}" if @sshObj.verbose and @sshObj.server.hasOwnProperty("passPhrase") 175 | @.emit 'msg', "#{@sshObj.server.host}: SSH standard prompt: #{@sshObj.standardPrompt}" if @sshObj.verbose 176 | @_runExit() 177 | else 178 | @.emit 'msg', "#{@sshObj.server.host}: SSH post authentication prompt detection" if @sshObj.debug 179 | #@sshObj.sshToNextHost = false 180 | 181 | if standardPrompt 182 | @.emit 'msg', "#{@sshObj.server.host}: SSH complete: normal prompt" if @sshObj.debug 183 | @sshObj.sessionText += "\n #{response}" if @sshObj.showBanner 184 | @sshObj.exitCommands.push "exit" 185 | @.emit 'msg', "#{@sshObj.connectedMessage}" 186 | @.emit 'msg', "#{@sshObj.readyMessage}" 187 | @.emit 'msg', "#{@sshObj.server.host}: SSH complete: next command" if @sshObj.debug 188 | @sshObj.sshToNextHost = false 189 | @_nextCommand() 190 | #Password or passphase detected a second time after authentication indicating failure. 191 | else if (passwordPrompt or passphrasePrompt) 192 | @.emit 'msg', "#{@sshObj.server.host}: SSH authentication failed" 193 | @.emit 'msg', "#{@sshObj.server.host}: SSH auth failed" if @sshObj.debug 194 | @.emit 'msg', "#{@sshObj.server.host}: SSH: failed response: #{response}" if @sshObj.verbose 195 | @sshObj.sshAuth = false 196 | using = switch 197 | when passwordPrompt then "SSH password: #{@sshObj.server.password}" 198 | when passphrasePrompt then "SSH passphrase: #{@sshObj.server.passPhrase}" 199 | @.emit 'error', "#{@sshObj.server.host}: SSH authentication failed for #{@sshObj.server.userName}@#{@sshObj.server.host}", "Nested host authentication" 200 | @.emit 'msg', "#{@sshObj.server.host}: SSH auth failed: Using " + using if @sshObj.debug 201 | 202 | #no connection so drop back to first host settings if there was one 203 | #@sshObj.sessionText += "#{@_buffer}" 204 | @.emit 'msg', "#{@sshObj.server.host}: SSH response: #{response}" if @sshObj.verbose 205 | if @_connections.length > 0 206 | return @_previousHost() 207 | 208 | @_runExit() 209 | 210 | _processNotifications: => 211 | #check for notifications in commands 212 | if @command 213 | #this is a message for the sessionText like an echo command in bash 214 | if (sessionNote = @command.match(/^`(.*)`$/)) 215 | @.emit 'msg', "#{@sshObj.server.host}: Notifications: sessionText output" if @sshObj.debug 216 | if @_connections.length > 0 217 | @sshObj.sessionText += "#{@sshObj.server.host}: Note: #{sessionNote[1]}#{@sshObj.enter}" 218 | else 219 | @sshObj.sessionText += "Note: #{sessionNote[1]}#{@sshObj.enter}" 220 | @.emit 'msg', sessionNote[1] if @sshObj.verbose 221 | @_nextCommand() 222 | #this is a message to output in process 223 | else if (msgNote = @command.match(/^msg:(.*)$/)) 224 | @.emit 'msg', "#{@sshObj.server.host}: Notifications: msg to output" if @sshObj.debug 225 | @.emit 'msg', "#{@sshObj.server.host}: Note: #{msgNote[1]}" 226 | @_nextCommand() 227 | else 228 | @.emit 'msg', "#{@sshObj.server.host}: Notifications: Normal Command to run" if @sshObj.debug 229 | @_checkCommand() 230 | 231 | _commandComplete: => 232 | response = @_buffer.trim() 233 | #check sudo su has been authenticated and add an extra exit command 234 | if @command isnt "" and @command isnt "exit" and @sshObj.sshToNextHost isnt true 235 | @.emit 'msg', "#{@sshObj.server.host}: Command complete:\nCommand:\n #{@command}\nResponse: #{response}" if @sshObj.verbose 236 | @sshObj.sessionText += response 237 | @.emit 'msg', "#{@sshObj.server.host}: Raising commandComplete event" if @sshObj.debug 238 | @.emit 'commandComplete', @command, @_buffer, @sshObj 239 | @_nextCommand() 240 | 241 | _nextCommand: => 242 | @_buffer = "" 243 | #process the next command if there are any 244 | if @sshObj.commands.length > 0 245 | @.emit 'msg', "#{@sshObj.server.host}: Host.commands: #{@sshObj.commands}" if @sshObj.verbose 246 | @command = @sshObj.commands.shift() 247 | @.emit 'msg', "#{@sshObj.server.host}: Next command from host.commands: #{@command}" if @sshObj.verbose 248 | @_processNotifications() 249 | else 250 | @.emit 'msg', "#{@sshObj.server.host}: No commands so exit" if @sshObj.debug 251 | #no more commands so exit 252 | @_runExit() 253 | 254 | _checkCommand: => 255 | #if there is still a command to run then run it or exit 256 | if @command != "" 257 | @_runCommand(@command) 258 | else 259 | #no more commands so exit 260 | @.emit 'msg', "#{@sshObj.server.host}: No command so exit" if @sshObj.debug 261 | @_runExit() 262 | 263 | _runCommand: (command) => 264 | @.emit 'msg', "#{@sshObj.server.host}: sending: #{command}" if @sshObj.verbose 265 | @.emit 'msg', "#{@sshObj.server.host}: run command" if @sshObj.debug 266 | @_stream.write "#{command}#{@sshObj.enter}" 267 | 268 | _previousHost: => 269 | @.emit 'msg', "#{@sshObj.server.host}: Load previous host config" if @sshObj.debug 270 | @.emit 'end', "#{@sshObj.server.host}: \n#{@sshObj.sessionText}", @sshObj 271 | @.emit 'msg', "#{@sshObj.server.host}: Previous hosts: #{@_connections.length}" if @sshObj.debug 272 | 273 | if @_connections.length > 0 274 | @sshObj = @_connections.pop() 275 | @.emit 'msg', "#{@sshObj.server.host}: Reload previous host" if @sshObj.debug 276 | #@_loadDefaults(@_runExit) 277 | @_runExit() 278 | else 279 | @_runExit() 280 | 281 | 282 | _nextHost: => 283 | nextHost = @sshObj.hosts.shift() 284 | @.emit 'msg', "#{@sshObj.server.host}: SSH to #{nextHost.server.host}" if @sshObj.debug 285 | @.emit 'msg', "#{@sshObj.server.host}: Clearing previous event handlers" if @sshObj.debug 286 | @.emit 'msg', "#{@sshObj.server.host}: Remove previous event handlers" if @sshObj.debug 287 | @_removeListeners() 288 | 289 | @_connections.push(@sshObj) 290 | 291 | @sshObj = nextHost 292 | 293 | @_initiate(@_sshConnect) 294 | 295 | 296 | _nextPrimaryHost: ( connect )=> 297 | if @_hosts.length > 0 298 | #check this is not loading the first primary host before removing listeners 299 | if @sshObj.server 300 | @.emit 'msg', "#{@sshObj.server.host}: Current primary host" if @sshObj.debug 301 | @.emit 'msg', "#{@sshObj.server.host}: Remove previous event handlers" if @sshObj.debug 302 | @.emit 'end', @_primaryhostSessionText, @sshObj 303 | @_removeListeners() 304 | @sshObj = @_hosts.shift() 305 | @_primaryhostSessionText = "#{@sshObj.server.host}: " 306 | 307 | @.emit 'msg', "#{@sshObj.server.host}: Next primary host" if @sshObj.debug 308 | 309 | @_initiate(connect) 310 | else 311 | @.emit 'msg', "#{@sshObj.server.host}: No more primary hosts" if @sshObj.debug 312 | @_runExit 313 | 314 | 315 | _sshConnect: => 316 | 317 | #add ssh commandline options from host.server.ssh 318 | sshFlags = "-x" 319 | sshOptions = "" 320 | if @sshObj.server.ssh 321 | sshFlags += @sshObj.server.ssh.forceProtocolVersion if @sshObj.server.ssh.forceProtocolVersion 322 | sshFlags += @sshObj.server.ssh.forceAddressType if @sshObj.server.ssh.forceAddressType 323 | sshFlags += "T" if @sshObj.server.ssh.disablePseudoTTY 324 | sshFlags += "t" if @sshObj.server.ssh.forcePseudoTTY 325 | sshFlags += "v" if @sshObj.server.ssh.verbose 326 | 327 | sshOptions += " -c " + @sshObj.server.ssh.cipherSpec if @sshObj.server.ssh.cipherSpec 328 | sshOptions += " -e " + @sshObj.server.ssh.escape if @sshObj.server.ssh.escape 329 | sshOptions += " -E " + @sshObj.server.ssh.logFile if @sshObj.server.ssh.logFile 330 | sshOptions += " -F " + @sshObj.server.ssh.configFile if @sshObj.server.ssh.configFile 331 | sshOptions += " -i " + @sshObj.server.ssh.identityFile if @sshObj.server.ssh.identityFile 332 | sshOptions += " -l " + @sshObj.server.ssh.loginName if @sshObj.server.ssh.loginName 333 | sshOptions += " -m " + @sshObj.server.ssh.macSpec if @sshObj.server.ssh.macSpec 334 | sshOptions += ' -o "#{option}={#value}"' for option,value of @sshObj.server.ssh.Options 335 | sshOptions += ' -o "StrictHostKeyChecking=no"' 336 | sshOptions += " -p #{@sshObj.server.port}" 337 | @sshObj.sshAuth = false 338 | @sshObj.sshToNextHost = true 339 | @command = "ssh #{sshFlags} #{sshOptions} #{@sshObj.server.userName}@#{@sshObj.server.host}" 340 | @.emit 'msg', "#{@sshObj.server.host}: SSH command: connect" if @sshObj.debug 341 | @_runCommand(@command) 342 | 343 | _runExit: => 344 | @.emit 'msg', "#{@sshObj.server.host}: Process an exit" if @sshObj.debug 345 | #run the exit commands loaded by ssh and sudo su commands 346 | if @sshObj.exitCommands and @sshObj.exitCommands.length > 0 347 | @.emit 'msg', "#{@sshObj.server.host}: Queued exit commands: #{@sshObj.exitCommands.length}" if @sshObj.debug 348 | @command = @sshObj.exitCommands.pop() 349 | if @_connections and @_connections.length > 0 350 | @.emit 'msg', "#{@sshObj.server.host}: Primary host: #{@_connections[0].server.host}" if @sshObj.verbose 351 | @_connections[0].sessionText += "\n#{@sshObj.server.host}: #{@sshObj.sessionText}" 352 | @_runCommand(@command) 353 | #more hosts to connect to so process the next one 354 | else if @sshObj.hosts and @sshObj.hosts.length > 0 355 | @.emit 'msg', "#{@sshObj.server.host}: Next host from this host" if @sshObj.debug 356 | @_nextHost() 357 | #Leaving last host so load previous host 358 | else if @_connections and @_connections.length > 0 359 | @.emit 'msg', "#{@sshObj.server.host}: load previous host" if @sshObj.debug 360 | @.emit 'msg', "#{@sshObj.server.host}: #{@sshObj.closedMessage}" 361 | @_previousHost() 362 | else if @command is "exit" 363 | @.emit 'msg', "#{@sshObj.server.host}: Manual exit command" if @sshObj.debug 364 | @_runCommand("exit") 365 | #Nothing more to do so end the stream with last exit 366 | else 367 | @close() 368 | 369 | close: => 370 | @.emit 'msg', "#{@sshObj.server.host}: Exit command: Stream: close" if @sshObj.debug 371 | @_stream.close() #"exit#{@sshObj.enter}" 372 | 373 | _removeListeners: => 374 | ### 375 | if @sshObj.debug 376 | @.emit 'msg', "#{@sshObj.server.host}: Event handler count:" 377 | @.emit 'msg', "keyboard-interactive: " + (@.listenerCount 'keyboard-interactive') 378 | @.emit 'msg', "error: " + (@.listenerCount "error") 379 | @.emit 'msg', "data: " + (@.listenerCount "data") 380 | @.emit 'msg', "stderrData: " + (@.listenerCount "stderrData") 381 | @.emit 'msg', "end: " + (@.listenerCount 'end') 382 | @.emit 'msg', "commandProcessing: " + (@.listenerCount 'commandProcessing') 383 | @.emit 'msg', "commandComplete: " + (@.listenerCount 'commandComplete') 384 | @.emit 'msg', "commandTimeout: " + (@.listenerCount 'commandTimeout') 385 | @.emit 'msg', "msg: " + (@.listenerCount 'msg') 386 | ### 387 | #changed to removing host defined listeners instead of all listeners 388 | @.removeListener "keyboard-interactive", @sshObj.onKeyboardInteractive if typeof @sshObj.onKeyboardInteractive == 'function' 389 | @.removeListener "stderrData", @sshObj.onStderrData if typeof @sshObj.onStderrData == 'function' 390 | @.removeListener "data", @sshObj.onData if typeof @sshObj.onData == 'function' 391 | @.removeListener "error", @sshObj.onError if typeof @sshObj.onError == 'function' 392 | @.removeListener "stderrData", @sshObj.onStderrData if typeof @sshObj.onStderrData == 'function' 393 | @.removeListener "end", @sshObj.onEnd if typeof @sshObj.onEnd == 'function' 394 | @.removeListener "commandProcessing", @sshObj.onCommandProcessing if typeof @sshObj.onCommandProcessing == 'function' 395 | @.removeListener "commandComplete", @sshObj.onCommandComplete if typeof @sshObj.onCommandComplete == 'function' 396 | @.removeListener "commandTimeout", @sshObj.onCommandTimeout if typeof @sshObj.onCommandTimeout == 'function' 397 | @.removeListener "msg", @sshObj.msg if typeof @sshObj.msg == 'function' 398 | 399 | clearTimeout @idleTimer if @idleTimer 400 | clearTimeout @dataReceivedTimer if @dataReceivedTimer 401 | 402 | constructor: (hosts) -> 403 | super hosts 404 | if typeIsArray(hosts) and hosts.length > 0 405 | @_hosts = hosts 406 | else 407 | @_hosts = [hosts] 408 | 409 | @ssh2Client = require('ssh2').Client 410 | 411 | #defined here to support msg events before the host is loaded 412 | @.on "msg", ( message ) => 413 | console.log message 414 | 415 | @.on "newPrimmaryHost", @_nextPrimaryHost 416 | 417 | @.on "data", (data) => 418 | @_onData( data ) 419 | 420 | @.on "stderrData", (data) => 421 | console.error data 422 | 423 | @.on "error", (err, type, close = false, callback) => 424 | @.emit 'msg', "Class.error: #{err}, #{type}" if @sshObj.debug 425 | if ( err instanceof Error ) 426 | @.emit 'msg', "Error: " + err.message + ", Level: " + err.level 427 | else 428 | @.emit 'msg', "#{type} error: " + err 429 | 430 | @_stream.close() if close 431 | 432 | if @_hosts[0].server.tryKeyboard 433 | @.on "keyboard-interactive", ( name, instructions, instructionsLang, prompts, finish ) => 434 | @.emit 'msg', "#{@sshObj.server.host}: Class.keyboard-interactive" if @sshObj.debug 435 | if @sshObj.verbose 436 | @.emit 'msg', "name: " + name 437 | @.emit 'msg', "instructions: " + instructions 438 | str = JSON.stringify(prompts, null, 4) 439 | @.emit 'msg', "Prompts object: " + str 440 | 441 | @_allSessions = "" 442 | 443 | 444 | _initiate: (action)=> 445 | @.emit 'msg', "#{@sshObj.server.host}: initiate" if @sshObj.debug 446 | 447 | @_loadDefaults() 448 | #event handlers 449 | 450 | if typeof action == 'function' 451 | action() 452 | 453 | _loadDefaults: () => 454 | 455 | #old hubot leftovers 456 | if @sshObj.msg and @sshObj.msg.send and typeof @sshObj.msg.send == 'function' 457 | @sshObj.msg = @sshObj.msg.send 458 | 459 | if @.listenerCount("msg") > 0 460 | @.removeAllListeners "msg" 461 | 462 | if typeof @sshObj.msg == 'function' 463 | @.on "msg", @sshObj.msg 464 | else 465 | @.on "msg", ( message ) => 466 | console.log message 467 | 468 | @.emit 'msg', "#{@sshObj.server.host}: Load Defaults" if @sshObj.debug 469 | @command = "" 470 | @_buffer = "" 471 | @sshObj.sshToNextHost = false 472 | @sshObj.connectedMessage = "Connected" unless @sshObj.connectedMessage 473 | @sshObj.readyMessage = "Ready" unless @sshObj.readyMessage 474 | @sshObj.closedMessage = "Closed" unless @sshObj.closedMessage 475 | @sshObj.showBanner = false unless @sshObj.showBanner 476 | @sshObj.verbose = false unless @sshObj.verbose 477 | @sshObj.debug = false unless @sshObj.debug 478 | @sshObj.hosts = [] unless @sshObj.hosts 479 | @sshObj.commands = [] unless @sshObj.commands 480 | @sshObj.standardPrompt = ">$%#" unless @sshObj.standardPrompt 481 | @sshObj.passwordPrompt = @sshObj.passwordPromt unless @sshObj.passwordPrompt 482 | @sshObj.passphrasePrompt = @sshObj.passphrasePromt unless @sshObj.passphrasePrompt 483 | @sshObj.passwordPrompt = ":" unless @sshObj.passwordPrompt 484 | @sshObj.passphrasePrompt = ":" unless @sshObj.passphrasePrompt 485 | @sshObj.passPromptText = "Password" unless @sshObj.passPromptText 486 | @sshObj.enter = "\n" unless @sshObj.enter #windows = "\r\n", Linux = "\n", Mac = "\r" 487 | @sshObj.asciiFilter = "[^\r\n\x20-\x7e]" unless @sshObj.asciiFilter 488 | @sshObj.disableColorFilter = false unless @sshObj.disableColorFilter is true 489 | @sshObj.disableASCIIFilter = false unless @sshObj.disableASCIIFilter is true 490 | @sshObj.textColorFilter = "(\[{1}[0-9;]+m{1})" unless @sshObj.textColorFilter 491 | @sshObj.exitCommands = [] unless @sshObj.exitCommands 492 | @sshObj.pwSent = false 493 | @sshObj.sshAuth = false 494 | @sshObj.server.hashKey = @sshObj.server.hashKey ? "" 495 | @sshObj.sessionText = "" unless @sshObj.sessionText 496 | @sshObj.streamEncoding = @sshObj.streamEncoding ? "utf8" 497 | @sshObj.window = true unless @sshObj.window 498 | @sshObj.pty = true unless @sshObj.pty 499 | @idleTime = @sshObj.idleTimeOut ? 5000 500 | @dataIdleTime = @sshObj.dataIdleTime ? 500 501 | @asciiFilter = new RegExp(@sshObj.asciiFilter,"g") unless @asciiFilter 502 | @textColorFilter = new RegExp(@sshObj.textColorFilter,"g") unless @textColorFilter 503 | @passwordPrompt = new RegExp(@sshObj.passPromptText+".*" + @sshObj.passwordPrompt + "\\s?$","i") unless @passwordPrompt 504 | @passphrasePrompt = new RegExp(@sshObj.passPromptText+".*" + @sshObj.passphrasePrompt + "\\s?$","i") unless @passphrasePrompt 505 | @standardPrompt = new RegExp("[" + @sshObj.standardPrompt + "]\\s?$") unless @standardPrompt 506 | 507 | 508 | @sshObj.onCommandProcessing = @sshObj.onCommandProcessing ? ( command, response, sshObj, stream ) => 509 | 510 | @sshObj.onCommandComplete = @sshObj.onCommandComplete ? ( command, response, sshObj ) => 511 | @.emit 'msg', "#{@sshObj.server.host}: Class.commandComplete" if @sshObj.debug 512 | 513 | @sshObj.onCommandTimeout = @sshObj.onCommandTimeout ? ( command, response, stream, connection ) => 514 | response = response.replace(@command, "") 515 | @.emit 'msg', "#{@sshObj.server.host}: Class.commandTimeout" if @sshObj.debug 516 | @.emit 'msg', "#{@sshObj.server.host}: Timeout command: #{command} response: #{response}" if @sshObj.verbose 517 | 518 | @.emit "error", "#{@sshObj.server.host}: Command timed out after #{@.idleTime/1000} seconds", "Timeout", true, (err, type)=> 519 | @sshObj.sessionText += @_buffer 520 | 521 | @.on "keyboard-interactive", @sshObj.onKeyboardInteractive if typeof @sshObj.onKeyboardInteractive == 'function' 522 | @.on "error", @sshObj.onError if typeof @sshObj.onError == 'function' 523 | @.on "data", @sshObj.onData if typeof @sshObj.onData == 'function' 524 | @.on "stderrData", @sshObj.onStderrData if typeof @sshObj.onStderrData == 'function' 525 | @.on "commandProcessing", @sshObj.onCommandProcessing 526 | @.on "commandComplete", @sshObj.onCommandComplete 527 | @.on "commandTimeout", @sshObj.onCommandTimeout 528 | @.on "end", @sshObj.onEnd if typeof @sshObj.onEnd == 'function' 529 | 530 | @.emit 'msg', "#{@sshObj.server.host}: Host loaded" if @sshObj.debug 531 | @.emit 'msg', @sshObj if @sshObj.verbose 532 | 533 | 534 | 535 | connect: (callback)=> 536 | @_callback = callback if typeof callback == 'function' 537 | 538 | @.emit "newPrimaryHost", @_nextPrimaryHost(@_connect) 539 | 540 | _connect: => 541 | 542 | @connection = new @ssh2Client() 543 | 544 | @connection.on "keyboard-interactive", (name, instructions, instructionsLang, prompts, finish) => 545 | @.emit 'msg', "#{@sshObj.server.host}: Connection.keyboard-interactive" if @sshObj.debug 546 | @.emit "keyboard-interactive", name, instructions, instructionsLang, prompts, finish 547 | 548 | @connection.on "connect", => 549 | @.emit 'msg', "#{@sshObj.server.host}: Connection.connect" if @sshObj.debug 550 | @.emit 'msg', @sshObj.connectedMessage 551 | 552 | @connection.on "ready", => 553 | @.emit 'msg', "#{@sshObj.server.host}: Connection.ready" if @sshObj.debug 554 | @.emit 'msg', @sshObj.readyMessage 555 | 556 | #open a shell 557 | @connection.shell @sshObj.window, { pty: @sshObj.pty }, (err, @_stream) => 558 | if err instanceof Error 559 | @.emit 'error', err, "Shell", true 560 | return 561 | @.emit 'msg', "#{@sshObj.server.host}: Connection.shell" if @sshObj.debug 562 | @_stream.setEncoding(@sshObj.streamEncoding); 563 | 564 | @_stream.on "error", (err) => 565 | @.emit 'msg', "#{@sshObj.server.host}: Stream.error" if @sshObj.debug 566 | @.emit 'error', err, "Stream" 567 | 568 | @_stream.stderr.on 'data', (data) => 569 | @.emit 'msg', "#{@sshObj.server.host}: Stream.stderr.data" if @sshObj.debug 570 | @.emit 'stderrData', data 571 | 572 | @_stream.on "data", (data)=> 573 | try 574 | @.emit 'data', data 575 | catch e 576 | err = new Error("#{e} #{e.stack}") 577 | err.level = "Data handling" 578 | @.emit 'error', err, "Stream.read", true 579 | 580 | @_stream.on "finish", => 581 | @.emit 'msg', "#{@sshObj.server.host}: Stream.finish" if @sshObj.debug 582 | 583 | @_primaryhostSessionText += @sshObj.sessionText+"\n" 584 | @_allSessions += @_primaryhostSessionText 585 | if @_hosts.length == 0 586 | @.emit 'end', @_allSessions, @sshObj 587 | 588 | @_stream.on "close", (code, signal) => 589 | @.emit 'msg', "#{@sshObj.server.host}: Stream.close" if @sshObj.debug 590 | @connection.end() 591 | 592 | @connection.on "error", (err) => 593 | @.emit 'msg', "#{@sshObj.server.host}: Connection.error" if @sshObj.debug 594 | @.emit "error", err, "Connection" 595 | 596 | 597 | @connection.on "close", (had_error) => 598 | @.emit 'msg', "#{@sshObj.server.host}: Connection.close" if @sshObj.debug 599 | clearTimeout @idleTimer if @idleTimer 600 | clearTimeout @dataReceivedTimer if @dataReceivedTimer 601 | if had_error 602 | @.emit "error", had_error, "Connection close" 603 | else 604 | @.emit 'msg', @sshObj.closedMessage 605 | 606 | if @_hosts.length == 0 607 | if typeof @_callback == 'function' 608 | @_callback @_allSessions 609 | @_removeListeners() 610 | return @_allSessions 611 | else 612 | @.emit "newPrimaryHost", @_nextPrimaryHost(@_connect) 613 | 614 | 615 | if @sshObj.server and @sshObj.commands 616 | try 617 | @sshObj.server.username = @sshObj.server.userName unless @sshObj.server.username 618 | @sshObj.server.hostHash = @sshObj.server.hashMethod unless @sshObj.server.hostHash 619 | @sshObj.server.passphrase = @sshObj.server.passPhrase unless @sshObj.server.passphrase 620 | @connection.connect @sshObj.server 621 | catch e 622 | @.emit 'error', "#{e} #{e.stack}", "Connection.connect", true 623 | else 624 | @.emit 'error', "Missing connection parameters", "Parameters", false, ( err, type, close ) -> 625 | @.emit 'msg', @sshObj.server 626 | @.emit 'msg', @sshObj.commands 627 | return @_stream 628 | 629 | 630 | module.exports = SSH2Shell 631 | -------------------------------------------------------------------------------- /test/.ignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmp-202/ssh2shell/fb62685cac8647a973adcae5a89c4e6dd0907f00/test/.ignore -------------------------------------------------------------------------------- /test/chaining.js: -------------------------------------------------------------------------------- 1 | var dotenv = require('dotenv').config(), 2 | debug = false, 3 | verbose = false, 4 | util = require('util'); 5 | 6 | var host = { 7 | server: { 8 | host: process.env.HOST, 9 | port: process.env.PORT, 10 | userName: process.env.USER_NAME, 11 | password: process.env.PASSWORD 12 | }, 13 | commands: ["echo chaining test success"], 14 | debug: debug, 15 | verbose: verbose 16 | }; 17 | 18 | var SSH2Shell = require ('../lib/ssh2shell'); 19 | 20 | //run the commands in the shell session 21 | 22 | var SSH = new SSH2Shell(host) 23 | 24 | var SSHconnect = util.promisify(SSH.connect) 25 | SSHconnect() 26 | .then(( sessionText ) => { 27 | console.log ( sessionText);}) 28 | .catch((error) => { 29 | console.log (error); 30 | }); -------------------------------------------------------------------------------- /test/devtest.js: -------------------------------------------------------------------------------- 1 | var dotenv = require('dotenv').config() 2 | 3 | var host = { 4 | server: { 5 | host: process.env.HOST, 6 | port: process.env.PORT, 7 | userName: process.env.USER_NAME, 8 | password: process.env.PASSWORD 9 | }, 10 | commands: ["ls -la"], 11 | msg: { 12 | send: function( message ) { 13 | console.log(message); 14 | } 15 | }, 16 | verbose: true, 17 | debug: true, 18 | onEnd: function (sessionText, sshObj) { 19 | setTimeout(() => { 20 | console.log(sessionText) 21 | }, 2000) 22 | }, 23 | onError: function (error) { 24 | console.log('there was an error') 25 | }, 26 | onCommandTimeout: function (command) { 27 | console.log('there was an timeout') 28 | reject('Timed Out!') 29 | } 30 | }; 31 | //until npm published use the cloned dir path. 32 | var SSH2Shell = require ('../lib/ssh2shell'); 33 | 34 | //run the commands in the shell session 35 | var SSH = new SSH2Shell(host); 36 | 37 | SSH.connect(); -------------------------------------------------------------------------------- /test/issue_69.js: -------------------------------------------------------------------------------- 1 | var dotenv = require('dotenv').config(), 2 | debug = true, 3 | verbose = true 4 | 5 | var host1 = { 6 | server: { 7 | host: process.env.HOST, 8 | port: process.env.PORT, 9 | userName: process.env.USER_NAME, 10 | password: process.env.PASSWORD 11 | }, 12 | connectedMessage:'connected to host1', 13 | debug: debug, 14 | verbose: verbose 15 | 16 | }; 17 | 18 | var host2 = { 19 | server: { 20 | host: process.env.SERVER2_HOST, 21 | port: process.env.PORT, 22 | userName: process.env.SERVER2_USER_NAME, 23 | password: process.env.SERVER2_PASSWORD 24 | 25 | }, 26 | connectedMessage: 'connected to host2', 27 | debug: debug, 28 | verbose: verbose 29 | }; 30 | 31 | var host3 = { 32 | server: { 33 | host: process.env.SERVER3_HOST, 34 | port: process.env.PORT, 35 | userName: process.env.SERVER3_USER_NAME, 36 | password: process.env.SERVER3_PASSWORD 37 | 38 | }, 39 | commands: ['echo server3'], 40 | connectedMessage:'connected to host3', 41 | debug: debug, 42 | verbose: verbose 43 | }; 44 | 45 | host2.hosts = [host3]; 46 | host1.hosts = [host2]; 47 | 48 | var SSH2Shell = require ('../lib/ssh2shell'), 49 | 50 | SSH = new SSH2Shell(host1); 51 | 52 | var callback = function( sessionText ){ 53 | console.log ( "-----Callback session text:\n" + sessionText); 54 | console.log ( "-----Callback end" ); 55 | } 56 | 57 | SSH.connect(callback); -------------------------------------------------------------------------------- /test/keyboard-interactive.js: -------------------------------------------------------------------------------- 1 | var dotenv = require('dotenv').config() 2 | 3 | var host = { 4 | server: { 5 | host: process.env.HOST, 6 | port: process.env.PORT, 7 | userName: process.env.USER_NAME, 8 | password: process.env.PASSWORD, 9 | tryKeyboard: true 10 | }, 11 | commands: [ 12 | "ls -la" 13 | ], 14 | verbose: false, 15 | debug: false, 16 | onEnd: function( sessionText, sshObj ){ 17 | this.emit("msg", sessionText); 18 | } 19 | }; 20 | //until npm published use the cloned dir path. 21 | var SSH2Shell = require ('../lib/ssh2shell'); 22 | 23 | //run the commands in the shell session 24 | var SSH = new SSH2Shell(host); 25 | 26 | SSH.connect(); -------------------------------------------------------------------------------- /test/lots_of_commands.js: -------------------------------------------------------------------------------- 1 | var dotenv = require('dotenv').config(), 2 | fs = require('fs') 3 | 4 | var host = { 5 | server: { 6 | host: process.env.HOST, 7 | port: process.env.PORT, 8 | userName: process.env.USER_NAME, 9 | password: process.env.PASSWORD 10 | }, 11 | debug: false, 12 | commands: [ 13 | "msg: first", 14 | "sudo apt-get -y update", 15 | "sudo apt-get -y update", 16 | "sudo apt-get -y update", 17 | "sudo apt-get -y update", 18 | "sudo apt-get -y update", 19 | "sudo apt-get -y update", 20 | "sudo apt-get -y update", 21 | "sudo apt-get -y update", 22 | "sudo apt-get -y update", 23 | "sudo apt-get -y update", 24 | "msg: 11", 25 | "sudo apt-get -y update", 26 | "sudo apt-get -y update", 27 | "sudo apt-get -y update", 28 | "sudo apt-get -y update", 29 | "sudo apt-get -y update", 30 | "sudo apt-get -y update", 31 | "sudo apt-get -y update", 32 | "sudo apt-get -y update", 33 | "sudo apt-get -y update", 34 | "sudo apt-get -y update", 35 | "msg: 21", 36 | "sudo apt-get -y update", 37 | "sudo apt-get -y update", 38 | "sudo apt-get -y update", 39 | "sudo apt-get -y update", 40 | "sudo apt-get -y update", 41 | "sudo apt-get -y update", 42 | "sudo apt-get -y update", 43 | "sudo apt-get -y update", 44 | "sudo apt-get -y update", 45 | "sudo apt-get -y update", 46 | "sudo apt-get -y update", 47 | "sudo apt-get -y update", 48 | "sudo apt-get -y update", 49 | "sudo apt-get -y update", 50 | "sudo apt-get -y update", 51 | "sudo apt-get -y update", 52 | "sudo apt-get -y update", 53 | "sudo apt-get -y update", 54 | "sudo apt-get -y update", 55 | "sudo apt-get -y update", 56 | "sudo apt-get -y update", 57 | "sudo apt-get -y update", 58 | "sudo apt-get -y update", 59 | "sudo apt-get -y update", 60 | "sudo apt-get -y update", 61 | "sudo apt-get -y update", 62 | "sudo apt-get -y update", 63 | "sudo apt-get -y update", 64 | "sudo apt-get -y update", 65 | "sudo apt-get -y update", 66 | "sudo apt-get -y update", 67 | "sudo apt-get -y update", 68 | "sudo apt-get -y update", 69 | "sudo apt-get -y update", 70 | "sudo apt-get -y update", 71 | "sudo apt-get -y update", 72 | "sudo apt-get -y update", 73 | "sudo apt-get -y update", 74 | "sudo apt-get -y update", 75 | "sudo apt-get -y update", 76 | "sudo apt-get -y update", 77 | "sudo apt-get -y update" 78 | ] 79 | }; 80 | //until npm published use the cloned dir path. 81 | var SSH2Shell = require ('../lib/ssh2shell'); 82 | 83 | //run the commands in the shell session 84 | var SSH = new SSH2Shell(host), 85 | locLog = fs.createWriteStream('Lots of commands.log') 86 | 87 | SSH.pipe( locLog ); 88 | SSH.connect(); -------------------------------------------------------------------------------- /test/multiple_primary_hosts.js: -------------------------------------------------------------------------------- 1 | var dotenv = require('dotenv').config(), 2 | debug = false, 3 | verbose = false 4 | 5 | 6 | //Host objects: 7 | var host1 = { 8 | server: { 9 | host: process.env.HOST, 10 | port: process.env.PORT, 11 | userName: process.env.USER_NAME, 12 | password: process.env.PASSWORD 13 | }, 14 | commands: ["echo Host_1"], 15 | connectedMessage: "Connected to host1", 16 | debug: debug, 17 | verbose: verbose 18 | }, 19 | 20 | host2 = { 21 | server: { 22 | host: process.env.SERVER2_HOST, 23 | port: process.env.PORT, 24 | userName: process.env.SERVER2_USER_NAME, 25 | password: process.env.SERVER2_PASSWORD 26 | }, 27 | commands: ["echo Host_2"], 28 | connectedMessage: "Connected to host2", 29 | debug: debug, 30 | verbose: verbose 31 | }, 32 | 33 | host3 = { 34 | server: { 35 | host: process.env.SERVER3_HOST, 36 | port: process.env.PORT, 37 | userName: process.env.SERVER3_USER_NAME, 38 | password: process.env.SERVER3_PASSWORD 39 | }, 40 | commands: ["echo Host_3"], 41 | connectedMessage: "Connected to host3", 42 | debug: debug, 43 | verbose: verbose, 44 | //Event handler only used by this host 45 | onCommandComplete: function( command, response, sshObj ) { 46 | this.emit("msg", sshObj.server.host + ": commandComplete only used on this host"); 47 | } 48 | } 49 | 50 | 51 | var SSH2Shell = require ('../lib/ssh2shell'), 52 | SSH = new SSH2Shell([host1,host2,host3]), 53 | callback = function( sessionText ){ 54 | console.log ( "-----Callback session text:\n" + sessionText); 55 | console.log ( "-----Callback end" ); 56 | } 57 | 58 | 59 | SSH.on ('end', function( sessionText, sshObj ) { 60 | this.emit("msg", sshObj.server.host + ": onEnd every host"); 61 | }) 62 | SSH.connect(callback); 63 | 64 | 65 | -------------------------------------------------------------------------------- /test/notifications.js: -------------------------------------------------------------------------------- 1 | var dotenv = require('dotenv').config() 2 | 3 | var host = { 4 | server: { 5 | host: process.env.HOST, 6 | port: process.env.PORT, 7 | userName: process.env.USER_NAME, 8 | password: process.env.PASSWORD 9 | }, 10 | commands: [ 11 | //hubot throwback 12 | "msg: using msg.send", 13 | "`All done!`", 14 | ], 15 | msg: { 16 | send: function( message ) { 17 | console.log(message); 18 | } 19 | }, 20 | verbose: false, 21 | debug: false 22 | }; 23 | 24 | //until npm published use the cloned dir path. 25 | var SSH2Shell = require ('../lib/ssh2shell'); 26 | 27 | //run the commands in the shell session 28 | var SSH = new SSH2Shell(host); 29 | 30 | var callback = function( sessionText ){ 31 | //show the full session output. This could be emailed or saved to a log file. 32 | console.log("\nThis is the full session response: \n\n" + sessionText); 33 | } 34 | 35 | SSH.connect(callback); 36 | 37 | -------------------------------------------------------------------------------- /test/pipe.js: -------------------------------------------------------------------------------- 1 | var dotenv = require('dotenv').config(), 2 | fs = require('fs') 3 | 4 | var host = { 5 | server: { 6 | host: process.env.HOST, 7 | port: process.env.PORT, 8 | userName: process.env.USER_NAME, 9 | password: process.env.PASSWORD, 10 | }, 11 | commands: [ 12 | "echo host pipe" 13 | ], 14 | debug: false 15 | } 16 | //until npm published use the cloned dir path. 17 | var SSH2Shell = require ('../lib/ssh2shell') 18 | 19 | //run the commands in the shell session 20 | var SSH = new SSH2Shell(host), 21 | callback = function( sessionText ){ 22 | console.log ( "-----Callback session text:\n" + sessionText); 23 | console.log ( "-----Callback end" ); 24 | }, 25 | firstLog = fs.createWriteStream('first.log'), 26 | secondLog = fs.createWriteStream('second.log') 27 | 28 | SSH.pipe(firstLog) 29 | SSH.pipe(secondLog) 30 | SSH.connect(callback) -------------------------------------------------------------------------------- /test/simple.js: -------------------------------------------------------------------------------- 1 | var dotenv = require('dotenv').config(), 2 | debug = false, 3 | verbose = false 4 | 5 | var host = { 6 | server: { 7 | host: process.env.HOST, 8 | port: process.env.PORT, 9 | userName: process.env.USER_NAME, 10 | password: process.env.PASSWORD 11 | }, 12 | commands: ["echo basic test success"], 13 | debug: debug, 14 | verbose: verbose 15 | }; 16 | 17 | var SSH2Shell = require ('../lib/ssh2shell'); 18 | 19 | //run the commands in the shell session 20 | var SSH = new SSH2Shell(host), 21 | callback = function( sessionText ){ 22 | console.log ( "-----Callback session text:\n" + sessionText); 23 | console.log ( "-----Callback end" ); 24 | } 25 | 26 | SSH.connect(callback) -------------------------------------------------------------------------------- /test/ssh_user_command.js: -------------------------------------------------------------------------------- 1 | var dotenv = require('dotenv').config() 2 | 3 | var sshObj = { 4 | server: { 5 | host: process.env.HOST, 6 | port: process.env.PORT, 7 | userName: process.env.USER_NAME, 8 | password: process.env.PASSWORD, 9 | }, 10 | commands: [ 11 | "echo ssh", 12 | "ssh -V", 13 | "echo ssh middle" 14 | ], 15 | verbose: false, 16 | debug: false 17 | }; 18 | //until npm published use the cloned dir path. 19 | var SSH2Shell = require ('../lib/ssh2shell'); 20 | 21 | //run the commands in the shell session 22 | var SSH = new SSH2Shell(sshObj); 23 | SSH.on('end', function( sessionText, sshObj ){ 24 | this.emit('msg', sessionText); 25 | }) 26 | SSH.connect(); -------------------------------------------------------------------------------- /test/sudo_no_pass.js: -------------------------------------------------------------------------------- 1 | var dotenv = require('dotenv').config(); 2 | 3 | /* 4 | * Used `sudo visudo` to add `[username] ALL=(ALL) NOPASSWD: ALL` on the first test 5 | * server to test no password required detecting normal prompt correctly to run the 6 | * next command 7 | */ 8 | var sshObj = { 9 | server: { 10 | host: process.env.SERVER2_HOST, 11 | port: process.env.PORT, 12 | userName: process.env.SERVER2_USER_NAME, 13 | password: process.env.SERVER2_PASSWORD 14 | }, 15 | commands: ["sudo apt-get update","sudo apt-get upgrade", "sudo apt autoremove"], 16 | userPromptSent: false, 17 | debug: false, 18 | verbose: false, 19 | onCommandProcessing: function( command, response, sshObj, stream ) { 20 | //Check the command and prompt exits and respond with a 'y' but only does it once 21 | if (response.indexOf("[Y/n]") != -1 && !sshObj.userPromptSent) { 22 | sshObj.userPromptSent = true 23 | stream.write("n\n") 24 | } 25 | }, 26 | onCommandComplete: function( command, response, sshObj ) { 27 | if(response.indexOf("[Y/n]") != -1 && sshObj.userPromptSent){ 28 | sshObj.userPromptSent = false; 29 | } 30 | } 31 | } 32 | //until npm published use the cloned dir path. 33 | var SSH2Shell = require ('../lib/ssh2shell'); 34 | 35 | //run the commands in the shell session 36 | var SSH = new SSH2Shell(sshObj); 37 | var callback = function(sessionText, sshObj) { 38 | console.log(`Session End: ` + sessionText); 39 | } 40 | SSH.connect(callback); 41 | -------------------------------------------------------------------------------- /test/sudo_su.js: -------------------------------------------------------------------------------- 1 | var dotenv = require('dotenv').config(); 2 | 3 | /* 4 | * Used `sudo visudo` to add `[username] ALL=(ALL) NOPASSWD: ALL` on the first test 5 | * server to test no password required detecting normal prompt correctly to run the 6 | * next command 7 | */ 8 | var sshObj = { 9 | server: { 10 | host: process.env.HOST, 11 | port: process.env.PORT, 12 | userName: process.env.USER_NAME, 13 | password: process.env.PASSWORD 14 | }, 15 | commands: [ 16 | "msg:Showing current directory", 17 | "echo \$(pwd)", 18 | "ls -al", 19 | "sudo -i", 20 | "msg:Showing root home directory", 21 | "cd \~", 22 | "echo \$(pwd)", 23 | "ls -al", 24 | "msg:exiting root", 25 | "exit", 26 | "msg:Changing to " + process.env.secondaryUser + " via su [username]", 27 | "su " + process.env.secondaryUser, 28 | "msg:Showing user home directory", 29 | "cd \~", 30 | "echo \$(pwd)", 31 | "ls -al", 32 | "msg:exiting user ", 33 | "exit", 34 | "msg:Changing user via sudo -u [username] -i", 35 | "sudo -u " + process.env.USER_NAME + " -i", 36 | "msg:Showing user directory", 37 | "cd \~", 38 | "echo \$(pwd)", 39 | "ls -la" 40 | ], 41 | msg: { 42 | send: function( message ) { 43 | console.log(message); 44 | } 45 | }, 46 | debug: false, 47 | verbose: false, 48 | suPassSent: false, //used by commandProcessing to only send password once 49 | rootPassSent: false, 50 | onCommandProcessing: function( command, response, sshObj, stream ) { 51 | //console.log("command processing:\ncommand: " + command + ", response: " + response + ", password sent: " + sshObj.rootPassSent + ", password: " + process.env.rootPassword); 52 | 53 | if (command === "su " + process.env.secondaryUser && response.indexOf("Password: ") != -1 && sshObj.suPassSent != true) { 54 | sshObj.commands.unshift("msg:Using secondary user password"); 55 | //this is required to stop "bounce" without this the password would be sent multiple times 56 | sshObj.suPassSent = true; 57 | stream.write(process.env.secondUserPassword + "\n"); 58 | }/* else if (command == "sudo -i" && response.match(/:\s$/i) && sshObj.rootPassSent != true) { 59 | sshObj.commands.unshift("msg:Using root user password"); 60 | //this is required to stop "bounce" without this the password would be sent multiple times 61 | sshObj.rootPassSent = true; 62 | stream.write(process.env.PASSWORD + "\n"); 63 | }*/ 64 | }, 65 | onEnd: function ( sessionText, sshObj ) { 66 | if(this.sshObj.debug){this.emit("msg", sshObj.server.host + ": host.onEnd")}; 67 | //show the full session output. This could be emailed or saved to a log file. 68 | this.emit("msg", "\nThis is the full session responses:\n" + sessionText); 69 | } 70 | }; 71 | //until npm published use the cloned dir path. 72 | var SSH2Shell = require ('../lib/ssh2shell'); 73 | 74 | //run the commands in the shell session 75 | var SSH = new SSH2Shell(sshObj); 76 | 77 | SSH.connect(); 78 | -------------------------------------------------------------------------------- /test/timeout.js: -------------------------------------------------------------------------------- 1 | //Every data response from the server resets the command timeout timer 2 | //So if you use stream.write() to send something to the server from within the timeout function 3 | //and the server responds then the timer is reset and the normal process starts again 4 | //If you are using the host.onCommandTimeout to send something to the host and it doesn't 5 | //respond then the script will hang. there is no way to reset the timeout timer. 6 | //If you set the host onCommandTimeout to do nothing an attach commandTimeout to the instance 7 | //you will be able to tigger a new timeout timer by using 8 | /* 9 | self.sshObj.idleTimer = setTimeout(function(){ 10 | self.emit('commandTimeout', command, response, stream, connection ) 11 | }, self._idleTime) 12 | */ 13 | //commandTimeout is actually a `didn't detect a defined prompt` timeout 14 | 15 | var dotenv = require('dotenv').config(), 16 | debug=true, 17 | verbose=false 18 | 19 | //Host objects: 20 | var host = { 21 | server: { 22 | host: process.env.HOST, 23 | port: process.env.PORT, 24 | userName: process.env.USER_NAME, 25 | password: process.env.PASSWORD 26 | }, 27 | commands: [ 28 | "msg:Testing idle time out", 29 | "read -n 1 -p \"First prompt to put valid input (Y,n): \" test", 30 | "read -n 1 -p \"Final unhandled time out: \" test", 31 | ], 32 | //can't use # and > because prompt detection triggers in the wrong place 33 | standardPrompt: "$", 34 | verbose: verbose, 35 | debug: debug, 36 | idleTimeOut: 5000, 37 | FirstPass: false, 38 | SecondPass: false, 39 | onCommandTimeout: function( command, response, stream, connection ){ 40 | //this.emit("msg", "response: [[" + response + " ]]") 41 | if(this.sshObj.debug){this.emit("msg", this.sshObj.server.host + ": host.onCommandTimeout")} 42 | 43 | var errorMessage, errorSource 44 | //this.emit("msg", this.sshObj.server.host + ": fp:" +this.sshObj.FirstPass) 45 | 46 | if (response.indexOf("(Y,n)") != -1 && this.sshObj.FirstPass != true){ 47 | if(this.sshObj.debug){this.emit("msg", this.sshObj.server.host + ": First prompt using correct input: Sending 'y' to the `(y,n):` prompt")} 48 | this.sshObj.sessionText += response + this.sshObj.enter 49 | this._buffer = "" 50 | this.sshObj.FirstPass = true 51 | 52 | stream.write("y" + this.sshObj.enter) 53 | return 54 | } 55 | 56 | if(this.sshObj.debug){this.emit("msg", this.sshObj.server.host + "Final timeout: All attempts completed")} 57 | if(this.sshObj.debug){this.emit("msg", this.sshObj.server.host + ": Timeout details:" + this.sshObj.enter + "Command: " + command + " " + this.sshObj.enter + "Response: " + response)} 58 | 59 | //everything failed so update sessionText and raise an error event that closes the connection 60 | if(!errorMessage){errorMessage = "Command"} 61 | if(!errorSource){errorSource = "Command Timeout"} 62 | this.emit("msg", this.sshObj.server.host + ": " + errorMessage + " timed out after " + (this.idleTime / 1000) + " seconds", errorSource, true) 63 | //this.emit("end", response, this.sshObj) 64 | this.sshObj.sessionText += response + this.sshObj.enter 65 | this.close() 66 | }, 67 | 68 | onEnd: function( sessionText, sshObj ) { 69 | if(this.sshObj.debug){this.emit("msg", sshObj.server.host + ": host.onEnd")} 70 | //show the full session output. self could be emailed or saved to a log file. 71 | this.emit("msg", "\nThis is the full session response:\n\n" + this.sshObj.sessionText + "\n") 72 | //this.close() 73 | } 74 | } 75 | //host1.hosts=[host] 76 | //until npm published use the cloned dir path. 77 | var SSH2Shell = require ('../lib/ssh2shell') 78 | 79 | //run the commands in the shell session 80 | var SSH = new SSH2Shell(host) 81 | 82 | SSH.connect() 83 | -------------------------------------------------------------------------------- /test/tunnel.js: -------------------------------------------------------------------------------- 1 | var dotenv = require('dotenv').config(); 2 | 3 | var conParamsHost1 = { 4 | host: process.env.HOST, 5 | port: process.env.PORT, 6 | userName: process.env.USER_NAME, 7 | password: process.env.PASSWORD 8 | }, 9 | conParamsHost2 = { 10 | host: process.env.SERVER2_HOST, 11 | port: process.env.PORT, 12 | userName: process.env.SERVER2_USER_NAME, 13 | password: process.env.SERVER2_PASSWORD 14 | }, 15 | //set to fail 16 | conParamsHost3 = { 17 | host: process.env.SERVER3_HOST, 18 | port: process.env.PORT, 19 | userName: process.env.SERVER3_USER_NAME, 20 | password: process.env.SERVER3_PASSWORD 21 | }, 22 | debug = true, 23 | verbose = false 24 | 25 | //Host objects: 26 | var host1 = { 27 | server: conParamsHost1, 28 | commands: [ "echo host1"], 29 | connectedMessage: "Connected to host1", 30 | debug: debug, 31 | verbose: verbose 32 | }, 33 | 34 | host2 = { 35 | server: conParamsHost2, 36 | commands: [ "echo host2" ], 37 | connectedMessage: "Connected to host2", 38 | debug: debug, 39 | verbose: verbose 40 | }, 41 | 42 | host3 = { 43 | server: conParamsHost3, 44 | commands: [ "echo host3"], 45 | connectedMessage: "Connected to host3", 46 | debug: debug, 47 | verbose: verbose 48 | } 49 | 50 | //host2.hosts = [ host3 ]; 51 | //Set the two hosts you are tunnelling to through host1 52 | host1.hosts = [ host2, host3 ]; 53 | 54 | //or the alternative nested tunnelling method outlined above: 55 | //host2.hosts = [ host3 ];ssh -q george@192.168.0.129 "echo 2>&1" && echo OK || echo NOK 56 | //host1.hosts = [ host2 ]; 57 | 58 | //Create the new instance 59 | //or SSH2Shell = require ('ssh2shell') 60 | var SSH2Shell = require ('../lib/ssh2shell'), 61 | SSH = new SSH2Shell(host1), 62 | callback = function( sessionText ){ 63 | console.log ( "-----Callback session text:\n" + sessionText); 64 | console.log ( "-----Callback end" ); 65 | } 66 | 67 | 68 | //Start the process 69 | SSH.connect(callback); 70 | 71 | 72 | -------------------------------------------------------------------------------- /test/tunnel_ssh_user_commands.js: -------------------------------------------------------------------------------- 1 | var dotenv = require('dotenv').config(); 2 | 3 | var conParamsHost1 = { 4 | host: process.env.HOST, 5 | port: process.env.PORT, 6 | userName: process.env.USER_NAME, 7 | password: process.env.PASSWORD 8 | }, 9 | conParamsHost2 = { 10 | host: process.env.SERVER2_HOST, 11 | port: process.env.PORT, 12 | userName: process.env.SERVER2_USER_NAME, 13 | password: process.env.SERVER2_PASSWORD 14 | }, 15 | //set to fail 16 | conParamsHost3 = { 17 | host: process.env.SERVER3_HOST, 18 | port: process.env.PORT, 19 | userName: process.env.SERVER3_USER_NAME, 20 | password: process.env.SERVER3_PASSWORD 21 | }, 22 | debug = false, 23 | verbose = false 24 | 25 | //Host objects: 26 | var host1 = { 27 | server: conParamsHost1, 28 | commands: [ "echo host1", "ssh -V"], 29 | connectedMessage: "Connected to host1", 30 | debug: debug, 31 | verbose: verbose 32 | }, 33 | 34 | host2 = { 35 | server: conParamsHost2, 36 | commands: [ "echo host2", "echo ssh" ], 37 | connectedMessage: "Connected to host2", 38 | debug: debug, 39 | verbose: verbose 40 | }, 41 | 42 | host3 = { 43 | server: conParamsHost3, 44 | commands: [ "echo host3", "echo ssh middle"], 45 | connectedMessage: "Connected to host3", 46 | debug: debug, 47 | verbose: verbose 48 | } 49 | 50 | //host2.hosts = [ host3 ]; 51 | //Set the two hosts you are tunnelling to through host1 52 | host1.hosts = [ host2, host3 ]; 53 | 54 | //or the alternative nested tunnelling method outlined above: 55 | //host2.hosts = [ host3 ];ssh -q george@192.168.0.129 "echo 2>&1" && echo OK || echo NOK 56 | //host1.hosts = [ host2 ]; 57 | 58 | //Create the new instance 59 | //or SSH2Shell = require ('ssh2shell') 60 | var SSH2Shell = require ('../lib/ssh2shell'), 61 | SSH = new SSH2Shell(host1), 62 | callback = function( sessionText ){ 63 | console.log ( "-----Callback session text:\n" + sessionText); 64 | console.log ( "-----Callback end" ); 65 | } 66 | 67 | 68 | //Start the process 69 | SSH.connect(callback); 70 | 71 | 72 | --------------------------------------------------------------------------------