ssh_tunnel_proxy 1.0.0
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/.vscode/launch.json +34 -0
- package/lib/index.js +284 -0
- package/lib/keypair_storage.js +69 -0
- package/lib/ngrok_service.js +36 -0
- package/package.json +29 -0
- package/test/test.js +234 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Use IntelliSense to learn about possible attributes.
|
|
3
|
+
// Hover to view descriptions of existing attributes.
|
|
4
|
+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"configurations": [
|
|
7
|
+
{
|
|
8
|
+
"name": "debug mocha",
|
|
9
|
+
"request": "launch",
|
|
10
|
+
"runtimeArgs": [
|
|
11
|
+
"run-script",
|
|
12
|
+
"test"
|
|
13
|
+
],
|
|
14
|
+
"runtimeVersion": "19.4.0",
|
|
15
|
+
"runtimeExecutable": "npm",
|
|
16
|
+
"skipFiles": [
|
|
17
|
+
"<node_internals>/**"
|
|
18
|
+
],
|
|
19
|
+
"type": "node"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"type": "node",
|
|
23
|
+
"runtimeVersion": "19.4.0",
|
|
24
|
+
"request": "launch",
|
|
25
|
+
"name": "Launch Program",
|
|
26
|
+
"cwd": "${fileDirname}",
|
|
27
|
+
"skipFiles": [
|
|
28
|
+
"<node_internals>/**"
|
|
29
|
+
],
|
|
30
|
+
"program": "${file}",
|
|
31
|
+
"args":["--source=themagician_composite"]
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
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 { EventEmitter } = require('node:events');
|
|
21
|
+
const { Client } = require('electron-ssh2');
|
|
22
|
+
|
|
23
|
+
const keypair_storage = require('./keypair_storage');
|
|
24
|
+
const { get_hostport } = require('./ngrok_service');
|
|
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;
|
|
32
|
+
const debug_ssh = false;
|
|
33
|
+
|
|
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
|
+
}
|
|
48
|
+
|
|
49
|
+
// ssh client debug function for verbose ssh connection details
|
|
50
|
+
const debug_client = (debug_ssh) ? (msg) => { console.log(msg); } : null;
|
|
51
|
+
|
|
52
|
+
// function to handle setting up tunnel and proxy forward ports
|
|
53
|
+
const do_ssh_connect = (opts) => {
|
|
54
|
+
|
|
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
|
+
}
|
|
61
|
+
|
|
62
|
+
// create a new ssh client connection with supplied credentials and hostname/port
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
|
|
65
|
+
var listeners = 0;
|
|
66
|
+
const ssh_client = new Client();
|
|
67
|
+
|
|
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
|
+
});
|
|
90
|
+
|
|
91
|
+
// when ssh client is ready, establish proxy forward ports to remote server
|
|
92
|
+
ssh_client.on('ready', () => {
|
|
93
|
+
|
|
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(':');
|
|
97
|
+
|
|
98
|
+
// create local websocket server
|
|
99
|
+
listener[opts.server_name][proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
|
|
100
|
+
|
|
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
|
+
}
|
|
106
|
+
|
|
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, () => {
|
|
143
|
+
|
|
144
|
+
// emit server listening on port message
|
|
145
|
+
var status_msg = 'SSH Server :: bound on ' + local_port;
|
|
146
|
+
sshEmitter.emit('status', status_msg, 'listening', local_port);
|
|
147
|
+
|
|
148
|
+
// if all listeners have been successfully established, resolve setup connection
|
|
149
|
+
if (listeners++ >= opts.proxy_ports.length - 1) {
|
|
150
|
+
resolve();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
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
|
+
}
|
|
168
|
+
|
|
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
|
+
}
|
|
177
|
+
|
|
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
|
+
}
|
|
197
|
+
|
|
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
|
+
}
|
|
203
|
+
|
|
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
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
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));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// make deep copy of opts for modification
|
|
232
|
+
var _opts = JSON.parse(JSON.stringify(opts));
|
|
233
|
+
|
|
234
|
+
// setup whitelist from param or opts in case of error retry
|
|
235
|
+
_opts.whitelist = (whitelist !== undefined) ? whitelist : (_opts.whitelist) ? _opts.whitelist : {};
|
|
236
|
+
|
|
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;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// if password is empty set to undefined
|
|
247
|
+
if (opts.password && !opts.password.length) {
|
|
248
|
+
_opts.password = undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
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);
|
|
254
|
+
|
|
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)
|
|
259
|
+
.catch((err) => {
|
|
260
|
+
if (debug) console.log('ngrok get_hostport connection error');
|
|
261
|
+
ssh_retry_connect(opts);
|
|
262
|
+
});
|
|
263
|
+
if (hostport && hostport.host) {
|
|
264
|
+
_opts.host = hostport.host;
|
|
265
|
+
_opts.port = hostport.port;
|
|
266
|
+
}
|
|
267
|
+
else return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// initiate ssh tunnel, block until tunnel is established or error
|
|
271
|
+
return await ssh_start_tunnel(_opts);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// export function to obtain ssh connection event emitter
|
|
275
|
+
const get_event_hook = () => { return sshEmitter; }
|
|
276
|
+
|
|
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
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
// 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);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
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);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
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
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
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
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
store_private_key: keytar.setPassword,
|
|
64
|
+
retrieve_private_key: keytar.getPassword,
|
|
65
|
+
remove_keypair: keytar.deletePassword,
|
|
66
|
+
generate_keypair: generate_keypair,
|
|
67
|
+
generate_and_store_keypair: generate_and_store_keypair,
|
|
68
|
+
get_public_key_from_keychain: get_public_key_from_keychain
|
|
69
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
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
|
+
}
|
|
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]
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
throw (new Error('get_ngrok_hostport: no endpoints found'));
|
|
30
|
+
}
|
|
31
|
+
return hostport_obj;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
get_hostport: get_ngrok_hostport
|
|
36
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ssh_tunnel_proxy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "obtain ngrok host port from api and establish a ssh connection",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "mocha"
|
|
8
|
+
},
|
|
9
|
+
"author": "Autonomous",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@ngrok/ngrok-api": "^0.9.0",
|
|
13
|
+
"bluebird": "^3.7.2",
|
|
14
|
+
"crypto": "^1.0.1",
|
|
15
|
+
"electron-ssh2": "^0.1.2",
|
|
16
|
+
"form-data": "^4.0.0",
|
|
17
|
+
"fs": "^0.0.1-security",
|
|
18
|
+
"keytar": "^7.9.0",
|
|
19
|
+
"net": "^1.0.2",
|
|
20
|
+
"ngrok": "^4.3.3",
|
|
21
|
+
"socksv5": "^0.0.6",
|
|
22
|
+
"sshpk": "^1.17.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"mocha": "^10.2.0",
|
|
26
|
+
"rewire": "^6.0.0",
|
|
27
|
+
"sinon": "^15.0.1"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/test/test.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/*
|
|
2
|
+
todo:
|
|
3
|
+
add generate and store keys test
|
|
4
|
+
add ngrok api test with stored api key in ssh_tunnel_proxy config
|
|
5
|
+
to run ssh tunnel api tests edit config.json and provide api keys for each service
|
|
6
|
+
*/
|
|
7
|
+
const assert = require('assert');
|
|
8
|
+
const rewire = require('rewire');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const sinon = require('sinon');
|
|
13
|
+
|
|
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');
|
|
21
|
+
|
|
22
|
+
// 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');
|
|
25
|
+
|
|
26
|
+
var myEmitter = get_event_hook();
|
|
27
|
+
|
|
28
|
+
const whitelist = {
|
|
29
|
+
80: true,
|
|
30
|
+
443: true,
|
|
31
|
+
22: true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// port value validation tests
|
|
35
|
+
describe('Validate ports', function () {
|
|
36
|
+
const validate_port_number = main.__get__('validate_port_number');
|
|
37
|
+
it('valid port number 1024', function () {
|
|
38
|
+
assert(validate_port_number(1024, whitelist), 'should be valid');
|
|
39
|
+
});
|
|
40
|
+
it('valid port number 80', function () {
|
|
41
|
+
assert(validate_port_number(80, whitelist), 'should be valid');
|
|
42
|
+
});
|
|
43
|
+
it('invalid port number -1', function () {
|
|
44
|
+
assert.equal(false, validate_port_number(-1, whitelist), 'should be invalid');
|
|
45
|
+
});
|
|
46
|
+
it('invalid port number 65536', function () {
|
|
47
|
+
assert.equal(false, validate_port_number(65536, whitelist), 'should be invalid');
|
|
48
|
+
});
|
|
49
|
+
it('invalid system port number 137', function () {
|
|
50
|
+
assert.equal(false, validate_port_number(137, whitelist), 'should be invalid');
|
|
51
|
+
});
|
|
52
|
+
it('port number nan', function () {
|
|
53
|
+
assert.equal(false, validate_port_number('nan', whitelist), 'should be invalid');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// local forward format and value validation test
|
|
58
|
+
describe('Validate local forwards', function () {
|
|
59
|
+
const validate_local_forward = main.__get__('validate_local_forward');
|
|
60
|
+
var local_forward = [
|
|
61
|
+
'8080:127.0.0.1:80',
|
|
62
|
+
'9000:192.168.43.5:9000',
|
|
63
|
+
'9000:192.168.43.5',
|
|
64
|
+
'192.168.43.5:9000',
|
|
65
|
+
'8137:127.0.0.1:137',
|
|
66
|
+
'-1:127.0.0.1:65537',
|
|
67
|
+
];
|
|
68
|
+
it('valid local forward to 80 ' + local_forward[0], function () {
|
|
69
|
+
assert(() => { return validate_local_forward([local_forward[0]], whitelist) }, 'should be valid');
|
|
70
|
+
});
|
|
71
|
+
it('valid local forward to 9000 ' + local_forward[1], function () {
|
|
72
|
+
assert(() => { return validate_local_forward([local_forward[1]], whitelist) }, 'should be valid');
|
|
73
|
+
});
|
|
74
|
+
it('invalid local forward format ' + local_forward[2], function () {
|
|
75
|
+
const err = {
|
|
76
|
+
message: 'Invalid local forward',
|
|
77
|
+
info: {
|
|
78
|
+
local_ports: '',
|
|
79
|
+
remote_ports: '',
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
assert.throws(() => { validate_local_forward([local_forward[2]], whitelist) }, err);
|
|
83
|
+
});
|
|
84
|
+
it('invalid local forward format ' + local_forward[3], function () {
|
|
85
|
+
const err = {
|
|
86
|
+
message: 'Invalid local forward',
|
|
87
|
+
info: {
|
|
88
|
+
local_ports: '192.168.43.5',
|
|
89
|
+
remote_ports: '',
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
assert.throws(() => { validate_local_forward([local_forward[3]], whitelist) }, err, 'should be invalid');
|
|
93
|
+
});
|
|
94
|
+
it('invalid local forward system port ' + local_forward[4], function () {
|
|
95
|
+
const err = {
|
|
96
|
+
message: 'Invalid local forward',
|
|
97
|
+
info: {
|
|
98
|
+
local_ports: '',
|
|
99
|
+
remote_ports: '137',
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
assert.throws(() => { validate_local_forward([local_forward[4]], whitelist) }, err, 'should be invalid');
|
|
103
|
+
});
|
|
104
|
+
it('invalid local forward port range ' + local_forward[5], function () {
|
|
105
|
+
const err = {
|
|
106
|
+
message: 'Invalid local forward',
|
|
107
|
+
info: {
|
|
108
|
+
local_ports: '-1',
|
|
109
|
+
remote_ports: '65537',
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
assert.throws(() => { validate_local_forward([local_forward[5]], whitelist) }, err, 'should be invalid');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// generate, store and retrieve private key test
|
|
117
|
+
describe('Store and retrieve private key in system keychain', function () {
|
|
118
|
+
const service_name = 'ssh_proxy_tunnel';
|
|
119
|
+
const account = 'test';
|
|
120
|
+
const keypair = generate_keypair();
|
|
121
|
+
it('generated key should contain a private key', function () {
|
|
122
|
+
assert(keypair.private_key, 'should not be empty');
|
|
123
|
+
});
|
|
124
|
+
it('generated key should contain a public key', function () {
|
|
125
|
+
assert(keypair.public_key, 'should not be empty');
|
|
126
|
+
});
|
|
127
|
+
it('should store generated private key', () => {
|
|
128
|
+
return store_private_key(service_name, account, keypair.private_key).then(result => {
|
|
129
|
+
assert.equal(result, undefined, 'should not return error');
|
|
130
|
+
})
|
|
131
|
+
});
|
|
132
|
+
it('stored private key should match generated key', () => {
|
|
133
|
+
return retrieve_private_key(service_name, account).then(stored_key => {
|
|
134
|
+
assert.equal(stored_key, keypair.private_key, 'should match');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
it('stored public key should match generated key', () => {
|
|
138
|
+
return get_public_key_from_keychain(service_name, account).then(stored_key => {
|
|
139
|
+
assert.equal(stored_key, keypair.public_key, 'should match');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
it('remove key should return true', () => {
|
|
143
|
+
return remove_keypair(service_name, account).then(result => {
|
|
144
|
+
assert('should return true');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
it('retrieve stored private key after delete should return null', () => {
|
|
148
|
+
return retrieve_private_key(service_name, account).then((result) => {
|
|
149
|
+
assert.equal(result, null, 'should return null');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
it('retrieve stored public key after delete should return null', () => {
|
|
153
|
+
return get_public_key_from_keychain(service_name, account).then((result) => {
|
|
154
|
+
assert.equal(result, null, 'should return null');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
/*
|
|
160
|
+
// generate and store private key test
|
|
161
|
+
describe('', function () {
|
|
162
|
+
it('', function () {
|
|
163
|
+
assert('', '');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
describe('Get ngrok hostport', function () {
|
|
169
|
+
const parse_ngrok_hostport = ngrok_service.__get__('parse_ngrok_hostport');
|
|
170
|
+
var test_endpoint = [{
|
|
171
|
+
hostport: '8.tcp.ngrok.io:17632'
|
|
172
|
+
}];
|
|
173
|
+
var result_opts = parse_ngrok_hostport(test_endpoint, opts);
|
|
174
|
+
it('host should match 8.tcp.ngrok.io', function () {
|
|
175
|
+
assert.equal(result_opts.host, '8.tcp.ngrok.io');
|
|
176
|
+
});
|
|
177
|
+
it('port should match 17632', function () {
|
|
178
|
+
assert.equal(result_opts.port, '17632');
|
|
179
|
+
});
|
|
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
|
+
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
/*
|
|
192
|
+
describe('ssh connection client ready', function () {
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('myEmitter should be enabled', function () {
|
|
196
|
+
assert(myEmitter, 'received event emitter');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('ssh connection integration test', function () {
|
|
200
|
+
|
|
201
|
+
it('ssh connect should invoke the ssh ready function', function (done) {
|
|
202
|
+
this.timeout(10000);
|
|
203
|
+
myEmitter.on('ssh_client_ready', (ssh_client) => {
|
|
204
|
+
assert(ssh_client, 'ssh client created');
|
|
205
|
+
done();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
var ws_socket = false;
|
|
209
|
+
it('two websocket servers should be created', function (done) {
|
|
210
|
+
this.timeout(18000);
|
|
211
|
+
myEmitter.on('websocket_server_created', (socket) => {
|
|
212
|
+
assert(socket, 'ssh websocket server created:' + socket.remotePort);
|
|
213
|
+
if (!ws_socket) done();
|
|
214
|
+
ws_socket = true;
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
connect_ssh(opts);
|
|
218
|
+
|
|
219
|
+
});
|
|
220
|
+
*/
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
// test generate keypair
|
|
224
|
+
var test_generate_keypair = function () {
|
|
225
|
+
const keypair = generate_keypair();
|
|
226
|
+
console.log('keypair:');
|
|
227
|
+
console.log(keypair.public_key);
|
|
228
|
+
console.log(keypair.private_key);
|
|
229
|
+
var homedir = os.homedir();
|
|
230
|
+
var fname = path.join(homedir, '.ssh', 'zm_id_rsa.pub')
|
|
231
|
+
//fs.writeFileSync(fname, keypair.public_key);
|
|
232
|
+
fname = path.join(homedir, '.ssh', 'zm_id_rsa')
|
|
233
|
+
//fs.writeFileSync(fname, keypair.private_key);
|
|
234
|
+
}
|