ssh_tunnel_proxy 1.2.2 → 1.2.4
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 +50 -54
- 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 2:', 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,11 +117,13 @@ 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
|
|
127
124
|
try {
|
|
125
|
+
var status_msg = 'SSH Server :: before listen on ' + local_port;
|
|
126
|
+
_this.emit('debug', status_msg);
|
|
128
127
|
_this.listener[server_name][proxy_port].listen(local_port, () => {
|
|
129
128
|
|
|
130
129
|
// emit server listening on port message
|
|
@@ -133,15 +132,15 @@ class sshTunnelProxy extends EventEmitter {
|
|
|
133
132
|
|
|
134
133
|
// if all listeners have been successfully established, resolve setup connection
|
|
135
134
|
if (listeners++ >= proxy_ports.length - 1) {
|
|
136
|
-
_this.emit('
|
|
135
|
+
_this.emit('ssh_tunnel_ready', {});
|
|
137
136
|
resolve();
|
|
138
137
|
}
|
|
139
138
|
});
|
|
140
139
|
} catch (err) {
|
|
141
|
-
_this.debug_en &&_this.debug('listen err:', err);
|
|
140
|
+
_this.debug_en && _this.debug('listen err:', err);
|
|
142
141
|
}
|
|
143
142
|
});
|
|
144
|
-
}
|
|
143
|
+
}
|
|
145
144
|
|
|
146
145
|
// handle setting up ssh client and proxy forward ports
|
|
147
146
|
do_ssh_connect(opts) {
|
|
@@ -149,7 +148,7 @@ class sshTunnelProxy extends EventEmitter {
|
|
|
149
148
|
const _this = this;
|
|
150
149
|
|
|
151
150
|
// create a new ssh client connection with supplied credentials and hostname/port
|
|
152
|
-
return new Promise((resolve
|
|
151
|
+
return new Promise((resolve) => {
|
|
153
152
|
|
|
154
153
|
// exit if connection already established, avoid redundant connection on retry
|
|
155
154
|
if (_this.listener[opts.server_name] && _this.listener[opts.server_name].isConnected) resolve();
|
|
@@ -161,46 +160,46 @@ class sshTunnelProxy extends EventEmitter {
|
|
|
161
160
|
_this.listener[opts.server_name] = {};
|
|
162
161
|
}
|
|
163
162
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
ssh_client.on('ready', () => {
|
|
167
|
-
_this.on_ssh_client_ready(ssh_client, opts.server_name, opts.proxy_ports, resolve);
|
|
163
|
+
_this.on('ready', () => {
|
|
164
|
+
_this.on_ssh_client_ready(opts.server_name, opts.proxy_ports, resolve);
|
|
168
165
|
});
|
|
169
166
|
|
|
170
|
-
|
|
171
|
-
_this.debug_en &&_this.debug('SSH Client :: end');
|
|
167
|
+
_this.on('end', () => {
|
|
168
|
+
_this.debug_en && _this.debug('SSH Client :: end');
|
|
172
169
|
_this.close_sockets(opts.server_name, opts.proxy_ports);
|
|
173
170
|
});
|
|
174
171
|
|
|
175
|
-
|
|
176
|
-
_this.debug_en &&_this.debug('SSH Client :: close');
|
|
172
|
+
_this.on('close', () => {
|
|
173
|
+
_this.debug_en && _this.debug('SSH Client :: close');
|
|
177
174
|
_this.close_sockets(opts.server_name, opts.proxy_ports);
|
|
178
175
|
});
|
|
179
176
|
|
|
180
|
-
|
|
181
|
-
_this.debug_en &&_this.debug('SSH Client :: error :: ' + err);
|
|
182
|
-
_this.emit('
|
|
177
|
+
_this.on('error', err => {
|
|
178
|
+
_this.debug_en && _this.debug('SSH Client :: error :: ' + err);
|
|
179
|
+
_this.emit('debug', err);
|
|
183
180
|
if (_this.listener[opts.server_name]) _this.listener[opts.server_name].isConnected = false;
|
|
184
181
|
_this.ssh_retry_connect(opts);
|
|
185
182
|
resolve();
|
|
186
183
|
});
|
|
187
184
|
|
|
188
|
-
|
|
189
|
-
_this.debug_en &&_this.debug('SSH Client :: handshake:', JSON.stringify(negotiated));
|
|
185
|
+
_this.on('handshake', negotiated => {
|
|
186
|
+
_this.debug_en && _this.debug('SSH Client :: handshake:', JSON.stringify(negotiated));
|
|
190
187
|
});
|
|
191
188
|
|
|
192
|
-
|
|
193
|
-
_this.debug_en &&_this.debug('SSH Client :: greeting:', message);
|
|
189
|
+
_this.on('greeting', (message) => {
|
|
190
|
+
_this.debug_en && _this.debug('SSH Client :: greeting:', message);
|
|
194
191
|
});
|
|
195
192
|
|
|
196
193
|
// initiate ssh client connection to remote host
|
|
197
|
-
|
|
194
|
+
// ssh client debug function for verbose ssh connection details
|
|
195
|
+
const debug_client = (this.debug_ssh) ? (...args) => { console.log(...args); } : null;
|
|
196
|
+
_this.connect({
|
|
198
197
|
host: opts.host,
|
|
199
198
|
port: opts.port,
|
|
200
199
|
username: opts.username,
|
|
201
200
|
password: opts.password,
|
|
202
201
|
privateKey: opts.private_key,
|
|
203
|
-
debug:
|
|
202
|
+
debug: debug_client,
|
|
204
203
|
keepaliveInterval: 10000
|
|
205
204
|
});
|
|
206
205
|
|
|
@@ -222,13 +221,14 @@ class sshTunnelProxy extends EventEmitter {
|
|
|
222
221
|
const remote_ports = [];
|
|
223
222
|
proxy_ports.forEach(proxy_port => {
|
|
224
223
|
const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
|
|
224
|
+
if (remote_hostname.length < 1) remote_ports.push(remote_port);
|
|
225
225
|
if (!this.validate_port_number(local_port, whitelist)) local_ports.push(local_port);
|
|
226
226
|
if (!this.validate_port_number(remote_port, whitelist)) remote_ports.push(remote_port);
|
|
227
227
|
});
|
|
228
228
|
if (local_ports.length || remote_ports.length) {
|
|
229
229
|
const err = new Error('Invalid local forward');
|
|
230
230
|
|
|
231
|
-
this.debug_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),
|
|
232
232
|
'\n', JSON.stringify(whitelist, null, 2));
|
|
233
233
|
|
|
234
234
|
err.info = {
|
|
@@ -241,7 +241,8 @@ class sshTunnelProxy extends EventEmitter {
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
// on network error, attempt to re-establish connection until 10 retries
|
|
244
|
-
|
|
244
|
+
//clearTimeout(this._readyTimeout);
|
|
245
|
+
ssh_retry_connect(opts) {
|
|
245
246
|
const _this = this;
|
|
246
247
|
const invoke = () => {
|
|
247
248
|
_this.connectSSH(opts);
|
|
@@ -250,9 +251,9 @@ class sshTunnelProxy extends EventEmitter {
|
|
|
250
251
|
}
|
|
251
252
|
|
|
252
253
|
// attempt to establish ssh tunnel to server with supplied parameters.
|
|
253
|
-
|
|
254
|
+
async ssh_start_tunnel(opts) {
|
|
254
255
|
const _this = this;
|
|
255
|
-
|
|
256
|
+
async function do_ssh_connect(resolve) {
|
|
256
257
|
|
|
257
258
|
// after successful tunnel setup complete, send events and reset retry counter
|
|
258
259
|
await _this.do_ssh_connect(opts).then(() => {
|
|
@@ -264,18 +265,16 @@ class sshTunnelProxy extends EventEmitter {
|
|
|
264
265
|
_this.emit('status', '', 'ready', hostport);
|
|
265
266
|
_this.retries = 0;
|
|
266
267
|
resolve();
|
|
267
|
-
}, (err) => {
|
|
268
|
-
_this.debug_en &&_this.debug('ssh_retry error:', err);
|
|
269
|
-
reject();
|
|
270
268
|
});
|
|
271
|
-
}
|
|
269
|
+
}
|
|
270
|
+
return new Promise(do_ssh_connect);
|
|
272
271
|
}
|
|
273
272
|
|
|
274
273
|
// setup ssh connection parameters and attempt to establish ssh tunnel
|
|
275
274
|
// todo: if opts have changed while service is running, shutdown current service and restart with new opts
|
|
276
275
|
async connectSSH(opts, whitelist) {
|
|
277
276
|
|
|
278
|
-
this.debug_en &&this.debug('connectSSH:\n', JSON.stringify(opts, null, 2));
|
|
277
|
+
this.debug_en && this.debug('connectSSH:\n', JSON.stringify(opts, null, 2));
|
|
279
278
|
|
|
280
279
|
// make deep copy of opts for modification
|
|
281
280
|
var _opts = JSON.parse(JSON.stringify(opts));
|
|
@@ -287,8 +286,8 @@ class sshTunnelProxy extends EventEmitter {
|
|
|
287
286
|
try {
|
|
288
287
|
this.validate_local_forward(_opts.proxy_ports, _opts.whitelist);
|
|
289
288
|
} catch (err) {
|
|
290
|
-
this.emit('
|
|
291
|
-
this.debug_en &&this.debug(err);
|
|
289
|
+
this.emit('debug', err);
|
|
290
|
+
this.debug_en && this.debug(err);
|
|
292
291
|
return err;
|
|
293
292
|
}
|
|
294
293
|
|
|
@@ -307,8 +306,8 @@ class sshTunnelProxy extends EventEmitter {
|
|
|
307
306
|
const _this = this;
|
|
308
307
|
const ngrokApi = new NgrokApi(_opts.ngrok_api);
|
|
309
308
|
var hostport = await ngrokApi.get_hostport()
|
|
310
|
-
.catch((
|
|
311
|
-
_this.debug_en &&_this.debug('ngrok get_hostport connection error');
|
|
309
|
+
.catch(() => {
|
|
310
|
+
_this.debug_en && _this.debug('ngrok get_hostport connection error');
|
|
312
311
|
if (this.listener[opts.server_name]) this.listener[opts.server_name].isConnected = false;
|
|
313
312
|
_this.ssh_retry_connect(_opts);
|
|
314
313
|
});
|
|
@@ -322,7 +321,7 @@ class sshTunnelProxy extends EventEmitter {
|
|
|
322
321
|
// initiate ssh tunnel, block until tunnel is established or error
|
|
323
322
|
return await this.ssh_start_tunnel(_opts)
|
|
324
323
|
.catch((err) => {
|
|
325
|
-
this.debug_en &&this.debug('ssh_start_tunnel catch:' + err);
|
|
324
|
+
this.debug_en && this.debug('ssh_start_tunnel catch:' + err);
|
|
326
325
|
});
|
|
327
326
|
}
|
|
328
327
|
|
|
@@ -340,15 +339,12 @@ class sshTunnelProxy extends EventEmitter {
|
|
|
340
339
|
keypairStorage.get_public_key_from_keychain(...args);
|
|
341
340
|
}
|
|
342
341
|
|
|
343
|
-
// ssh client debug function for verbose ssh connection details
|
|
344
|
-
debug_client = (this.debug_ssh) ? (msg) => { console.log(msg); } : null;
|
|
345
|
-
|
|
346
342
|
debug(...args) { console.log(...args); }
|
|
347
343
|
|
|
348
344
|
}
|
|
349
345
|
|
|
350
346
|
module.exports = {
|
|
351
|
-
SSHTunnelProxy:
|
|
352
|
-
KeypairStorage:KeypairStorage,
|
|
353
|
-
NgrokApi:NgrokApi
|
|
347
|
+
SSHTunnelProxy: SSHTunnelProxy,
|
|
348
|
+
KeypairStorage: KeypairStorage,
|
|
349
|
+
NgrokApi: NgrokApi
|
|
354
350
|
}
|
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.4",
|
|
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
|
-
}
|