ssh_tunnel_proxy 1.2.7 → 1.2.9

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.
@@ -28,7 +28,7 @@
28
28
  "<node_internals>/**"
29
29
  ],
30
30
  "program": "${file}",
31
- "args":["-c","-e=uptime","-e=ls -all"]
31
+ "args":["-c","-r=nqs"]
32
32
  }
33
33
  ]
34
34
  }
package/README.md CHANGED
@@ -55,7 +55,37 @@ node main.js -c
55
55
  ],
56
56
  "ngrok_api": "<ngrok api key>"
57
57
  }]
58
+ ```
59
+
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:
58
66
 
67
+ ```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();
88
+ }
59
89
  ```
60
90
 
61
91
  The following code is an example of use of the api with electronjs.
package/lib/index.js CHANGED
@@ -18,6 +18,7 @@ License: MIT
18
18
 
19
19
  const net = require('net');
20
20
  const process = require('process');
21
+ const { Buffer } = require('node:buffer');
21
22
  const { Client } = require('electron-ssh2');
22
23
 
23
24
  const KeypairStorage = require('./keypair_storage');
@@ -101,83 +102,102 @@ class SSHTunnelProxy extends Client {
101
102
  }
102
103
 
103
104
  // function to setup proxy forwards for ssh tunnel
104
- on_ssh_client_ready(server_name, proxy_ports, resolve) {
105
+ setupProxyPorts(server_name, proxy_ports) {
105
106
 
106
- var listeners = 0;
107
- if (this.listener[server_name]) this.listener[server_name].isConnected = true;
108
- const _this = this;
107
+ return new Promise((resolve, reject) => {
109
108
 
110
- // iterate through a list of local forward ports and create a local proxy port
111
- proxy_ports.forEach(proxy_port => {
109
+ var listeners = 0;
110
+ if (this.listener[server_name]) this.listener[server_name].isConnected = true;
111
+ const _this = this;
112
112
 
113
- const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
113
+ // iterate through a list of local forward ports and create a local proxy port
114
+ proxy_ports.forEach(proxy_port => {
114
115
 
115
- // create local socket server
116
- const server = this.listener[server_name][proxy_port];
117
- if (server && server.listening) {
118
- this.debug_en && this.debug('SSH Server :: closing forward 2:', proxy_port)
119
- server.close();
120
- }
121
- this.listener[server_name][proxy_port] = null;
122
- this.listener[server_name][proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
116
+ const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
123
117
 
124
- if (this.debug_en) {
125
- var debug_msg = 'SSH Server :: connection on ' + local_port + ' ' + socket.remotePort;
126
- _this.emit('debug', debug_msg);
127
- _this.debug(debug_msg);
118
+ // create local socket server
119
+ const server = this.listener[server_name][proxy_port];
120
+ if (server && server.listening) {
121
+ this.debug_en && this.debug('SSH Server :: closing forward 2:', proxy_port)
122
+ server.close();
128
123
  }
124
+ this.listener[server_name][proxy_port] = null;
125
+ this.listener[server_name][proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
129
126
 
130
- // create a proxy forward between local and remote ports
131
- _this.setup_ssh_forward(socket, remote_hostname, remote_port);
132
- });
127
+ if (this.debug_en) {
128
+ var debug_msg = 'SSH Server :: connection on ' + local_port + ' ' + socket.remotePort;
129
+ _this.emit('debug', debug_msg);
130
+ _this.debug(debug_msg);
131
+ }
133
132
 
134
- // start listening on port
135
- try {
136
- var status_msg = 'SSH Server :: before listen on ' + local_port;
137
- _this.emit('debug', status_msg);
138
- _this.listener[server_name][proxy_port].listen(local_port, () => {
133
+ // create a proxy forward between local and remote ports
134
+ _this.setup_ssh_forward(socket, remote_hostname, remote_port);
135
+ });
139
136
 
140
- // emit server listening on port message
141
- var status_msg = 'SSH Server :: bound on ' + local_port;
137
+ // start listening on port
138
+ try {
139
+ var status_msg = 'SSH Server :: before listen on ' + local_port;
142
140
  _this.emit('debug', status_msg);
143
-
144
- // if all listeners have been successfully established, resolve setup connection
145
- if (listeners++ >= proxy_ports.length - 1) {
146
- _this.emit('ssh_tunnel_ready', {});
147
- if (_this._tunnelReadyTimeout) {
148
- clearTimeout(this._tunnelReadyTimeout);
149
- _this._tunnelReadyTimeout = undefined
141
+ _this.listener[server_name][proxy_port].listen(local_port, () => {
142
+
143
+ // emit server listening on port message
144
+ var status_msg = 'SSH Server :: bound on ' + local_port;
145
+ _this.emit('debug', status_msg);
146
+
147
+ // if all listeners have been successfully established, resolve setup connection
148
+ if (listeners++ >= proxy_ports.length - 1) {
149
+ _this.emit('ssh_tunnel_ready', {});
150
+ if (_this._tunnelReadyTimeout) {
151
+ clearTimeout(this._tunnelReadyTimeout);
152
+ _this._tunnelReadyTimeout = undefined
153
+ }
154
+ resolve();
150
155
  }
151
- resolve();
152
- }
153
- });
154
- } catch (err) {
155
- _this.debug_en && _this.debug('listen err:', err);
156
- }
156
+ });
157
+ } catch (err) {
158
+ _this.debug_en && _this.debug('listen err:', err);
159
+ reject(err);
160
+ }
161
+ });
157
162
  });
158
163
  }
159
164
 
160
- exec_next() {
161
- const _this = this;
162
- const remote_exec = (err, stream) => {
163
- if (err) throw err;
164
- stream.on('close', () => {
165
- process.stdout.write('-- exec completed --\n\n');
166
- if (!_this.exec_next()) process.exit();
167
- }).on('data', (data) => {
168
- process.stdout.write(data);
169
- }).stderr.on('data', (data) => {
170
- process.stderr.write(data);
171
- });
172
- }
173
- if (this.exec_cmds.length > 0) {
174
- const cmd = this.exec_cmds[0];
175
- this.exec_cmds.shift();
176
- console.log('$: ' + cmd);
165
+ execCmd(cmd, dataStream, errStream) {
166
+
167
+ return new Promise((resolve, reject) => {
168
+ var bufs = [];
169
+ var errs = [];
170
+
171
+ const remote_exec = (err, stream) => {
172
+ if (err) {
173
+ reject(err);
174
+ return;
175
+ }
176
+ stream.on('close', () => {
177
+ var buf = Buffer.concat(bufs);
178
+ var err = Buffer.concat(errs);
179
+ 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
+ }
189
+
190
+ if (errStream) {
191
+ stream.stderr.pipe(errStream);
192
+ } else {
193
+ stream.stderr.on('data', (data) => {
194
+ errs.push(data);
195
+ });
196
+ }
197
+
198
+ }
177
199
  this.exec(cmd, remote_exec);
178
- return true;
179
- }
180
- return false;
200
+ })
181
201
  }
182
202
 
183
203
  // handle remote shell stream processing
@@ -227,7 +247,7 @@ class SSHTunnelProxy extends Client {
227
247
  _this.listener[opts.server_name] = {};
228
248
  }
229
249
 
230
- _this.on('ready', () => {
250
+ _this.on('ready', async () => {
231
251
 
232
252
  // stop any pending retry timeouts
233
253
  if (_this._tunnelReadyTimeout) {
@@ -239,18 +259,24 @@ class SSHTunnelProxy extends Client {
239
259
  if (opts.shell) {
240
260
  _this.shell(_this.remote_shell);
241
261
 
242
- // if exec requested, exec series of cmds
262
+ // if exec requested, exec series of cmds
243
263
  } else {
244
264
  if (opts.exec && opts.exec.length > 0) {
245
- _this.exec_cmds = opts.exec;
246
- _this.exec_next();
265
+ for (var i = 0; i < opts.exec.length; i++) {
266
+ var cmd = opts.exec[i];
267
+ console.log(opts.username + '@' + opts.hostname + ':' + cmd);
268
+ var result = await _this.execCmd(cmd);
269
+ console.log(result.toString());
270
+ }
271
+ process.exit();
247
272
  }
248
273
  }
249
274
 
250
275
  // if local forward requested, setup local forwarding ports to remote host
251
276
  if (opts.proxy_ports) {
252
- _this.on_ssh_client_ready(opts.server_name, opts.proxy_ports, resolve);
277
+ await _this.setupProxyPorts(opts.server_name, opts.proxy_ports);
253
278
  }
279
+ resolve();
254
280
  });
255
281
 
256
282
  _this.on('end', () => {
@@ -310,6 +336,9 @@ class SSHTunnelProxy extends Client {
310
336
 
311
337
  // validate local forwards for correct format and valid ports
312
338
  validate_local_forward(proxy_ports, whitelist) {
339
+
340
+ if (!proxy_ports) return;
341
+
313
342
  const local_ports = [];
314
343
  const remote_ports = [];
315
344
  proxy_ports.forEach(proxy_port => {
package/main.js CHANGED
@@ -6,6 +6,7 @@ const keypairStorage = new KeypairStorage();
6
6
  const homedir = require('os').homedir();
7
7
  var debug = false;
8
8
  var exec = [];
9
+ var remote_host = null;
9
10
  var proxies = [];
10
11
  var configs = null;
11
12
  var host = null;
@@ -92,6 +93,10 @@ function main(args) {
92
93
  case '--ngrok':
93
94
  ngrokAPIKey = value;
94
95
  break;
96
+ case '-r':
97
+ case '--remote':
98
+ remote_host = value;
99
+ break;
95
100
  case '-R':
96
101
  case '--RemoteForward':
97
102
  forwardIn.push(value);
@@ -140,6 +145,9 @@ function main(args) {
140
145
  // skip tunnel if not enabled
141
146
  if (!config.enabled) return;
142
147
 
148
+ // if remote host specified, only select tunnel for remote host
149
+ if (remote_host && remote_host !== config.alias) return;
150
+
143
151
  // if system key storage name specified get key from system keychain
144
152
  if (config.server_name) {
145
153
  const service_name = config.service_name || 'ssh_tunnel_proxy';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssh_tunnel_proxy",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "description": "obtain ngrok host port from api and establish a ssh connection",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -18,7 +18,9 @@
18
18
  "net": "^1.0.2",
19
19
  "ngrok": "^4.3.3",
20
20
  "socksv5": "^0.0.6",
21
- "sshpk": "^1.17.0"
21
+ "split": "^1.0.1",
22
+ "sshpk": "^1.17.0",
23
+ "through": "^2.3.8"
22
24
  },
23
25
  "devDependencies": {
24
26
  "eslint": "^8.33.0",
@@ -0,0 +1,140 @@
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();