ssh_tunnel_proxy 1.2.9 → 1.2.11

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.
package/README.md CHANGED
@@ -1,94 +1,107 @@
1
1
  # ssh_tunnel_proxy
2
2
  Initiate a ssh reverse tunnel proxy with forwarding ports
3
3
 
4
- ssh_tunnel_proxy can function as a stand-alone api, nodejs command line script or as part of an electron_js app. To setup a ssh tunnel, parameters are suppled for host, port, authentication and a list of proxy ports. If a ngrok api key is provided the host and port of the ngrok tunnel are obtained. A list of port forwards is provided and is validated to restrict connections to system ports on the remote host to a list of pre-defined ports such as http or https. After establishing a ssh connection on the remote server, local proxy port forwards are opened. If the connection is interrupted or connection errors occur, attempts are made re-establish the tunnel.
4
+ ssh_tunnel_proxy is a wrapper to ssh2 that provides async functionality as well as an extension to the api to include methods to setup a list of proxy forwards, exec a list of commands or startup a terminal shell. To setup a ssh tunnel, parameters are suppled for host, port, authentication and a list of proxy ports or commands to invoke. If a ngrok api key is provided the host and port of the ngrok tunnel are obtained. If the connection is interrupted or connection errors occur, attempts are made re-establish the tunnel.
5
5
 
6
- Example command line to establish a list of local forwards:
6
+ In addition to the node api, a command line function called ssh-node2 is included to start ssh sessions in a manner similar to the ssh command line utility.
7
+
8
+ ### Command line examples
9
+
10
+ Connect to remote host and establish local forwards:
11
+
12
+ ```
13
+ ./ssh2-node -u=<username> -h=192.168.1.1 -k=~/.ssh/<private_key> -L=8180:192.168.1.1:80
14
+ ```
15
+
16
+ Connect to host using parameters stored in ~/.config/ssh_tunnel_proxy/config.json:
7
17
 
8
18
  ```
9
- node main.js -c
19
+ ./ssh2-node rh2
10
20
  ```
11
21
 
12
22
  default config file, located at:
13
23
  ~/.config/ssh_tunnel_proxy/config.json
14
24
  ```json
15
25
  [{
16
- "enabled": true,
26
+ "hostname":"rh2",
17
27
  "username": "<username>",
18
- "password": "",
19
- "host": "",
20
- "port": "",
21
28
  "proxy_ports": [
22
29
  "8280:127.0.0.1:80",
23
30
  "9000:127.0.0.1:9000",
24
31
  "8122:192.168.2.1:22"
25
32
  ],
26
- "whitelist": {
27
- "80": true,
28
- "443": true,
29
- "22": true
30
- },
31
- "service_name": "ssh_proxy_client",
32
- "server_name": "test",
33
+ "private_key_filename":"~/.ssh/<private key>",
33
34
  "ngrok_api": "<ngrok api key>"
34
35
  }]
35
36
  ```
36
37
 
37
- Command line to execute a series of commands on remote host:
38
+ Execute a series of commands on remote host
38
39
  ```
39
- node main.js -c -e='uptime' -e='ls -all'
40
+ ./ssh-node2 rh2 -e='uptime' -e='ls -all'
40
41
  ```
41
42
 
42
- Or execute commands defined in default config:
43
- ```
44
- node main.js -c
45
- ```
46
- ```json
47
- [{
48
- "enabled": true,
49
- "username": "<username>",
50
- "service_name": "ssh_proxy_client",
51
- "server_name": "test",
52
- "exec" : [
53
- "uptime",
54
- "ls -all"
55
- ],
56
- "ngrok_api": "<ngrok api key>"
57
- }]
58
- ```
43
+ ### Api examples
59
44
 
60
- Start a terminal session on the remote host:
61
- ```
62
- node main.js -c -S
63
- ```
64
-
65
- Example use of api to exec remote commands using async await and processing result through streams. Complete example is in test/test_remote_exec_streams.js:
45
+ Exec remote commands using async await and processing result through streams.
66
46
 
67
47
  ```js
68
- async function test_exec_stream(opts) {
69
- sshTunnelProxy.debug_en = true;
70
- await sshTunnelProxy.connectSSH(opts);
71
-
72
- // exec remote command and save result to string
73
- const uptime_result = await sshTunnelProxy.execCmd('uptime');
74
- console.log('uptime:' + uptime_result.toString());
75
-
76
- // exec remote command and pipe data through tunnel until end of data
77
- const tunnel = new PassThrough();
78
- tunnel.pipe(split())
79
- .pipe(to_ls_JSON)
80
- .pipe(to_JSON_string)
81
- .pipe(process.stdout);
82
-
83
- await sshTunnelProxy.execCmd('ls -all', tunnel);
84
-
85
- // stream processing complete
86
- console.log('done');
87
- process.exit();
48
+ // send result of cmd through pipeline, generating a stream of json objects
49
+ function lsTest(cmdProxy, cmd, destination) {
50
+
51
+ return new Promise( (resolveCmd) => {
52
+
53
+ // set input of pipeline to split data into lines (npm i split)
54
+ const tunnel = split();
55
+
56
+ // when pipeline is ready exec shell cmd
57
+ const pipelineReady = (socket) => {
58
+
59
+ return new Promise((resolve) => {
60
+
61
+ // invoke command on remote host and send results to pipeline
62
+ cmdProxy.execCmd(cmd, tunnel)
63
+
64
+ // stream processing complete, cleanup pipeline and exit
65
+ .then(() => {
66
+ //self.cleanupPipeline(socket);
67
+ resolveCmd();
68
+ });
69
+ resolve();
70
+ })
71
+ }
72
+
73
+ // pipe shell cmd result through json parser pipeline to destination
74
+ pipeline(tunnel,
75
+ self.parse(),
76
+ self.toJSONString(),
77
+ destination,
78
+ pipelineReady
79
+ );
80
+ })
81
+ }
82
+
83
+ async function runCmd() {
84
+
85
+ const opts = {
86
+ "hostname":"rh2",
87
+ "username": "<username>",
88
+ "private_key_filename":"~/.ssh/<private key>",
89
+ "ngrok_api": "<ngrok api key>"
90
+ }
91
+
92
+ const sshTunnelProxy = new SSHTunnelProxy();
93
+
94
+ // connect to remote host
95
+ await sshTunnelProxy.connectSSH(opts);
96
+
97
+ // invoke ls -all on remote host and parse result to json object string
98
+ await lsTest(sshTunnelProxy, 'ls -all', process.stdout);
88
99
  }
100
+
101
+ runCmd();
89
102
  ```
90
103
 
91
- The following code is an example of use of the api with electronjs.
104
+ Example of use of the api with electronjs.
92
105
 
93
106
  main.js:
94
107
  ```js
package/lib/index.js CHANGED
@@ -17,6 +17,7 @@ License: MIT
17
17
  */
18
18
 
19
19
  const net = require('net');
20
+ const fs = require('fs');
20
21
  const process = require('process');
21
22
  const { Buffer } = require('node:buffer');
22
23
  const { Client } = require('electron-ssh2');
@@ -26,6 +27,8 @@ const NgrokApi = require('./ngrok_service');
26
27
 
27
28
  const keypairStorage = new KeypairStorage();
28
29
 
30
+ const homedir = require('os').homedir();
31
+
29
32
  class SSHTunnelProxy extends Client {
30
33
 
31
34
  constructor() {
@@ -38,15 +41,16 @@ class SSHTunnelProxy extends Client {
38
41
  }
39
42
 
40
43
  // function to close all active sockets for tunnel restart and exception handling
41
- close_sockets(server_name, proxy_ports) {
44
+ close_sockets(proxy_ports) {
42
45
 
43
- if (!this.listener[server_name]) return;
46
+ if (!proxy_ports) return;
44
47
 
45
48
  proxy_ports.forEach(proxy_port => {
46
- const server = this.listener[server_name][proxy_port];
49
+ const server = this.listener[proxy_port];
47
50
  if (server && server.listening) {
48
51
  this.debug_en && this.debug('SSH Server :: closing forward:', proxy_port)
49
52
  server.close();
53
+ delete this.listener[proxy_port];
50
54
  }
51
55
  });
52
56
  }
@@ -102,12 +106,17 @@ class SSHTunnelProxy extends Client {
102
106
  }
103
107
 
104
108
  // function to setup proxy forwards for ssh tunnel
105
- setupProxyPorts(server_name, proxy_ports) {
109
+ setupProxyPorts(proxy_ports) {
106
110
 
107
111
  return new Promise((resolve, reject) => {
108
112
 
113
+ if (!proxy_ports) {
114
+ resolve();
115
+ return;
116
+ }
117
+
109
118
  var listeners = 0;
110
- if (this.listener[server_name]) this.listener[server_name].isConnected = true;
119
+ this.listener.isConnected = true;
111
120
  const _this = this;
112
121
 
113
122
  // iterate through a list of local forward ports and create a local proxy port
@@ -116,13 +125,12 @@ class SSHTunnelProxy extends Client {
116
125
  const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
117
126
 
118
127
  // create local socket server
119
- const server = this.listener[server_name][proxy_port];
128
+ const server = _this.listener[proxy_port];
120
129
  if (server && server.listening) {
121
- this.debug_en && this.debug('SSH Server :: closing forward 2:', proxy_port)
130
+ _this.debug_en && this.debug('SSH Server :: closing forward 2:', proxy_port)
122
131
  server.close();
123
132
  }
124
- this.listener[server_name][proxy_port] = null;
125
- this.listener[server_name][proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
133
+ _this.listener[proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
126
134
 
127
135
  if (this.debug_en) {
128
136
  var debug_msg = 'SSH Server :: connection on ' + local_port + ' ' + socket.remotePort;
@@ -138,7 +146,7 @@ class SSHTunnelProxy extends Client {
138
146
  try {
139
147
  var status_msg = 'SSH Server :: before listen on ' + local_port;
140
148
  _this.emit('debug', status_msg);
141
- _this.listener[server_name][proxy_port].listen(local_port, () => {
149
+ _this.listener[proxy_port].listen(local_port, () => {
142
150
 
143
151
  // emit server listening on port message
144
152
  var status_msg = 'SSH Server :: bound on ' + local_port;
@@ -149,7 +157,7 @@ class SSHTunnelProxy extends Client {
149
157
  _this.emit('ssh_tunnel_ready', {});
150
158
  if (_this._tunnelReadyTimeout) {
151
159
  clearTimeout(this._tunnelReadyTimeout);
152
- _this._tunnelReadyTimeout = undefined
160
+ _this._tunnelReadyTimeout = undefined;
153
161
  }
154
162
  resolve();
155
163
  }
@@ -173,27 +181,24 @@ class SSHTunnelProxy extends Client {
173
181
  reject(err);
174
182
  return;
175
183
  }
176
- stream.on('close', () => {
184
+
185
+ const onClose = () => {
177
186
  var buf = Buffer.concat(bufs);
178
187
  var err = Buffer.concat(errs);
188
+ stream.removeListener('close', onClose);
179
189
  resolve(buf, err);
180
- })
181
-
182
- if (dataStream) {
183
- stream.pipe(dataStream);
184
- } else {
185
- stream.on('data', (data) => {
186
- bufs.push(data);
187
- })
188
190
  }
191
+ stream.on('close', onClose);
189
192
 
190
- if (errStream) {
191
- stream.stderr.pipe(errStream);
192
- } else {
193
- stream.stderr.on('data', (data) => {
194
- errs.push(data);
195
- });
196
- }
193
+ stream.on('data', (data) => {
194
+ if (dataStream) dataStream.write(data);
195
+ else bufs.push(data);
196
+ })
197
+
198
+ stream.stderr.on('data', (data) => {
199
+ if (errStream) errStream.write(data);
200
+ else errs.push(data);
201
+ });
197
202
 
198
203
  }
199
204
  this.exec(cmd, remote_exec);
@@ -238,14 +243,10 @@ class SSHTunnelProxy extends Client {
238
243
  return new Promise((resolve) => {
239
244
 
240
245
  // exit if connection already established, avoid redundant connection on retry
241
- if (_this.listener[opts.server_name] && _this.listener[opts.server_name].isConnected) resolve();
246
+ if (_this.listener.isConnected) resolve();
242
247
 
243
248
  // close open sockets on server, otherwise initialize open sockets storage
244
- if (_this.listener[opts.server_name]) {
245
- _this.close_sockets(opts.server_name, opts.proxy_ports);
246
- } else {
247
- _this.listener[opts.server_name] = {};
248
- }
249
+ _this.close_sockets(opts.proxy_ports);
249
250
 
250
251
  _this.on('ready', async () => {
251
252
 
@@ -255,47 +256,48 @@ class SSHTunnelProxy extends Client {
255
256
  _this._tunnelReadyTimeout = undefined;
256
257
  }
257
258
 
259
+ // if local forwarding requested, setup local forwarding ports to remote host
260
+ if (opts.proxy_ports) {
261
+ await _this.setupProxyPorts(opts.proxy_ports);
262
+ }
263
+
258
264
  // if shell requested, enable remote terminal
259
265
  if (opts.shell) {
260
266
  _this.shell(_this.remote_shell);
261
267
 
262
- // if exec requested, exec series of cmds
268
+ // otherwise, if exec requested, exec series of cmds
263
269
  } else {
264
270
  if (opts.exec && opts.exec.length > 0) {
265
271
  for (var i = 0; i < opts.exec.length; i++) {
266
272
  var cmd = opts.exec[i];
267
- console.log(opts.username + '@' + opts.hostname + ':' + cmd);
273
+ _this.debug_en && _this.debug(opts.username + '@' + opts.hostname + ':' + cmd);
268
274
  var result = await _this.execCmd(cmd);
269
- console.log(result.toString());
275
+ _this.debug_en && _this.debug(result.toString());
270
276
  }
271
277
  process.exit();
272
278
  }
273
279
  }
274
280
 
275
- // if local forward requested, setup local forwarding ports to remote host
276
- if (opts.proxy_ports) {
277
- await _this.setupProxyPorts(opts.server_name, opts.proxy_ports);
278
- }
279
281
  resolve();
280
282
  });
281
283
 
282
284
  _this.on('end', () => {
283
285
  _this.debug_en && _this.debug('SSH Client :: end');
284
- _this.close_sockets(opts.server_name, opts.proxy_ports);
286
+ _this.close_sockets(opts.proxy_ports);
285
287
  });
286
288
 
287
289
  _this.on('close', () => {
288
290
  _this.debug_en && _this.debug('SSH Client :: close');
289
- _this.close_sockets(opts.server_name, opts.proxy_ports);
291
+ _this.close_sockets(opts.proxy_ports);
290
292
  });
291
293
 
292
294
  _this.on('error', err => {
293
295
  _this.debug_en && _this.debug('SSH Client :: error :: ' + err);
294
296
  _this.emit('debug', err);
295
- if (_this.listener[opts.server_name]) _this.listener[opts.server_name].isConnected = false;
297
+ _this.listener.isConnected = false;
296
298
  if (_this._tunnelReadyTimeout) {
297
- clearTimeout(this._tunnelReadyTimeout);
298
- _this._tunnelReadyTimeout = undefined
299
+ clearTimeout(this._tunnelReadyTimeout.elReadyTimeout);
300
+ _this._tunnelReadyTimeout = undefined;
299
301
  }
300
302
  _this.ssh_retry_connect(opts);
301
303
  resolve();
@@ -309,19 +311,19 @@ class SSHTunnelProxy extends Client {
309
311
  _this.debug_en && _this.debug('SSH Client :: greeting:', message);
310
312
  });
311
313
 
312
- // initiate ssh client connection to remote host
313
- // ssh client debug function for verbose ssh connection details
314
- const debug_client = (this.debug_ssh) ? (...args) => { console.log(...args); } : null;
315
- _this.connect({
316
- host: opts.host,
317
- port: opts.port,
318
- username: opts.username,
319
- password: opts.password,
320
- privateKey: opts.private_key,
321
- debug: debug_client,
322
- keepaliveInterval: 10000
323
- });
314
+ // create deep copy of opts as connect options
315
+ const config = JSON.parse(JSON.stringify(opts));
316
+
317
+ // add default ssh2 config options
318
+ config.debug = (this.debug_ssh) ? (...args) => { console.log(...args); } : null;
319
+ config.keepaliveInterval = opts.keepaliveInterval || 10000;
324
320
 
321
+ // remove ssh_tunnel_proxy config extensions
322
+ const ssh_tunnel_extensions = ['alias', 'disabled', 'hostname', 'ngrok_api', 'server_name', 'service_name', 'whitelist', 'private_key_filename'];
323
+ for (var i = 0; i < ssh_tunnel_extensions.length; i++) delete config[ssh_tunnel_extensions[i]];
324
+
325
+ // connect to remote host
326
+ _this.connect(config);
325
327
  });
326
328
  }
327
329
 
@@ -330,7 +332,7 @@ class SSHTunnelProxy extends Client {
330
332
  if (isNaN(port_str)) return false;
331
333
  const port = parseInt(port_str);
332
334
  if (port < 1 || port > 65535) return false;
333
- if (port < 1024 && whitelist[port] === undefined) return false;
335
+ if (port < 1024 && whitelist && whitelist[port] === undefined) return false;
334
336
  return true;
335
337
  }
336
338
 
@@ -396,13 +398,11 @@ class SSHTunnelProxy extends Client {
396
398
  // todo: if opts have changed while service is running, shutdown current service and restart with new opts
397
399
  async connectSSH(opts, whitelist) {
398
400
 
399
- this.debug_en && this.debug('connectSSH:\n', JSON.stringify(opts, null, 2));
400
-
401
401
  // make deep copy of opts for modification
402
402
  var _opts = JSON.parse(JSON.stringify(opts));
403
403
 
404
404
  // setup whitelist from param or opts in case of error retry
405
- _opts.whitelist = (whitelist !== undefined) ? whitelist : (_opts.whitelist) ? _opts.whitelist : {};
405
+ _opts.whitelist = (whitelist !== undefined) ? whitelist : (_opts.whitelist) ? _opts.whitelist : null;
406
406
 
407
407
  // validate local port forwards, emit error and quit if invalid
408
408
  try {
@@ -418,9 +418,24 @@ class SSHTunnelProxy extends Client {
418
418
  _opts.password = undefined;
419
419
  }
420
420
 
421
+ if (_opts.private_key_filename) {
422
+ try {
423
+ if (_opts.private_key_filename[0] == '~') {
424
+ _opts.private_key_filename = homedir + _opts.private_key_filename.substr(1);
425
+ }
426
+ _opts.privateKey = fs.readFileSync(_opts.private_key_filename).toString();
427
+ } catch (err) {
428
+ console.log('Error loading key:', err);
429
+ }
430
+ }
431
+
421
432
  // retrieve private key from system keychain with supplied service name and server
422
433
  // if no key found, authentication is supplied username, password
423
- _opts.private_key = await keypairStorage.get_keypair(opts.service_name, opts.server_name);
434
+ if (_opts.service_name && _opts.server_name) {
435
+ _opts.privateKey = await keypairStorage.get_keypair(opts.service_name, opts.server_name);
436
+ } else {
437
+ if (!_opts.server_name) _opts.server_name = 'sshtun';
438
+ }
424
439
 
425
440
  // if ngrok api key provided, obtain hostname and hostport from ngrok api
426
441
  // if no ngrok tunnel specified, use supplied host and port
@@ -430,7 +445,7 @@ class SSHTunnelProxy extends Client {
430
445
  var hostport = await ngrokApi.get_hostport()
431
446
  .catch(() => {
432
447
  _this.debug_en && _this.debug('ngrok get_hostport connection error');
433
- if (this.listener[opts.server_name]) this.listener[opts.server_name].isConnected = false;
448
+ _this.listener.isConnected = false;
434
449
  _this.ssh_retry_connect(_opts);
435
450
  });
436
451
  if (hostport && hostport.host) {
@@ -440,6 +455,8 @@ class SSHTunnelProxy extends Client {
440
455
  else return null;
441
456
  }
442
457
 
458
+ this.debug_en && this.debug('connectSSH:\n', JSON.stringify(_opts, null, 2));
459
+
443
460
  // initiate ssh tunnel, block until tunnel is established or error
444
461
  return await this.ssh_start_tunnel(_opts)
445
462
  .catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssh_tunnel_proxy",
3
- "version": "1.2.9",
3
+ "version": "1.2.11",
4
4
  "description": "obtain ngrok host port from api and establish a ssh connection",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/node
1
2
  const process = require('process');
2
3
  const fs = require('fs');
3
4
  const { SSHTunnelProxy, KeypairStorage } = require('./lib/index.js');
@@ -21,14 +22,6 @@ var service = null;
21
22
  var account = null;
22
23
  var shell = null;
23
24
 
24
- function get_key_from_file(filename) {
25
- try {
26
- return fs.readFileSync(filename);
27
- } catch (err) {
28
- console.log('Error loading key:', err);
29
- }
30
- }
31
-
32
25
  function get_config(filename) {
33
26
  try {
34
27
  configs = JSON.parse(fs.readFileSync(filename), 'utf8');
@@ -41,7 +34,7 @@ function get_default_config() {
41
34
  try {
42
35
  configs = JSON.parse(fs.readFileSync(homedir + '/.config/ssh_tunnel_proxy/config.json'), 'utf8');
43
36
  } catch (err) {
44
- console.log('Error loading config file:', err);
37
+ //console.log('Error loading config file:', err);
45
38
  }
46
39
  }
47
40
 
@@ -114,15 +107,21 @@ function main(args) {
114
107
  username = value;
115
108
  break;
116
109
  default:
117
- if (i > 1) console.log('Invalid argument:', arg);
110
+ if (i == 2) remote_host = cmd;
111
+ if (i > 2) console.log('Invalid argument:', arg);
118
112
  }
119
113
  }
120
114
 
121
115
  // prevent process from exiting
122
- process.stdin.resume();
116
+ //process.stdin.resume();
123
117
 
124
- // if no config file specified, build config from args
118
+ // if no config file specified attempt to load default config
125
119
  if (!configs) {
120
+ get_default_config();
121
+ }
122
+
123
+ // no default config build config from args
124
+ if (!remote_host) {
126
125
  var config = {
127
126
  enabled: true,
128
127
  username: username,
@@ -140,24 +139,28 @@ function main(args) {
140
139
  }
141
140
 
142
141
  // process each tunnel config in ssh proxy tunnel list
142
+ var remote_server = null;
143
143
  configs.forEach(async config => {
144
144
 
145
145
  // skip tunnel if not enabled
146
- if (!config.enabled) return;
146
+ if (config.disabled) return;
147
147
 
148
148
  // if remote host specified, only select tunnel for remote host
149
- if (remote_host && remote_host !== config.alias) return;
149
+ if (remote_host && remote_host !== config.hostname) return;
150
+ config.server_name = config.hostname || remote_host || 'ssh_tunnel_proxy';
151
+ remote_server = remote_host;
150
152
 
151
153
  // if system key storage name specified get key from system keychain
152
154
  if (config.server_name) {
153
155
  const service_name = config.service_name || 'ssh_tunnel_proxy';
154
156
  config.private_key = await keypairStorage.get_keypair(service_name, config.server_name);
155
157
  }
158
+
156
159
  // if private key filename specified, read private key
157
- else if (config.private_key) {
158
- config.private_key = get_key_from_file(config.private_key);
160
+ if (privateKey) {
161
+ config.private_key_filename = privateKey;
159
162
  }
160
- if (shell) {
163
+ if (shell || exec.length === 0) {
161
164
  config.shell = true;
162
165
  }
163
166
  if (exec.length > 0) {
@@ -169,7 +172,9 @@ function main(args) {
169
172
  sshTunnelProxy.debug_en = debug;
170
173
  sshTunnelProxy.connectSSH(config);
171
174
  });
172
-
175
+ if (remote_host && !remote_server) {
176
+ console.log(`Remote host ${remote_host} not found`);
177
+ }
173
178
 
174
179
  }
175
180
 
@@ -0,0 +1,217 @@
1
+ /*
2
+ test exec cmd on remote server, parse to json object stream
3
+ */
4
+
5
+ //const assert = require('assert');
6
+ //const { describe, it } = require('mocha');
7
+
8
+ const fs = require('fs');
9
+ const process = require('node:process');
10
+ const { SSHTunnelProxy } = require('..');
11
+ const { PassThrough, pipeline } = require("stream");
12
+ const through = require('through');
13
+ const split = require('split');
14
+
15
+ // get config file containing api keys
16
+ const homedir = require('os').homedir();
17
+ var config = null;
18
+ try {
19
+ config = JSON.parse(fs.readFileSync(homedir + '/.config/ssh_tunnel_proxy/config.json'), 'utf8');
20
+ } catch (err) {
21
+ console.log('Error reading config:' + err);
22
+ process.exit();
23
+ }
24
+
25
+ if (!config && !config[1]) {
26
+ console.log('Specified config not found in configs');
27
+ process.exit();
28
+ }
29
+
30
+ const opts = config[1];
31
+
32
+ const sshTunnelProxy = new SSHTunnelProxy();
33
+
34
+ // parse shell command result into array of strings
35
+ function parse_cmd(str) {
36
+
37
+ let result = [];
38
+
39
+ let regex = /(([\w-/_~.\:\[\]]+)|("(.*?)")|('(.*?)'))/g;
40
+ let groups = [2, 4, 6];
41
+ let match;
42
+
43
+ while ((match = regex.exec(str)) !== null) {
44
+ // This is necessary to avoid infinite loops
45
+ // with zero-width matches
46
+ if (match.index === regex.lastIndex) {
47
+ regex.lastIndex++;
48
+ }
49
+
50
+ // For this to work the regex groups need to
51
+ // be mutually exclusive
52
+ groups.forEach(function (group) {
53
+ if (match[group]) {
54
+ result.push(match[group]);
55
+ }
56
+ });
57
+ /*
58
+ let log_matches = false;
59
+ // show matches for debugging
60
+ log_matches && match.forEach(function (m, group) {
61
+ if (m) {
62
+ console.log(`Match '${m}' found in group: ${group}`);
63
+ }
64
+ });
65
+ */
66
+ }
67
+ return result;
68
+ }
69
+
70
+ // generate object from arrays of names/values
71
+ function toObj(names, values) {
72
+ const obj = {};
73
+ for (var i = 0; i < names.length; i++) {
74
+ obj[names[i]] = values[i];
75
+ }
76
+ return obj;
77
+ }
78
+
79
+
80
+ // merge array items beginning at start into single item and append back to array
81
+ function mergeArray(ar, start) {
82
+ const ar2 = ar.slice(start);
83
+ const ar1 = ar.slice(0, start);
84
+ const item = ar2.join(' ');
85
+ ar1.push(item);
86
+ return ar1;
87
+ }
88
+
89
+ /*
90
+ Feb 4 21:34:17 tim-ThinkPad-P50s systemd[1]: Started Run anacron jobs.
91
+ */
92
+ function parseSyslog(line) {
93
+ const format = ['M', 'D', 'T', 'host', 'proc', 'msg'];
94
+ const values = parse_cmd(line);
95
+ const log = mergeArray(values, 5);
96
+ const obj = toObj(format, log);
97
+ if (obj.M && obj.D && obj.T) {
98
+ obj.date = obj.M + ' ' + obj.D + ' ' + obj.T;
99
+ delete obj.M;
100
+ delete obj.D;
101
+ delete obj.T;
102
+ }
103
+ return obj;
104
+ }
105
+
106
+ // parse ls long line output into json object
107
+ function processLSLong(line) {
108
+ const format10 = ['pm', 'links', 'user', 'group', 'size', 'M', 'D', 'H', 'MM', 'name'];
109
+ const format9 = ['pm', 'links', 'user', 'group', 'size', 'M', 'D', 'Y', 'name'];
110
+ const values = parse_cmd(line);
111
+ const obj = toObj((values.length < 10) ? format9 : format10, values);
112
+ if (obj.pm) {
113
+ obj.type = obj.pm.substring(0, 1);
114
+ obj.pm = obj.pm.substring(1);
115
+ }
116
+ if (obj.M && obj.D) {
117
+ obj.date = obj.M + ' ' + obj.D;
118
+ delete obj.M;
119
+ delete obj.D;
120
+ if (obj.Y) {
121
+ obj.date += ' ' + obj.Y;
122
+ delete obj.Y;
123
+ }
124
+ }
125
+ if (obj.H && obj.MM) {
126
+ obj.time = obj.H + ':' + obj.MM;
127
+ delete obj.H;
128
+ delete obj.MM;
129
+ }
130
+ return obj;
131
+ }
132
+
133
+ function to_lsParse() {
134
+ return new through(function (data) {
135
+ const obj = processLSLong(data);
136
+ if (obj.name) this.queue(obj);
137
+ });
138
+ }
139
+
140
+ function to_syslogParse() {
141
+ return new through(function (data) {
142
+ const obj = parseSyslog(data);
143
+ if (obj.proc) this.queue(obj);
144
+ });
145
+ }
146
+
147
+ function to_JSONString() {
148
+ return new through(function (data) {
149
+ this.queue(JSON.stringify(data) + '\n');
150
+ })
151
+ }
152
+
153
+ // lsLongShellProc conversion tests
154
+ async function lsTest(sshTunnelProxy) {
155
+ return new Promise(async (resolve) => {
156
+
157
+ // exec remote command and pipe data through tunnel until end of data
158
+ const tunnel = new PassThrough();
159
+ pipeline(tunnel,
160
+ split(),
161
+ to_lsParse(),
162
+ to_JSONString(),
163
+ process.stdout,
164
+ () => { }
165
+ );
166
+
167
+ const lscmd = 'ls -all';
168
+ console.log('\ninvoking ' + lscmd + ' on remote host:\n');
169
+ await sshTunnelProxy.execCmd(lscmd, tunnel);
170
+
171
+ // stream processing complete
172
+ console.log('ls -all completed');
173
+ resolve();
174
+ })
175
+ }
176
+
177
+ // lsLongShellProc conversion tests
178
+ async function syslogTest(sshTunnelProxy) {
179
+ return new Promise(async (resolve) => {
180
+
181
+ // exec remote command and pipe data through tunnel until end of data
182
+ const tunnel = new PassThrough();
183
+ pipeline(tunnel,
184
+ split(),
185
+ to_syslogParse(),
186
+ to_JSONString(),
187
+ process.stdout,
188
+ () => { }
189
+ );
190
+ const syslogcmd = 'tail /var/log/syslog';
191
+ console.log('\ninvoking ' + syslogcmd + ' on remote host:\n');
192
+ await sshTunnelProxy.execCmd(syslogcmd, tunnel);
193
+
194
+ // stream processing complete
195
+ console.log(syslogcmd+' completed');
196
+ resolve();
197
+ })
198
+ }
199
+
200
+ async function runTests() {
201
+
202
+ // connect to remote host
203
+ sshTunnelProxy.debug_en = true;
204
+ await sshTunnelProxy.connectSSH(opts);
205
+
206
+ // invoke ls -all on remote host and parse result to json object string
207
+ await lsTest(sshTunnelProxy);
208
+
209
+ // invoke tail /var/log/syslog on remote host and parse result to json object string
210
+ await syslogTest(sshTunnelProxy);
211
+
212
+ process.exit();
213
+ }
214
+
215
+ runTests();
216
+
217
+
@@ -1,140 +0,0 @@
1
- /*
2
-
3
- test remote exec streams
4
-
5
- */
6
-
7
- //const assert = require('assert');
8
- //const { describe, it } = require('mocha');
9
- const process = require('process');
10
- const fs = require('fs');
11
- const { PassThrough } = require("stream");
12
- const through = require('through');
13
- const split = require('split');
14
-
15
- const { SSHTunnelProxy } = require('../lib/index.js');
16
-
17
- // get config file containing api keys
18
- const homedir = require('os').homedir();
19
- var config = null;
20
- try {
21
- config = JSON.parse(fs.readFileSync(homedir + '/.config/ssh_tunnel_proxy/config.json'), 'utf8');
22
- } catch (err) {
23
- console.log('Error reading config');
24
- process.exit();
25
- }
26
-
27
- if (!config && !config[1]) {
28
- console.log('Specified config not found in configs');
29
- process.exit();
30
- }
31
-
32
- const opts = config[1];
33
-
34
- const sshTunnelProxy = new SSHTunnelProxy();
35
-
36
- // parse shell command result into array of strings
37
- function parse_cmd(str) {
38
-
39
- let result = [];
40
- let log_matches = false;
41
-
42
- let regex = /(([\w-/_~]+)|("(.*?)")|('(.*?)'))/g;
43
- let groups = [2, 4, 6];
44
- let match;
45
-
46
- while ((match = regex.exec(str)) !== null) {
47
- // This is necessary to avoid infinite loops
48
- // with zero-width matches
49
- if (match.index === regex.lastIndex) {
50
- regex.lastIndex++;
51
- }
52
-
53
- // For this to work the regex groups need to
54
- // be mutually exclusive
55
- groups.forEach(function (group) {
56
- if (match[group]) {
57
- result.push(match[group]);
58
- }
59
- });
60
-
61
- // show matches for debugging
62
- log_matches && match.forEach(function (m, group) {
63
- if (m) {
64
- console.log(`Match '${m}' found in group: ${group}`);
65
- }
66
- });
67
- }
68
- return result;
69
- }
70
-
71
- // generate object from arrays of names/values
72
- function toObj(names, values) {
73
- const obj = {};
74
- for (var i = 0; i < names.length; i++) {
75
- obj[names[i]] = values[i];
76
- }
77
- return obj;
78
- }
79
-
80
- // stream that receives single line of ls long data and converts to json object
81
- const to_ls_JSON = new through(function (data) {
82
- const file = processLSLong(data);
83
- if (file.name) this.queue(file);
84
- });
85
-
86
- // stream that receives objects and converts to JSON strings
87
- const to_JSON_string = new through(function (data) {
88
- this.queue(JSON.stringify(data) + '\n');
89
- });
90
-
91
- // parse ls long line output into json object
92
- function processLSLong(line) {
93
- const format10 = ['pm', 'links', 'user', 'group', 'size', 'M', 'D', 'H', 'MM', 'name'];
94
- const format9 = ['pm', 'links', 'user', 'group', 'size', 'M', 'D', 'Y', 'name'];
95
- const values = parse_cmd(line);
96
- const obj = toObj((values.length < 10) ? format9 : format10, values);
97
- if (obj.pm) {
98
- obj.type = obj.pm.substring(0, 1);
99
- obj.pm = obj.pm.substring(1);
100
- }
101
- if (obj.M && obj.D) {
102
- obj.date = obj.M + ' ' + obj.D;
103
- delete obj.M;
104
- delete obj.D;
105
- if (obj.Y) {
106
- obj.date += ' ' + obj.Y;
107
- delete obj.Y;
108
- }
109
- }
110
- if (obj.H && obj.MM) {
111
- obj.time = obj.H + ':' + obj.MM;
112
- delete obj.H;
113
- delete obj.MM;
114
- }
115
- return obj;
116
- }
117
-
118
- async function test_exec_stream() {
119
- sshTunnelProxy.debug_en = true;
120
- await sshTunnelProxy.connectSSH(opts);
121
-
122
- // exec remote command and save result to string
123
- const uptime_result = await sshTunnelProxy.execCmd('uptime');
124
- console.log('uptime:' + uptime_result.toString());
125
-
126
- // exec remote command and pipe data through tunnel until no data
127
- const tunnel = new PassThrough();
128
- tunnel.pipe(split())
129
- .pipe(to_ls_JSON)
130
- .pipe(to_JSON_string)
131
- .pipe(process.stdout);
132
-
133
- await sshTunnelProxy.execCmd('ls -all', tunnel);
134
-
135
- // stream processing complete
136
- console.log('done');
137
- process.exit();
138
- }
139
-
140
- test_exec_stream();