ssh_tunnel_proxy 1.2.11 → 2.0.3

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/lib/index.js DELETED
@@ -1,489 +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 fs = require('fs');
21
- const process = require('process');
22
- const { Buffer } = require('node:buffer');
23
- const { Client } = require('electron-ssh2');
24
-
25
- const KeypairStorage = require('./keypair_storage');
26
- const NgrokApi = require('./ngrok_service');
27
-
28
- const keypairStorage = new KeypairStorage();
29
-
30
- const homedir = require('os').homedir();
31
-
32
- class SSHTunnelProxy extends Client {
33
-
34
- constructor() {
35
- super();
36
- this.listener = {};
37
- this.retries = 0;
38
- this.debug_en = false;
39
- this.debug_ssh = false;
40
- this._tunnelReadyTimeout = undefined;
41
- }
42
-
43
- // function to close all active sockets for tunnel restart and exception handling
44
- close_sockets(proxy_ports) {
45
-
46
- if (!proxy_ports) return;
47
-
48
- proxy_ports.forEach(proxy_port => {
49
- const server = this.listener[proxy_port];
50
- if (server && server.listening) {
51
- this.debug_en && this.debug('SSH Server :: closing forward:', proxy_port)
52
- server.close();
53
- delete this.listener[proxy_port];
54
- }
55
- });
56
- }
57
-
58
- // create forward out on socket connection
59
- setup_ssh_forward(socket, remote_hostname, remote_port) {
60
- const _this = this;
61
-
62
- // setup stream pipeline when port forward is ready
63
- const on_setup_ssh_forward = (err, stream) => {
64
-
65
- if (err) {
66
- _this.emit('debug', err);
67
- _this.debug_en && _this.debug('socket forward error:', err);
68
- return;
69
- }
70
-
71
- stream.on('end', () => {
72
- socket.resume();
73
- });
74
-
75
- // pipe the data from the local socket to the remote port and visa versa
76
- stream.pipe(socket).pipe(stream);
77
-
78
- const shutdown_forward = () => {
79
- stream.unpipe(socket);
80
- socket.unpipe(stream);
81
- stream.end();
82
- }
83
-
84
- // if socket ends, close stream and pipes
85
- socket.on('close', () => {
86
- shutdown_forward();
87
- });
88
-
89
- // if socket error, emit error and close stream
90
- socket.on('error', (err) => {
91
- _this.emit('debug', err);
92
- _this.debug_en && _this.debug('socket on error:', err);
93
- shutdown_forward();
94
- });
95
- }
96
-
97
- // create port forward
98
- _this.forwardOut(
99
- socket.remoteAddress,
100
- socket.remotePort,
101
- remote_hostname,
102
- remote_port,
103
- on_setup_ssh_forward
104
- );
105
-
106
- }
107
-
108
- // function to setup proxy forwards for ssh tunnel
109
- setupProxyPorts(proxy_ports) {
110
-
111
- return new Promise((resolve, reject) => {
112
-
113
- if (!proxy_ports) {
114
- resolve();
115
- return;
116
- }
117
-
118
- var listeners = 0;
119
- this.listener.isConnected = true;
120
- const _this = this;
121
-
122
- // iterate through a list of local forward ports and create a local proxy port
123
- proxy_ports.forEach(proxy_port => {
124
-
125
- const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
126
-
127
- // create local socket server
128
- const server = _this.listener[proxy_port];
129
- if (server && server.listening) {
130
- _this.debug_en && this.debug('SSH Server :: closing forward 2:', proxy_port)
131
- server.close();
132
- }
133
- _this.listener[proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
134
-
135
- if (this.debug_en) {
136
- var debug_msg = 'SSH Server :: connection on ' + local_port + ' ' + socket.remotePort;
137
- _this.emit('debug', debug_msg);
138
- _this.debug(debug_msg);
139
- }
140
-
141
- // create a proxy forward between local and remote ports
142
- _this.setup_ssh_forward(socket, remote_hostname, remote_port);
143
- });
144
-
145
- // start listening on port
146
- try {
147
- var status_msg = 'SSH Server :: before listen on ' + local_port;
148
- _this.emit('debug', status_msg);
149
- _this.listener[proxy_port].listen(local_port, () => {
150
-
151
- // emit server listening on port message
152
- var status_msg = 'SSH Server :: bound on ' + local_port;
153
- _this.emit('debug', status_msg);
154
-
155
- // if all listeners have been successfully established, resolve setup connection
156
- if (listeners++ >= proxy_ports.length - 1) {
157
- _this.emit('ssh_tunnel_ready', {});
158
- if (_this._tunnelReadyTimeout) {
159
- clearTimeout(this._tunnelReadyTimeout);
160
- _this._tunnelReadyTimeout = undefined;
161
- }
162
- resolve();
163
- }
164
- });
165
- } catch (err) {
166
- _this.debug_en && _this.debug('listen err:', err);
167
- reject(err);
168
- }
169
- });
170
- });
171
- }
172
-
173
- execCmd(cmd, dataStream, errStream) {
174
-
175
- return new Promise((resolve, reject) => {
176
- var bufs = [];
177
- var errs = [];
178
-
179
- const remote_exec = (err, stream) => {
180
- if (err) {
181
- reject(err);
182
- return;
183
- }
184
-
185
- const onClose = () => {
186
- var buf = Buffer.concat(bufs);
187
- var err = Buffer.concat(errs);
188
- stream.removeListener('close', onClose);
189
- resolve(buf, err);
190
- }
191
- stream.on('close', onClose);
192
-
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
- });
202
-
203
- }
204
- this.exec(cmd, remote_exec);
205
- })
206
- }
207
-
208
- // handle remote shell stream processing
209
- remote_shell(err, stream) {
210
- const _this = this;
211
- if (err) throw err;
212
-
213
- // disable local echo of input chars, use remote output only
214
- process.stdin.setRawMode(true);
215
-
216
- // forward data from local terminal to remote host
217
- const stdinListener = (data) => {
218
- stream.stdin.write(data);
219
- };
220
-
221
- // shutdown this process when stream ends (user logs out)
222
- stream.on('close', function () {
223
- process.stdin.setRawMode(false);
224
- process.stdin.removeListener("data", stdinListener);
225
- process.exit();
226
- }).stderr.on('data', function (data) {
227
- _this.debug_en && _this.debug('shell' + data);
228
- });
229
-
230
- // skip next stops double printing of input
231
- stream.stdout.on("data", (data) => {
232
- process.stdout.write(data);
233
- })
234
- process.stdin.on("data", stdinListener)
235
- }
236
-
237
- // handle setting up ssh client and proxy forward ports
238
- do_ssh_connect(opts) {
239
-
240
- const _this = this;
241
-
242
- // create a new ssh client connection with supplied credentials and hostname/port
243
- return new Promise((resolve) => {
244
-
245
- // exit if connection already established, avoid redundant connection on retry
246
- if (_this.listener.isConnected) resolve();
247
-
248
- // close open sockets on server, otherwise initialize open sockets storage
249
- _this.close_sockets(opts.proxy_ports);
250
-
251
- _this.on('ready', async () => {
252
-
253
- // stop any pending retry timeouts
254
- if (_this._tunnelReadyTimeout) {
255
- clearTimeout(this._tunnelReadyTimeout);
256
- _this._tunnelReadyTimeout = undefined;
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
-
264
- // if shell requested, enable remote terminal
265
- if (opts.shell) {
266
- _this.shell(_this.remote_shell);
267
-
268
- // otherwise, if exec requested, exec series of cmds
269
- } else {
270
- if (opts.exec && opts.exec.length > 0) {
271
- for (var i = 0; i < opts.exec.length; i++) {
272
- var cmd = opts.exec[i];
273
- _this.debug_en && _this.debug(opts.username + '@' + opts.hostname + ':' + cmd);
274
- var result = await _this.execCmd(cmd);
275
- _this.debug_en && _this.debug(result.toString());
276
- }
277
- process.exit();
278
- }
279
- }
280
-
281
- resolve();
282
- });
283
-
284
- _this.on('end', () => {
285
- _this.debug_en && _this.debug('SSH Client :: end');
286
- _this.close_sockets(opts.proxy_ports);
287
- });
288
-
289
- _this.on('close', () => {
290
- _this.debug_en && _this.debug('SSH Client :: close');
291
- _this.close_sockets(opts.proxy_ports);
292
- });
293
-
294
- _this.on('error', err => {
295
- _this.debug_en && _this.debug('SSH Client :: error :: ' + err);
296
- _this.emit('debug', err);
297
- _this.listener.isConnected = false;
298
- if (_this._tunnelReadyTimeout) {
299
- clearTimeout(this._tunnelReadyTimeout.elReadyTimeout);
300
- _this._tunnelReadyTimeout = undefined;
301
- }
302
- _this.ssh_retry_connect(opts);
303
- resolve();
304
- });
305
-
306
- _this.on('handshake', negotiated => {
307
- _this.debug_en && _this.debug('SSH Client :: handshake:', JSON.stringify(negotiated));
308
- });
309
-
310
- _this.on('greeting', (message) => {
311
- _this.debug_en && _this.debug('SSH Client :: greeting:', message);
312
- });
313
-
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;
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);
327
- });
328
- }
329
-
330
- // validate port number - check for nan, out of valid port range, unauthorized system ports
331
- validate_port_number(port_str, whitelist) {
332
- if (isNaN(port_str)) return false;
333
- const port = parseInt(port_str);
334
- if (port < 1 || port > 65535) return false;
335
- if (port < 1024 && whitelist && whitelist[port] === undefined) return false;
336
- return true;
337
- }
338
-
339
- // validate local forwards for correct format and valid ports
340
- validate_local_forward(proxy_ports, whitelist) {
341
-
342
- if (!proxy_ports) return;
343
-
344
- const local_ports = [];
345
- const remote_ports = [];
346
- proxy_ports.forEach(proxy_port => {
347
- const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
348
- if (remote_hostname.length < 1) remote_ports.push(remote_port);
349
- if (!this.validate_port_number(local_port, whitelist)) local_ports.push(local_port);
350
- if (!this.validate_port_number(remote_port, whitelist)) remote_ports.push(remote_port);
351
- });
352
- if (local_ports.length || remote_ports.length) {
353
- const err = new Error('Invalid local forward');
354
-
355
- this.debug_en && this.debug('invalid ports found:\n', JSON.stringify(proxy_ports, null, 2),
356
- '\n', JSON.stringify(whitelist, null, 2));
357
-
358
- err.info = {
359
- local_ports: local_ports.join(','),
360
- remote_ports: remote_ports.join(',')
361
- };
362
- throw (err);
363
- }
364
- return true;
365
- }
366
-
367
- // on network error, attempt to re-establish connection until 10 retries
368
- //clearTimeout(this._readyTimeout);
369
- ssh_retry_connect(opts) {
370
- const _this = this;
371
- const invoke = () => {
372
- _this.connectSSH(opts);
373
- }
374
- if (this.retries++ < 10) this._tunnelReadyTimeout = setTimeout(invoke, 5000);
375
- }
376
-
377
- // attempt to establish ssh tunnel to server with supplied parameters.
378
- async ssh_start_tunnel(opts) {
379
- const _this = this;
380
- async function do_ssh_connect(resolve) {
381
-
382
- // after successful tunnel setup complete, send events and reset retry counter
383
- await _this.do_ssh_connect(opts).then(() => {
384
- const hostport = opts.host + ':' + opts.port;
385
- if (_this.debug_en) {
386
- const msg = 'SSH connection to ' + opts.server_name + ' established at ' + hostport;
387
- _this.debug(msg);
388
- }
389
- //_this.emit('status', '', 'ready', hostport);
390
- _this.retries = 0;
391
- resolve();
392
- });
393
- }
394
- return new Promise(do_ssh_connect);
395
- }
396
-
397
- // setup ssh connection parameters and attempt to establish ssh tunnel
398
- // todo: if opts have changed while service is running, shutdown current service and restart with new opts
399
- async connectSSH(opts, whitelist) {
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 : null;
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
- 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
-
432
- // retrieve private key from system keychain with supplied service name and server
433
- // if no key found, authentication is supplied username, password
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
- }
439
-
440
- // if ngrok api key provided, obtain hostname and hostport from ngrok api
441
- // if no ngrok tunnel specified, use supplied host and port
442
- if (_opts.ngrok_api) {
443
- const _this = this;
444
- const ngrokApi = new NgrokApi(_opts.ngrok_api);
445
- var hostport = await ngrokApi.get_hostport()
446
- .catch(() => {
447
- _this.debug_en && _this.debug('ngrok get_hostport connection error');
448
- _this.listener.isConnected = false;
449
- _this.ssh_retry_connect(_opts);
450
- });
451
- if (hostport && hostport.host) {
452
- _opts.host = hostport.host;
453
- _opts.port = hostport.port;
454
- }
455
- else return null;
456
- }
457
-
458
- this.debug_en && this.debug('connectSSH:\n', JSON.stringify(_opts, null, 2));
459
-
460
- // initiate ssh tunnel, block until tunnel is established or error
461
- return await this.ssh_start_tunnel(_opts)
462
- .catch((err) => {
463
- this.debug_en && this.debug('ssh_start_tunnel catch:' + err);
464
- });
465
- }
466
-
467
- // resume connection, if previously online
468
- onNetworkOnline() { }
469
-
470
- // connection down, shutdown tunnel
471
- onNetworkOffline() { }
472
-
473
- generateAndStoreKeypair(...args) {
474
- keypairStorage.generate_and_store_keypair(...args);
475
- }
476
-
477
- getPublicKey(...args) {
478
- keypairStorage.get_public_key_from_keychain(...args);
479
- }
480
-
481
- debug(...args) { console.log(...args); }
482
-
483
- }
484
-
485
- module.exports = {
486
- SSHTunnelProxy: SSHTunnelProxy,
487
- KeypairStorage: KeypairStorage,
488
- NgrokApi: NgrokApi
489
- }
@@ -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;