ssh_tunnel_proxy 1.2.2 → 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 +15 -0
- package/lib/index.js +46 -53
- package/lib/keypair_storage.js +1 -1
- package/main.js +2 -2
- package/package.json +2 -2
- package/test/test.js +4 -19
package/.eslintrc.json
ADDED
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
_this.debug_en &&_this.debug('SSH Client :: error :: ' + err);
|
|
182
|
-
_this.emit('
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
251
|
+
async ssh_start_tunnel(opts) {
|
|
254
252
|
const _this = this;
|
|
255
|
-
|
|
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('
|
|
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((
|
|
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:
|
|
352
|
-
KeypairStorage:KeypairStorage,
|
|
353
|
-
NgrokApi:NgrokApi
|
|
344
|
+
SSHTunnelProxy: SSHTunnelProxy,
|
|
345
|
+
KeypairStorage: KeypairStorage,
|
|
346
|
+
NgrokApi: NgrokApi
|
|
354
347
|
}
|
package/lib/keypair_storage.js
CHANGED
|
@@ -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((
|
|
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.
|
|
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
|
-
}
|