ssh_tunnel_proxy 1.2.10 → 2.0.1

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 (39) hide show
  1. package/README.md +144 -67
  2. package/dist/cjs/index.d.ts +65 -0
  3. package/dist/cjs/index.js +496 -0
  4. package/dist/cjs/index.js.map +1 -0
  5. package/dist/cjs/keypair_storage.d.ts +12 -0
  6. package/dist/cjs/keypair_storage.js +60 -0
  7. package/dist/cjs/keypair_storage.js.map +1 -0
  8. package/dist/cjs/ngrok_service.d.ts +11 -0
  9. package/dist/cjs/ngrok_service.js +40 -0
  10. package/dist/cjs/ngrok_service.js.map +1 -0
  11. package/dist/cjs/ssh2-node.d.ts +1 -0
  12. package/dist/cjs/ssh2-node.js +188 -0
  13. package/dist/cjs/ssh2-node.js.map +1 -0
  14. package/dist/esm/index.d.ts +65 -0
  15. package/dist/esm/index.js +491 -0
  16. package/dist/esm/index.js.map +1 -0
  17. package/dist/esm/keypair_storage.d.ts +12 -0
  18. package/dist/esm/keypair_storage.js +57 -0
  19. package/dist/esm/keypair_storage.js.map +1 -0
  20. package/dist/esm/ngrok_service.d.ts +11 -0
  21. package/dist/esm/ngrok_service.js +37 -0
  22. package/dist/esm/ngrok_service.js.map +1 -0
  23. package/dist/esm/ssh2-node.d.ts +1 -0
  24. package/dist/esm/ssh2-node.js +184 -0
  25. package/dist/esm/ssh2-node.js.map +1 -0
  26. package/dist/types/index.d.ts +65 -0
  27. package/dist/types/keypair_storage.d.ts +12 -0
  28. package/dist/types/ngrok_service.d.ts +11 -0
  29. package/dist/types/ssh2-node.d.ts +1 -0
  30. package/package.json +38 -8
  31. package/ssh2-node +2 -0
  32. package/.eslintrc.json +0 -15
  33. package/.vscode/launch.json +0 -34
  34. package/lib/index.js +0 -472
  35. package/lib/keypair_storage.js +0 -76
  36. package/lib/ngrok_service.js +0 -47
  37. package/main.js +0 -176
  38. package/test/test.js +0 -214
  39. package/test/test_remote_exec.js +0 -219
package/lib/index.js DELETED
@@ -1,472 +0,0 @@
1
- /*
2
-
3
- ssh_tunnel_proxy - initiate a ssh reverse tunnel proxy with forwarding ports
4
- with connection optional ssh tunnel service ngrok
5
-
6
- ssh_tunnel_proxy can function as a stand-alone api or part of an electron_js app. To establish a ssh tunnel proxy a keypair is generated on the client and stored in the system keychain.
7
- If a ngrok api key is provided the ngrok api endpoint method is invoked to obtain the hostport of
8
- the tunnel. A list of port forwards is provided to the connect_api function and ports validated to
9
- restrict connections to system ports on the remote host to a set of pre-defined ports such as http,https.
10
- After establishing a ssh connection to the remote server, local proxy port forwards are opened. If
11
- the connection is interrupted then the ssh_connect method will attempt to re-establish the connection.
12
-
13
- Author: Autonomous
14
- First release: 1-29-2023
15
- License: MIT
16
-
17
- */
18
-
19
- const net = require('net');
20
- const process = require('process');
21
- const { Buffer } = require('node:buffer');
22
- const { Client } = require('electron-ssh2');
23
-
24
- const KeypairStorage = require('./keypair_storage');
25
- const NgrokApi = require('./ngrok_service');
26
-
27
- const keypairStorage = new KeypairStorage();
28
-
29
- class SSHTunnelProxy extends Client {
30
-
31
- constructor() {
32
- super();
33
- this.listener = {};
34
- this.retries = 0;
35
- this.debug_en = false;
36
- this.debug_ssh = false;
37
- this._tunnelReadyTimeout = undefined;
38
- }
39
-
40
- // function to close all active sockets for tunnel restart and exception handling
41
- close_sockets(server_name, proxy_ports) {
42
-
43
- if (!this.listener[server_name] || !proxy_ports) return;
44
-
45
- proxy_ports.forEach(proxy_port => {
46
- const server = this.listener[server_name][proxy_port];
47
- if (server && server.listening) {
48
- this.debug_en && this.debug('SSH Server :: closing forward:', proxy_port)
49
- server.close();
50
- }
51
- });
52
- }
53
-
54
- // create forward out on socket connection
55
- setup_ssh_forward(socket, remote_hostname, remote_port) {
56
- const _this = this;
57
-
58
- // setup stream pipeline when port forward is ready
59
- const on_setup_ssh_forward = (err, stream) => {
60
-
61
- if (err) {
62
- _this.emit('debug', err);
63
- _this.debug_en && _this.debug('socket forward error:', err);
64
- return;
65
- }
66
-
67
- stream.on('end', () => {
68
- socket.resume();
69
- });
70
-
71
- // pipe the data from the local socket to the remote port and visa versa
72
- stream.pipe(socket).pipe(stream);
73
-
74
- const shutdown_forward = () => {
75
- stream.unpipe(socket);
76
- socket.unpipe(stream);
77
- stream.end();
78
- }
79
-
80
- // if socket ends, close stream and pipes
81
- socket.on('close', () => {
82
- shutdown_forward();
83
- });
84
-
85
- // if socket error, emit error and close stream
86
- socket.on('error', (err) => {
87
- _this.emit('debug', err);
88
- _this.debug_en && _this.debug('socket on error:', err);
89
- shutdown_forward();
90
- });
91
- }
92
-
93
- // create port forward
94
- _this.forwardOut(
95
- socket.remoteAddress,
96
- socket.remotePort,
97
- remote_hostname,
98
- remote_port,
99
- on_setup_ssh_forward
100
- );
101
-
102
- }
103
-
104
- // function to setup proxy forwards for ssh tunnel
105
- setupProxyPorts(server_name, proxy_ports) {
106
-
107
- return new Promise((resolve, reject) => {
108
-
109
- if (!proxy_ports) {
110
- resolve();
111
- return;
112
- }
113
-
114
- var listeners = 0;
115
- if (this.listener[server_name]) this.listener[server_name].isConnected = true;
116
- const _this = this;
117
-
118
- // iterate through a list of local forward ports and create a local proxy port
119
- proxy_ports.forEach(proxy_port => {
120
-
121
- const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
122
-
123
- // create local socket server
124
- const server = this.listener[server_name][proxy_port];
125
- if (server && server.listening) {
126
- this.debug_en && this.debug('SSH Server :: closing forward 2:', proxy_port)
127
- server.close();
128
- }
129
- this.listener[server_name][proxy_port] = null;
130
- this.listener[server_name][proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
131
-
132
- if (this.debug_en) {
133
- var debug_msg = 'SSH Server :: connection on ' + local_port + ' ' + socket.remotePort;
134
- _this.emit('debug', debug_msg);
135
- _this.debug(debug_msg);
136
- }
137
-
138
- // create a proxy forward between local and remote ports
139
- _this.setup_ssh_forward(socket, remote_hostname, remote_port);
140
- });
141
-
142
- // start listening on port
143
- try {
144
- var status_msg = 'SSH Server :: before listen on ' + local_port;
145
- _this.emit('debug', status_msg);
146
- _this.listener[server_name][proxy_port].listen(local_port, () => {
147
-
148
- // emit server listening on port message
149
- var status_msg = 'SSH Server :: bound on ' + local_port;
150
- _this.emit('debug', status_msg);
151
-
152
- // if all listeners have been successfully established, resolve setup connection
153
- if (listeners++ >= proxy_ports.length - 1) {
154
- _this.emit('ssh_tunnel_ready', {});
155
- if (_this._tunnelReadyTimeout) {
156
- clearTimeout(this._tunnelReadyTimeout);
157
- _this._tunnelReadyTimeout = undefined
158
- }
159
- resolve();
160
- }
161
- });
162
- } catch (err) {
163
- _this.debug_en && _this.debug('listen err:', err);
164
- reject(err);
165
- }
166
- });
167
- });
168
- }
169
-
170
- execCmd(cmd, dataStream, errStream) {
171
-
172
- return new Promise((resolve, reject) => {
173
- var bufs = [];
174
- var errs = [];
175
-
176
- const remote_exec = (err, stream) => {
177
- if (err) {
178
- reject(err);
179
- return;
180
- }
181
- stream.on('close', () => {
182
- var buf = Buffer.concat(bufs);
183
- var err = Buffer.concat(errs);
184
- //stream.removeListener('exit', onExit);
185
- resolve(buf, err);
186
- })
187
-
188
- stream.on('data', (data) => {
189
- if (dataStream) dataStream.write(data);
190
- else bufs.push(data);
191
- })
192
-
193
- stream.stderr.on('data', (data) => {
194
- if (errStream) errStream.write(data);
195
- else errs.push(data);
196
- });
197
-
198
- }
199
- this.exec(cmd, remote_exec);
200
- })
201
- }
202
-
203
- // handle remote shell stream processing
204
- remote_shell(err, stream) {
205
- const _this = this;
206
- if (err) throw err;
207
-
208
- // disable local echo of input chars, use remote output only
209
- process.stdin.setRawMode(true);
210
-
211
- // forward data from local terminal to remote host
212
- const stdinListener = (data) => {
213
- stream.stdin.write(data);
214
- };
215
-
216
- // shutdown this process when stream ends (user logs out)
217
- stream.on('close', function () {
218
- process.stdin.setRawMode(false);
219
- process.stdin.removeListener("data", stdinListener);
220
- process.exit();
221
- }).stderr.on('data', function (data) {
222
- _this.debug_en && _this.debug('shell' + data);
223
- });
224
-
225
- // skip next stops double printing of input
226
- stream.stdout.on("data", (data) => {
227
- process.stdout.write(data);
228
- })
229
- process.stdin.on("data", stdinListener)
230
- }
231
-
232
- // handle setting up ssh client and proxy forward ports
233
- do_ssh_connect(opts) {
234
-
235
- const _this = this;
236
-
237
- // create a new ssh client connection with supplied credentials and hostname/port
238
- return new Promise((resolve) => {
239
-
240
- // exit if connection already established, avoid redundant connection on retry
241
- if (_this.listener[opts.server_name] && _this.listener[opts.server_name].isConnected) resolve();
242
-
243
- // 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
-
250
- _this.on('ready', async () => {
251
-
252
- // stop any pending retry timeouts
253
- if (_this._tunnelReadyTimeout) {
254
- clearTimeout(this._tunnelReadyTimeout);
255
- _this._tunnelReadyTimeout = undefined;
256
- }
257
-
258
- // if shell requested, enable remote terminal
259
- if (opts.shell) {
260
- _this.shell(_this.remote_shell);
261
-
262
- // if exec requested, exec series of cmds
263
- } else {
264
- if (opts.exec && opts.exec.length > 0) {
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();
272
- }
273
- }
274
-
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
- resolve();
280
- });
281
-
282
- _this.on('end', () => {
283
- _this.debug_en && _this.debug('SSH Client :: end');
284
- _this.close_sockets(opts.server_name, opts.proxy_ports);
285
- });
286
-
287
- _this.on('close', () => {
288
- _this.debug_en && _this.debug('SSH Client :: close');
289
- _this.close_sockets(opts.server_name, opts.proxy_ports);
290
- });
291
-
292
- _this.on('error', err => {
293
- _this.debug_en && _this.debug('SSH Client :: error :: ' + err);
294
- _this.emit('debug', err);
295
- if (_this.listener[opts.server_name]) _this.listener[opts.server_name].isConnected = false;
296
- if (_this._tunnelReadyTimeout) {
297
- clearTimeout(this._tunnelReadyTimeout);
298
- _this._tunnelReadyTimeout = undefined
299
- }
300
- _this.ssh_retry_connect(opts);
301
- resolve();
302
- });
303
-
304
- _this.on('handshake', negotiated => {
305
- _this.debug_en && _this.debug('SSH Client :: handshake:', JSON.stringify(negotiated));
306
- });
307
-
308
- _this.on('greeting', (message) => {
309
- _this.debug_en && _this.debug('SSH Client :: greeting:', message);
310
- });
311
-
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
- });
324
-
325
- });
326
- }
327
-
328
- // validate port number - check for nan, out of valid port range, unauthorized system ports
329
- validate_port_number(port_str, whitelist) {
330
- if (isNaN(port_str)) return false;
331
- const port = parseInt(port_str);
332
- if (port < 1 || port > 65535) return false;
333
- if (port < 1024 && whitelist[port] === undefined) return false;
334
- return true;
335
- }
336
-
337
- // validate local forwards for correct format and valid ports
338
- validate_local_forward(proxy_ports, whitelist) {
339
-
340
- if (!proxy_ports) return;
341
-
342
- const local_ports = [];
343
- const remote_ports = [];
344
- proxy_ports.forEach(proxy_port => {
345
- const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
346
- if (remote_hostname.length < 1) remote_ports.push(remote_port);
347
- if (!this.validate_port_number(local_port, whitelist)) local_ports.push(local_port);
348
- if (!this.validate_port_number(remote_port, whitelist)) remote_ports.push(remote_port);
349
- });
350
- if (local_ports.length || remote_ports.length) {
351
- const err = new Error('Invalid local forward');
352
-
353
- this.debug_en && this.debug('invalid ports found:\n', JSON.stringify(proxy_ports, null, 2),
354
- '\n', JSON.stringify(whitelist, null, 2));
355
-
356
- err.info = {
357
- local_ports: local_ports.join(','),
358
- remote_ports: remote_ports.join(',')
359
- };
360
- throw (err);
361
- }
362
- return true;
363
- }
364
-
365
- // on network error, attempt to re-establish connection until 10 retries
366
- //clearTimeout(this._readyTimeout);
367
- ssh_retry_connect(opts) {
368
- const _this = this;
369
- const invoke = () => {
370
- _this.connectSSH(opts);
371
- }
372
- if (this.retries++ < 10) this._tunnelReadyTimeout = setTimeout(invoke, 5000);
373
- }
374
-
375
- // attempt to establish ssh tunnel to server with supplied parameters.
376
- async ssh_start_tunnel(opts) {
377
- const _this = this;
378
- async function do_ssh_connect(resolve) {
379
-
380
- // after successful tunnel setup complete, send events and reset retry counter
381
- await _this.do_ssh_connect(opts).then(() => {
382
- const hostport = opts.host + ':' + opts.port;
383
- if (_this.debug_en) {
384
- const msg = 'SSH connection to ' + opts.server_name + ' established at ' + hostport;
385
- _this.debug(msg);
386
- }
387
- //_this.emit('status', '', 'ready', hostport);
388
- _this.retries = 0;
389
- resolve();
390
- });
391
- }
392
- return new Promise(do_ssh_connect);
393
- }
394
-
395
- // setup ssh connection parameters and attempt to establish ssh tunnel
396
- // todo: if opts have changed while service is running, shutdown current service and restart with new opts
397
- async connectSSH(opts, whitelist) {
398
-
399
- this.debug_en && this.debug('connectSSH:\n', JSON.stringify(opts, null, 2));
400
-
401
- // make deep copy of opts for modification
402
- var _opts = JSON.parse(JSON.stringify(opts));
403
-
404
- // setup whitelist from param or opts in case of error retry
405
- _opts.whitelist = (whitelist !== undefined) ? whitelist : (_opts.whitelist) ? _opts.whitelist : {};
406
-
407
- // validate local port forwards, emit error and quit if invalid
408
- try {
409
- this.validate_local_forward(_opts.proxy_ports, _opts.whitelist);
410
- } catch (err) {
411
- this.emit('debug', err);
412
- this.debug_en && this.debug(err);
413
- return err;
414
- }
415
-
416
- // if password is empty set to undefined
417
- if (opts.password && !opts.password.length) {
418
- _opts.password = undefined;
419
- }
420
-
421
- // retrieve private key from system keychain with supplied service name and server
422
- // if no key found, authentication is supplied username, password
423
- _opts.private_key = await keypairStorage.get_keypair(opts.service_name, opts.server_name);
424
-
425
- // if ngrok api key provided, obtain hostname and hostport from ngrok api
426
- // if no ngrok tunnel specified, use supplied host and port
427
- if (_opts.ngrok_api) {
428
- const _this = this;
429
- const ngrokApi = new NgrokApi(_opts.ngrok_api);
430
- var hostport = await ngrokApi.get_hostport()
431
- .catch(() => {
432
- _this.debug_en && _this.debug('ngrok get_hostport connection error');
433
- if (this.listener[opts.server_name]) this.listener[opts.server_name].isConnected = false;
434
- _this.ssh_retry_connect(_opts);
435
- });
436
- if (hostport && hostport.host) {
437
- _opts.host = hostport.host;
438
- _opts.port = hostport.port;
439
- }
440
- else return null;
441
- }
442
-
443
- // initiate ssh tunnel, block until tunnel is established or error
444
- return await this.ssh_start_tunnel(_opts)
445
- .catch((err) => {
446
- this.debug_en && this.debug('ssh_start_tunnel catch:' + err);
447
- });
448
- }
449
-
450
- // resume connection, if previously online
451
- onNetworkOnline() { }
452
-
453
- // connection down, shutdown tunnel
454
- onNetworkOffline() { }
455
-
456
- generateAndStoreKeypair(...args) {
457
- keypairStorage.generate_and_store_keypair(...args);
458
- }
459
-
460
- getPublicKey(...args) {
461
- keypairStorage.get_public_key_from_keychain(...args);
462
- }
463
-
464
- debug(...args) { console.log(...args); }
465
-
466
- }
467
-
468
- module.exports = {
469
- SSHTunnelProxy: SSHTunnelProxy,
470
- KeypairStorage: KeypairStorage,
471
- NgrokApi: NgrokApi
472
- }
@@ -1,76 +0,0 @@
1
- /*
2
-
3
- keypair_storage - generate and store keypairs in the system keychain
4
-
5
- keypair_storage provides a set of functions to generate ssh ed25519 keypairs. Private keys
6
- are stored in the system keychain and retrieved by service_name and server_name parameters.
7
-
8
- Author: Autonomous
9
- First release: 1-29-2023
10
- License: MIT
11
-
12
- */
13
-
14
- const sshpk = require('sshpk');
15
- const keytar = require('keytar');
16
-
17
- class KeypairStorage {
18
- constructor() {
19
-
20
- }
21
-
22
- // export function to generate keypair and store under service name and account
23
- generate_and_store_keypair(service_name, account) {
24
- const keypair = this.generate_keypair();
25
- return new Promise((resolve, reject) => {
26
- keytar.setPassword(service_name, account, keypair.private_key).then(() => {
27
- resolve(keypair.public_key);
28
- }, (err) => {
29
- reject(err);
30
- });
31
- });
32
- }
33
-
34
- // export function to obtain public key from system's keychain stored under service_name and account
35
- get_public_key_from_keychain(service_name, account) {
36
- return new Promise((resolve, reject) => {
37
- keytar.getPassword(service_name, account).then((private_key) => {
38
- var public_key = this.get_public_key_from_private(private_key);
39
- resolve(public_key);
40
- }, (err) => {
41
- reject(err);
42
- });
43
- });
44
- }
45
-
46
- get_public_key_from_private(private_key) {
47
- if (private_key) {
48
- var key = sshpk.parsePrivateKey(private_key, 'pem');
49
- if (key) return key.toPublic().toString('ssh');
50
- else return null;
51
- }
52
- return null;
53
- }
54
-
55
- // generate EdDSA keypair
56
- generate_keypair() {
57
- // generate private key then obtain public key from private
58
- // note: EdDSA is required for nodejs ssh
59
- const privateKey = sshpk.generatePrivateKey('ed25519').toString('ssh');
60
- const publicKey = this.get_public_key_from_private(privateKey);
61
- return {
62
- public_key: publicKey,
63
- private_key: privateKey
64
- };
65
- }
66
- get_keypair(service, server) {
67
- return keytar.getPassword(service, server);
68
- }
69
- set_keypair(service, server, key) {
70
- return keytar.setPassword(service, server, key);
71
- }
72
- delete_keypair(service, server) {
73
- return keytar.deletePassword(service, server);
74
- }
75
- }
76
- module.exports = KeypairStorage;
@@ -1,47 +0,0 @@
1
- /*
2
-
3
- ngrok_service - obtain hostport from ngrok api and split into host, port
4
-
5
- Author: Autonomous
6
- First release: 1-29-2023
7
- License: MIT
8
-
9
- */
10
-
11
- // get endpoint hostname and hostport from ngrok api
12
- const { Ngrok } = require('@ngrok/ngrok-api');
13
-
14
- class NgrokApi {
15
-
16
- constructor(apiToken) {
17
- this.ngrok = new Ngrok({ apiToken: apiToken });
18
- }
19
-
20
- get_hostport() {
21
- const _this = this;
22
- return new Promise((resolve, reject) => {
23
- _this.ngrok.endpoints.list()
24
- .then((endpoints) => {
25
- const hostport_obj = _this.parse_ngrok_hostport(endpoints);
26
- resolve(hostport_obj);
27
- }, (err) => {
28
- reject(err);
29
- });
30
- });
31
- }
32
-
33
- // retrieve hostport from api response
34
- parse_ngrok_hostport(endpoints) {
35
- if (endpoints[0] && endpoints[0].hostport) {
36
- const hostport = endpoints[0].hostport.split(':');
37
- var hostport_obj = {
38
- host: hostport[0],
39
- port: hostport[1]
40
- }
41
- return hostport_obj;
42
- } else {
43
- throw (new Error('get_ngrok_hostport: no endpoints found'));
44
- }
45
- }
46
- }
47
- module.exports = NgrokApi;