ssh_tunnel_proxy 1.0.0 → 1.1.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.
package/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
2
+
3
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
4
+
5
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # ssh_tunnel_proxy
2
+ Initiate a ssh reverse tunnel proxy with forwarding ports
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.
7
+
8
+ Example code for electron_js:
9
+ '''
10
+ async function init_sshTunnelProxy(win) {
11
+
12
+ const SSHTunnelProxy = require('ssh_tunnel_proxy');
13
+ const sshTunnelProxy = new SSHTunnelProxy();
14
+
15
+ ipcMain.on('connect_ssh_sync', async function(event,opts) {
16
+ // only allow connection on remote server to system ports http,https,ssh and user ports >1023
17
+ const portWhitelist = {
18
+ 80:true,
19
+ 443:true,
20
+ 22:true
21
+ }
22
+ await sshTunnelProxy.connectSSH(opts, portWhitelist);
23
+ event.returnValue = 'connected';
24
+ });
25
+
26
+ ipcMain.on('generate_keypair', async (event, ...args) => await sshTunnelProxy.generateAndStoreKeypair(...args));
27
+
28
+ ipcMain.on('get_public_key', async (event, ...args) => await sshTunnelProxy.getPublicKey(...args));
29
+
30
+ ipcMain.on('network_online', (event, ...args) => sshTunnelProxy.onNetworkOnline(...args));
31
+
32
+ ipcMain.on('network_offline', (event, ...args) => sshTunnelProxy.onNetworkOffline(...args));
33
+
34
+ sshTunnelProxy.on('ready',(...args) => win.webContents.send('ready',...args));
35
+
36
+ sshTunnelProxy.on('debug',(...args) => win.webContents.send('debug',...args));
37
+
38
+ sshTunnelProxy.on('error',(...args) => win.webContents.send('error',...args));
39
+
40
+ };
41
+ '''
package/lib/index.js CHANGED
@@ -20,265 +20,322 @@ const net = require('net');
20
20
  const { EventEmitter } = require('node:events');
21
21
  const { Client } = require('electron-ssh2');
22
22
 
23
- const keypair_storage = require('./keypair_storage');
24
- const { get_hostport } = require('./ngrok_service');
23
+ const KeypairStorage = require('./keypair_storage');
24
+ const NgrokApi = require('./ngrok_service');
25
25
 
26
- // emit events to enable hooks to be established during tunnel setup and error handling
27
- class SSHEmitter extends EventEmitter { };
28
- const sshEmitter = new SSHEmitter();
29
-
30
- // debugging options
31
- const debug = false;
26
+ // debug logging options
27
+ const en = true;
32
28
  const debug_ssh = false;
33
29
 
34
- // storage for all open sockets on each server
35
- var listener = {};
36
-
37
- // function to close all active sockets for tunnel restart and exception handling
38
- const close_sockets = (server_name, proxy_ports) => {
39
- if (!listener[server_name]) return;
40
- proxy_ports.forEach(proxy_port => {
41
- const server = listener[server_name][proxy_port];
42
- if (server && server.listening) {
43
- if (debug) console.log('closing server:', proxy_port)
44
- server.close();
45
- }
46
- });
47
- }
30
+ const keypairStorage = new KeypairStorage();
31
+
32
+ class sshTunnelProxy extends EventEmitter {
33
+
34
+ constructor() {
35
+ super();
36
+ this.listener = {};
37
+ this.retries = 0;
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]) return;
48
44
 
49
- // ssh client debug function for verbose ssh connection details
50
- const debug_client = (debug_ssh) ? (msg) => { console.log(msg); } : null;
45
+ proxy_ports.forEach(proxy_port => {
46
+ const server = this.listener[server_name][proxy_port];
47
+ if (server && server.listening) {
48
+ 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(ssh_client, 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('error', err);
63
+ en && _this.debug('socket forward error:', err);
64
+ return;
65
+ }
66
+
67
+ // pipe the data from the local socket to the remote port and visa versa
68
+ socket.pipe(stream);
69
+ stream.pipe(socket);
70
+
71
+ // if socket ends, close stream and pipes
72
+ socket.on('close', () => {
73
+ stream.end();
74
+ });
75
+
76
+ // if socket error, emit error and close stream
77
+ socket.on('error', (err) => {
78
+ _this.emit('error', err);
79
+ en && _this.debug('socket on error:', err);
80
+ stream.end();
81
+ });
82
+ }
51
83
 
52
- // function to handle setting up tunnel and proxy forward ports
53
- const do_ssh_connect = (opts) => {
84
+ // create port forward
85
+ ssh_client.forwardOut(
86
+ socket.remoteAddress,
87
+ socket.remotePort,
88
+ remote_hostname,
89
+ remote_port,
90
+ on_setup_ssh_forward
91
+ );
54
92
 
55
- // close open sockets on server, otherwise initialize open sockets storage
56
- if (listener[opts.server_name]) {
57
- close_sockets(opts.server_name, opts.proxy_ports);
58
- } else {
59
- listener[opts.server_name] = {};
60
93
  }
61
94
 
62
- // create a new ssh client connection with supplied credentials and hostname/port
63
- return new Promise((resolve, reject) => {
95
+ // function to setup proxy forwards for ssh tunnel
96
+ on_ssh_client_ready(ssh_client, server_name, proxy_ports, resolve) {
64
97
 
65
98
  var listeners = 0;
66
- const ssh_client = new Client();
99
+ if (this.listener[server_name]) this.listener[server_name].isConnected = true;
100
+ const _this = this;
67
101
 
68
- // setup ssh client event listeners
69
- ssh_client.on('end', () => {
70
- if (debug) console.log('SSH Client :: end');
71
- close_sockets(opts.server_name, opts.proxy_ports);
72
- });
73
- ssh_client.on('close', () => {
74
- if (debug) console.log('SSH Client :: close');
75
- close_sockets(opts.server_name, opts.proxy_ports);
76
- });
77
- ssh_client.on('error', err => {
78
- if (debug) console.log('SSH Client :: error :: ' + err);
79
- sshEmitter.emit('error', err);
80
- close_sockets(opts.server_name, opts.proxy_ports);
81
- ssh_retry_connect(opts);
82
- resolve();
83
- });
84
- ssh_client.on('handshake', negotiated => {
85
- if (debug) console.log('SSH Client :: handshake:', JSON.stringify(negotiated));
86
- });
87
- ssh_client.on('banner', (message, language) => {
88
- if (debug) console.log('SSH Client :: banner:', message);
89
- });
102
+ // iterate through a list of local forward ports and create a local proxy port
103
+ proxy_ports.forEach(proxy_port => {
90
104
 
91
- // when ssh client is ready, establish proxy forward ports to remote server
92
- ssh_client.on('ready', () => {
105
+ const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
93
106
 
94
- // iterate through a list of local forward ports and create a local proxy port
95
- opts.proxy_ports.forEach(proxy_port => {
96
- const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
107
+ // create local socket server
108
+ this.listener[server_name][proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
97
109
 
98
- // create local websocket server
99
- listener[opts.server_name][proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
110
+ if (en) {
111
+ var debug_msg = 'SSH Server :: connection on ' + local_port + ' ' + socket.remotePort;
112
+ _this.emit('debug', debug_msg);
113
+ _this.debug(debug_msg);
114
+ }
100
115
 
101
- if (debug) {
102
- var debug_msg = 'SSH Server :: connection on ' + local_port + ' ' + socket.remotePort;
103
- sshEmitter.emit('debug', debug_msg);
104
- console.log(debug_msg);
105
- }
116
+ // create a proxy forward between local and remote ports
117
+ _this.setup_ssh_forward(ssh_client, socket, remote_hostname, remote_port);
118
+ });
106
119
 
107
- // create a proxy forward between the local port and remote port
108
- ssh_client.forwardOut(
109
- socket.remoteAddress,
110
- socket.remotePort,
111
- remote_hostname,
112
- remote_port,
113
- (err, stream) => {
114
- if (err) {
115
- sshEmitter.emit('error', err);
116
- if (debug) console.log('socket forward error:', err);
117
- //reject(err);
118
- return;
119
- }
120
-
121
- // pipe the data from the local socket to the remote port and visa versa
122
- socket.pipe(stream);
123
- stream.pipe(socket);
124
-
125
- // if socket ends, close stream and pipes
126
- socket.on('close', () => {
127
- stream.end();
128
- });
129
-
130
- // if socket error, emit error and close stream
131
- socket.on('error', (err) => {
132
- sshEmitter.emit('error', err);
133
- if (debug) console.log('socket on error:', err);
134
- stream.end();
135
- });
136
-
137
- }
138
- );
139
- })
140
-
141
- // start listening on port
142
- listener[opts.server_name][proxy_port].listen(local_port, () => {
120
+ // start listening on port
121
+ try {
122
+ _this.listener[server_name][proxy_port].listen(local_port, () => {
143
123
 
144
124
  // emit server listening on port message
145
125
  var status_msg = 'SSH Server :: bound on ' + local_port;
146
- sshEmitter.emit('status', status_msg, 'listening', local_port);
126
+ _this.emit('debug', status_msg);
147
127
 
148
128
  // if all listeners have been successfully established, resolve setup connection
149
- if (listeners++ >= opts.proxy_ports.length - 1) {
129
+ if (listeners++ >= proxy_ports.length - 1) {
130
+ _this.emit('ready', {});
150
131
  resolve();
151
132
  }
152
133
  });
153
- });
134
+ } catch (err) {
135
+ en && _this.debug('listen err:', err);
136
+ }
154
137
  });
138
+ };
155
139
 
156
- // initiate ssh client connection to remote host
157
- ssh_client.connect({
158
- host: opts.host,
159
- port: opts.port,
160
- username: opts.username,
161
- password: opts.password,
162
- privateKey: opts.private_key,
163
- debug: debug_client,
164
- keepaliveInterval: 10000
165
- });
166
- });
167
- }
140
+ // handle setting up ssh client and proxy forward ports
141
+ do_ssh_connect(opts) {
168
142
 
169
- // validate port number - check for nan, out of valid port range, unauthorized system ports
170
- const validate_port_number = function (port_str, whitelist) {
171
- if (isNaN(port_str)) return false;
172
- const port = parseInt(port_str);
173
- if (port < 1 || port > 65535) return false;
174
- if (port < 1024 && whitelist[port] === undefined) return false;
175
- return true;
176
- }
143
+ const _this = this;
177
144
 
178
- // validate local forwards for correct format and valid ports
179
- const validate_local_forward = function (proxy_ports, whitelist) {
180
- const local_ports = [];
181
- const remote_ports = [];
182
- proxy_ports.forEach(proxy_port => {
183
- const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
184
- if (!validate_port_number(local_port, whitelist)) local_ports.push(local_port);
185
- if (!validate_port_number(remote_port, whitelist)) remote_ports.push(remote_port);
186
- });
187
- if (local_ports.length || remote_ports.length) {
188
- const err = new Error('Invalid local forward');
189
- err.info = {
190
- local_ports: local_ports.join(','),
191
- remote_ports: remote_ports.join(',')
192
- };
193
- throw (err);
194
- }
195
- return true;
196
- }
145
+ // create a new ssh client connection with supplied credentials and hostname/port
146
+ return new Promise((resolve, reject) => {
197
147
 
198
- // on network error, attempt to re-establish connection until 10 retries
199
- let retries = 0;
200
- const ssh_retry_connect = (opts) => {
201
- if (retries++ < 10) setTimeout(connect_ssh, 5000, opts);
202
- }
148
+ // exit if connection already established, avoid redundant connection on retry
149
+ if (_this.listener[opts.server_name] && _this.listener[opts.server_name].isConnected) resolve();
150
+
151
+ // close open sockets on server, otherwise initialize open sockets storage
152
+ if (_this.listener[opts.server_name]) {
153
+ _this.close_sockets(opts.server_name, opts.proxy_ports);
154
+ } else {
155
+ _this.listener[opts.server_name] = {};
156
+ }
157
+
158
+ const ssh_client = new Client();
159
+
160
+ ssh_client.on('ready', () => {
161
+ _this.on_ssh_client_ready(ssh_client, opts.server_name, opts.proxy_ports, resolve);
162
+ });
163
+
164
+ ssh_client.on('end', () => {
165
+ en && _this.debug('SSH Client :: end');
166
+ _this.close_sockets(opts.server_name, opts.proxy_ports);
167
+ });
168
+
169
+ ssh_client.on('close', () => {
170
+ en && _this.debug('SSH Client :: close');
171
+ _this.close_sockets(opts.server_name, opts.proxy_ports);
172
+ });
173
+
174
+ ssh_client.on('error', err => {
175
+ en && _this.debug('SSH Client :: error :: ' + err);
176
+ _this.emit('error', err);
177
+ _this.close_sockets(opts.server_name, opts.proxy_ports);
178
+ if (_this.listener[opts.server_name]) _this.listener[opts.server_name].isConnected = false;
179
+ _this.ssh_retry_connect(opts);
180
+ resolve();
181
+ });
182
+
183
+ ssh_client.on('handshake', negotiated => {
184
+ en && _this.debug('SSH Client :: handshake:', JSON.stringify(negotiated));
185
+ });
186
+
187
+ ssh_client.on('greeting', (message, language) => {
188
+ en && _this.debug('SSH Client :: greeting:', message);
189
+ });
190
+
191
+ // initiate ssh client connection to remote host
192
+ ssh_client.connect({
193
+ host: opts.host,
194
+ port: opts.port,
195
+ username: opts.username,
196
+ password: opts.password,
197
+ privateKey: opts.private_key,
198
+ debug: _this.debug_client,
199
+ keepaliveInterval: 10000
200
+ });
203
201
 
204
- // attempt to establish ssh tunnel to server with supplied parameters.
205
- const ssh_start_tunnel = async (opts) => {
206
- return new Promise(async (resolve, reject) => {
207
-
208
- // after successful tunnel setup complete, send events and reset retry counter
209
- await do_ssh_connect(opts).then(() => {
210
- const hostport = opts.host + ':' + opts.port;
211
- const msg = 'SSH connection to ' + opts.server_name + ' established at ' + hostport;
212
- if (debug) console.log(msg);
213
- sshEmitter.emit('status', '', 'ready', hostport);
214
- retries = 0;
215
- resolve();
216
- }, (err) => {
217
- if (debug) console.log('ssh_retry error:', err);
218
- reject();
219
202
  });
220
- });
221
- }
203
+ }
222
204
 
223
- // export function to setup ssh connection parameters and attempt to establish ssh tunnel
224
- // todo: if opts have changed while service is running, shutdown current service and restart with new opts
225
- const connect_ssh = async function (opts, whitelist) {
226
- if (debug) {
227
- console.log('connect_ssh:');
228
- console.log(JSON.stringify(opts, null, 2));
205
+ // validate port number - check for nan, out of valid port range, unauthorized system ports
206
+ validate_port_number(port_str, whitelist) {
207
+ if (isNaN(port_str)) return false;
208
+ const port = parseInt(port_str);
209
+ if (port < 1 || port > 65535) return false;
210
+ if (port < 1024 && whitelist[port] === undefined) return false;
211
+ return true;
229
212
  }
230
213
 
231
- // make deep copy of opts for modification
232
- var _opts = JSON.parse(JSON.stringify(opts));
214
+ // validate local forwards for correct format and valid ports
215
+ validate_local_forward(proxy_ports, whitelist) {
216
+ const local_ports = [];
217
+ const remote_ports = [];
218
+ proxy_ports.forEach(proxy_port => {
219
+ const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
220
+ if (!this.validate_port_number(local_port, whitelist)) local_ports.push(local_port);
221
+ if (!this.validate_port_number(remote_port, whitelist)) remote_ports.push(remote_port);
222
+ });
223
+ if (local_ports.length || remote_ports.length) {
224
+ const err = new Error('Invalid local forward');
233
225
 
234
- // setup whitelist from param or opts in case of error retry
235
- _opts.whitelist = (whitelist !== undefined) ? whitelist : (_opts.whitelist) ? _opts.whitelist : {};
226
+ en && this.debug('invalid ports found:\n', JSON.stringify(proxy_ports, null, 2),
227
+ '\n', JSON.stringify(whitelist, null, 2));
236
228
 
237
- // validate local port forwards, emit error and quit if invalid
238
- try {
239
- validate_local_forward(_opts.proxy_ports, _opts.whitelist);
240
- } catch (err) {
241
- sshEmitter.emit('error', err);
242
- if (debug) console.log(err);
243
- return err;
229
+ err.info = {
230
+ local_ports: local_ports.join(','),
231
+ remote_ports: remote_ports.join(',')
232
+ };
233
+ throw (err);
234
+ }
235
+ return true;
244
236
  }
245
237
 
246
- // if password is empty set to undefined
247
- if (opts.password && !opts.password.length) {
248
- _opts.password = undefined;
238
+ // on network error, attempt to re-establish connection until 10 retries
239
+ ssh_retry_connect = (opts) => {
240
+ if (this.retries++ < 10) setTimeout(this.connect_ssh, 5000, opts);
241
+ }
242
+
243
+ // attempt to establish ssh tunnel to server with supplied parameters.
244
+ ssh_start_tunnel = async (opts) => {
245
+ const _this = this;
246
+ return new Promise(async (resolve, reject) => {
247
+
248
+ // after successful tunnel setup complete, send events and reset retry counter
249
+ await _this.do_ssh_connect(opts).then(() => {
250
+ const hostport = opts.host + ':' + opts.port;
251
+ if (en) {
252
+ const msg = 'SSH connection to ' + opts.server_name + ' established at ' + hostport;
253
+ _this.debug(msg);
254
+ }
255
+ _this.emit('status', '', 'ready', hostport);
256
+ _this.retries = 0;
257
+ resolve();
258
+ }, (err) => {
259
+ en && _this.debug('ssh_retry error:', err);
260
+ reject();
261
+ });
262
+ });
249
263
  }
250
264
 
251
- // retrieve private key from system keychain with supplied service name and server
252
- // if no key found, authentication is supplied username, password
253
- _opts.private_key = await keypair_storage.retrieve_private_key(opts.service_name, opts.server_name);
265
+ // setup ssh connection parameters and attempt to establish ssh tunnel
266
+ // todo: if opts have changed while service is running, shutdown current service and restart with new opts
267
+ async connectSSH(opts, whitelist) {
254
268
 
255
- // if ngrok api key provided, obtain hostname and hostport from ngrok api
256
- // if no ngrok tunnel specified, use supplied host and port
257
- if (_opts.ngrok_api) {
258
- var hostport = await get_hostport(_opts.ngrok_api)
269
+ en && this.debug('connect_ssh:\n', JSON.stringify(opts, null, 2));
270
+
271
+ // make deep copy of opts for modification
272
+ var _opts = JSON.parse(JSON.stringify(opts));
273
+
274
+ // setup whitelist from param or opts in case of error retry
275
+ _opts.whitelist = (whitelist !== undefined) ? whitelist : (_opts.whitelist) ? _opts.whitelist : {};
276
+
277
+ // validate local port forwards, emit error and quit if invalid
278
+ try {
279
+ this.validate_local_forward(_opts.proxy_ports, _opts.whitelist);
280
+ } catch (err) {
281
+ this.emit('error', err);
282
+ en && this.debug(err);
283
+ return err;
284
+ }
285
+
286
+ // if password is empty set to undefined
287
+ if (opts.password && !opts.password.length) {
288
+ _opts.password = undefined;
289
+ }
290
+
291
+ // retrieve private key from system keychain with supplied service name and server
292
+ // if no key found, authentication is supplied username, password
293
+ _opts.private_key = await keypairStorage.get_keypair(opts.service_name, opts.server_name);
294
+
295
+ // if ngrok api key provided, obtain hostname and hostport from ngrok api
296
+ // if no ngrok tunnel specified, use supplied host and port
297
+ if (_opts.ngrok_api) {
298
+ const _this = this;
299
+ const ngrokApi = new NgrokApi(_opts.ngrok_api);
300
+ var hostport = await ngrokApi.get_hostport()
301
+ .catch((err) => {
302
+ en && _this.debug('ngrok get_hostport connection error');
303
+ if (this.listener[opts.server_name]) this.listener[opts.server_name].isConnected = false;
304
+ _this.ssh_retry_connect(_opts);
305
+ });
306
+ if (hostport && hostport.host) {
307
+ _opts.host = hostport.host;
308
+ _opts.port = hostport.port;
309
+ }
310
+ else return null;
311
+ }
312
+
313
+ // initiate ssh tunnel, block until tunnel is established or error
314
+ return await this.ssh_start_tunnel(_opts)
259
315
  .catch((err) => {
260
- if (debug) console.log('ngrok get_hostport connection error');
261
- ssh_retry_connect(opts);
316
+ en && this.debug('ssh_start_tunnel catch:' + err);
262
317
  });
263
- if (hostport && hostport.host) {
264
- _opts.host = hostport.host;
265
- _opts.port = hostport.port;
266
- }
267
- else return null;
268
318
  }
269
319
 
270
- // initiate ssh tunnel, block until tunnel is established or error
271
- return await ssh_start_tunnel(_opts);
272
- }
320
+ // resume connection, if previously online
321
+ onNetworkOnline() { }
273
322
 
274
- // export function to obtain ssh connection event emitter
275
- const get_event_hook = () => { return sshEmitter; }
323
+ // connection down, shutdown tunnel
324
+ onNetworkOffline() { }
325
+
326
+ generateAndStoreKeypair(...args) {
327
+ keypairStorage.generate_and_store_keypair(...args);
328
+ }
329
+
330
+ getPublicKey(...args) {
331
+ keypairStorage.get_public_key_from_keychain(...args);
332
+ }
333
+
334
+ // ssh client debug function for verbose ssh connection details
335
+ debug_client = (debug_ssh) ? (msg) => { console.log(msg); } : null;
336
+
337
+ debug = (...args) => { console.log(...args); }
276
338
 
277
- module.exports = {
278
- connect_ssh: connect_ssh,
279
- generate_and_store_keypair: keypair_storage.generate_and_store_keypair,
280
- generate_keypair: keypair_storage.generate_keypair,
281
- get_public_key: keypair_storage.get_public_key_from_keychain,
282
- remove_keypair: keypair_storage.remove_keypair,
283
- get_event_hook: get_event_hook
284
339
  }
340
+
341
+ module.exports = sshTunnelProxy;
@@ -14,56 +14,76 @@ License: MIT
14
14
  const sshpk = require('sshpk');
15
15
  const keytar = require('keytar');
16
16
 
17
- // export function to generate keypair and store under service name and account
18
- const generate_and_store_keypair = (service_name, account) => {
19
- const keypair = generate_keypair();
20
- return new Promise((resolve, reject) => {
21
- keytar.setPassword(service_name, account, keypair.private_key).then((result)=>{
22
- resolve(keypair.public_key);
23
- }, (err) => {
24
- reject(err);
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((result) => {
27
+ resolve(keypair.public_key);
28
+ }, (err) => {
29
+ reject(err);
30
+ });
25
31
  });
26
- });
27
- }
32
+ }
28
33
 
29
- // export function to obtain public key from system's keychain stored under service_name and account
30
- const get_public_key_from_keychain = (service_name, account) => {
31
- return new Promise((resolve, reject) => {
32
- keytar.getPassword(service_name, account).then((private_key) => {
33
- var public_key = get_public_key_from_private(private_key);
34
- resolve(public_key);
35
- }, (err) => {
36
- reject(err);
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
+ });
37
43
  });
38
- });
39
- }
44
+ }
40
45
 
41
- const get_public_key_from_private = (private_key) => {
42
- if (private_key) {
43
- var key = sshpk.parsePrivateKey(private_key, 'pem');
44
- if (key) return key.toPublic().toString('ssh');
45
- else return null;
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;
46
53
  }
47
- return null;
48
- }
49
54
 
50
- // generate EdDSA keypair
51
- const generate_keypair = () => {
52
- // generate private key then obtain public key from private
53
- // note: EdDSA is required for nodejs ssh
54
- const privateKey = sshpk.generatePrivateKey('ed25519').toString('ssh');
55
- const publicKey = get_public_key_from_private(privateKey);
56
- return {
57
- public_key: publicKey,
58
- private_key: privateKey
59
- };
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
+ }
60
75
  }
61
-
76
+ module.exports = KeypairStorage;
77
+ /*
62
78
  module.exports = {
63
79
  store_private_key: keytar.setPassword,
64
80
  retrieve_private_key: keytar.getPassword,
65
81
  remove_keypair: keytar.deletePassword,
66
82
  generate_keypair: generate_keypair,
67
83
  generate_and_store_keypair: generate_and_store_keypair,
68
- get_public_key_from_keychain: get_public_key_from_keychain
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
69
88
  }
89
+ */
@@ -11,26 +11,37 @@ License: MIT
11
11
  // get endpoint hostname and hostport from ngrok api
12
12
  const { Ngrok } = require('@ngrok/ngrok-api');
13
13
 
14
- const get_ngrok_hostport = async function (ngrok_api) {
15
- const ngrok = new Ngrok({ apiToken: ngrok_api });
16
- var endpoints = await ngrok.endpoints.list();
17
- return parse_ngrok_hostport(endpoints);
18
- }
14
+ class NgrokApi {
15
+
16
+ constructor(apiToken) {
17
+ this.ngrok = new Ngrok({ apiToken: apiToken });
18
+ }
19
19
 
20
- // retrieve hostport from api response
21
- const parse_ngrok_hostport = function (endpoints) {
22
- if (endpoints[0] && endpoints[0].hostport) {
23
- const hostport = endpoints[0].hostport.split(':');
24
- var hostport_obj = {
25
- host:hostport[0],
26
- port:hostport[1]
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'));
27
44
  }
28
- } else {
29
- throw (new Error('get_ngrok_hostport: no endpoints found'));
30
45
  }
31
- return hostport_obj;
32
46
  }
33
-
34
- module.exports = {
35
- get_hostport: get_ngrok_hostport
36
- }
47
+ module.exports = NgrokApi;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssh_tunnel_proxy",
3
- "version": "1.0.0",
3
+ "version": "1.1.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
@@ -4,26 +4,23 @@ add generate and store keys test
4
4
  add ngrok api test with stored api key in ssh_tunnel_proxy config
5
5
  to run ssh tunnel api tests edit config.json and provide api keys for each service
6
6
  */
7
+
7
8
  const assert = require('assert');
8
- const rewire = require('rewire');
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
- const os = require('os');
12
11
  const sinon = require('sinon');
13
12
 
14
- const { connect_ssh, get_event_hook } = require('../lib/index.js');
15
- const { store_private_key, retrieve_private_key, generate_and_store_keypair, get_public_key_from_keychain, remove_keypair, generate_keypair } = require('../lib/keypair_storage');
16
- const { get_hostport } = require('../lib/ngrok_service');
17
-
18
- var main = rewire('../lib/index.js');
19
- var ngrok_service = rewire('../lib/ngrok_service.js');
20
- var keypair_storage = rewire('../lib/keypair_storage.js');
13
+ const SSHTunnelProxy = require('../lib/index.js');
14
+ const KeypairStorage = require('../lib/keypair_storage.js');
15
+ const NgrokApi = require('../lib/ngrok_service');
21
16
 
22
17
  // get config file containing api keys
23
- var homedir = require('os').homedir();
24
- var opts = JSON.parse(fs.readFileSync(path.join(homedir, '.config/ssh_tunnel_proxy/config.json')), 'utf8');
18
+ const homedir = require('os').homedir();
19
+ const opts = JSON.parse(fs.readFileSync(homedir + '/.config/ssh_tunnel_proxy/config.json'), 'utf8');
25
20
 
26
- var myEmitter = get_event_hook();
21
+ const sshTunnelProxy = new SSHTunnelProxy();
22
+ const keypairStorage = new KeypairStorage();
23
+ const ngrokApi = new NgrokApi(opts.ngrok_api);
27
24
 
28
25
  const whitelist = {
29
26
  80: true,
@@ -33,30 +30,30 @@ const whitelist = {
33
30
 
34
31
  // port value validation tests
35
32
  describe('Validate ports', function () {
36
- const validate_port_number = main.__get__('validate_port_number');
33
+ //const validate_port_number = sshTunnelProxy.__get__('validate_port_number');
37
34
  it('valid port number 1024', function () {
38
- assert(validate_port_number(1024, whitelist), 'should be valid');
35
+ assert(sshTunnelProxy.validate_port_number(1024, whitelist), 'should be valid');
39
36
  });
40
37
  it('valid port number 80', function () {
41
- assert(validate_port_number(80, whitelist), 'should be valid');
38
+ assert(sshTunnelProxy.validate_port_number(80, whitelist), 'should be valid');
42
39
  });
43
40
  it('invalid port number -1', function () {
44
- assert.equal(false, validate_port_number(-1, whitelist), 'should be invalid');
41
+ assert.equal(false, sshTunnelProxy.validate_port_number(-1, whitelist), 'should be invalid');
45
42
  });
46
43
  it('invalid port number 65536', function () {
47
- assert.equal(false, validate_port_number(65536, whitelist), 'should be invalid');
44
+ assert.equal(false, sshTunnelProxy.validate_port_number(65536, whitelist), 'should be invalid');
48
45
  });
49
46
  it('invalid system port number 137', function () {
50
- assert.equal(false, validate_port_number(137, whitelist), 'should be invalid');
47
+ assert.equal(false, sshTunnelProxy.validate_port_number(137, whitelist), 'should be invalid');
51
48
  });
52
49
  it('port number nan', function () {
53
- assert.equal(false, validate_port_number('nan', whitelist), 'should be invalid');
50
+ assert.equal(false, sshTunnelProxy.validate_port_number('nan', whitelist), 'should be invalid');
54
51
  });
55
52
  });
56
53
 
57
54
  // local forward format and value validation test
58
55
  describe('Validate local forwards', function () {
59
- const validate_local_forward = main.__get__('validate_local_forward');
56
+ //const validate_local_forward = sshTunnelProxy.__get__('validate_local_forward');
60
57
  var local_forward = [
61
58
  '8080:127.0.0.1:80',
62
59
  '9000:192.168.43.5:9000',
@@ -66,10 +63,10 @@ describe('Validate local forwards', function () {
66
63
  '-1:127.0.0.1:65537',
67
64
  ];
68
65
  it('valid local forward to 80 ' + local_forward[0], function () {
69
- assert(() => { return validate_local_forward([local_forward[0]], whitelist) }, 'should be valid');
66
+ assert(() => { return sshTunnelProxy.validate_local_forward([local_forward[0]], whitelist) }, 'should be valid');
70
67
  });
71
68
  it('valid local forward to 9000 ' + local_forward[1], function () {
72
- assert(() => { return validate_local_forward([local_forward[1]], whitelist) }, 'should be valid');
69
+ assert(() => { return sshTunnelProxy.validate_local_forward([local_forward[1]], whitelist) }, 'should be valid');
73
70
  });
74
71
  it('invalid local forward format ' + local_forward[2], function () {
75
72
  const err = {
@@ -79,7 +76,7 @@ describe('Validate local forwards', function () {
79
76
  remote_ports: '',
80
77
  }
81
78
  };
82
- assert.throws(() => { validate_local_forward([local_forward[2]], whitelist) }, err);
79
+ assert.throws(() => { sshTunnelProxy.validate_local_forward([local_forward[2]], whitelist) }, err);
83
80
  });
84
81
  it('invalid local forward format ' + local_forward[3], function () {
85
82
  const err = {
@@ -89,7 +86,7 @@ describe('Validate local forwards', function () {
89
86
  remote_ports: '',
90
87
  }
91
88
  };
92
- assert.throws(() => { validate_local_forward([local_forward[3]], whitelist) }, err, 'should be invalid');
89
+ assert.throws(() => { sshTunnelProxy.validate_local_forward([local_forward[3]], whitelist) }, err, 'should be invalid');
93
90
  });
94
91
  it('invalid local forward system port ' + local_forward[4], function () {
95
92
  const err = {
@@ -99,7 +96,7 @@ describe('Validate local forwards', function () {
99
96
  remote_ports: '137',
100
97
  }
101
98
  };
102
- assert.throws(() => { validate_local_forward([local_forward[4]], whitelist) }, err, 'should be invalid');
99
+ assert.throws(() => { sshTunnelProxy.validate_local_forward([local_forward[4]], whitelist) }, err, 'should be invalid');
103
100
  });
104
101
  it('invalid local forward port range ' + local_forward[5], function () {
105
102
  const err = {
@@ -109,7 +106,7 @@ describe('Validate local forwards', function () {
109
106
  remote_ports: '65537',
110
107
  }
111
108
  };
112
- assert.throws(() => { validate_local_forward([local_forward[5]], whitelist) }, err, 'should be invalid');
109
+ assert.throws(() => { sshTunnelProxy.validate_local_forward([local_forward[5]], whitelist) }, err, 'should be invalid');
113
110
  });
114
111
  });
115
112
 
@@ -117,7 +114,7 @@ describe('Validate local forwards', function () {
117
114
  describe('Store and retrieve private key in system keychain', function () {
118
115
  const service_name = 'ssh_proxy_tunnel';
119
116
  const account = 'test';
120
- const keypair = generate_keypair();
117
+ const keypair = keypairStorage.generate_keypair();
121
118
  it('generated key should contain a private key', function () {
122
119
  assert(keypair.private_key, 'should not be empty');
123
120
  });
@@ -125,32 +122,32 @@ describe('Store and retrieve private key in system keychain', function () {
125
122
  assert(keypair.public_key, 'should not be empty');
126
123
  });
127
124
  it('should store generated private key', () => {
128
- return store_private_key(service_name, account, keypair.private_key).then(result => {
125
+ return keypairStorage.set_keypair(service_name, account, keypair.private_key).then(result => {
129
126
  assert.equal(result, undefined, 'should not return error');
130
127
  })
131
128
  });
132
129
  it('stored private key should match generated key', () => {
133
- return retrieve_private_key(service_name, account).then(stored_key => {
130
+ return keypairStorage.get_keypair(service_name, account).then(stored_key => {
134
131
  assert.equal(stored_key, keypair.private_key, 'should match');
135
132
  });
136
133
  });
137
134
  it('stored public key should match generated key', () => {
138
- return get_public_key_from_keychain(service_name, account).then(stored_key => {
135
+ return keypairStorage.get_public_key_from_keychain(service_name, account).then(stored_key => {
139
136
  assert.equal(stored_key, keypair.public_key, 'should match');
140
137
  });
141
138
  });
142
139
  it('remove key should return true', () => {
143
- return remove_keypair(service_name, account).then(result => {
140
+ return keypairStorage.delete_keypair(service_name, account).then(result => {
144
141
  assert('should return true');
145
142
  });
146
143
  });
147
144
  it('retrieve stored private key after delete should return null', () => {
148
- return retrieve_private_key(service_name, account).then((result) => {
145
+ return keypairStorage.get_keypair(service_name, account).then((result) => {
149
146
  assert.equal(result, null, 'should return null');
150
147
  });
151
148
  });
152
149
  it('retrieve stored public key after delete should return null', () => {
153
- return get_public_key_from_keychain(service_name, account).then((result) => {
150
+ return keypairStorage.get_public_key_from_keychain(service_name, account).then((result) => {
154
151
  assert.equal(result, null, 'should return null');
155
152
  });
156
153
  });
@@ -166,26 +163,25 @@ describe('', function () {
166
163
  */
167
164
 
168
165
  describe('Get ngrok hostport', function () {
169
- const parse_ngrok_hostport = ngrok_service.__get__('parse_ngrok_hostport');
166
+ //const parse_ngrok_hostport = ngrok_service.__get__('parse_ngrok_hostport');
170
167
  var test_endpoint = [{
171
168
  hostport: '8.tcp.ngrok.io:17632'
172
169
  }];
173
- var result_opts = parse_ngrok_hostport(test_endpoint, opts);
170
+ var result_opts = ngrokApi.parse_ngrok_hostport(test_endpoint, opts);
174
171
  it('host should match 8.tcp.ngrok.io', function () {
175
172
  assert.equal(result_opts.host, '8.tcp.ngrok.io');
176
173
  });
177
174
  it('port should match 17632', function () {
178
175
  assert.equal(result_opts.port, '17632');
179
176
  });
180
- it('host, port should be obtained from api', ()=> {
181
- get_hostport(opts).then((result) => {
182
- assert(result.host,'host should exist');
183
- assert(result.port,'port should exist');
184
- }, (err)=>{
185
- assert.equal(err,null);
186
- });
187
-
177
+ it('host, port should be obtained from api', async () => {
178
+ result = await ngrokApi.get_hostport(opts);
179
+ assert(result.host, 'host should exist');
180
+ assert(result.port, 'port should exist');
181
+ }, (err) => {
182
+ assert.equal(err, null);
188
183
  });
184
+
189
185
  });
190
186
 
191
187
  /*
@@ -226,7 +222,7 @@ var test_generate_keypair = function () {
226
222
  console.log('keypair:');
227
223
  console.log(keypair.public_key);
228
224
  console.log(keypair.private_key);
229
- var homedir = os.homedir();
225
+ var homedir = homedir();
230
226
  var fname = path.join(homedir, '.ssh', 'zm_id_rsa.pub')
231
227
  //fs.writeFileSync(fname, keypair.public_key);
232
228
  fname = path.join(homedir, '.ssh', 'zm_id_rsa')