ssh_tunnel_proxy 1.2.1 → 1.2.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/.eslintrc.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "env": {
3
+ "browser": true,
4
+ "commonjs": true,
5
+ "es2021": true
6
+ },
7
+ "extends": "eslint:recommended",
8
+ "overrides": [
9
+ ],
10
+ "parserOptions": {
11
+ "ecmaVersion": "latest"
12
+ },
13
+ "rules": {
14
+ }
15
+ }
package/README.md CHANGED
@@ -1,9 +1,7 @@
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 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.
5
-
6
- If a ngrok api key is provided the ngrok api endpoint method is invoked to obtain the hostport of the tunnel. A list of port forwards is provided to the connect_api function and ports validated to restrict connections to system ports on the remote host to a set of pre-defined ports such as http,https. After establishing a ssh connection to the remote server, local proxy port forwards are opened. If the connection is interrupted then the ssh_connect method will attempt to re-establish the connection.
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.
7
5
 
8
6
  Example command line usage:
9
7
 
@@ -31,11 +29,14 @@ config file, located at:
31
29
  "ngrok_api": "<ngrok api key>"
32
30
  }]
33
31
 
32
+ ```
34
33
  start tunnel with default config, located at ~/.config/ssh_tunnel_proxy/config.json
34
+
35
35
  ```
36
36
  node main.js -c -d
37
+ ```
37
38
 
38
- The following code is an example of use of the api with electronjs and needs to be install in main.js, preload.js and the render process.
39
+ The following code is an example of use of the api with electronjs and is to be installed in main.js, preload.js and the render process.
39
40
  main.js:
40
41
  ```js
41
42
 
package/lib/index.js CHANGED
@@ -17,18 +17,14 @@ License: MIT
17
17
  */
18
18
 
19
19
  const net = require('net');
20
- const { EventEmitter } = require('node:events');
21
20
  const { Client } = require('electron-ssh2');
22
21
 
23
22
  const KeypairStorage = require('./keypair_storage');
24
23
  const NgrokApi = require('./ngrok_service');
25
24
 
26
- // debug logging options
27
- const debug_ssh = false;
28
-
29
25
  const keypairStorage = new KeypairStorage();
30
26
 
31
- class sshTunnelProxy extends EventEmitter {
27
+ class SSHTunnelProxy extends Client {
32
28
 
33
29
  constructor() {
34
30
  super();
@@ -46,14 +42,14 @@ class sshTunnelProxy extends EventEmitter {
46
42
  proxy_ports.forEach(proxy_port => {
47
43
  const server = this.listener[server_name][proxy_port];
48
44
  if (server && server.listening) {
49
- this.debug_en &&this.debug('SSH Server :: closing forward:', proxy_port)
45
+ this.debug_en && this.debug('SSH Server :: closing forward:', proxy_port)
50
46
  server.close();
51
47
  }
52
48
  });
53
49
  }
54
50
 
55
51
  // create forward out on socket connection
56
- setup_ssh_forward(ssh_client, socket, remote_hostname, remote_port) {
52
+ setup_ssh_forward(socket, remote_hostname, remote_port) {
57
53
  const _this = this;
58
54
 
59
55
  // setup stream pipeline when port forward is ready
@@ -61,7 +57,7 @@ class sshTunnelProxy extends EventEmitter {
61
57
 
62
58
  if (err) {
63
59
  _this.emit('debug', err);
64
- _this.debug_en &&_this.debug('socket forward error:', err);
60
+ _this.debug_en && _this.debug('socket forward error:', err);
65
61
  return;
66
62
  }
67
63
 
@@ -77,13 +73,13 @@ class sshTunnelProxy extends EventEmitter {
77
73
  // if socket error, emit error and close stream
78
74
  socket.on('error', (err) => {
79
75
  _this.emit('debug', err);
80
- _this.debug_en &&_this.debug('socket on error:', err);
76
+ _this.debug_en && _this.debug('socket on error:', err);
81
77
  stream.end();
82
78
  });
83
79
  }
84
80
 
85
81
  // create port forward
86
- ssh_client.forwardOut(
82
+ _this.forwardOut(
87
83
  socket.remoteAddress,
88
84
  socket.remotePort,
89
85
  remote_hostname,
@@ -94,7 +90,7 @@ class sshTunnelProxy extends EventEmitter {
94
90
  }
95
91
 
96
92
  // function to setup proxy forwards for ssh tunnel
97
- on_ssh_client_ready(ssh_client, server_name, proxy_ports, resolve) {
93
+ on_ssh_client_ready(server_name, proxy_ports, resolve) {
98
94
 
99
95
  var listeners = 0;
100
96
  if (this.listener[server_name]) this.listener[server_name].isConnected = true;
@@ -108,9 +104,10 @@ class sshTunnelProxy extends EventEmitter {
108
104
  // create local socket server
109
105
  const server = this.listener[server_name][proxy_port];
110
106
  if (server && server.listening) {
111
- this.debug_en &&this.debug('SSH Server :: closing forward:', proxy_port)
107
+ this.debug_en && this.debug('SSH Server :: closing forward:', proxy_port)
112
108
  server.close();
113
109
  }
110
+ this.listener[server_name][proxy_port] = null;
114
111
  this.listener[server_name][proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
115
112
 
116
113
  if (this.debug_en) {
@@ -120,7 +117,7 @@ class sshTunnelProxy extends EventEmitter {
120
117
  }
121
118
 
122
119
  // create a proxy forward between local and remote ports
123
- _this.setup_ssh_forward(ssh_client, socket, remote_hostname, remote_port);
120
+ _this.setup_ssh_forward(socket, remote_hostname, remote_port);
124
121
  });
125
122
 
126
123
  // start listening on port
@@ -138,10 +135,10 @@ class sshTunnelProxy extends EventEmitter {
138
135
  }
139
136
  });
140
137
  } catch (err) {
141
- _this.debug_en &&_this.debug('listen err:', err);
138
+ _this.debug_en && _this.debug('listen err:', err);
142
139
  }
143
140
  });
144
- };
141
+ }
145
142
 
146
143
  // handle setting up ssh client and proxy forward ports
147
144
  do_ssh_connect(opts) {
@@ -149,7 +146,7 @@ class sshTunnelProxy extends EventEmitter {
149
146
  const _this = this;
150
147
 
151
148
  // create a new ssh client connection with supplied credentials and hostname/port
152
- return new Promise((resolve, reject) => {
149
+ return new Promise((resolve) => {
153
150
 
154
151
  // exit if connection already established, avoid redundant connection on retry
155
152
  if (_this.listener[opts.server_name] && _this.listener[opts.server_name].isConnected) resolve();
@@ -161,46 +158,46 @@ class sshTunnelProxy extends EventEmitter {
161
158
  _this.listener[opts.server_name] = {};
162
159
  }
163
160
 
164
- const ssh_client = new Client();
165
-
166
- ssh_client.on('ready', () => {
167
- _this.on_ssh_client_ready(ssh_client, opts.server_name, opts.proxy_ports, resolve);
161
+ _this.on('ready', () => {
162
+ _this.on_ssh_client_ready(opts.server_name, opts.proxy_ports, resolve);
168
163
  });
169
164
 
170
- ssh_client.on('end', () => {
171
- _this.debug_en &&_this.debug('SSH Client :: end');
165
+ _this.on('end', () => {
166
+ _this.debug_en && _this.debug('SSH Client :: end');
172
167
  _this.close_sockets(opts.server_name, opts.proxy_ports);
173
168
  });
174
169
 
175
- ssh_client.on('close', () => {
176
- _this.debug_en &&_this.debug('SSH Client :: close');
170
+ _this.on('close', () => {
171
+ _this.debug_en && _this.debug('SSH Client :: close');
177
172
  _this.close_sockets(opts.server_name, opts.proxy_ports);
178
173
  });
179
174
 
180
- ssh_client.on('error', err => {
181
- _this.debug_en &&_this.debug('SSH Client :: error :: ' + err);
182
- _this.emit('error', err);
175
+ _this.on('error', err => {
176
+ _this.debug_en && _this.debug('SSH Client :: error :: ' + err);
177
+ _this.emit('debug', err);
183
178
  if (_this.listener[opts.server_name]) _this.listener[opts.server_name].isConnected = false;
184
179
  _this.ssh_retry_connect(opts);
185
180
  resolve();
186
181
  });
187
182
 
188
- ssh_client.on('handshake', negotiated => {
189
- _this.debug_en &&_this.debug('SSH Client :: handshake:', JSON.stringify(negotiated));
183
+ _this.on('handshake', negotiated => {
184
+ _this.debug_en && _this.debug('SSH Client :: handshake:', JSON.stringify(negotiated));
190
185
  });
191
186
 
192
- ssh_client.on('greeting', (message, language) => {
193
- _this.debug_en &&_this.debug('SSH Client :: greeting:', message);
187
+ _this.on('greeting', (message) => {
188
+ _this.debug_en && _this.debug('SSH Client :: greeting:', message);
194
189
  });
195
190
 
196
191
  // initiate ssh client connection to remote host
197
- ssh_client.connect({
192
+ // ssh client debug function for verbose ssh connection details
193
+ const debug_client = (this.debug_ssh) ? (...args) => { console.log(...args); } : null;
194
+ _this.connect({
198
195
  host: opts.host,
199
196
  port: opts.port,
200
197
  username: opts.username,
201
198
  password: opts.password,
202
199
  privateKey: opts.private_key,
203
- debug: _this.debug_client,
200
+ debug: debug_client,
204
201
  keepaliveInterval: 10000
205
202
  });
206
203
 
@@ -222,13 +219,14 @@ class sshTunnelProxy extends EventEmitter {
222
219
  const remote_ports = [];
223
220
  proxy_ports.forEach(proxy_port => {
224
221
  const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
222
+ if (remote_hostname.length < 1) remote_ports.push(remote_port);
225
223
  if (!this.validate_port_number(local_port, whitelist)) local_ports.push(local_port);
226
224
  if (!this.validate_port_number(remote_port, whitelist)) remote_ports.push(remote_port);
227
225
  });
228
226
  if (local_ports.length || remote_ports.length) {
229
227
  const err = new Error('Invalid local forward');
230
228
 
231
- this.debug_en &&this.debug('invalid ports found:\n', JSON.stringify(proxy_ports, null, 2),
229
+ this.debug_en && this.debug('invalid ports found:\n', JSON.stringify(proxy_ports, null, 2),
232
230
  '\n', JSON.stringify(whitelist, null, 2));
233
231
 
234
232
  err.info = {
@@ -241,7 +239,7 @@ class sshTunnelProxy extends EventEmitter {
241
239
  }
242
240
 
243
241
  // on network error, attempt to re-establish connection until 10 retries
244
- ssh_retry_connect = (opts) => {
242
+ ssh_retry_connect(opts) {
245
243
  const _this = this;
246
244
  const invoke = () => {
247
245
  _this.connectSSH(opts);
@@ -250,9 +248,9 @@ class sshTunnelProxy extends EventEmitter {
250
248
  }
251
249
 
252
250
  // attempt to establish ssh tunnel to server with supplied parameters.
253
- ssh_start_tunnel = async (opts) => {
251
+ async ssh_start_tunnel(opts) {
254
252
  const _this = this;
255
- return new Promise(async (resolve, reject) => {
253
+ async function do_ssh_connect(resolve) {
256
254
 
257
255
  // after successful tunnel setup complete, send events and reset retry counter
258
256
  await _this.do_ssh_connect(opts).then(() => {
@@ -264,18 +262,16 @@ class sshTunnelProxy extends EventEmitter {
264
262
  _this.emit('status', '', 'ready', hostport);
265
263
  _this.retries = 0;
266
264
  resolve();
267
- }, (err) => {
268
- _this.debug_en &&_this.debug('ssh_retry error:', err);
269
- reject();
270
265
  });
271
- });
266
+ }
267
+ return new Promise(do_ssh_connect);
272
268
  }
273
269
 
274
270
  // setup ssh connection parameters and attempt to establish ssh tunnel
275
271
  // todo: if opts have changed while service is running, shutdown current service and restart with new opts
276
272
  async connectSSH(opts, whitelist) {
277
273
 
278
- this.debug_en &&this.debug('connectSSH:\n', JSON.stringify(opts, null, 2));
274
+ this.debug_en && this.debug('connectSSH:\n', JSON.stringify(opts, null, 2));
279
275
 
280
276
  // make deep copy of opts for modification
281
277
  var _opts = JSON.parse(JSON.stringify(opts));
@@ -287,8 +283,8 @@ class sshTunnelProxy extends EventEmitter {
287
283
  try {
288
284
  this.validate_local_forward(_opts.proxy_ports, _opts.whitelist);
289
285
  } catch (err) {
290
- this.emit('error', err);
291
- this.debug_en &&this.debug(err);
286
+ this.emit('debug', err);
287
+ this.debug_en && this.debug(err);
292
288
  return err;
293
289
  }
294
290
 
@@ -307,8 +303,8 @@ class sshTunnelProxy extends EventEmitter {
307
303
  const _this = this;
308
304
  const ngrokApi = new NgrokApi(_opts.ngrok_api);
309
305
  var hostport = await ngrokApi.get_hostport()
310
- .catch((err) => {
311
- _this.debug_en &&_this.debug('ngrok get_hostport connection error');
306
+ .catch(() => {
307
+ _this.debug_en && _this.debug('ngrok get_hostport connection error');
312
308
  if (this.listener[opts.server_name]) this.listener[opts.server_name].isConnected = false;
313
309
  _this.ssh_retry_connect(_opts);
314
310
  });
@@ -322,7 +318,7 @@ class sshTunnelProxy extends EventEmitter {
322
318
  // initiate ssh tunnel, block until tunnel is established or error
323
319
  return await this.ssh_start_tunnel(_opts)
324
320
  .catch((err) => {
325
- this.debug_en &&this.debug('ssh_start_tunnel catch:' + err);
321
+ this.debug_en && this.debug('ssh_start_tunnel catch:' + err);
326
322
  });
327
323
  }
328
324
 
@@ -340,15 +336,12 @@ class sshTunnelProxy extends EventEmitter {
340
336
  keypairStorage.get_public_key_from_keychain(...args);
341
337
  }
342
338
 
343
- // ssh client debug function for verbose ssh connection details
344
- debug_client = (this.debug_ssh) ? (msg) => { console.log(msg); } : null;
345
-
346
339
  debug(...args) { console.log(...args); }
347
340
 
348
341
  }
349
342
 
350
343
  module.exports = {
351
- SSHTunnelProxy:sshTunnelProxy,
352
- KeypairStorage:KeypairStorage,
353
- NgrokApi:NgrokApi
344
+ SSHTunnelProxy: SSHTunnelProxy,
345
+ KeypairStorage: KeypairStorage,
346
+ NgrokApi: NgrokApi
354
347
  }
@@ -23,7 +23,7 @@ class KeypairStorage {
23
23
  generate_and_store_keypair(service_name, account) {
24
24
  const keypair = this.generate_keypair();
25
25
  return new Promise((resolve, reject) => {
26
- keytar.setPassword(service_name, account, keypair.private_key).then((result) => {
26
+ keytar.setPassword(service_name, account, keypair.private_key).then(() => {
27
27
  resolve(keypair.public_key);
28
28
  }, (err) => {
29
29
  reject(err);
package/main.js CHANGED
@@ -1,7 +1,6 @@
1
1
  const process = require('process');
2
2
  const fs = require('fs');
3
3
  const { SSHTunnelProxy, KeypairStorage } = require('./lib/index.js');
4
- const { config } = require('process');
5
4
 
6
5
  const keypairStorage = new KeypairStorage();
7
6
  const homedir = require('os').homedir();
@@ -11,6 +10,7 @@ var configs = null;
11
10
  var host = null;
12
11
  var port = null;
13
12
  var username = null;
13
+ var password = null;
14
14
  var forwardOut = [];
15
15
  var forwardIn = [];
16
16
  var ngrokAPIKey = null;
@@ -108,7 +108,7 @@ function main(args) {
108
108
 
109
109
  // if no config file specified, build config from args
110
110
  if (!configs) {
111
- config = {
111
+ var config = {
112
112
  enabled: true,
113
113
  username: username,
114
114
  password: password,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssh_tunnel_proxy",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "obtain ngrok host port from api and establish a ssh connection",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -11,7 +11,6 @@
11
11
  "dependencies": {
12
12
  "@ngrok/ngrok-api": "^0.9.0",
13
13
  "bluebird": "^3.7.2",
14
- "crypto": "^1.0.1",
15
14
  "electron-ssh2": "^0.1.2",
16
15
  "form-data": "^4.0.0",
17
16
  "fs": "^0.0.1-security",
@@ -22,6 +21,7 @@
22
21
  "sshpk": "^1.17.0"
23
22
  },
24
23
  "devDependencies": {
24
+ "eslint": "^8.33.0",
25
25
  "mocha": "^10.2.0",
26
26
  "rewire": "^6.0.0",
27
27
  "sinon": "^15.0.1"
package/test/test.js CHANGED
@@ -6,9 +6,8 @@ to run ssh tunnel api tests edit config.json and provide api keys for each servi
6
6
  */
7
7
 
8
8
  const assert = require('assert');
9
+ const { describe, it } = require('mocha');
9
10
  const fs = require('fs');
10
- const path = require('path');
11
- const sinon = require('sinon');
12
11
 
13
12
  const { SSHTunnelProxy, KeypairStorage, NgrokApi } = require('../lib/index.js');
14
13
 
@@ -136,8 +135,8 @@ describe('Store and retrieve private key in system keychain', function () {
136
135
  });
137
136
  });
138
137
  it('remove key should return true', () => {
139
- return keypairStorage.delete_keypair(service_name, account).then(result => {
140
- assert('should return true');
138
+ return keypairStorage.delete_keypair(service_name, account).then((result) => {
139
+ assert(result, 'should return true');
141
140
  });
142
141
  });
143
142
  it('retrieve stored private key after delete should return null', () => {
@@ -174,7 +173,7 @@ describe('Get ngrok hostport', function () {
174
173
  assert.equal(result_opts.port, '17632');
175
174
  });
176
175
  it('host, port should be obtained from api', async () => {
177
- result = await ngrokApi.get_hostport(opts);
176
+ const result = await ngrokApi.get_hostport(opts);
178
177
  assert(result.host, 'host should exist');
179
178
  assert(result.port, 'port should exist');
180
179
  }, (err) => {
@@ -213,17 +212,3 @@ describe('ssh connection integration test', function () {
213
212
 
214
213
  });
215
214
  */
216
-
217
-
218
- // test generate keypair
219
- var test_generate_keypair = function () {
220
- const keypair = generate_keypair();
221
- console.log('keypair:');
222
- console.log(keypair.public_key);
223
- console.log(keypair.private_key);
224
- var homedir = homedir();
225
- var fname = path.join(homedir, '.ssh', 'zm_id_rsa.pub')
226
- //fs.writeFileSync(fname, keypair.public_key);
227
- fname = path.join(homedir, '.ssh', 'zm_id_rsa')
228
- //fs.writeFileSync(fname, keypair.private_key);
229
- }