ssh_tunnel_proxy 1.1.5 → 1.2.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.
@@ -22,13 +22,13 @@
22
22
  "type": "node",
23
23
  "runtimeVersion": "19.4.0",
24
24
  "request": "launch",
25
- "name": "Launch Program",
25
+ "name": "Launch node cli",
26
26
  "cwd": "${fileDirname}",
27
27
  "skipFiles": [
28
28
  "<node_internals>/**"
29
29
  ],
30
30
  "program": "${file}",
31
- "args":["--source=themagician_composite"]
31
+ "args":["-c","-d"]
32
32
  }
33
33
  ]
34
34
  }
package/README.md CHANGED
@@ -1,14 +1,44 @@
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 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
- If a ngrok api key is provided the ngrok api endpoint method is invoked to obtain the hostport of
6
- 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 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
5
 
8
- Example code for electron_js:
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.
7
+
8
+ Example command line usage:
9
+
10
+ config file, located at:
11
+ ~/.config/ssh_tunnel_proxy/config.json
12
+ ```json
13
+ [{
14
+ "enabled": true,
15
+ "username": "<username>",
16
+ "password": "",
17
+ "host": "",
18
+ "port": "",
19
+ "proxy_ports": [
20
+ "8280:127.0.0.1:80",
21
+ "9000:127.0.0.1:9000",
22
+ "8122:192.168.2.1:22"
23
+ ],
24
+ "whitelist": {
25
+ "80": true,
26
+ "443": true,
27
+ "22": true
28
+ },
29
+ "service_name": "ssh_proxy_client",
30
+ "server_name": "test",
31
+ "ngrok_api": "<ngrok api key>"
32
+ }]
33
+
34
+ start tunnel with default config, located at ~/.config/ssh_tunnel_proxy/config.json
35
+ ```
36
+ node main.js -c -d
37
+
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
+ main.js:
9
40
  ```js
10
41
 
11
- // electronjs main.js
12
42
  async function init_sshTunnelProxy(win) {
13
43
 
14
44
  const SSHTunnelProxy = require('ssh_tunnel_proxy');
@@ -40,8 +70,10 @@ async function init_sshTunnelProxy(win) {
40
70
  sshTunnelProxy.on('error',(...args) => win.webContents.send('error',...args));
41
71
 
42
72
  };
73
+ ```
43
74
 
44
- // electronjs preload.js
75
+ preload.js:
76
+ ```js
45
77
  const { contextBridge, ipcRenderer } = require('electron')
46
78
 
47
79
  contextBridge.exposeInMainWorld('sshModule', {
@@ -54,8 +86,10 @@ contextBridge.exposeInMainWorld('sshModule', {
54
86
  debug: (callback) => ipcRenderer.on('debug', callback),
55
87
  error: (callback) => ipcRenderer.on('error', callback)
56
88
  });
89
+ ```
57
90
 
58
- // electronjs render process, setup listeners and initiate ssh tunnel
91
+ // render process
92
+ ```js
59
93
  // initialize ssh message handlers
60
94
  sshModule.ready((event, data) => {
61
95
  });
package/lib/index.js CHANGED
@@ -24,7 +24,6 @@ const KeypairStorage = require('./keypair_storage');
24
24
  const NgrokApi = require('./ngrok_service');
25
25
 
26
26
  // debug logging options
27
- const en = true;
28
27
  const debug_ssh = false;
29
28
 
30
29
  const keypairStorage = new KeypairStorage();
@@ -35,6 +34,8 @@ class sshTunnelProxy extends EventEmitter {
35
34
  super();
36
35
  this.listener = {};
37
36
  this.retries = 0;
37
+ this.debug_en = false;
38
+ this.debug_ssh = false;
38
39
  }
39
40
 
40
41
  // function to close all active sockets for tunnel restart and exception handling
@@ -45,7 +46,7 @@ class sshTunnelProxy extends EventEmitter {
45
46
  proxy_ports.forEach(proxy_port => {
46
47
  const server = this.listener[server_name][proxy_port];
47
48
  if (server && server.listening) {
48
- en && this.debug('SSH Server :: closing forward:', proxy_port)
49
+ this.debug_en &&this.debug('SSH Server :: closing forward:', proxy_port)
49
50
  server.close();
50
51
  }
51
52
  });
@@ -59,8 +60,8 @@ class sshTunnelProxy extends EventEmitter {
59
60
  const on_setup_ssh_forward = (err, stream) => {
60
61
 
61
62
  if (err) {
62
- _this.emit('error', err);
63
- en && _this.debug('socket forward error:', err);
63
+ _this.emit('debug', err);
64
+ _this.debug_en &&_this.debug('socket forward error:', err);
64
65
  return;
65
66
  }
66
67
 
@@ -75,8 +76,8 @@ class sshTunnelProxy extends EventEmitter {
75
76
 
76
77
  // if socket error, emit error and close stream
77
78
  socket.on('error', (err) => {
78
- _this.emit('error', err);
79
- en && _this.debug('socket on error:', err);
79
+ _this.emit('debug', err);
80
+ _this.debug_en &&_this.debug('socket on error:', err);
80
81
  stream.end();
81
82
  });
82
83
  }
@@ -105,9 +106,14 @@ class sshTunnelProxy extends EventEmitter {
105
106
  const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
106
107
 
107
108
  // create local socket server
109
+ const server = this.listener[server_name][proxy_port];
110
+ if (server && server.listening) {
111
+ this.debug_en &&this.debug('SSH Server :: closing forward:', proxy_port)
112
+ server.close();
113
+ }
108
114
  this.listener[server_name][proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
109
115
 
110
- if (en) {
116
+ if (this.debug_en) {
111
117
  var debug_msg = 'SSH Server :: connection on ' + local_port + ' ' + socket.remotePort;
112
118
  _this.emit('debug', debug_msg);
113
119
  _this.debug(debug_msg);
@@ -132,7 +138,7 @@ class sshTunnelProxy extends EventEmitter {
132
138
  }
133
139
  });
134
140
  } catch (err) {
135
- en && _this.debug('listen err:', err);
141
+ _this.debug_en &&_this.debug('listen err:', err);
136
142
  }
137
143
  });
138
144
  };
@@ -162,30 +168,29 @@ class sshTunnelProxy extends EventEmitter {
162
168
  });
163
169
 
164
170
  ssh_client.on('end', () => {
165
- en && _this.debug('SSH Client :: end');
171
+ _this.debug_en &&_this.debug('SSH Client :: end');
166
172
  _this.close_sockets(opts.server_name, opts.proxy_ports);
167
173
  });
168
174
 
169
175
  ssh_client.on('close', () => {
170
- en && _this.debug('SSH Client :: close');
176
+ _this.debug_en &&_this.debug('SSH Client :: close');
171
177
  _this.close_sockets(opts.server_name, opts.proxy_ports);
172
178
  });
173
179
 
174
180
  ssh_client.on('error', err => {
175
- en && _this.debug('SSH Client :: error :: ' + err);
181
+ _this.debug_en &&_this.debug('SSH Client :: error :: ' + err);
176
182
  _this.emit('error', err);
177
- _this.close_sockets(opts.server_name, opts.proxy_ports);
178
183
  if (_this.listener[opts.server_name]) _this.listener[opts.server_name].isConnected = false;
179
184
  _this.ssh_retry_connect(opts);
180
185
  resolve();
181
186
  });
182
187
 
183
188
  ssh_client.on('handshake', negotiated => {
184
- en && _this.debug('SSH Client :: handshake:', JSON.stringify(negotiated));
189
+ _this.debug_en &&_this.debug('SSH Client :: handshake:', JSON.stringify(negotiated));
185
190
  });
186
191
 
187
192
  ssh_client.on('greeting', (message, language) => {
188
- en && _this.debug('SSH Client :: greeting:', message);
193
+ _this.debug_en &&_this.debug('SSH Client :: greeting:', message);
189
194
  });
190
195
 
191
196
  // initiate ssh client connection to remote host
@@ -223,7 +228,7 @@ class sshTunnelProxy extends EventEmitter {
223
228
  if (local_ports.length || remote_ports.length) {
224
229
  const err = new Error('Invalid local forward');
225
230
 
226
- en && this.debug('invalid ports found:\n', JSON.stringify(proxy_ports, null, 2),
231
+ this.debug_en &&this.debug('invalid ports found:\n', JSON.stringify(proxy_ports, null, 2),
227
232
  '\n', JSON.stringify(whitelist, null, 2));
228
233
 
229
234
  err.info = {
@@ -237,7 +242,11 @@ class sshTunnelProxy extends EventEmitter {
237
242
 
238
243
  // on network error, attempt to re-establish connection until 10 retries
239
244
  ssh_retry_connect = (opts) => {
240
- if (this.retries++ < 10) setTimeout(this.connect_ssh, 5000, opts);
245
+ const _this = this;
246
+ const invoke = () => {
247
+ _this.connectSSH(opts);
248
+ }
249
+ if (this.retries++ < 10) setTimeout(invoke, 5000);
241
250
  }
242
251
 
243
252
  // attempt to establish ssh tunnel to server with supplied parameters.
@@ -248,7 +257,7 @@ class sshTunnelProxy extends EventEmitter {
248
257
  // after successful tunnel setup complete, send events and reset retry counter
249
258
  await _this.do_ssh_connect(opts).then(() => {
250
259
  const hostport = opts.host + ':' + opts.port;
251
- if (en) {
260
+ if (_this.debug_en) {
252
261
  const msg = 'SSH connection to ' + opts.server_name + ' established at ' + hostport;
253
262
  _this.debug(msg);
254
263
  }
@@ -256,7 +265,7 @@ class sshTunnelProxy extends EventEmitter {
256
265
  _this.retries = 0;
257
266
  resolve();
258
267
  }, (err) => {
259
- en && _this.debug('ssh_retry error:', err);
268
+ _this.debug_en &&_this.debug('ssh_retry error:', err);
260
269
  reject();
261
270
  });
262
271
  });
@@ -266,7 +275,7 @@ class sshTunnelProxy extends EventEmitter {
266
275
  // todo: if opts have changed while service is running, shutdown current service and restart with new opts
267
276
  async connectSSH(opts, whitelist) {
268
277
 
269
- en && this.debug('connect_ssh:\n', JSON.stringify(opts, null, 2));
278
+ this.debug_en &&this.debug('connectSSH:\n', JSON.stringify(opts, null, 2));
270
279
 
271
280
  // make deep copy of opts for modification
272
281
  var _opts = JSON.parse(JSON.stringify(opts));
@@ -279,7 +288,7 @@ class sshTunnelProxy extends EventEmitter {
279
288
  this.validate_local_forward(_opts.proxy_ports, _opts.whitelist);
280
289
  } catch (err) {
281
290
  this.emit('error', err);
282
- en && this.debug(err);
291
+ this.debug_en &&this.debug(err);
283
292
  return err;
284
293
  }
285
294
 
@@ -299,7 +308,7 @@ class sshTunnelProxy extends EventEmitter {
299
308
  const ngrokApi = new NgrokApi(_opts.ngrok_api);
300
309
  var hostport = await ngrokApi.get_hostport()
301
310
  .catch((err) => {
302
- en && _this.debug('ngrok get_hostport connection error');
311
+ _this.debug_en &&_this.debug('ngrok get_hostport connection error');
303
312
  if (this.listener[opts.server_name]) this.listener[opts.server_name].isConnected = false;
304
313
  _this.ssh_retry_connect(_opts);
305
314
  });
@@ -313,7 +322,7 @@ class sshTunnelProxy extends EventEmitter {
313
322
  // initiate ssh tunnel, block until tunnel is established or error
314
323
  return await this.ssh_start_tunnel(_opts)
315
324
  .catch((err) => {
316
- en && this.debug('ssh_start_tunnel catch:' + err);
325
+ this.debug_en &&this.debug('ssh_start_tunnel catch:' + err);
317
326
  });
318
327
  }
319
328
 
@@ -332,10 +341,14 @@ class sshTunnelProxy extends EventEmitter {
332
341
  }
333
342
 
334
343
  // ssh client debug function for verbose ssh connection details
335
- debug_client = (debug_ssh) ? (msg) => { console.log(msg); } : null;
344
+ debug_client = (this.debug_ssh) ? (msg) => { console.log(msg); } : null;
336
345
 
337
- debug = (...args) => { console.log(...args); }
346
+ debug(...args) { console.log(...args); }
338
347
 
339
348
  }
340
349
 
341
- module.exports = sshTunnelProxy;
350
+ module.exports = {
351
+ SSHTunnelProxy:sshTunnelProxy,
352
+ KeypairStorage:KeypairStorage,
353
+ NgrokApi:NgrokApi
354
+ }
@@ -74,16 +74,3 @@ class KeypairStorage {
74
74
  }
75
75
  }
76
76
  module.exports = KeypairStorage;
77
- /*
78
- module.exports = {
79
- store_private_key: keytar.setPassword,
80
- retrieve_private_key: keytar.getPassword,
81
- remove_keypair: keytar.deletePassword,
82
- generate_keypair: generate_keypair,
83
- generate_and_store_keypair: generate_and_store_keypair,
84
- get_public_key_from_keychain: get_public_key_from_keychain,
85
- get_keypair: get_keypair,
86
- set_keypair: set_keypair,
87
- delete_keypair: delete_keypair
88
- }
89
- */
package/main.js ADDED
@@ -0,0 +1,153 @@
1
+ const process = require('process');
2
+ const fs = require('fs');
3
+ const { SSHTunnelProxy, KeypairStorage } = require('./lib/index.js');
4
+ const { config } = require('process');
5
+
6
+ const keypairStorage = new KeypairStorage();
7
+ const homedir = require('os').homedir();
8
+ var debug = false;
9
+ var proxies = [];
10
+ var configs = null;
11
+ var host = null;
12
+ var port = null;
13
+ var username = null;
14
+ var forwardOut = [];
15
+ var forwardIn = [];
16
+ var ngrokAPIKey = null;
17
+ var privateKey = null;
18
+ var service = null;
19
+ var account = null;
20
+
21
+ function get_key_from_file(filename) {
22
+ try {
23
+ return fs.readFileSync(filename);
24
+ } catch (err) {
25
+ console.log('Error loading key:', err);
26
+ }
27
+ }
28
+
29
+ function get_config(filename) {
30
+ try {
31
+ configs = JSON.parse(fs.readFileSync(filename), 'utf8');
32
+ } catch (err) {
33
+ console.log('Error loading config file:', err);
34
+ }
35
+ }
36
+
37
+ function get_default_config() {
38
+ try {
39
+ configs = JSON.parse(fs.readFileSync(homedir + '/.config/ssh_tunnel_proxy/config.json'), 'utf8');
40
+ } catch (err) {
41
+ console.log('Error loading config file:', err);
42
+ }
43
+ }
44
+
45
+ function main(args) {
46
+
47
+ // parse command line args
48
+ for (let i = 0; i < args.length; i++) {
49
+ const arg = args[i].split('=');
50
+ const cmd = arg[0];
51
+ const value = arg[1];
52
+ switch (cmd) {
53
+ case '-d':
54
+ case '--debug':
55
+ debug = true;
56
+ break;
57
+ case '-c':
58
+ case '--config':
59
+ if (value) get_config(value);
60
+ else get_default_config();
61
+ break;
62
+ case '-h':
63
+ case '--host':
64
+ host = value;
65
+ break;
66
+ case '-p':
67
+ case '--port':
68
+ port = value;
69
+ break;
70
+ case '-u':
71
+ case '--username':
72
+ username = value;
73
+ break;
74
+ case '-P':
75
+ case '--password':
76
+ password = value;
77
+ break;
78
+ case '-k':
79
+ case '--key':
80
+ privateKey = value;
81
+ break;
82
+ case '-s':
83
+ case '--service':
84
+ service = value;
85
+ break;
86
+ case '--a':
87
+ account = value;
88
+ break;
89
+ case '-L':
90
+ case '--LocalForward':
91
+ forwardOut.push(value);
92
+ break;
93
+ case '-R':
94
+ case '--RemoteForward':
95
+ forwardIn.push(value);
96
+ break;
97
+ case '-n':
98
+ case '--ngrok':
99
+ ngrokAPIKey = value;
100
+ break;
101
+ default:
102
+ if (i > 1) console.log('Invalid argument:', arg);
103
+ }
104
+ }
105
+
106
+ // prevent process from exiting
107
+ process.stdin.resume();
108
+
109
+ // if no config file specified, build config from args
110
+ if (!configs) {
111
+ config = {
112
+ enabled: true,
113
+ username: username,
114
+ password: password,
115
+ host: host,
116
+ port: port,
117
+ proxy_ports: forwardOut,
118
+ remote_ports: forwardIn,
119
+ service_name: service,
120
+ server_name: account,
121
+ ngrok_api: ngrokAPIKey,
122
+ private_key: privateKey
123
+ };
124
+ configs = [config];
125
+ }
126
+
127
+ // process each tunnel config in ssh proxy tunnel list
128
+ configs.forEach(async config => {
129
+
130
+ // skip tunnel if not enabled
131
+ if (!config.enabled) return;
132
+
133
+ // if system key storage name specified get key from system keychain
134
+ if (config.server_name) {
135
+ const service_name = config.service_name || 'ssh_tunnel_proxy';
136
+ config.private_key = await keypairStorage.get_keypair(service_name, config.server_name);
137
+ }
138
+ // if private key filename specified, read private key
139
+ else if (config.private_key) {
140
+ config.private_key = get_key_from_file(config.private_key);
141
+ }
142
+
143
+ // create new tunnel and start connection
144
+ var sshTunnelProxy = new SSHTunnelProxy();
145
+ proxies.push(sshTunnelProxy);
146
+ sshTunnelProxy.debug_en = debug;
147
+ sshTunnelProxy.connectSSH(config);
148
+ });
149
+
150
+
151
+ }
152
+
153
+ main(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssh_tunnel_proxy",
3
- "version": "1.1.5",
3
+ "version": "1.2.1",
4
4
  "description": "obtain ngrok host port from api and establish a ssh connection",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
package/test/test.js CHANGED
@@ -10,13 +10,12 @@ const fs = require('fs');
10
10
  const path = require('path');
11
11
  const sinon = require('sinon');
12
12
 
13
- const SSHTunnelProxy = require('../lib/index.js');
14
- const KeypairStorage = require('../lib/keypair_storage.js');
15
- const NgrokApi = require('../lib/ngrok_service');
13
+ const { SSHTunnelProxy, KeypairStorage, NgrokApi } = require('../lib/index.js');
16
14
 
17
15
  // get config file containing api keys
18
16
  const homedir = require('os').homedir();
19
- const opts = JSON.parse(fs.readFileSync(homedir + '/.config/ssh_tunnel_proxy/config.json'), 'utf8');
17
+ const config = JSON.parse(fs.readFileSync(homedir + '/.config/ssh_tunnel_proxy/config.json'), 'utf8');
18
+ const opts = config[0];
20
19
 
21
20
  const sshTunnelProxy = new SSHTunnelProxy();
22
21
  const keypairStorage = new KeypairStorage();