ssh2shell 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +567 -747
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -2,44 +2,8 @@ ssh2shell
2
2
  =========
3
3
  [![NPM](https://nodei.co/npm/ssh2shell.png?downloads=true&stars=true)](https://nodei.co/npm/ssh2shell/)
4
4
 
5
- Wrapper class for [ssh2](https://www.npmjs.org/package/ssh2) shell command.
6
-
7
- *ssh2shell supports the following functionality:*
8
- * SSH shell connection to one or more hosts.
9
- * Run multiple commands sequentially within the context of the previous commands result.
10
- * SSH tunnelling to multiple hosts using nested host objects.
11
- * When tunnelling each host has its own connection parameters, commands, command handlers,
12
- event handlers and debug or verbose settings.
13
- * Supports `sudo`, `sudo su` and `su user` commands.
14
- * Supports changing default prompt matching regular expressions for different prompt requirements
15
- * Ability to respond to prompts resulting from a command as it is being run.
16
- * Ability to check the last command and conditions within the response text before the
17
- next command is run.
18
- * Performing actions based on a command run and the response received.
19
- Things like adding or removing commands, responding using stream.write() or processing the command response text.
20
- * See progress messages, messages you define, error messages and debug messages of script process
21
- or verbose output of responses from the host.
22
- * Access to full session response text in the end event triggered when each host connection is closed.
23
- * Run notification commands that are processed either as messages to the full session text only or
24
- messages outputted to your console only.
25
- * Add event handlers either to the class instance or define them within host object.
26
- * Create bash scripts on the fly, run them and remove them.
27
- * Server SSH fingerprint validation.
28
- * Access to [SSH2.connect parameters](https://github.com/mscdex/ssh2#client-methods) for first host connection.
29
- * Keyboard-interactive authentication.
30
- * Pipe stream.data into another writeable stream using SSH2shell.pipe(writableStream). Pipe commands can be chained.
31
- * Also process raw stream.data through SSH2shell.on('data') or host.onData event handlers as the stream receives each
32
- character.
33
- * Use a callback function to handle final session text. The callback function can be defined in the host object or
34
- passed to SSH2shell.connect(callback). The callback function has access to the `this` keyword.
35
-
36
-
37
- Code:
38
- -----
39
- The Class is written in coffee script which can be found here: `./src/ssh2shell.coffee`.
40
- It has comments not found in the build output JavaScript file `./lib/ssh2shell.js`.
41
-
42
-
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
+
43
7
  Installation:
44
8
  ------------
45
9
  ```
@@ -52,18 +16,16 @@ Minimal Example:
52
16
  ```javascript
53
17
  //host configuration with connection settings and commands
54
18
  var host = {
55
- server: {
19
+ server: {
56
20
  host: "127.0.0.1",
57
21
  userName: "test",
58
- password: "1234",
22
+ password: "password",
59
23
  },
60
24
  commands: [ "echo $(pwd)", "ls -l" ]
61
25
  };
62
26
 
63
27
  var SSH2Shell = require ('ssh2shell'),
64
- //Create a new instance passing in the host object
65
- SSH = new SSH2Shell(host),
66
- //Use a callback function to process the full session text
28
+ SSH = new SSH2Shell(host),
67
29
  callback = function(sessionText){
68
30
  console.log(sessionText)
69
31
  }
@@ -72,73 +34,60 @@ var SSH2Shell = require ('ssh2shell'),
72
34
  SSH.connect(callback);
73
35
  ```
74
36
 
37
+
75
38
  Host Configuration:
76
39
  ------------
77
- SSH2Shell expects an object with the following structure to be passed to its constructor:
40
+ SSH2Shell requires an object with the following structure:
78
41
  ```javascript
79
- //Host object
42
+ //Host cobfiguration
80
43
  host = {
81
44
  server: {
82
- //required connection settings
83
- host: "IP Address",
84
- port: "external port number",
85
- userName: "user name",
86
- //optional or required settings subject to authentication method used
87
- password: "user password",
45
+ //host connection settings
46
+ host: "192.168.0.2",
47
+ port: "22", //optional
48
+ userName: "username",
49
+ //optional or required settings subject to authentication method
50
+ password: "password",
88
51
  passPhrase: "privateKeyPassphrase",
89
52
  privateKey: require('fs').readFileSync('/path/to/private/key/id_rsa'),
90
- //Optional: ssh2.connect config parameters
53
+ //Optional: ssh2.connect options
91
54
  //See https://github.com/mscdex/ssh2#client-methods
92
- //ssh2.connect parameters are only valid for the first host connection.
93
- //Other host connections use the ssh command/s to connect not ssh2.
94
55
 
95
- //Optional: ssh options only for making secondary host ssh connections when tunnelling:
96
- //see http://man.openbsd.org/ssh for definitions of options below.
97
- ssh: {
98
- //Optional
99
- forceProtocolVersion: true/false,
100
- forceAddressType: true/false,
101
- disablePseudoTTY: true/false,
102
- forcePseudoTTY: true/false,
103
- verbose: true/false,
104
- cipherSpec: "",
105
- escape: "",
106
- logFile: "",
107
- configFile: "",
108
- identityFile: "",
109
- loginName: "",
110
- macSpec: "",
111
- Options: {}
112
- }
56
+ //Optional: ssh options only for making secondary host ssh connections.
57
+ //see http://man.openbsd.org/ssh for definitions of options.
58
+ ssh: { //Options }
113
59
  },
114
60
  //Optional: Array of host objects for multiple host connections
115
- hosts: [host2, host3, host4, host5],
61
+ hosts: [],
116
62
 
117
63
  //Optional: Characters used in prompt detection
64
+ //defaults
118
65
  standardPrompt: ">$%#",
119
66
  passwordPrompt: ":",
120
67
  passphrasePrompt: ":",
121
68
  passPromptText: "Password",
122
69
 
123
- //Optional: exclude or include host banner after connection and window size
70
+ //Optional: exclude or include host banner after connection
124
71
  showBanner: false,
125
- window: false, //https://github.com/mscdex/ssh2#pseudo-tty-settings use {cols:200}
72
+ //https://github.com/mscdex/ssh2#pseudo-tty-settings
73
+ window: false,
126
74
 
127
75
  //Optional: Enter key character to send as end of line.
128
- enter: "\n", //Linux = "\n" | "\x0a\, Mac = "\r" | "x0d"
76
+ enter: "\n",
129
77
 
130
78
  //Optional: stream encoding
131
79
  streamEncoding: "utf8",
132
80
 
133
- //Optional: Regular expressions to clean output text
81
+ //Optional: Regular expressions to simplify response
82
+ //defaults:
134
83
  asciiFilter: "[^\r\n\x20-\x7e]", //removes non-standard ASCII
135
84
  disableColorFilter: false, //turns colour filtering on and off
136
85
  textColorFilter: "(\[{1}[0-9;]+m{1})", //removes colour formatting codes
137
86
 
138
87
  //Required: array of commands
139
- commands: ["cd /var/logs", "ls -al", "msg:Listed dir", "cd /home/user"],
88
+ commands: ["cd /var/logs", "ls -al"],
140
89
 
141
- //Optional: Used by this.emit("msg", "my message") to output a message
90
+ //Optional: Used by this.emit("msg", "my message")
142
91
  msg: function( message ) {
143
92
  console.log(message);
144
93
  }
@@ -148,121 +97,98 @@ host = {
148
97
  verbose: false, //outputs all received content
149
98
  debug: false, //outputs information about each process step
150
99
 
151
- //Optional: Command time-out timer max interval in milliseconds
100
+ //Optional: Command time-out to stop a command hanging and not returning to a command prompt
152
101
  idleTimeOut: 5000, //integer
153
102
 
154
- //Optional: timeout between data events. max interval in milliseconds
103
+ //Optional: timeout interval when waiting for a host response.
155
104
  dataIdleTimeOut: 500, //integer
156
105
 
157
106
  //Optional: Messages returned on each connection event.
107
+ //default:
158
108
  connectedMessage: "Connected",
159
109
  readyMessage: "Ready",
160
110
  closedMessage: "Closed",
161
111
 
162
- //Optional: Host event handlers
163
- //These event handlers only apply to the host object they are defined within.
164
- //Host event function definitions replace the default event handlers defined
165
- //in the class instead of adding another handler to the listeners.
166
- //`this.` is correctly linked to the instance within host defined event handlers.
112
+ //Optional: Host event handlers .
113
+ //Host defined event handlers are set as the default event handlers.
167
114
 
168
- //Optional: Keyboard interactive authentication event handler
169
- //This event is only used for the first host connecting through ssh2.connect
170
- //Required if the first host.server.tryKeyboard is set to true.
115
+ //Optional:
171
116
  onKeyboardInteractive: function(name, instructions, instructionsLang, prompts, finish){
172
- //See https://github.com/mscdex/ssh2#client-events
173
- //name, instructions, instructionsLang don't seem to be of interest for authenticating
174
- //prompts is an object of expected prompts and if they are to be shown to the user
175
- //finish is the function to be called with an array of responses in the same order as
176
- //the prompts parameter defined them.
177
- //See [Client events](https://github.com/mscdex/ssh2#client-events) for more information
178
- //if a non-standard prompt results from a successful connection then handle its
179
- //detection and response in onCommandProcessing or commandTimeout.
180
- //see text/keyboard-interactivetest.js
117
+ //See https://github.com/mscdex/ssh2#client-events
181
118
  },
182
119
 
183
- //Optional: data is triggered on every stream data event providing the raw stream output
184
- //without SSH2shell interacting with it.
120
+ //Optional:
185
121
  onData: function( data ) {
186
- //data is a string chunk received from the connected host
122
+ //data is the raw response from the host
187
123
  },
188
124
 
189
- //Optional: The pipe event is raised when readStream.pipe() adds a writeable stream to
190
- //receive output
191
- onPipe: function( source ) {
192
- //source is the read stream the write stream will receive output from
193
- }
125
+ //Optional:
126
+ onPipe: function( stream ) {
127
+ //a read stream that will receive raw resonse data
128
+ },
194
129
 
195
- //Optional: The unpipe event is raised when readStream.unpipe() removes a writeable stream
196
- //so it no longer receives output
197
- onUnpipe: function( source ) {
198
- //source is the read stream to remove from being able to write its output.
130
+ //Optional:
131
+ onUnpipe: function( steam ) {
132
+ //removes the steam added in onPipe.
199
133
  },
200
134
 
201
- //Optional: Command processing is triggered on every data read event (one character at a time)
202
- //until prompt is detected.
135
+ //Optional:
203
136
  onCommandProcessing: function( command, response, sshObj, stream ) {
204
- //command is the last command run. This is "" just after connection before the first prompt
205
- //response is the buffer that is being loaded with each data event not the char received.
206
- //sshObj is the current host object.
207
- //stream gives stream.write() access if a response is required outside the normal command flow.
137
+ //Event rasised when data is received from the host before the command prompt is detected.
138
+ //command: is the last command run.
139
+ //response: is the buffer that is being loaded with each data event.
140
+ //sshObj: is the current host object.
141
+ //stream: gives stream.write() access if a response is required outside the normal command flow.
208
142
  },
209
143
 
210
- //Optional: Command complete is raised when a standard prompt is detected.
144
+ //Optional:
211
145
  onCommandComplete: function( command, response, sshObj ) {
212
- //response is the full response from the host for the last command
213
- //sshObj is the current host object.
146
+ //Event raised when a standard prompt is detected after a command is run.
147
+ //response: is the full response from the host for the last command.
148
+ //sshObj: is the current host object.
214
149
  },
215
150
 
216
- //Optional: Command timeout is raised when a standard prompt is not detected and no data is
217
- //received from the host after host.idleTimeout value.
218
- //This stops the connection from hanging when no prompt is detected usually because the
219
- // host is requiring a response it will never get. This event
151
+ //Optional:
220
152
  onCommandTimeout: function( command, response, stream, connection ) {
221
- //command is the last command run or "" if no prompt after connection
222
- //response is the text received up to the time out.
223
- //stream object is used to send text to the host without having to close the connection.
224
- //connection gives access to close the connection if all else fails
225
- //The connection will hang if you send text to the host but get no response and you don't
226
- //close the connection.
153
+ //Event is raised when a standard prompt is not detected and no data is
154
+ //received from the host within the host.idleTimeout value.
155
+ //command: is the last command run or "" when first connected.
156
+ //response: is the text received up to the time out.
157
+ //stream: gives stream.write() access if a response is required outside the normal command flow.
158
+ //connection: gives access to close the connection if all else fails
227
159
  //The timer can be reset from within this event in case a stream write gets no response.
228
- //See test/timeouttest.js for and example of multiple commandTimeout triggers.
229
160
  },
230
161
 
231
- //Recommended: The end event is raised when the stream.on ("finish") event is triggered as the
232
- //connection is closed or when any nested host connection is closed.
233
- //This is where you handle the full session text for a host connection.
234
- //When connecting to multiple hosts the primary host end event receives the full concatenated
235
- //session text for all hosts, including its own, to process.
162
+ //Optional:
236
163
  onEnd: function( sessionText, sshObj ) {
164
+ //is raised when the stream.on ("finish") event is triggered
237
165
  //SessionText is the full text for this hosts session
238
166
  //sshObj is the host object
239
167
  },
240
168
 
241
- //Optional: Run when an error event is raised be it connection or otherwise.
169
+ //Optional:
242
170
  onError: function( err, type, close = false, callback ) {
243
- //err is either an Error object or a string containing the error message
244
- //type is a string containing the error type
245
- //close is a Boolean value indicating if the connection should be closed or not
246
- //callback is the function to run with handling the error in the form of function(err,type)
247
- //if overwriting the default definition remember to handle closing the connection based on close
171
+ //Run when an error event is raised.
172
+ //err: is either an Error object or a string containing the error message
173
+ //type: is a string containing the error type
174
+ //close: is a Boolean value indicating if the connection should be closed or not
175
+ //callback: is the function to run when handling the error defined as function(err,type){}
176
+ //if using this remember to handle closing the connection based on the close parameter.
248
177
  //To close the connection us this.connection.close()
249
178
  },
250
179
 
251
- //Optional: callback function definition called when the stream closes
180
+ //Optional:
252
181
  callback: function( sessionText ){
253
- //sessionText is the full session response filtered and notifications added
182
+ //sessionText: is the full session response
254
183
  //Is overridden by SSH2shell.connect(callback)
255
184
  }
256
185
  };
257
186
  ```
258
187
  * Host.server will accept current [SSH2.client.connect parameters](https://github.com/mscdex/ssh2#client-methods).
259
188
  * Optional host properties or event handlers do not need to be included if you are not changing them.
260
- * See the end of the readme for event handlers available to be added to the instance listeners.
261
189
  * Host event handlers completely replace the default event handler definitions in the class when defined.
262
- * The instance `this` keyword is available within host event handlers to give access to ssh2shell object api like
263
- this.emit() and other functions.
264
190
  * `this.sshObj` or sshObj variable passed into a function provides access to all the host config, some instance
265
- variables and the current array of commands for the host. The sshObj.commands array changes with host connection.
191
+ variables and the current array of commands for the host. The sshObj.commands array changes with each host connection.
266
192
 
267
193
 
268
194
  ssh2shell API
@@ -271,15 +197,15 @@ SSH2Shell extends events.EventEmitter
271
197
 
272
198
  *Methods*
273
199
 
274
- * .connect(callback(sessionText)) Is the main function to establish the connection and handle data events from the server which triggers
275
- the rest of the process. It take in an optional callback function that receives the full session text as its parameter.
200
+ * .connect(callback(sessionText)) Is the main function to establish the connection and handle data events from the server.
201
+ It takes in an optional callback function for processing the full session text.
276
202
 
277
203
  * .emit("eventName", function, parms,... ). Raises the event based on the name in the first string and takes input
278
204
  parameters based on the handler function definition.
279
205
 
280
- * .pipe(destination) binds a writable stream to the output of the read stream but only after the connection is established.
206
+ * .pipe(stream) binds a writable stream to the output of the read stream but only after the connection is established.
281
207
 
282
- * .unpipe(destination) removes piped streams but can only be called after a connection has been made.
208
+ * .unpipe(stream) removes a piped stream but can only be called after a connection has been made.
283
209
 
284
210
  *Variables*
285
211
 
@@ -288,63 +214,24 @@ SSH2Shell extends events.EventEmitter
288
214
  * .command is the current command being run until a new prompt is detected and the next command replaces it.
289
215
 
290
216
 
291
- Test Files:
292
- -----
293
- ```javascript
294
- //single host test
295
- cp .env-example .env
296
- //set the env variables to match your test host/vm username and password.
297
- node text/simple.js
298
-
299
- //simple pipe example
300
- node test/pipe.js
301
-
302
- //multiple nested hosts
303
- //requires the additional details added to .env file for each server
304
- //my tests were done using three VM hosts
305
- node test/tunneltest.js
306
-
307
- //test the command idle time out timer and provide an example of a more complicated timeout handler
308
- node test/timeouttest.js
309
-
310
- //Test multiple sudo and su combinations for changing user
311
- //Issue #10
312
- //Also test Prompt detection with no password requested
313
- //Issue #14
314
- node test/sudosutest.js
315
-
316
- //Test using notification commands as the last command
317
- //Issue #11
318
- node test/notificationstest.js
319
-
320
- //Test keyboard-interactive authentication on the host that has it enabled
321
- node test/keyboard-interactivetest.js
322
- ```
323
-
324
-
325
217
  Usage:
326
218
  ======
327
219
  Connecting to a single host:
328
220
  ----------------------------
329
221
 
330
222
  *How to:*
331
- * Use an .env file for server values loaded by dotenv from the root of the project.
332
223
  * Connect using a key pair with pass phrase.
333
224
  * Use sudo su with user password.
334
225
  * Set commands.
335
226
  * Test the response of a command and add more commands and notifications in the host.onCommandComplete event handler.
336
227
  * Use the two notification types in the commands array.
337
228
  * Use msg: notifications to track progress in the console as the process completes.
338
- * Email the final full session text to yourself.
339
229
 
340
- (will require a package json with ssh2shell, dotenv and email defined as dependencies)
341
230
 
342
231
  *.env*
343
232
  ```
344
- HOST=192.168.0.1
345
- PORT=22
346
- USER_NAME=myuser
347
- PASSWORD=mypassword
233
+ HOST=
234
+ USER_NAME=
348
235
  PRIV_KEY_PATH=~/.ssh/id_rsa
349
236
  PASS_PHRASE=myPassPhrase
350
237
  ```
@@ -355,17 +242,16 @@ var dotenv = require('dotenv').config(),
355
242
  Email = require('email');
356
243
 
357
244
  var host = {
358
- server: {
359
- host: process.env.HOST,
360
- port: process.env.PORT,
361
- userName: process.env.USER_NAME,
362
- password: process.env.PASSWORD,
363
- passPhrase: process.env.PASS_PHRASE,
364
- privateKey: require('fs').readFileSync(process.env.PRIV_KEY_PATH)
245
+ server: {
246
+ host: "192.168.0.1",
247
+ userName: "myuser",
248
+ password: "sudoPassword",
249
+ passPhrase: "myPassPhrase",
250
+ privateKey: require('fs').readFileSync("~/.ssh/id_rsa")
365
251
  },
366
252
  commands: [
367
253
  "`This is a message that will be added to the full sessionText`",
368
- "msg:This is a message that will be handled by the msg.send code",
254
+ "msg:This is a message that will be displayed during the process",
369
255
  "echo $(pwd)",
370
256
  "sudo su",
371
257
  "msg:changing directory",
@@ -373,16 +259,14 @@ var host = {
373
259
  "ls -l",
374
260
  "msg:Confirming the current path",
375
261
  "echo $(pwd)",
376
- "msg:Getting the directory listing to confirm the extra command was added",
262
+ "msg:Command dynamically added",
377
263
  "ls -l",
378
264
  "`All done!`"
379
265
  ],
380
266
 
381
267
  onCommandComplete: function( command, response, sshObj ) {
382
268
  //confirm it is the root home dir and change to root's .ssh folder
383
- if(sshObj.debug){
384
- this.emit("msg", this.sshObj.server.host + ": host.onCommandComplete event, command: " + command);
385
- }
269
+
386
270
  if (command === "echo $(pwd)" && response.indexOf("/root") != -1 ) {
387
271
  //unshift will add the command as the next command, use push to add command as the last command
388
272
  sshObj.commands.unshift("msg:The command and response check worked. Added another cd command.");
@@ -393,22 +277,6 @@ var host = {
393
277
  this.emit("msg", response);
394
278
  }
395
279
  },
396
-
397
- onEnd: function( sessionText, sshObj ) {
398
- //email the session text instead of outputting it to the console
399
- if(sshObj.debug){this.emit("msg", this.sshObj.server.host + ": host.onEnd event");}
400
- var sessionEmail = new Email({
401
- from: "me@example.com",
402
- to: "me@example.com",
403
- subject: "Automated SSH Session Response",
404
- body: "\nThis is the full session responses for " + sshObj.server.host + ":\n\n" + sessionText
405
- });
406
- this.emit("msg", "Sending session response email");
407
-
408
- // if callback is provided, errors will be passed into it
409
- // else errors will be thrown
410
- sessionEmail.send(function(err){ sshObj.msg.send('error', err, 'Email'); });
411
- }
412
280
  };
413
281
 
414
282
 
@@ -416,10 +284,6 @@ var host = {
416
284
  var SSH2Shell = require ('ssh2shell'),
417
285
  SSH = new SSH2Shell(host);
418
286
 
419
- //Start the process
420
- SSH.connect();
421
-
422
- or
423
287
  //use a callback
424
288
  var callback = function (sessionText){
425
289
  console.log (sessionText);
@@ -427,276 +291,309 @@ var callback = function (sessionText){
427
291
  SSH.connect(callback);
428
292
  ```
429
293
 
430
- Tunnelling nested host objects:
431
- ---------------------------------
432
- SSH tunnelling has been incorporated into core of the class process enabling nested host objects.
433
- The new `hosts: [ host1, host2]` setting can make multiple host connections possible.
434
- Each host config object has its own server settings, commands, command handlers and
435
- event handlers. Each host object also has its own hosts array that other host objects can be added to.
436
- This provides for different host connection sequences to any depth of recursion.
437
294
 
438
- **SSH connections:**
439
- Once the primary host connection is made all other connections are made using an ssh command from the
440
- current host to the next. I have given access to a number of ssh command options by adding
441
- an optional host.server.ssh object. All host.server.ssh properties are optional.
442
- host.server.ssh.options allows setting any ssh config option from the command.
295
+ Authentication:
296
+ ---------------
297
+ * Each host authenticates with its own host.server parameters.
298
+ * When using key authentication you may require a valid pass phrase if your key was created with one.
299
+ * When using fingerprint validation both host.server.hashMethod property and host.server.hostVerifier function must be
300
+ set.
301
+ * When using keyboard-interactive authentication both host.server.tryKeyboard and instance.on ("keayboard-interactive",
302
+ function...) or host.onKeyboardInteractive() must be defined.
303
+ * Set the default cyphers and keys.
443
304
 
444
- ```
305
+ Default Cyphers:
306
+ ---------------
307
+ Default Cyphers and Keys used in the initial ssh connection can be redefined by setting the ssh2.connect.algorithms through the host.server.algorithms option.
308
+ As with this property all ssh2.connect properties are set in the host.server object.
309
+
310
+ *Example:*
311
+ ```javascript
445
312
  var host = {
446
- server: { ...,
447
- ssh: {
448
- forceProtocolVersion: true/false,
449
- forceAddressType: true/false,
450
- disablePseudoTTY: true/false,
451
- forcePseudoTTY: true/false,
452
- verbose: true/false,
453
- cipherSpec: "",
454
- escape: "",
455
- logFile: "",
456
- configFile: "",
457
- identityFile: "",
458
- loginName: "",
459
- macSpec: "",
460
- options: {}
461
- }
462
- }
313
+ server: {
314
+ host: "<host IP>",
315
+ port: "22",
316
+ userName: "<username>",
317
+ password: "<password>",
318
+ hashMethod: "md5", //optional "md5" or "sha1" default is "md5"
319
+ //other ssh2.connect options
320
+ algorithms: {
321
+ kex: [
322
+ 'diffie-hellman-group1-sha1',
323
+ 'ecdh-sha2-nistp256',
324
+ 'ecdh-sha2-nistp384',
325
+ 'ecdh-sha2-nistp521',
326
+ 'diffie-hellman-group-exchange-sha256',
327
+ 'diffie-hellman-group14-sha1'],
328
+ cipher: [
329
+ 'aes128-ctr',
330
+ 'aes192-ctr',
331
+ 'aes256-ctr',
332
+ 'aes128-gcm',
333
+ 'aes128-gcm@openssh.com',
334
+ 'aes256-gcm',
335
+ 'aes256-gcm@openssh.com',
336
+ 'aes256-cbc'
337
+ ]
338
+ }
339
+
340
+ },
341
+ ......
463
342
  }
464
343
  ```
465
344
 
345
+ Fingerprint Validation:
346
+ ---------------
347
+ At connection time the hash of the server’s public key can be compared with the hash the client had previously recorded
348
+ for that server. This stops "man in the middle" attacks where you are redirected to a different server as you connect
349
+ to the server you expected to. This hash only changes with a reinstall of SSH, a key change on the server or a load
350
+ balancer is now in place.
466
351
 
467
- **Tunnelling Example:**
468
- This example shows two hosts (server2, server3) that are connected to via server1. The two host configs are add
469
- to server1.hosts array.
352
+ __*Note:*
353
+ Fingerprint check doesn't work the same way for tunnelling. The first host will validate using this method but the
354
+ subsequent connections would have to be handled by your commands. Only the first host uses the SSH2 connection method
355
+ that does the validation.__
470
356
 
357
+ To use fingerprint validation you first need the server hash string which can be obtained using ssh2shell as follows:
358
+ * Set host.verbose to true then set host.server.hashKey to any non-empty string (say "1234").
359
+ * Validation will be checked and fail causing the connection to terminate.
360
+ * A verbose message will return both the server hash and client hash values that failed comparison.
361
+ * This is also what will happen if your hash fails the comparison with the server in the normal verification process.
362
+ * Turn on verbose in the host object, run your script with hashKey unset and check the very start of the text returned
363
+ for the servers hash value.
364
+ * The servers hash value can be saved to a variable outside the host or class so you can access it without having to
365
+ parse response text.
366
+
367
+ *Example:*
368
+ ```javascript
369
+ //Define the hostValidation function in the host.server config.
370
+ //hashKey needs to be defined at the top level if you want to access the server hash at run time
371
+ var serverHash, host;
372
+ //don't set expectedHash if you want to know the server hash
373
+ var expectedHash
374
+ expectedHash = "85:19:8a:fb:60:4b:94:13:5c:ea:fe:3b:99:c7:a5:4e";
375
+
376
+ host = {
377
+ server: {
378
+ //other normal connection params,
379
+ hashMethod: "md5", //"md5" or "sha1"
380
+ //hostVerifier function must be defined and return true for match of false for failure.
381
+ hostVerifier: function(hashedKey) {
382
+ var recievedHash;
383
+
384
+ expectedHash = expectedHash + "".replace(/[:]/g, "").toLowerCase();
385
+ recievedHash = hashedKey + "".replace(/[:]/g, "").toLowerCase();
386
+ if (expectedHash === "") {
387
+ //No expected hash so save save what was received from the host (hashedKey)
388
+ //serverHash needs to be defined before host object
389
+ serverHash = hashedKey;
390
+ console.log("Server hash: " + serverHash);
391
+ return true;
392
+ } else if (recievedHash === expectedHash) {
393
+ console.log("Hash values matched");
394
+ return true;
395
+ }
396
+ //Output the failed comparison to the console if you want to see what went wrong
397
+ console.log("Hash values: Server = " + recievedHash + " <> Client = " + expectedHash);
398
+ return false;
399
+ },
400
+ },
401
+ //Other settings
402
+ };
403
+
404
+ var SSH2Shell = require ('ssh2shell'),
405
+ SSH = new SSH2Shell(host);
406
+ SSH.connect();
471
407
  ```
472
- server1.hosts = [server2, server3]
473
- server2.hosts = []
474
- server3.hosts = []
475
- ```
476
- *The following would also be valid:*
408
+ __*Note:*
409
+ host.server.hashMethod only supports md5 or sha1 according to the current SSH2 documentation anything else may produce
410
+ undesired results.__
411
+
412
+
413
+ Keyboard-interactive
414
+ ----------------------
415
+ Keyboard-interactive authentication is available when both host.server.tryKeyboard is set to true and the event handler
416
+ keyboard-interactive is defined as below. The keyboard-interactive event handler can only be used on the first connection.
417
+
418
+ Also see [test/keyboard-interactivetest.js](https://github.com/cmp-202/ssh2shell/blob/master/test/keyboard-interactivetest.js) for the full example
419
+
420
+ *Defining the event handler:*
421
+ ```javascript
422
+ //this is required
423
+ host.server.tryKeyboard = true;
424
+
425
+ var SSH2Shell = require ('../lib/ssh2shell');
426
+ var SSH = new SSH2Shell(host);
427
+
428
+ //Add the keyboard-interactive handler
429
+ //The event function must call finish() with an array of responses in the same order as prompts received
430
+ // in the prompts array
431
+ SSH.on ('keyboard-interactive', function(name, instructions, instructionsLang, prompts, finish){
432
+ if (this.sshObj.debug) {this.emit('msg', this.sshObj.server.host + ": Keyboard-interactive");}
433
+ if (this.sshObj.verbose){
434
+ this.emit('msg', "name: " + name);
435
+ this.emit('msg', "instructions: " + instructions);
436
+ var str = JSON.stringify(prompts, null, 4);
437
+ this.emit('msg', "Prompts object: " + str);
438
+ }
439
+ //The example presumes only the password is required
440
+ finish([this.sshObj.server.password] );
441
+ });
442
+
443
+ SSH.connect();
477
444
  ```
478
- server1.hosts = [server2]
479
- server2.hosts = [server3]
480
- server3.hosts = []
445
+
446
+ Or
447
+
448
+ ```javascript
449
+ host = {
450
+ ...,
451
+ onKeyboardInteractive: function(name, instructions, instructionsLang, prompts, finish){
452
+ if (this.sshObj.debug) {this.emit('msg', this.sshObj.server.host + ": Keyboard-interactive");}
453
+ if (this.sshObj.verbose){
454
+ this.emit('msg', "name: " + name);
455
+ this.emit('msg', "instructions: " + instructions);
456
+ var str = JSON.stringify(prompts, null, 4);
457
+ this.emit('msg', "Prompts object: " + str);
458
+ }
459
+ //The example presumes only the password is required
460
+ finish([this.sshObj.server.password] );
461
+ },
462
+ ...
463
+ }
481
464
  ```
482
465
 
483
- *The nested process:*
484
466
 
485
- 1. The primary host (server1) is connected and all its commands completed.
486
- 2. Server1.hosts array is checked for other hosts and the next host popped off the array.
487
- 3. Server1 is stored for use later and server2's host object is loaded as the current host.
488
- 4. A connection to server2 is made using its server parameters via running the ssh command on the primary.
489
- 5. Server2's commands are completed and server2.hosts array is checked for other hosts.
490
- 6. With no hosts found the connection to server2 is closed triggering an end event (calling server2.onEnd function if defined).
491
- 5. Server1 host object is reloaded as the current host object and server2 host object discarded.
492
- 6. Server1.hosts array is checked for other hosts and the next host popped off the array.
493
- 7. Server1's host object is stored again and server3's host object is loaded as the current host.
494
- 8. Server3 is connected to and it completes its process.
495
- 9. Server3.hosts is checked and with no hosts found the connection is closed and the end event is triggered.
496
- 9. Server1 is loaded for the last time.
497
- 10. The session text for each connection is appended to the session text for the primary host.
498
- 11. With no further hosts to load the connection is closed triggering an end event for the last time.
499
- 12. As all sessions are closed the process ends.
467
+ Trouble shooting:
468
+ -----------------
500
469
 
470
+ * Adding msg command `"msg:Doing something"` to your commands array at key points will help you track the sequence of
471
+ what has been done as the process runs. (See examples)
472
+ * `Error: Unable to parse private key while generating public key (expected sequence)` is caused by the pass phrase
473
+ being incorrect. This confused me because it doesn't indicate the pass phrase was the problem but it does indicate
474
+ that it could not decrypt the private key.
475
+ * Recheck your pass phrase for typos or missing chars.
476
+ * Try connecting manually to the host using the exact pass phrase used by the code to confirm it works.
477
+ * I did read of people having problems with the pass phrase or password having an \n added when used from an
478
+ external file causing it to fail. They had to add .trim() when setting it.
479
+ * If your password is incorrect the connection will return an error.
480
+ * There is an optional debug setting in the host object that will output process information when set to true and
481
+ passwords for failed authentication of sudo commands and tunnelling. `host.debug = true`
482
+ * The class now has an idle time out timer (default:5000ms) to stop unexpected command prompts from causing the process
483
+ hang without error. The default time out can be changed by setting the host.idleTimeOut with a value in milliseconds.
484
+ (1000 = 1 sec)
501
485
 
502
- **_Note:_**
503
- * A host object needs to be defined before it is added to another host.hosts array.
504
- * Only the primary host objects connected, ready and closed messages will be used by ssh2shell.
505
- * Connection events only apply to the primary host. Keyboard-interactive event handler is one of these.
506
486
 
487
+ Verbose and Debug:
488
+ ------------------
489
+ * When host.verbose is set to true each command complete raises a msg event outputting the response text.
490
+ * When host.debug is set to true each process step raises a msg event to help identify what the internal processes were.
507
491
 
508
- *How to:*
509
- * Define nested hosts
510
- * Use unique host connection settings for each host
511
- * Defining different commands and command event handlers for each host
512
- * Sharing duplicate functions between host objects
513
- * What host object attributes you can leave out of primary and secondary host objects
514
- * Unique event handlers set in host objects, common event handler set on class instance
492
+ __*Note:*
493
+ Do not add debug or verbose to the onCommandProccessing event which is called every time a response is received from the host.__
515
494
 
516
- **_Note:_**
517
- * Change debug to true to see the full process for each host.
495
+ Add your own verbose messages as follows:
496
+ `if(this.sshObj.verbose){this.emit("msg", this.sshObj.server.host + ": response: " + response);} //response might need to be changed to this._buffer`
518
497
 
519
- ```javascript
520
- var dotenv = require('dotenv');
521
- dotenv.load();
498
+ Add your own debug messages as follows:
499
+ `if(this.sshObj.debug){this.emit("msg", this.sshObj.server.host + ": eventName");} //where eventName is the text identifying what happened`
522
500
 
523
- //Host connection and authentication parameters
524
- var conParamsHost1 = {
525
- host: process.env.SERVER1_HOST,
526
- port: process.env.SERVER1_PORT,
527
- userName: process.env.SERVER1_USER_NAME,
528
- password: process.env.SERVER1_PASSWORD,
529
- passPhrase: process.env.SERVER1_PASS_PHRASE,
530
- privateKey: require('fs').readFileSync(process.env.SERVER1_PRIV_KEY_PATH)
531
- },
532
- conParamsHost2 = {
533
- host: process.env.SERVER2_HOST,
534
- port: process.env.SERVER2_PORT,
535
- userName: process.env.SERVER2_USER_NAME,
536
- password: process.env.SERVER2_PASSWORD
537
- },
538
- conParamsHost3 = {
539
- host: process.env.SERVER3_HOST,
540
- port: process.env.SERVER3_PORT,
541
- userName: process.env.SERVER3_USER_NAME,
542
- password: process.env.SERVER3_PASSWORD
543
- }
544
-
501
+ Notification commands:
502
+ ----------------------
503
+ There are two notification commands that are added to the host.commands array but are not run as a command on the host.
545
504
 
546
- //Host objects:
547
- var host1 = {
548
- server: conParamsHost1,
549
- commands: [
550
- "msg:connected to host: passed. Listing dir.",
551
- "ls -l"
552
- ],
553
- debug: false,
554
- onCommandComplete: function( command, response, sshObj ) {
555
- //we are listing the dir so output it to the msg handler
556
- if(sshObj.debug){
557
- this.emit("msg", this.sshObj.server.host + ": host.onCommandComplete host1, command: " + command);
558
- }
559
- },
560
- onEnd: function( sessionText, sshObj ) {
561
- //show the full session output for all hosts.
562
- if(sshObj.debug){this.emit("msg", this.sshObj.server.host + ": primary host.onEnd all sessiontText");}
563
- this.emit ("msg", "\nAll Hosts SessiontText ---------------------------------------\n");
564
- this.emit ("msg", sshObj.server.host + ":\n" + sessionText);
565
- this.emit ("msg", "\nEnd sessiontText ---------------------------------------------\n");
566
- })
567
- },
505
+ 1. `"msg:This is a message"`. outputs to the console.
506
+ 2. "\`SessionText notification\`" will take the text between "\` \`" and add it to the sessionText only.
507
+ * 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.
568
508
 
569
- host2 = {
570
- server: conParamsHost2,
571
- commands: [
572
- "msg:connected to host: passed",
573
- "msg:Changing to root dir",
574
- "cd ~/",
575
- "msg:Listing dir",
576
- "ls -l"
577
- ],
578
- debug: false,
579
- connectedMessage: "Connected to host2",
580
- onCommandComplete: function( command, response, sshObj ) {
581
- //we are listing the dir so output it to the msg handler
582
- if(sshObj.debug){
583
- this.emit("msg", this.sshObj.server.host + ": host.onCommandComplete host2, command: " + command);
584
- }
585
- if (command.indexOf("cd") != -1){
586
- this.emit("msg", this.sshObj.server.host + ": Just ran a cd command:\n");
587
- this.emit("msg", response);
588
- }
589
- }
590
- },
591
509
 
592
- host3 = {
593
- server: conParamsHost3,
594
- commands: [
595
- "msg:connected to host: passed",
596
- "hostname"
597
- ],
598
- debug: false,
599
- connectedMessage: "Connected to host3",
600
- onCommandComplete: function( command, response, sshObj) {
601
- //we are listing the dir so output it to the msg handler
602
- if(sshObj.debug){
603
- this.emit("msg", this.sshObj.server.host + ": host.onCommandComplete host3, command: " + command);
604
- }
605
- if (command.indexOf("cd") != -1){
606
- this.emit("msg", this.sshObj.server.host + ": Just ran hostname command:\n");
607
- this.emit("msg", response);
608
- }
609
- }
610
- }
510
+ Sudo and su Commands:
511
+ --------------
512
+ It is possible to use `sudo [command]`, `sudo su`, `su [username]` and `sudo -u [username] -i`.
513
+ Sudo commands uses the password for the current session and is processed by ssh2shell. Su on the other hand uses the password
514
+ of root or an other user (`su seconduser`) and requires you detect the password prompt in `host.onCommandProcessing` and run `this.runCommand(password)`.
611
515
 
612
- //Set the two hosts you are tunnelling to through host1
613
- host1.hosts = [ host2, host3 ];
516
+ See: [su VS sudo su VS sudo -u -i](http://johnkpaul.tumblr.com/post/19841381351/su-vs-sudo-su-vs-sudo-u-i) for
517
+ clarification about the difference between the commands.
614
518
 
615
- //or the alternative nested tunnelling method outlined above:
616
- //host2.hosts = [ host3 ];
617
- //host1.hosts = [ host2 ];
519
+ See: [test/sudosutest.js](https://github.com/cmp-202/ssh2shell/blob/master/test/sudosutest.js) for a working code example.
618
520
 
619
- //Create the new instance
620
- var SSH2Shell = require ('ssh2shell'),
621
- SSH = new SSH2Shell(host1);
622
521
 
623
- //Add a commandComplete handler used by all hosts
624
- SSH.on ('commandComplete', function onCommandComplete( command, response, sshObj ) {
625
- //we are listing the dir so output it to the msg handler
626
- if(sshObj.debug){
627
- this.emit("msg", this.sshObj.server.host + ": instance.onCommandComplete, command: " + command);
628
- }
629
- if (command == "ls -l"){
630
- this.emit("msg", this.sshObj.server.host + ":\n" + response);
631
- }
632
- }
633
-
634
- //Add an on end event handler used by all hosts
635
- SSH.on ('end', function( sessionText, sshObj ) {
636
- //show the full session output. This could be emailed or saved to a log file.
637
- if(sshObj.debug){this.emit("msg", this.sshObj.server.host + ": instanse.onEnd all hosts");}
638
-
639
- this.emit ("msg", "\nSessiontText -------------------------------------------------\n");
640
- this.emit ("msg", sshObj.server.host + ":\n" + sessionText);
641
- this.emit ("msg", "\nEnd sessiontText ---------------------------------------------\n");
642
- });
522
+ Prompt detection override:
523
+ -------------------------
524
+ Used to detect different prompts.
643
525
 
644
- //Start the process
645
- SSH.connect();
526
+ These are optional settings.
527
+ ```
528
+ host.standardPrompt = ">$%#";
529
+ host.passwordPrompt = ":";
530
+ host.passphrasePrompt = ":";
531
+ ```
646
532
 
647
- ```
533
+
534
+ Text regular expression filters:
535
+ -------------------------------
536
+ There are two regular expression filters that remove unwanted text from response data.
537
+
538
+ The first removes non-standard ascii and the second removes ANSI text formatting codes. Both of these can be modified in
539
+ your host object to override defaults. It is also possible to output the ANSI codes by setting disableColorFilter to true.
540
+
541
+ Defaults:
542
+ ```javascript
543
+ host.asciiFilter = "[^\r\n\x20-\x7e]"
544
+ host.disableColorFilter = false //or true
545
+ host.textColorFilter = "(\[{1}[0-9;]+m{1})"
546
+ ```
648
547
 
548
+
549
+ Responding to non-standard command prompts:
550
+ ----------------------
551
+ In host.onCommandProcessing event handler the prompt can be detected and a response can be sent via `stream.write("y\n")`.
649
552
 
650
- Trouble shooting:
651
- -----------------
553
+ `host.onCommandProcessing` definition replaces the default handler and runs only for the current host connection
554
+ ```javascript
555
+ host.onCommandProcessing = function( command, response, sshObj, stream ) {
556
+ //Check the command and prompt exits and respond with a 'y' but only does it once
557
+ if (command == "apt-get install nano" && response.indexOf("[y/N]?") != -1 && sshObj.firstRun != true) {
558
+ //This debug message will only run when conditions are met not on every data event so is ok here
559
+ sshObj.firstRun = true
560
+ stream.write('y\n');
561
+ }
562
+ };
563
+ \\sshObj.firstRun could be reset to false in host.onCommandComplete to allow for detect another non-standard prompt
564
+ ```
652
565
 
653
- * Adding msg command `"msg:Doing something"` to your commands array at key points will help you track the sequence of
654
- what has been done as the process runs. (See examples)
655
- * `Error: Unable to parse private key while generating public key (expected sequence)` is caused by the pass phrase
656
- being incorrect. This confused me because it doesn't indicate the pass phrase was the problem but it does indicate
657
- that it could not decrypt the private key.
658
- * Recheck your pass phrase for typos or missing chars.
659
- * Try connecting manually to the host using the exact pass phrase used by the code to confirm it works.
660
- * I did read of people having problems with the pass phrase or password having an \n added when used from an
661
- external file causing it to fail. They had to add .trim() when setting it.
662
- * If your password is incorrect the connection will return an error.
663
- * There is an optional debug setting in the host object that will output process information when set to true and
664
- passwords for failed authentication of sudo commands and tunnelling. `host.debug = true`
665
- * The class now has an idle time out timer (default:5000ms) to stop unexpected command prompts from causing the process
666
- hang without error. The default time out can be changed by setting the host.idleTimeOut with a value in milliseconds.
667
- (1000 = 1 sec)
566
+ Instance definition that runs in parallel with every other commandProcessing for every host connection
567
+ To handle all hosts the same add an event handler to the class instance
568
+ Don't define an event handler in the host object with the same code, it will do it twice!
569
+ ```javascript
570
+ var SSH2Shell = require ('ssh2shell');
571
+ var SSH = new SSH2Shell(host);
668
572
 
573
+ SSH.on ('commandProcessing', function onCommandProcessing( command, response, sshObj, stream ) {
669
574
 
670
- Verbose and Debug:
671
- ------------------
672
- * When host.verbose is set to true each command complete raises a msg event outputting the response text.
673
- * When host.debug is set to true each process step raises a msg event to help identify what the internal process of
674
- each step was.
575
+ //Check the command and prompt exits and respond with a 'y'
576
+ if (command == "apt-get install nano" && response.indexOf("[y/N]?") != -1 && sshObj.firstRun != true ) {
577
+ //This debug message will only run when conditions are met not on every data event so is ok here
578
+ if (sshObj.debug) {this.emit('msg', this.sshObj.server.host + ": Responding to nano install");}
579
+ sshObj.firstRun = true
580
+ stream.write('y\n');
581
+ }
582
+ };
583
+ ```
675
584
 
676
585
  __*Note:*
677
- Do not add these to the commandProccessing event which is called every time a character is received from the host__
678
-
679
- Add your own verbose messages as follows:
680
- `if(this.sshObj.verbose){this.emit("msg", this.sshObj.server.host + ": response: " + response);}` //response might need
681
- to be changed to this._buffer
682
-
683
- Add your own debug messages as follows:
684
- `if(this.sshObj.debug){this.emit("msg", this.sshObj.server.host + ": eventName");}` //where eventName is the text
685
- identifying what happened
586
+ If no prompt is detected `commandTimeout` will be raised after the idleTimeOut period.__
686
587
 
687
588
 
688
- Command Time-out Event Handler
589
+ Command Time-out event
689
590
  ---------------
690
- When the program doesn't detect a standard prompt and doesn't receive any more data the onCommandTimeout event triggers
691
- after the host.idleTimeOut value (in ms). This is usually because an unexpected prompt on the server is requiring a
692
- response that isn't handled or the host is not responding at all. In either case detection of the standard prompt will
693
- never happen causing the program to hang, perpetually waiting for a response it won’t get. The commandTimeout stops this.
694
- The commandTimeout event can enable you to handle such prompts without having to disconnect by providing the response
695
- the host requires. The host then replies with more text triggering a data received event resetting the timer and
696
- enabling the process to continue. It is recommended to close the connection as a default action if all else fails so you
697
- are not left with a hanging script again. The default action is to add the last response text to the session text and
698
- disconnect. Enabling host.debug would also provide the process path leading up to disconnection which in conjunction with
699
- the session text would clarify what command and output triggered the event.
591
+ When onCommandTimeout event triggers after the host.idleTimeOut is exceeded.
592
+ This is because there is no new data and no prompt is detected to run the next command.
593
+ It is recommended to close the connection as a default action if all else fails so you are not left with a hanging script.
594
+ The default action is to add the last response text to the session text and disconnect.
595
+ Enabling host.debug would also provide the process path leading up to disconnection which in conjunction with
596
+ the session text would clarify what command and output triggered the timeout event.
700
597
 
701
598
  **Note: If you receive garble back before the clear response you may need to save the previous response text to the
702
599
  sessionText and clear the buffer before using stream.write() in commandTimeout.
@@ -705,7 +602,6 @@ the session text would clarify what command and output triggered the event.
705
602
 
706
603
  ```javascript
707
604
  host.onCommandTimeout = function( command, response, stream, connection ) {
708
- if(sshObj.debug){this.emit("msg", this.sshObj.server.host + ": host.onCommandTimeout");}
709
605
  if (command === "atp-get install node"
710
606
  && response.indexOf("[Y/n]?") != -1
711
607
  && this.sshObj.nodePrompt != true) {
@@ -722,27 +618,11 @@ host.onCommandTimeout = function( command, response, stream, connection ) {
722
618
  })
723
619
  }
724
620
  }
621
+ ```
725
622
 
726
623
  or
727
624
 
728
- host.onCommandTimeout = function( command, response, stream, connection ) {
729
- if(sshObj.debug){this.emit("msg", this.sshObj.server.host + ": host.onCommandTimeout");}
730
- if (command === "" && response === "Connected to port 22" && this.sshObj.noFirstPrompt != true) {
731
- //Setting this.sshObj.noFirstPrompt stops a response loop
732
- this.sshObj.noFirstPrompt = true
733
- stream.write('\n')
734
- return true
735
- }
736
- //emit an error that passes true for the close parameter and callback the loads the last of session text
737
- this.emit ("error", this.sshObj.server.host
738
- + ": Command timed out after #{this._idleTime/1000} seconds. command: "
739
- + command, "Timeout", true, function(err, type){
740
- this.sshObj.sessionText += response
741
- })
742
- }
743
-
744
- or
745
-
625
+ ```javascript
746
626
  //reset the default handler to do nothing so it doesn't close the connection
747
627
  host.onCommandTimeout = function( command, response, stream, connection ) {};
748
628
 
@@ -752,323 +632,235 @@ var SSH2Shell = require ('ssh2shell'),
752
632
 
753
633
  //And do it all in the instance event handler
754
634
  SSH.on ('commandTimeout',function( command, response, stream, connection ){
755
- if(sshObj.debug){this.emit("msg", this.sshObj.server.host + ": instance.onCommandTimeout");}
756
635
  //first test should only pass once to stop a response loop
757
636
  if (command === "atp-get install node"
758
637
  && response.indexOf("[Y/n]?") != -1
759
638
  && this.sshObj.nodePrompt != true) {
760
639
  this.sshObj.nodePrompt = true;
761
640
  stream.write('y\n');
641
+
762
642
  return true;
763
643
  }
764
644
  this.sshObj.sessionText += response;
645
+ //emit an error that closes the connection
765
646
  this.emit("error", this.sshObj.server.host + ": Command \`" + command + "\` timed out after "
766
647
  + (this._idleTime / 1000) + " seconds. command: " + command, "Command Timeout", true);
767
648
  });
768
649
 
769
650
  SSH.on ('end', function( sessionText, sshObj ) {
770
- //show the full session output. This could be emailed or saved to a log file.
771
- if(sshObj.debug){this.emit("msg", this.sshObj.server.host + ": instance.onEnd");}
772
651
  this.emit("msg","\nSession text for " + sshObj.server.host + ":\n" + sessionText);
773
652
  });
774
653
 
775
654
  SSH.connect();
776
655
  ```
777
656
 
778
- Authentication:
779
- ---------------
780
- * Each host authenticates with its own host.server parameters.
781
- * When using key authentication you may require a valid pass phrase if your key was created with one.
782
- * When using fingerprint validation both host.server.hashMethod property and host.server.hostVerifier function must be
783
- set.
784
- * When using keyboard-interactive authentication both host.server.tryKeyboard and instance.on ("keayboard-interactive",
785
- function...) or host.onKeyboardInteractive() must be defined.
786
- * Set the default cyphers and keys.
787
-
788
- Default Cyphers:
789
- ---------------
790
- Default Cyphers and Keys used in the initial ssh connection can be redefined by setting the ssh2.connect.algorithms through the host.server.algorithms option.
791
- As with this property all ssh2.connect properties are set in the host.server object.
792
-
793
- *Example:*
794
- ```javascript
795
- var host = {
796
- server: {
797
- host: "<host IP>",
798
- port: "22",
799
- userName: "<username>",
800
- password: "<password>",
801
- hashMethod: "md5", //optional "md5" or "sha1" default is "md5"
802
- //other ssh2.connect options
803
- algorithms: {
804
- kex: [
805
- 'diffie-hellman-group1-sha1',
806
- 'ecdh-sha2-nistp256',
807
- 'ecdh-sha2-nistp384',
808
- 'ecdh-sha2-nistp521',
809
- 'diffie-hellman-group-exchange-sha256',
810
- 'diffie-hellman-group14-sha1'],
811
- cipher: [
812
- 'aes128-ctr',
813
- 'aes192-ctr',
814
- 'aes256-ctr',
815
- 'aes128-gcm',
816
- 'aes128-gcm@openssh.com',
817
- 'aes256-gcm',
818
- 'aes256-gcm@openssh.com',
819
- 'aes256-cbc'
820
- ]
821
- }
822
-
823
- },
824
- ......
825
- }
826
- ```
827
-
828
- Fingerprint Validation:
829
- ---------------
830
- At connection time the hash of the server’s public key can be compared with the hash the client had previously recorded
831
- for that server. This stops "man in the middle" attacks where you are redirected to a different server as you connect
832
- to the server you expected to. This hash only changes with a reinstall of SSH, a key change on the server or a load
833
- balancer is now in place.
834
-
835
- __*Note:*
836
- Fingerprint check doesn't work the same way for tunnelling. The first host will validate using this method but the
837
- subsequent connections would have to be handled by your commands. Only the first host uses the SSH2 connection method
838
- that does the validation.__
839
-
840
- To use fingerprint validation you first need the server hash string which can be obtained using ssh2shell as follows:
841
- * Set host.verbose to true then set host.server.hashKey to any non-empty string (say "1234").
842
- * Validation will be checked and fail causing the connection to terminate.
843
- * A verbose message will return both the server hash and client hash values that failed comparison.
844
- * This is also what will happen if your hash fails the comparison with the server in the normal verification process.
845
- * Turn on verbose in the host object, run your script with hashKey unset and check the very start of the text returned
846
- for the servers hash value.
847
- * The servers hash value can be saved to a variable outside the host or class so you can access it without having to
848
- parse response text.
849
-
850
- *Example:*
851
- ```javascript
852
- //Define the hostValidation function in the host.server config.
853
- //hashKey needs to be defined at the top level if you want to access the server hash at run time
854
- var serverHash, host;
855
- //don't set expectedHash if you want to know the server hash
856
- var expectedHash
857
- expectedHash = "85:19:8a:fb:60:4b:94:13:5c:ea:fe:3b:99:c7:a5:4e";
858
657
 
859
- host = {
860
- server: {
861
- //other normal connection params,
862
- hashMethod: "md5", //"md5" or "sha1"
863
- //hostVerifier function must be defined and return true for match of false for failure.
864
- hostVerifier: function(hashedKey) {
865
- var recievedHash;
866
-
867
- expectedHash = expectedHash + "".replace(/[:]/g, "").toLowerCase();
868
- recievedHash = hashedKey + "".replace(/[:]/g, "").toLowerCase();
869
- if (expectedHash === "") {
870
- //No expected hash so save save what was received from the host (hashedKey)
871
- //serverHash needs to be defined before host object
872
- serverHash = hashedKey;
873
- console.log("Server hash: " + serverHash);
874
- return true;
875
- } else if (recievedHash === expectedHash) {
876
- console.log("Hash values matched");
877
- return true;
878
- }
879
- //Output the failed comparison to the console if you want to see what went wrong
880
- console.log("Hash values: Server = " + recievedHash + " <> Client = " + expectedHash);
881
- return false;
882
- },
883
- },
884
- //Other settings
885
- };
886
-
887
- var SSH2Shell = require ('ssh2shell'),
888
- SSH = new SSH2Shell(host);
889
- SSH.connect();
890
- ```
891
- __*Note:*
892
- host.server.hashMethod only supports md5 or sha1 according to the current SSH2 documentation anything else may produce
893
- undesired results.__
894
-
895
-
896
- Keyboard-interactive
897
- ----------------------
898
- Keyboard-interactive authentication is available when both host.server.tryKeyboard is set to true and the event handler
899
- keyboard-interactive is defined as below. The keyboard-interactive event handler can only be used on the first connection.
900
-
901
- Also see [test/keyboard-interactivetest.js](https://github.com/cmp-202/ssh2shell/blob/master/test/keyboard-interactivetest.js) for the full example
658
+ Tunnelling nested host objects:
659
+ ---------------------------------
660
+ `hosts: [ host1, host2]` setting can make multiple host connections possible.
661
+ Each host config object has its own server settings, commands, command handlers and
662
+ event handlers. Each host object also has its own hosts array that other host objects can be added to.
663
+ This provides for different host connection sequences to multiple depth of connections.
902
664
 
903
- *Defining the event handler:*
904
- ```javascript
905
- //this is required
906
- host.server.tryKeyboard = true;
665
+ **SSH connections:**
666
+ Once the primary host connection is made all other connections are made using an ssh command from the
667
+ current host to the next. A number of ssh command options are set using an optional `host.server.ssh` object.
668
+ `host.server.ssh.options` allows setting any ssh config options.
907
669
 
908
- var SSH2Shell = require ('../lib/ssh2shell');
909
- var SSH = new SSH2Shell(host);
910
-
911
- //Add the keyboard-interactive handler
912
- //The event function must call finish() with an array of responses in the same order as prompts received
913
- // in the prompts array
914
- SSH.on ('keyboard-interactive', function(name, instructions, instructionsLang, prompts, finish){
915
- if (this.sshObj.debug) {this.emit('msg', this.sshObj.server.host + ": Keyboard-interactive");}
916
- if (this.sshObj.verbose){
917
- this.emit('msg', "name: " + name);
918
- this.emit('msg', "instructions: " + instructions);
919
- var str = JSON.stringify(prompts, null, 4);
920
- this.emit('msg', "Prompts object: " + str);
921
- }
922
- //The example presumes only the password is required
923
- finish([this.sshObj.server.password] );
924
- });
925
-
926
- SSH.connect();
927
670
  ```
928
-
929
- Or
930
-
931
- ```javascript
932
- host = {
933
- ...,
934
- onKeyboardInteractive: function(name, instructions, instructionsLang, prompts, finish){
935
- if (this.sshObj.debug) {this.emit('msg', this.sshObj.server.host + ": Keyboard-interactive");}
936
- if (this.sshObj.verbose){
937
- this.emit('msg', "name: " + name);
938
- this.emit('msg', "instructions: " + instructions);
939
- var str = JSON.stringify(prompts, null, 4);
940
- this.emit('msg', "Prompts object: " + str);
941
- }
942
- //The example presumes only the password is required
943
- finish([this.sshObj.server.password] );
944
- },
945
- ...
671
+ var host = {
672
+ server: { ...,
673
+ ssh: {
674
+ forceProtocolVersion: true/false,
675
+ forceAddressType: true/false,
676
+ disablePseudoTTY: true/false,
677
+ forcePseudoTTY: true/false,
678
+ verbose: true/false,
679
+ cipherSpec: "",
680
+ escape: "",
681
+ logFile: "",
682
+ configFile: "",
683
+ identityFile: "",
684
+ loginName: "",
685
+ macSpec: "",
686
+ options: {}
687
+ }
688
+ }
946
689
  }
947
690
  ```
948
691
 
949
- Sudo and su Commands:
950
- --------------
951
- It is possible to use `sudo [command]`, `sudo su`, `su [username]` and `sudo -u [username] -i`. Sudo commands uses the
952
- password for the user that is accessing the server and is handled by SSH2shell. Su on the other hand uses the password
953
- of root or the other user (`su seconduser`) and requires you detect the password prompt in onCommandProcessing.
954
692
 
955
- See: [su VS sudo su VS sudo -u -i](http://johnkpaul.tumblr.com/post/19841381351/su-vs-sudo-su-vs-sudo-u-i) for
956
- clarification about the difference between the commands.
693
+ **Tunnelling Example:**
694
+ This example shows two hosts (server2, server3) that are connected to via server1.
695
+ The two host configs are added to server1.hosts array.
957
696
 
958
- See: [test/sudosutest.js](https://github.com/cmp-202/ssh2shell/blob/master/test/sudosutest.js) for a working code example.
697
+ ```
698
+ server1.hosts = [server2, server3]
699
+ server2.hosts = []
700
+ server3.hosts = []
701
+ ```
702
+ *The following would also be valid:*
703
+ ```
704
+ server1.hosts = [server2]
705
+ server2.hosts = [server3]
706
+ server3.hosts = []
707
+ ```
959
708
 
709
+ *The nested process:*
960
710
 
961
- Notification commands:
962
- ----------------------
963
- There are two notification commands that are added to the host.commands array but are not run as a command on the host.
711
+ 1. The primary host (server1) is connected and all its commands completed.
712
+ 2. Server1.hosts array is checked for other hosts.
713
+ 3. Server1 is stored for use later and server2's host object is loaded as the current host.
714
+ 4. A connection to the next host is made using its server parameters via running the ssh command in the current session.
715
+ 5. That hosts commands are completed and it's hosts array is checked for other hosts.
716
+ 6. If no hosts are found the connection is closed triggering an end event for the current host if defined.
717
+ 5. Server1 host object is reloaded as the current host object and server2 host object discarded.
718
+ 6. Server1.hosts array is checked for other hosts and the next host popped off the array.
719
+ 7. Server1's host object is restored and the process repeats for an all other hosts.
720
+ 8. Server1 is loaded for the last time and all hosts sessionText's are appended to `this.callback(sessionText)`.
721
+ 10. As all sessions are closed, the process ends.
722
+
723
+
724
+ **_Note:_**
725
+ * A host object needs to be defined before it is added to another host.hosts array.
726
+ * Only the primary host objects connected, ready and closed messages will be used by ssh2shell.
727
+ * Connection events only apply to the first host. Keyboard-interactive event handler is one of these.
964
728
 
965
- 1. `"msg:This is a message intended for monitoring the process as it runs"`. The `msg:` command raises a onMsg(message)
966
- event.
967
- * The text after `msg:` is passed to the message property of the onMsg event.
968
- 2. "\`SessionText notification\`" will take the text between "\` \`" and add it to the sessionText.
969
- * The reason for not using echo or printf commands as a normal command is that you see both the command and the message
970
- in the sessionText which is pointless when all you want is the message.
971
729
 
730
+ *How to:*
731
+ * Define nested hosts
732
+ * Use unique host connection settings for each host
733
+ * Defining different commands and command event handlers for each host
734
+ * Sharing duplicate functions between host objects
735
+ * What host object attributes you can leave out of primary and secondary host objects
736
+ * Unique event handlers set in host objects, common event handler set on class instance
972
737
 
973
- Prompt detection override:
974
- -------------------------
975
- The following properties have been added to the host object making it possible to override prompt string values used
976
- with regular expressions to for prompt detection. Being able to change these values enables you to easily manage all
977
- sorts of prompt options subject to you server prompts.
738
+ **_Note:_**
739
+ * Change debug to true to see the full process for each host.
978
740
 
979
- These are optional settings.
980
- ```
981
- host.standardPrompt = ">$%#";
982
- host.passwordPrompt = ":";
983
- host.passphrasePrompt = ":";
984
- ```
985
-
986
-
987
- Text regular expression filters:
988
- -------------------------------
989
- There are two regular expression filters that remove unwanted text from response data.
990
-
991
- The first removes non-standard ascii and the second removes ANSI text formatting codes. Both of these can be modified in
992
- your host object to override defaults. It is also possible to output the ANSI codes by setting disableColorFilter to true.
993
-
994
- These are optional settings
995
741
  ```javascript
996
- host.asciiFilter = "[^\r\n\x20-\x7e]"
997
- host.disableColorFilter = false //or true
998
- host.textColorFilter = "(\[{1}[0-9;]+m{1})"
999
- ```
742
+ var dotenv = require('dotenv');
743
+ dotenv.load();
1000
744
 
745
+ //Host connection and authentication parameters
746
+ var conParamsHost1 = {
747
+ host: process.env.SERVER1_HOST,
748
+ port: process.env.SERVER1_PORT,
749
+ userName: process.env.SERVER1_USER_NAME,
750
+ password: process.env.SERVER1_PASSWORD,
751
+ passPhrase: process.env.SERVER1_PASS_PHRASE,
752
+ privateKey: require('fs').readFileSync(process.env.SERVER1_PRIV_KEY_PATH)
753
+ },
754
+ conParamsHost2 = {
755
+ host: process.env.SERVER2_HOST,
756
+ port: process.env.SERVER2_PORT,
757
+ userName: process.env.SERVER2_USER_NAME,
758
+ password: process.env.SERVER2_PASSWORD
759
+ },
760
+ conParamsHost3 = {
761
+ host: process.env.SERVER3_HOST,
762
+ port: process.env.SERVER3_PORT,
763
+ userName: process.env.SERVER3_USER_NAME,
764
+ password: process.env.SERVER3_PASSWORD
765
+ }
1001
766
 
1002
- Responding to non-standard command prompts:
1003
- ----------------------
1004
- When running commands there are cases that you might need to respond to specific prompt that results from the command
1005
- being run. The command response check method is the same as in the example for the host.onCommandComplete event handler
1006
- but in this case we use it in the host.onCommandProcessing event handler. The stream object is available in
1007
- onCommandProcessing to the prompt directly using strea.write("y\n"), note "\n" might be required to complete the
1008
- response.
1009
767
 
1010
- Host definition that replaces the default handler and runs only for the current host connection
1011
- ```javascript
1012
- host.onCommandProcessing = function( command, response, sshObj, stream ) {
1013
- //Check the command and prompt exits and respond with a 'y' but only does it once
1014
- if (command == "apt-get install nano" && response.indexOf("[y/N]?") != -1 && sshObj.firstRun != true) {
1015
- //This debug message will only run when conditions are met not on every data event so is ok here
1016
- if (sshObj.debug) {this.emit('msg', this.sshObj.server.host + ": Responding to nano install");}
1017
- sshObj.firstRun = true
1018
- stream.write('y\n');
1019
- }
1020
- };
1021
- \\sshObj.firstRun can be reset to false in onCommandComplete to allow for another non-standard prompt
1022
- ```
768
+ //Host objects:
769
+ var host1 = {
770
+ server: conParamsHost1,
771
+ commands: [
772
+ "msg:connected to host: passed. Listing dir.",
773
+ "ls -l"
774
+ ],
775
+ debug: false,
776
+ onCommandComplete: function( command, response, sshObj ) {
777
+ //we are listing the dir so output it to the msg handler
778
+ if(sshObj.debug){
779
+ this.emit("msg", this.sshObj.server.host + ": host.onCommandComplete host1, command: " + command);
780
+ }
781
+ },
782
+ onEnd: function( sessionText, sshObj ) {
783
+ //show the full session output for all hosts.
784
+ if(sshObj.debug){this.emit("msg", this.sshObj.server.host + ": primary host.onEnd all sessiontText");}
785
+ this.emit ("msg", "\nAll Hosts SessiontText ---------------------------------------\n");
786
+ this.emit ("msg", sshObj.server.host + ":\n" + sessionText);
787
+ this.emit ("msg", "\nEnd sessiontText ---------------------------------------------\n");
788
+ })
789
+ },
1023
790
 
1024
- Instance definition that runs in parallel with every other commandProcessing for every host connection
1025
- ```javascript
1026
- //To handle all hosts the same add an event handler to the class instance
1027
- //Don't define an event handler in the host object with the same code, it will do it twice!
1028
- var SSH2Shell = require ('../lib/ssh2shell');
1029
- var SSH = new SSH2Shell(host);
791
+ host2 = {
792
+ server: conParamsHost2,
793
+ commands: [
794
+ "msg:connected to host: passed",
795
+ "msg:Changing to root dir",
796
+ "cd ~/",
797
+ "msg:Listing dir",
798
+ "ls -l"
799
+ ],
800
+ debug: false,
801
+ connectedMessage: "Connected to host2",
802
+ onCommandComplete: function( command, response, sshObj ) {
803
+ //we are listing the dir so output it to the msg handler
804
+ if(sshObj.debug){
805
+ this.emit("msg", this.sshObj.server.host + ": host.onCommandComplete host2, command: " + command);
806
+ }
807
+ if (command.indexOf("cd") != -1){
808
+ this.emit("msg", this.sshObj.server.host + ": Just ran a cd command:\n");
809
+ this.emit("msg", response);
810
+ }
811
+ }
812
+ },
1030
813
 
1031
- SSH.on ('commandProcessing', function onCommandProcessing( command, response, sshObj, stream ) {
814
+ host3 = {
815
+ server: conParamsHost3,
816
+ commands: [
817
+ "msg:connected to host: passed",
818
+ "hostname"
819
+ ],
820
+ debug: false,
821
+ connectedMessage: "Connected to host3",
822
+ onCommandComplete: function( command, response, sshObj) {
823
+ //we are listing the dir so output it to the msg handler
824
+ if(sshObj.debug){
825
+ this.emit("msg", this.sshObj.server.host + ": host.onCommandComplete host3, command: " + command);
826
+ }
827
+ if (command.indexOf("cd") != -1){
828
+ this.emit("msg", this.sshObj.server.host + ": Just ran hostname command:\n");
829
+ this.emit("msg", response);
830
+ }
831
+ }
832
+ }
1032
833
 
1033
- //Check the command and prompt exits and respond with a 'y'
1034
- if (command == "apt-get install nano" && response.indexOf("[y/N]?") != -1 && sshObj.firstRun != true ) {
1035
- //This debug message will only run when conditions are met not on every data event so is ok here
1036
- if (sshObj.debug) {this.emit('msg', this.sshObj.server.host + ": Responding to nano install");}
1037
- sshObj.firstRun = true
1038
- stream.write('y\n');
1039
- }
1040
- };
1041
- ```
834
+ //Set the two hosts you are tunnelling to through host1
835
+ host1.hosts = [ host2, host3 ];
1042
836
 
1043
- __*Note:*
1044
- If there is no response from the server the commandTimeout will be triggered after the idleTimeOut period.__
837
+ //or the alternative nested tunnelling method outlined above:
838
+ //host2.hosts = [ host3 ];
839
+ //host1.hosts = [ host2 ];
1045
840
 
1046
- Bash scripts on the fly:
1047
- ------------------------
1048
- 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
1049
- or get the script on the fly. You can echo/printf the script content into a file as a command, ensure it is executable,
1050
- run it and then delete it. The other option is to curl or wget the script from a remote location but
1051
- this has some risks associated with it. I like to know what is in the script I am running.
841
+ //Create the new instance
842
+ var SSH2Shell = require ('ssh2shell'),
843
+ SSH = new SSH2Shell(host1);
1052
844
 
1053
- **Note** # and > in the following commands with conflict with the host.standardPrompt definition ">$%#" change it to "$%"
845
+ //Add a commandComplete handler used by all hosts
846
+ SSH.on ('commandComplete', function onCommandComplete( command, response, sshObj ) {
847
+ //we are listing the dir so output it to the msg handler
848
+ if (command == "ls -l"){
849
+ this.emit("msg", this.sshObj.server.host + ":\n" + response);
850
+ }
851
+ }
852
+
853
+ //Add an on end event handler used by all hosts
854
+ SSH.on ('end', function( sessionText, sshObj ) {
855
+ //show the full session output. This could be emailed or saved to a log file.
856
+ this.emit ("msg", "\nSessiontText -------------------------------------------------\n");
857
+ this.emit ("msg", sshObj.server.host + ":\n" + sessionText);
858
+ this.emit ("msg", "\nEnd sessiontText ---------------------------------------------\n");
859
+ });
1054
860
 
1055
- ```
1056
- host.commands = [ "some commands here",
1057
- "if [ ! -f myscript.sh ]; then printf '#!/bin/bash\n" +
1058
- " #\n" +
1059
- " current=$(pwd);\n" +
1060
- "cd ..;\n" +
1061
- "if [ -f myfile ]; then" +
1062
- "sed \"/^[ \\t]*$/d\" ${current}/myfile | while read line; do\n" +
1063
- "printf \"Doing some stuff\";\n" +
1064
- "printf $line;\n" +
1065
- "done\n" +
1066
- "fi\n' > myscript.sh;" +
1067
- "fi",
1068
- "sudo chmod 700 myscript.sh",
1069
- "./myscript.sh",
1070
- "rm myscript.sh"
1071
- ],
861
+ //Start the process
862
+ SSH.connect();
863
+
1072
864
  ```
1073
865
 
1074
866
 
@@ -1086,12 +878,12 @@ There are two ways to add event handlers:
1086
878
  * Instance variables and functions are available through `this` including the Emitter functions like
1087
879
  this.emit("myEvent", properties).
1088
880
  * Connect, ready, error and close events are not available for definition in the host object.
1089
- * Defining a host event replaces the default event handler. Again while that host is connected.
881
+ * Defining a host event handler replaces the default coded event handler.
1090
882
 
1091
883
  2. Add an event handler, as defined below, to the class instance.
1092
884
  * Handlers added to the class instance will be triggered every time the event is raised in parallel with any other
1093
885
  handlers of the same name.
1094
- * It will not replace the internal event handler of the class be it set by the class default or a host definition.
886
+ * It will not replace the internal event handler of the class set by the class default or host definition.
1095
887
 
1096
888
  An event can be raised using `this.emit('eventName', parameters)`.
1097
889
 
@@ -1191,3 +983,31 @@ ssh2shell.on ("Unpipe", function onUnpipe(source){
1191
983
  //Source is the read stream to remove from outputting data
1192
984
  });
1193
985
  ```
986
+
987
+ Bash scripts on the fly:
988
+ ------------------------
989
+ 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
990
+ or get the script on the fly. You can echo/printf the script content into a file as a command, ensure it is executable,
991
+ run it and then delete it. The other option is to curl or wget the script from a remote location but
992
+ this has some risks associated with it. I like to know what is in the script I am running.
993
+
994
+ **Note** # and > in the following commands with conflict with the host.standardPrompt definition ">$%#" change it to "$%"
995
+
996
+ ```
997
+ host.commands = [ "some commands here",
998
+ "if [ ! -f myscript.sh ]; then printf '#!/bin/bash\n" +
999
+ " #\n" +
1000
+ " current=$(pwd);\n" +
1001
+ "cd ..;\n" +
1002
+ "if [ -f myfile ]; then" +
1003
+ "sed \"/^[ \\t]*$/d\" ${current}/myfile | while read line; do\n" +
1004
+ "printf \"Doing some stuff\";\n" +
1005
+ "printf $line;\n" +
1006
+ "done\n" +
1007
+ "fi\n' > myscript.sh;" +
1008
+ "fi",
1009
+ "sudo chmod 700 myscript.sh",
1010
+ "./myscript.sh",
1011
+ "rm myscript.sh"
1012
+ ],
1013
+ ```