ssh_tunnel_proxy 1.2.11 → 2.0.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/README.md +86 -5
- package/dist/cjs/index.d.ts +65 -0
- package/dist/cjs/index.js +486 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/keypair_storage.d.ts +12 -0
- package/dist/cjs/keypair_storage.js +120 -0
- package/dist/cjs/keypair_storage.js.map +1 -0
- package/dist/cjs/ngrok_service.d.ts +11 -0
- package/dist/cjs/ngrok_service.js +40 -0
- package/dist/cjs/ngrok_service.js.map +1 -0
- package/dist/cjs/ssh2-node.d.ts +1 -0
- package/dist/cjs/ssh2-node.js +188 -0
- package/dist/cjs/ssh2-node.js.map +1 -0
- package/dist/esm/index.d.ts +65 -0
- package/dist/esm/index.js +481 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/keypair_storage.d.ts +12 -0
- package/dist/esm/keypair_storage.js +117 -0
- package/dist/esm/keypair_storage.js.map +1 -0
- package/dist/esm/ngrok_service.d.ts +11 -0
- package/dist/esm/ngrok_service.js +37 -0
- package/dist/esm/ngrok_service.js.map +1 -0
- package/dist/esm/ssh2-node.d.ts +1 -0
- package/dist/esm/ssh2-node.js +184 -0
- package/dist/esm/ssh2-node.js.map +1 -0
- package/dist/types/index.d.ts +65 -0
- package/dist/types/keypair_storage.d.ts +12 -0
- package/dist/types/ngrok_service.d.ts +11 -0
- package/dist/types/ssh2-node.d.ts +1 -0
- package/package.json +39 -9
- package/ssh2-node +2 -181
- package/.eslintrc.json +0 -15
- package/.vscode/launch.json +0 -34
- package/lib/index.js +0 -489
- package/lib/keypair_storage.js +0 -76
- package/lib/ngrok_service.js +0 -47
- package/test/test.js +0 -214
- package/test/test_remote_exec.js +0 -217
package/lib/index.js
DELETED
|
@@ -1,489 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
|
|
3
|
-
ssh_tunnel_proxy - initiate a ssh reverse tunnel proxy with forwarding ports
|
|
4
|
-
with connection optional ssh tunnel service ngrok
|
|
5
|
-
|
|
6
|
-
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.
|
|
7
|
-
If a ngrok api key is provided the ngrok api endpoint method is invoked to obtain the hostport of
|
|
8
|
-
the tunnel. A list of port forwards is provided to the connect_api function and ports validated to
|
|
9
|
-
restrict connections to system ports on the remote host to a set of pre-defined ports such as http,https.
|
|
10
|
-
After establishing a ssh connection to the remote server, local proxy port forwards are opened. If
|
|
11
|
-
the connection is interrupted then the ssh_connect method will attempt to re-establish the connection.
|
|
12
|
-
|
|
13
|
-
Author: Autonomous
|
|
14
|
-
First release: 1-29-2023
|
|
15
|
-
License: MIT
|
|
16
|
-
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
const net = require('net');
|
|
20
|
-
const fs = require('fs');
|
|
21
|
-
const process = require('process');
|
|
22
|
-
const { Buffer } = require('node:buffer');
|
|
23
|
-
const { Client } = require('electron-ssh2');
|
|
24
|
-
|
|
25
|
-
const KeypairStorage = require('./keypair_storage');
|
|
26
|
-
const NgrokApi = require('./ngrok_service');
|
|
27
|
-
|
|
28
|
-
const keypairStorage = new KeypairStorage();
|
|
29
|
-
|
|
30
|
-
const homedir = require('os').homedir();
|
|
31
|
-
|
|
32
|
-
class SSHTunnelProxy extends Client {
|
|
33
|
-
|
|
34
|
-
constructor() {
|
|
35
|
-
super();
|
|
36
|
-
this.listener = {};
|
|
37
|
-
this.retries = 0;
|
|
38
|
-
this.debug_en = false;
|
|
39
|
-
this.debug_ssh = false;
|
|
40
|
-
this._tunnelReadyTimeout = undefined;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// function to close all active sockets for tunnel restart and exception handling
|
|
44
|
-
close_sockets(proxy_ports) {
|
|
45
|
-
|
|
46
|
-
if (!proxy_ports) return;
|
|
47
|
-
|
|
48
|
-
proxy_ports.forEach(proxy_port => {
|
|
49
|
-
const server = this.listener[proxy_port];
|
|
50
|
-
if (server && server.listening) {
|
|
51
|
-
this.debug_en && this.debug('SSH Server :: closing forward:', proxy_port)
|
|
52
|
-
server.close();
|
|
53
|
-
delete this.listener[proxy_port];
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// create forward out on socket connection
|
|
59
|
-
setup_ssh_forward(socket, remote_hostname, remote_port) {
|
|
60
|
-
const _this = this;
|
|
61
|
-
|
|
62
|
-
// setup stream pipeline when port forward is ready
|
|
63
|
-
const on_setup_ssh_forward = (err, stream) => {
|
|
64
|
-
|
|
65
|
-
if (err) {
|
|
66
|
-
_this.emit('debug', err);
|
|
67
|
-
_this.debug_en && _this.debug('socket forward error:', err);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
stream.on('end', () => {
|
|
72
|
-
socket.resume();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// pipe the data from the local socket to the remote port and visa versa
|
|
76
|
-
stream.pipe(socket).pipe(stream);
|
|
77
|
-
|
|
78
|
-
const shutdown_forward = () => {
|
|
79
|
-
stream.unpipe(socket);
|
|
80
|
-
socket.unpipe(stream);
|
|
81
|
-
stream.end();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// if socket ends, close stream and pipes
|
|
85
|
-
socket.on('close', () => {
|
|
86
|
-
shutdown_forward();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// if socket error, emit error and close stream
|
|
90
|
-
socket.on('error', (err) => {
|
|
91
|
-
_this.emit('debug', err);
|
|
92
|
-
_this.debug_en && _this.debug('socket on error:', err);
|
|
93
|
-
shutdown_forward();
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// create port forward
|
|
98
|
-
_this.forwardOut(
|
|
99
|
-
socket.remoteAddress,
|
|
100
|
-
socket.remotePort,
|
|
101
|
-
remote_hostname,
|
|
102
|
-
remote_port,
|
|
103
|
-
on_setup_ssh_forward
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// function to setup proxy forwards for ssh tunnel
|
|
109
|
-
setupProxyPorts(proxy_ports) {
|
|
110
|
-
|
|
111
|
-
return new Promise((resolve, reject) => {
|
|
112
|
-
|
|
113
|
-
if (!proxy_ports) {
|
|
114
|
-
resolve();
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
var listeners = 0;
|
|
119
|
-
this.listener.isConnected = true;
|
|
120
|
-
const _this = this;
|
|
121
|
-
|
|
122
|
-
// iterate through a list of local forward ports and create a local proxy port
|
|
123
|
-
proxy_ports.forEach(proxy_port => {
|
|
124
|
-
|
|
125
|
-
const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
|
|
126
|
-
|
|
127
|
-
// create local socket server
|
|
128
|
-
const server = _this.listener[proxy_port];
|
|
129
|
-
if (server && server.listening) {
|
|
130
|
-
_this.debug_en && this.debug('SSH Server :: closing forward 2:', proxy_port)
|
|
131
|
-
server.close();
|
|
132
|
-
}
|
|
133
|
-
_this.listener[proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
|
|
134
|
-
|
|
135
|
-
if (this.debug_en) {
|
|
136
|
-
var debug_msg = 'SSH Server :: connection on ' + local_port + ' ' + socket.remotePort;
|
|
137
|
-
_this.emit('debug', debug_msg);
|
|
138
|
-
_this.debug(debug_msg);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// create a proxy forward between local and remote ports
|
|
142
|
-
_this.setup_ssh_forward(socket, remote_hostname, remote_port);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// start listening on port
|
|
146
|
-
try {
|
|
147
|
-
var status_msg = 'SSH Server :: before listen on ' + local_port;
|
|
148
|
-
_this.emit('debug', status_msg);
|
|
149
|
-
_this.listener[proxy_port].listen(local_port, () => {
|
|
150
|
-
|
|
151
|
-
// emit server listening on port message
|
|
152
|
-
var status_msg = 'SSH Server :: bound on ' + local_port;
|
|
153
|
-
_this.emit('debug', status_msg);
|
|
154
|
-
|
|
155
|
-
// if all listeners have been successfully established, resolve setup connection
|
|
156
|
-
if (listeners++ >= proxy_ports.length - 1) {
|
|
157
|
-
_this.emit('ssh_tunnel_ready', {});
|
|
158
|
-
if (_this._tunnelReadyTimeout) {
|
|
159
|
-
clearTimeout(this._tunnelReadyTimeout);
|
|
160
|
-
_this._tunnelReadyTimeout = undefined;
|
|
161
|
-
}
|
|
162
|
-
resolve();
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
} catch (err) {
|
|
166
|
-
_this.debug_en && _this.debug('listen err:', err);
|
|
167
|
-
reject(err);
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
execCmd(cmd, dataStream, errStream) {
|
|
174
|
-
|
|
175
|
-
return new Promise((resolve, reject) => {
|
|
176
|
-
var bufs = [];
|
|
177
|
-
var errs = [];
|
|
178
|
-
|
|
179
|
-
const remote_exec = (err, stream) => {
|
|
180
|
-
if (err) {
|
|
181
|
-
reject(err);
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const onClose = () => {
|
|
186
|
-
var buf = Buffer.concat(bufs);
|
|
187
|
-
var err = Buffer.concat(errs);
|
|
188
|
-
stream.removeListener('close', onClose);
|
|
189
|
-
resolve(buf, err);
|
|
190
|
-
}
|
|
191
|
-
stream.on('close', onClose);
|
|
192
|
-
|
|
193
|
-
stream.on('data', (data) => {
|
|
194
|
-
if (dataStream) dataStream.write(data);
|
|
195
|
-
else bufs.push(data);
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
stream.stderr.on('data', (data) => {
|
|
199
|
-
if (errStream) errStream.write(data);
|
|
200
|
-
else errs.push(data);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
this.exec(cmd, remote_exec);
|
|
205
|
-
})
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// handle remote shell stream processing
|
|
209
|
-
remote_shell(err, stream) {
|
|
210
|
-
const _this = this;
|
|
211
|
-
if (err) throw err;
|
|
212
|
-
|
|
213
|
-
// disable local echo of input chars, use remote output only
|
|
214
|
-
process.stdin.setRawMode(true);
|
|
215
|
-
|
|
216
|
-
// forward data from local terminal to remote host
|
|
217
|
-
const stdinListener = (data) => {
|
|
218
|
-
stream.stdin.write(data);
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
// shutdown this process when stream ends (user logs out)
|
|
222
|
-
stream.on('close', function () {
|
|
223
|
-
process.stdin.setRawMode(false);
|
|
224
|
-
process.stdin.removeListener("data", stdinListener);
|
|
225
|
-
process.exit();
|
|
226
|
-
}).stderr.on('data', function (data) {
|
|
227
|
-
_this.debug_en && _this.debug('shell' + data);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// skip next stops double printing of input
|
|
231
|
-
stream.stdout.on("data", (data) => {
|
|
232
|
-
process.stdout.write(data);
|
|
233
|
-
})
|
|
234
|
-
process.stdin.on("data", stdinListener)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// handle setting up ssh client and proxy forward ports
|
|
238
|
-
do_ssh_connect(opts) {
|
|
239
|
-
|
|
240
|
-
const _this = this;
|
|
241
|
-
|
|
242
|
-
// create a new ssh client connection with supplied credentials and hostname/port
|
|
243
|
-
return new Promise((resolve) => {
|
|
244
|
-
|
|
245
|
-
// exit if connection already established, avoid redundant connection on retry
|
|
246
|
-
if (_this.listener.isConnected) resolve();
|
|
247
|
-
|
|
248
|
-
// close open sockets on server, otherwise initialize open sockets storage
|
|
249
|
-
_this.close_sockets(opts.proxy_ports);
|
|
250
|
-
|
|
251
|
-
_this.on('ready', async () => {
|
|
252
|
-
|
|
253
|
-
// stop any pending retry timeouts
|
|
254
|
-
if (_this._tunnelReadyTimeout) {
|
|
255
|
-
clearTimeout(this._tunnelReadyTimeout);
|
|
256
|
-
_this._tunnelReadyTimeout = undefined;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// if local forwarding requested, setup local forwarding ports to remote host
|
|
260
|
-
if (opts.proxy_ports) {
|
|
261
|
-
await _this.setupProxyPorts(opts.proxy_ports);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// if shell requested, enable remote terminal
|
|
265
|
-
if (opts.shell) {
|
|
266
|
-
_this.shell(_this.remote_shell);
|
|
267
|
-
|
|
268
|
-
// otherwise, if exec requested, exec series of cmds
|
|
269
|
-
} else {
|
|
270
|
-
if (opts.exec && opts.exec.length > 0) {
|
|
271
|
-
for (var i = 0; i < opts.exec.length; i++) {
|
|
272
|
-
var cmd = opts.exec[i];
|
|
273
|
-
_this.debug_en && _this.debug(opts.username + '@' + opts.hostname + ':' + cmd);
|
|
274
|
-
var result = await _this.execCmd(cmd);
|
|
275
|
-
_this.debug_en && _this.debug(result.toString());
|
|
276
|
-
}
|
|
277
|
-
process.exit();
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
resolve();
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
_this.on('end', () => {
|
|
285
|
-
_this.debug_en && _this.debug('SSH Client :: end');
|
|
286
|
-
_this.close_sockets(opts.proxy_ports);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
_this.on('close', () => {
|
|
290
|
-
_this.debug_en && _this.debug('SSH Client :: close');
|
|
291
|
-
_this.close_sockets(opts.proxy_ports);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
_this.on('error', err => {
|
|
295
|
-
_this.debug_en && _this.debug('SSH Client :: error :: ' + err);
|
|
296
|
-
_this.emit('debug', err);
|
|
297
|
-
_this.listener.isConnected = false;
|
|
298
|
-
if (_this._tunnelReadyTimeout) {
|
|
299
|
-
clearTimeout(this._tunnelReadyTimeout.elReadyTimeout);
|
|
300
|
-
_this._tunnelReadyTimeout = undefined;
|
|
301
|
-
}
|
|
302
|
-
_this.ssh_retry_connect(opts);
|
|
303
|
-
resolve();
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
_this.on('handshake', negotiated => {
|
|
307
|
-
_this.debug_en && _this.debug('SSH Client :: handshake:', JSON.stringify(negotiated));
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
_this.on('greeting', (message) => {
|
|
311
|
-
_this.debug_en && _this.debug('SSH Client :: greeting:', message);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
// create deep copy of opts as connect options
|
|
315
|
-
const config = JSON.parse(JSON.stringify(opts));
|
|
316
|
-
|
|
317
|
-
// add default ssh2 config options
|
|
318
|
-
config.debug = (this.debug_ssh) ? (...args) => { console.log(...args); } : null;
|
|
319
|
-
config.keepaliveInterval = opts.keepaliveInterval || 10000;
|
|
320
|
-
|
|
321
|
-
// remove ssh_tunnel_proxy config extensions
|
|
322
|
-
const ssh_tunnel_extensions = ['alias', 'disabled', 'hostname', 'ngrok_api', 'server_name', 'service_name', 'whitelist', 'private_key_filename'];
|
|
323
|
-
for (var i = 0; i < ssh_tunnel_extensions.length; i++) delete config[ssh_tunnel_extensions[i]];
|
|
324
|
-
|
|
325
|
-
// connect to remote host
|
|
326
|
-
_this.connect(config);
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// validate port number - check for nan, out of valid port range, unauthorized system ports
|
|
331
|
-
validate_port_number(port_str, whitelist) {
|
|
332
|
-
if (isNaN(port_str)) return false;
|
|
333
|
-
const port = parseInt(port_str);
|
|
334
|
-
if (port < 1 || port > 65535) return false;
|
|
335
|
-
if (port < 1024 && whitelist && whitelist[port] === undefined) return false;
|
|
336
|
-
return true;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// validate local forwards for correct format and valid ports
|
|
340
|
-
validate_local_forward(proxy_ports, whitelist) {
|
|
341
|
-
|
|
342
|
-
if (!proxy_ports) return;
|
|
343
|
-
|
|
344
|
-
const local_ports = [];
|
|
345
|
-
const remote_ports = [];
|
|
346
|
-
proxy_ports.forEach(proxy_port => {
|
|
347
|
-
const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
|
|
348
|
-
if (remote_hostname.length < 1) remote_ports.push(remote_port);
|
|
349
|
-
if (!this.validate_port_number(local_port, whitelist)) local_ports.push(local_port);
|
|
350
|
-
if (!this.validate_port_number(remote_port, whitelist)) remote_ports.push(remote_port);
|
|
351
|
-
});
|
|
352
|
-
if (local_ports.length || remote_ports.length) {
|
|
353
|
-
const err = new Error('Invalid local forward');
|
|
354
|
-
|
|
355
|
-
this.debug_en && this.debug('invalid ports found:\n', JSON.stringify(proxy_ports, null, 2),
|
|
356
|
-
'\n', JSON.stringify(whitelist, null, 2));
|
|
357
|
-
|
|
358
|
-
err.info = {
|
|
359
|
-
local_ports: local_ports.join(','),
|
|
360
|
-
remote_ports: remote_ports.join(',')
|
|
361
|
-
};
|
|
362
|
-
throw (err);
|
|
363
|
-
}
|
|
364
|
-
return true;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// on network error, attempt to re-establish connection until 10 retries
|
|
368
|
-
//clearTimeout(this._readyTimeout);
|
|
369
|
-
ssh_retry_connect(opts) {
|
|
370
|
-
const _this = this;
|
|
371
|
-
const invoke = () => {
|
|
372
|
-
_this.connectSSH(opts);
|
|
373
|
-
}
|
|
374
|
-
if (this.retries++ < 10) this._tunnelReadyTimeout = setTimeout(invoke, 5000);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// attempt to establish ssh tunnel to server with supplied parameters.
|
|
378
|
-
async ssh_start_tunnel(opts) {
|
|
379
|
-
const _this = this;
|
|
380
|
-
async function do_ssh_connect(resolve) {
|
|
381
|
-
|
|
382
|
-
// after successful tunnel setup complete, send events and reset retry counter
|
|
383
|
-
await _this.do_ssh_connect(opts).then(() => {
|
|
384
|
-
const hostport = opts.host + ':' + opts.port;
|
|
385
|
-
if (_this.debug_en) {
|
|
386
|
-
const msg = 'SSH connection to ' + opts.server_name + ' established at ' + hostport;
|
|
387
|
-
_this.debug(msg);
|
|
388
|
-
}
|
|
389
|
-
//_this.emit('status', '', 'ready', hostport);
|
|
390
|
-
_this.retries = 0;
|
|
391
|
-
resolve();
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
return new Promise(do_ssh_connect);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// setup ssh connection parameters and attempt to establish ssh tunnel
|
|
398
|
-
// todo: if opts have changed while service is running, shutdown current service and restart with new opts
|
|
399
|
-
async connectSSH(opts, whitelist) {
|
|
400
|
-
|
|
401
|
-
// make deep copy of opts for modification
|
|
402
|
-
var _opts = JSON.parse(JSON.stringify(opts));
|
|
403
|
-
|
|
404
|
-
// setup whitelist from param or opts in case of error retry
|
|
405
|
-
_opts.whitelist = (whitelist !== undefined) ? whitelist : (_opts.whitelist) ? _opts.whitelist : null;
|
|
406
|
-
|
|
407
|
-
// validate local port forwards, emit error and quit if invalid
|
|
408
|
-
try {
|
|
409
|
-
this.validate_local_forward(_opts.proxy_ports, _opts.whitelist);
|
|
410
|
-
} catch (err) {
|
|
411
|
-
this.emit('debug', err);
|
|
412
|
-
this.debug_en && this.debug(err);
|
|
413
|
-
return err;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// if password is empty set to undefined
|
|
417
|
-
if (opts.password && !opts.password.length) {
|
|
418
|
-
_opts.password = undefined;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
if (_opts.private_key_filename) {
|
|
422
|
-
try {
|
|
423
|
-
if (_opts.private_key_filename[0] == '~') {
|
|
424
|
-
_opts.private_key_filename = homedir + _opts.private_key_filename.substr(1);
|
|
425
|
-
}
|
|
426
|
-
_opts.privateKey = fs.readFileSync(_opts.private_key_filename).toString();
|
|
427
|
-
} catch (err) {
|
|
428
|
-
console.log('Error loading key:', err);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// retrieve private key from system keychain with supplied service name and server
|
|
433
|
-
// if no key found, authentication is supplied username, password
|
|
434
|
-
if (_opts.service_name && _opts.server_name) {
|
|
435
|
-
_opts.privateKey = await keypairStorage.get_keypair(opts.service_name, opts.server_name);
|
|
436
|
-
} else {
|
|
437
|
-
if (!_opts.server_name) _opts.server_name = 'sshtun';
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// if ngrok api key provided, obtain hostname and hostport from ngrok api
|
|
441
|
-
// if no ngrok tunnel specified, use supplied host and port
|
|
442
|
-
if (_opts.ngrok_api) {
|
|
443
|
-
const _this = this;
|
|
444
|
-
const ngrokApi = new NgrokApi(_opts.ngrok_api);
|
|
445
|
-
var hostport = await ngrokApi.get_hostport()
|
|
446
|
-
.catch(() => {
|
|
447
|
-
_this.debug_en && _this.debug('ngrok get_hostport connection error');
|
|
448
|
-
_this.listener.isConnected = false;
|
|
449
|
-
_this.ssh_retry_connect(_opts);
|
|
450
|
-
});
|
|
451
|
-
if (hostport && hostport.host) {
|
|
452
|
-
_opts.host = hostport.host;
|
|
453
|
-
_opts.port = hostport.port;
|
|
454
|
-
}
|
|
455
|
-
else return null;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
this.debug_en && this.debug('connectSSH:\n', JSON.stringify(_opts, null, 2));
|
|
459
|
-
|
|
460
|
-
// initiate ssh tunnel, block until tunnel is established or error
|
|
461
|
-
return await this.ssh_start_tunnel(_opts)
|
|
462
|
-
.catch((err) => {
|
|
463
|
-
this.debug_en && this.debug('ssh_start_tunnel catch:' + err);
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// resume connection, if previously online
|
|
468
|
-
onNetworkOnline() { }
|
|
469
|
-
|
|
470
|
-
// connection down, shutdown tunnel
|
|
471
|
-
onNetworkOffline() { }
|
|
472
|
-
|
|
473
|
-
generateAndStoreKeypair(...args) {
|
|
474
|
-
keypairStorage.generate_and_store_keypair(...args);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
getPublicKey(...args) {
|
|
478
|
-
keypairStorage.get_public_key_from_keychain(...args);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
debug(...args) { console.log(...args); }
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
module.exports = {
|
|
486
|
-
SSHTunnelProxy: SSHTunnelProxy,
|
|
487
|
-
KeypairStorage: KeypairStorage,
|
|
488
|
-
NgrokApi: NgrokApi
|
|
489
|
-
}
|
package/lib/keypair_storage.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
|
|
3
|
-
keypair_storage - generate and store keypairs in the system keychain
|
|
4
|
-
|
|
5
|
-
keypair_storage provides a set of functions to generate ssh ed25519 keypairs. Private keys
|
|
6
|
-
are stored in the system keychain and retrieved by service_name and server_name parameters.
|
|
7
|
-
|
|
8
|
-
Author: Autonomous
|
|
9
|
-
First release: 1-29-2023
|
|
10
|
-
License: MIT
|
|
11
|
-
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const sshpk = require('sshpk');
|
|
15
|
-
const keytar = require('keytar');
|
|
16
|
-
|
|
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(() => {
|
|
27
|
-
resolve(keypair.public_key);
|
|
28
|
-
}, (err) => {
|
|
29
|
-
reject(err);
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
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
|
-
});
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
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;
|
|
53
|
-
}
|
|
54
|
-
|
|
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
|
-
}
|
|
75
|
-
}
|
|
76
|
-
module.exports = KeypairStorage;
|
package/lib/ngrok_service.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
|
|
3
|
-
ngrok_service - obtain hostport from ngrok api and split into host, port
|
|
4
|
-
|
|
5
|
-
Author: Autonomous
|
|
6
|
-
First release: 1-29-2023
|
|
7
|
-
License: MIT
|
|
8
|
-
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
// get endpoint hostname and hostport from ngrok api
|
|
12
|
-
const { Ngrok } = require('@ngrok/ngrok-api');
|
|
13
|
-
|
|
14
|
-
class NgrokApi {
|
|
15
|
-
|
|
16
|
-
constructor(apiToken) {
|
|
17
|
-
this.ngrok = new Ngrok({ apiToken: apiToken });
|
|
18
|
-
}
|
|
19
|
-
|
|
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'));
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
module.exports = NgrokApi;
|