ssh_tunnel_proxy 1.2.9 → 1.2.11
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 +73 -60
- package/lib/index.js +80 -63
- package/package.json +1 -1
- package/{main.js → ssh2-node} +23 -18
- package/test/test_remote_exec.js +217 -0
- package/test/test_remote_exec_streams.js +0 -140
package/README.md
CHANGED
|
@@ -1,94 +1,107 @@
|
|
|
1
1
|
# ssh_tunnel_proxy
|
|
2
2
|
Initiate a ssh reverse tunnel proxy with forwarding ports
|
|
3
3
|
|
|
4
|
-
ssh_tunnel_proxy
|
|
4
|
+
ssh_tunnel_proxy is a wrapper to ssh2 that provides async functionality as well as an extension to the api to include methods to setup a list of proxy forwards, exec a list of commands or startup a terminal shell. To setup a ssh tunnel, parameters are suppled for host, port, authentication and a list of proxy ports or commands to invoke. If a ngrok api key is provided the host and port of the ngrok tunnel are obtained. If the connection is interrupted or connection errors occur, attempts are made re-establish the tunnel.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
In addition to the node api, a command line function called ssh-node2 is included to start ssh sessions in a manner similar to the ssh command line utility.
|
|
7
|
+
|
|
8
|
+
### Command line examples
|
|
9
|
+
|
|
10
|
+
Connect to remote host and establish local forwards:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
./ssh2-node -u=<username> -h=192.168.1.1 -k=~/.ssh/<private_key> -L=8180:192.168.1.1:80
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Connect to host using parameters stored in ~/.config/ssh_tunnel_proxy/config.json:
|
|
7
17
|
|
|
8
18
|
```
|
|
9
|
-
node
|
|
19
|
+
./ssh2-node rh2
|
|
10
20
|
```
|
|
11
21
|
|
|
12
22
|
default config file, located at:
|
|
13
23
|
~/.config/ssh_tunnel_proxy/config.json
|
|
14
24
|
```json
|
|
15
25
|
[{
|
|
16
|
-
"
|
|
26
|
+
"hostname":"rh2",
|
|
17
27
|
"username": "<username>",
|
|
18
|
-
"password": "",
|
|
19
|
-
"host": "",
|
|
20
|
-
"port": "",
|
|
21
28
|
"proxy_ports": [
|
|
22
29
|
"8280:127.0.0.1:80",
|
|
23
30
|
"9000:127.0.0.1:9000",
|
|
24
31
|
"8122:192.168.2.1:22"
|
|
25
32
|
],
|
|
26
|
-
"
|
|
27
|
-
"80": true,
|
|
28
|
-
"443": true,
|
|
29
|
-
"22": true
|
|
30
|
-
},
|
|
31
|
-
"service_name": "ssh_proxy_client",
|
|
32
|
-
"server_name": "test",
|
|
33
|
+
"private_key_filename":"~/.ssh/<private key>",
|
|
33
34
|
"ngrok_api": "<ngrok api key>"
|
|
34
35
|
}]
|
|
35
36
|
```
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
Execute a series of commands on remote host
|
|
38
39
|
```
|
|
39
|
-
|
|
40
|
+
./ssh-node2 rh2 -e='uptime' -e='ls -all'
|
|
40
41
|
```
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
node main.js -c
|
|
45
|
-
```
|
|
46
|
-
```json
|
|
47
|
-
[{
|
|
48
|
-
"enabled": true,
|
|
49
|
-
"username": "<username>",
|
|
50
|
-
"service_name": "ssh_proxy_client",
|
|
51
|
-
"server_name": "test",
|
|
52
|
-
"exec" : [
|
|
53
|
-
"uptime",
|
|
54
|
-
"ls -all"
|
|
55
|
-
],
|
|
56
|
-
"ngrok_api": "<ngrok api key>"
|
|
57
|
-
}]
|
|
58
|
-
```
|
|
43
|
+
### Api examples
|
|
59
44
|
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
node main.js -c -S
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Example use of api to exec remote commands using async await and processing result through streams. Complete example is in test/test_remote_exec_streams.js:
|
|
45
|
+
Exec remote commands using async await and processing result through streams.
|
|
66
46
|
|
|
67
47
|
```js
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
48
|
+
// send result of cmd through pipeline, generating a stream of json objects
|
|
49
|
+
function lsTest(cmdProxy, cmd, destination) {
|
|
50
|
+
|
|
51
|
+
return new Promise( (resolveCmd) => {
|
|
52
|
+
|
|
53
|
+
// set input of pipeline to split data into lines (npm i split)
|
|
54
|
+
const tunnel = split();
|
|
55
|
+
|
|
56
|
+
// when pipeline is ready exec shell cmd
|
|
57
|
+
const pipelineReady = (socket) => {
|
|
58
|
+
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
|
|
61
|
+
// invoke command on remote host and send results to pipeline
|
|
62
|
+
cmdProxy.execCmd(cmd, tunnel)
|
|
63
|
+
|
|
64
|
+
// stream processing complete, cleanup pipeline and exit
|
|
65
|
+
.then(() => {
|
|
66
|
+
//self.cleanupPipeline(socket);
|
|
67
|
+
resolveCmd();
|
|
68
|
+
});
|
|
69
|
+
resolve();
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// pipe shell cmd result through json parser pipeline to destination
|
|
74
|
+
pipeline(tunnel,
|
|
75
|
+
self.parse(),
|
|
76
|
+
self.toJSONString(),
|
|
77
|
+
destination,
|
|
78
|
+
pipelineReady
|
|
79
|
+
);
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function runCmd() {
|
|
84
|
+
|
|
85
|
+
const opts = {
|
|
86
|
+
"hostname":"rh2",
|
|
87
|
+
"username": "<username>",
|
|
88
|
+
"private_key_filename":"~/.ssh/<private key>",
|
|
89
|
+
"ngrok_api": "<ngrok api key>"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const sshTunnelProxy = new SSHTunnelProxy();
|
|
93
|
+
|
|
94
|
+
// connect to remote host
|
|
95
|
+
await sshTunnelProxy.connectSSH(opts);
|
|
96
|
+
|
|
97
|
+
// invoke ls -all on remote host and parse result to json object string
|
|
98
|
+
await lsTest(sshTunnelProxy, 'ls -all', process.stdout);
|
|
88
99
|
}
|
|
100
|
+
|
|
101
|
+
runCmd();
|
|
89
102
|
```
|
|
90
103
|
|
|
91
|
-
|
|
104
|
+
Example of use of the api with electronjs.
|
|
92
105
|
|
|
93
106
|
main.js:
|
|
94
107
|
```js
|
package/lib/index.js
CHANGED
|
@@ -17,6 +17,7 @@ License: MIT
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
const net = require('net');
|
|
20
|
+
const fs = require('fs');
|
|
20
21
|
const process = require('process');
|
|
21
22
|
const { Buffer } = require('node:buffer');
|
|
22
23
|
const { Client } = require('electron-ssh2');
|
|
@@ -26,6 +27,8 @@ const NgrokApi = require('./ngrok_service');
|
|
|
26
27
|
|
|
27
28
|
const keypairStorage = new KeypairStorage();
|
|
28
29
|
|
|
30
|
+
const homedir = require('os').homedir();
|
|
31
|
+
|
|
29
32
|
class SSHTunnelProxy extends Client {
|
|
30
33
|
|
|
31
34
|
constructor() {
|
|
@@ -38,15 +41,16 @@ class SSHTunnelProxy extends Client {
|
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
// function to close all active sockets for tunnel restart and exception handling
|
|
41
|
-
close_sockets(
|
|
44
|
+
close_sockets(proxy_ports) {
|
|
42
45
|
|
|
43
|
-
if (!
|
|
46
|
+
if (!proxy_ports) return;
|
|
44
47
|
|
|
45
48
|
proxy_ports.forEach(proxy_port => {
|
|
46
|
-
const server = this.listener[
|
|
49
|
+
const server = this.listener[proxy_port];
|
|
47
50
|
if (server && server.listening) {
|
|
48
51
|
this.debug_en && this.debug('SSH Server :: closing forward:', proxy_port)
|
|
49
52
|
server.close();
|
|
53
|
+
delete this.listener[proxy_port];
|
|
50
54
|
}
|
|
51
55
|
});
|
|
52
56
|
}
|
|
@@ -102,12 +106,17 @@ class SSHTunnelProxy extends Client {
|
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
// function to setup proxy forwards for ssh tunnel
|
|
105
|
-
setupProxyPorts(
|
|
109
|
+
setupProxyPorts(proxy_ports) {
|
|
106
110
|
|
|
107
111
|
return new Promise((resolve, reject) => {
|
|
108
112
|
|
|
113
|
+
if (!proxy_ports) {
|
|
114
|
+
resolve();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
109
118
|
var listeners = 0;
|
|
110
|
-
|
|
119
|
+
this.listener.isConnected = true;
|
|
111
120
|
const _this = this;
|
|
112
121
|
|
|
113
122
|
// iterate through a list of local forward ports and create a local proxy port
|
|
@@ -116,13 +125,12 @@ class SSHTunnelProxy extends Client {
|
|
|
116
125
|
const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
|
|
117
126
|
|
|
118
127
|
// create local socket server
|
|
119
|
-
const server =
|
|
128
|
+
const server = _this.listener[proxy_port];
|
|
120
129
|
if (server && server.listening) {
|
|
121
|
-
|
|
130
|
+
_this.debug_en && this.debug('SSH Server :: closing forward 2:', proxy_port)
|
|
122
131
|
server.close();
|
|
123
132
|
}
|
|
124
|
-
|
|
125
|
-
this.listener[server_name][proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
|
|
133
|
+
_this.listener[proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
|
|
126
134
|
|
|
127
135
|
if (this.debug_en) {
|
|
128
136
|
var debug_msg = 'SSH Server :: connection on ' + local_port + ' ' + socket.remotePort;
|
|
@@ -138,7 +146,7 @@ class SSHTunnelProxy extends Client {
|
|
|
138
146
|
try {
|
|
139
147
|
var status_msg = 'SSH Server :: before listen on ' + local_port;
|
|
140
148
|
_this.emit('debug', status_msg);
|
|
141
|
-
_this.listener[
|
|
149
|
+
_this.listener[proxy_port].listen(local_port, () => {
|
|
142
150
|
|
|
143
151
|
// emit server listening on port message
|
|
144
152
|
var status_msg = 'SSH Server :: bound on ' + local_port;
|
|
@@ -149,7 +157,7 @@ class SSHTunnelProxy extends Client {
|
|
|
149
157
|
_this.emit('ssh_tunnel_ready', {});
|
|
150
158
|
if (_this._tunnelReadyTimeout) {
|
|
151
159
|
clearTimeout(this._tunnelReadyTimeout);
|
|
152
|
-
_this._tunnelReadyTimeout = undefined
|
|
160
|
+
_this._tunnelReadyTimeout = undefined;
|
|
153
161
|
}
|
|
154
162
|
resolve();
|
|
155
163
|
}
|
|
@@ -173,27 +181,24 @@ class SSHTunnelProxy extends Client {
|
|
|
173
181
|
reject(err);
|
|
174
182
|
return;
|
|
175
183
|
}
|
|
176
|
-
|
|
184
|
+
|
|
185
|
+
const onClose = () => {
|
|
177
186
|
var buf = Buffer.concat(bufs);
|
|
178
187
|
var err = Buffer.concat(errs);
|
|
188
|
+
stream.removeListener('close', onClose);
|
|
179
189
|
resolve(buf, err);
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
if (dataStream) {
|
|
183
|
-
stream.pipe(dataStream);
|
|
184
|
-
} else {
|
|
185
|
-
stream.on('data', (data) => {
|
|
186
|
-
bufs.push(data);
|
|
187
|
-
})
|
|
188
190
|
}
|
|
191
|
+
stream.on('close', onClose);
|
|
189
192
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
+
});
|
|
197
202
|
|
|
198
203
|
}
|
|
199
204
|
this.exec(cmd, remote_exec);
|
|
@@ -238,14 +243,10 @@ class SSHTunnelProxy extends Client {
|
|
|
238
243
|
return new Promise((resolve) => {
|
|
239
244
|
|
|
240
245
|
// exit if connection already established, avoid redundant connection on retry
|
|
241
|
-
if (_this.listener
|
|
246
|
+
if (_this.listener.isConnected) resolve();
|
|
242
247
|
|
|
243
248
|
// close open sockets on server, otherwise initialize open sockets storage
|
|
244
|
-
|
|
245
|
-
_this.close_sockets(opts.server_name, opts.proxy_ports);
|
|
246
|
-
} else {
|
|
247
|
-
_this.listener[opts.server_name] = {};
|
|
248
|
-
}
|
|
249
|
+
_this.close_sockets(opts.proxy_ports);
|
|
249
250
|
|
|
250
251
|
_this.on('ready', async () => {
|
|
251
252
|
|
|
@@ -255,47 +256,48 @@ class SSHTunnelProxy extends Client {
|
|
|
255
256
|
_this._tunnelReadyTimeout = undefined;
|
|
256
257
|
}
|
|
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
|
+
|
|
258
264
|
// if shell requested, enable remote terminal
|
|
259
265
|
if (opts.shell) {
|
|
260
266
|
_this.shell(_this.remote_shell);
|
|
261
267
|
|
|
262
|
-
|
|
268
|
+
// otherwise, if exec requested, exec series of cmds
|
|
263
269
|
} else {
|
|
264
270
|
if (opts.exec && opts.exec.length > 0) {
|
|
265
271
|
for (var i = 0; i < opts.exec.length; i++) {
|
|
266
272
|
var cmd = opts.exec[i];
|
|
267
|
-
|
|
273
|
+
_this.debug_en && _this.debug(opts.username + '@' + opts.hostname + ':' + cmd);
|
|
268
274
|
var result = await _this.execCmd(cmd);
|
|
269
|
-
|
|
275
|
+
_this.debug_en && _this.debug(result.toString());
|
|
270
276
|
}
|
|
271
277
|
process.exit();
|
|
272
278
|
}
|
|
273
279
|
}
|
|
274
280
|
|
|
275
|
-
// if local forward requested, setup local forwarding ports to remote host
|
|
276
|
-
if (opts.proxy_ports) {
|
|
277
|
-
await _this.setupProxyPorts(opts.server_name, opts.proxy_ports);
|
|
278
|
-
}
|
|
279
281
|
resolve();
|
|
280
282
|
});
|
|
281
283
|
|
|
282
284
|
_this.on('end', () => {
|
|
283
285
|
_this.debug_en && _this.debug('SSH Client :: end');
|
|
284
|
-
_this.close_sockets(opts.
|
|
286
|
+
_this.close_sockets(opts.proxy_ports);
|
|
285
287
|
});
|
|
286
288
|
|
|
287
289
|
_this.on('close', () => {
|
|
288
290
|
_this.debug_en && _this.debug('SSH Client :: close');
|
|
289
|
-
_this.close_sockets(opts.
|
|
291
|
+
_this.close_sockets(opts.proxy_ports);
|
|
290
292
|
});
|
|
291
293
|
|
|
292
294
|
_this.on('error', err => {
|
|
293
295
|
_this.debug_en && _this.debug('SSH Client :: error :: ' + err);
|
|
294
296
|
_this.emit('debug', err);
|
|
295
|
-
|
|
297
|
+
_this.listener.isConnected = false;
|
|
296
298
|
if (_this._tunnelReadyTimeout) {
|
|
297
|
-
clearTimeout(this._tunnelReadyTimeout);
|
|
298
|
-
_this._tunnelReadyTimeout = undefined
|
|
299
|
+
clearTimeout(this._tunnelReadyTimeout.elReadyTimeout);
|
|
300
|
+
_this._tunnelReadyTimeout = undefined;
|
|
299
301
|
}
|
|
300
302
|
_this.ssh_retry_connect(opts);
|
|
301
303
|
resolve();
|
|
@@ -309,19 +311,19 @@ class SSHTunnelProxy extends Client {
|
|
|
309
311
|
_this.debug_en && _this.debug('SSH Client :: greeting:', message);
|
|
310
312
|
});
|
|
311
313
|
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
username: opts.username,
|
|
319
|
-
password: opts.password,
|
|
320
|
-
privateKey: opts.private_key,
|
|
321
|
-
debug: debug_client,
|
|
322
|
-
keepaliveInterval: 10000
|
|
323
|
-
});
|
|
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;
|
|
324
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);
|
|
325
327
|
});
|
|
326
328
|
}
|
|
327
329
|
|
|
@@ -330,7 +332,7 @@ class SSHTunnelProxy extends Client {
|
|
|
330
332
|
if (isNaN(port_str)) return false;
|
|
331
333
|
const port = parseInt(port_str);
|
|
332
334
|
if (port < 1 || port > 65535) return false;
|
|
333
|
-
if (port < 1024 && whitelist[port] === undefined) return false;
|
|
335
|
+
if (port < 1024 && whitelist && whitelist[port] === undefined) return false;
|
|
334
336
|
return true;
|
|
335
337
|
}
|
|
336
338
|
|
|
@@ -396,13 +398,11 @@ class SSHTunnelProxy extends Client {
|
|
|
396
398
|
// todo: if opts have changed while service is running, shutdown current service and restart with new opts
|
|
397
399
|
async connectSSH(opts, whitelist) {
|
|
398
400
|
|
|
399
|
-
this.debug_en && this.debug('connectSSH:\n', JSON.stringify(opts, null, 2));
|
|
400
|
-
|
|
401
401
|
// make deep copy of opts for modification
|
|
402
402
|
var _opts = JSON.parse(JSON.stringify(opts));
|
|
403
403
|
|
|
404
404
|
// setup whitelist from param or opts in case of error retry
|
|
405
|
-
_opts.whitelist = (whitelist !== undefined) ? whitelist : (_opts.whitelist) ? _opts.whitelist :
|
|
405
|
+
_opts.whitelist = (whitelist !== undefined) ? whitelist : (_opts.whitelist) ? _opts.whitelist : null;
|
|
406
406
|
|
|
407
407
|
// validate local port forwards, emit error and quit if invalid
|
|
408
408
|
try {
|
|
@@ -418,9 +418,24 @@ class SSHTunnelProxy extends Client {
|
|
|
418
418
|
_opts.password = undefined;
|
|
419
419
|
}
|
|
420
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
|
+
|
|
421
432
|
// retrieve private key from system keychain with supplied service name and server
|
|
422
433
|
// if no key found, authentication is supplied username, password
|
|
423
|
-
|
|
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
|
+
}
|
|
424
439
|
|
|
425
440
|
// if ngrok api key provided, obtain hostname and hostport from ngrok api
|
|
426
441
|
// if no ngrok tunnel specified, use supplied host and port
|
|
@@ -430,7 +445,7 @@ class SSHTunnelProxy extends Client {
|
|
|
430
445
|
var hostport = await ngrokApi.get_hostport()
|
|
431
446
|
.catch(() => {
|
|
432
447
|
_this.debug_en && _this.debug('ngrok get_hostport connection error');
|
|
433
|
-
|
|
448
|
+
_this.listener.isConnected = false;
|
|
434
449
|
_this.ssh_retry_connect(_opts);
|
|
435
450
|
});
|
|
436
451
|
if (hostport && hostport.host) {
|
|
@@ -440,6 +455,8 @@ class SSHTunnelProxy extends Client {
|
|
|
440
455
|
else return null;
|
|
441
456
|
}
|
|
442
457
|
|
|
458
|
+
this.debug_en && this.debug('connectSSH:\n', JSON.stringify(_opts, null, 2));
|
|
459
|
+
|
|
443
460
|
// initiate ssh tunnel, block until tunnel is established or error
|
|
444
461
|
return await this.ssh_start_tunnel(_opts)
|
|
445
462
|
.catch((err) => {
|
package/package.json
CHANGED
package/{main.js → ssh2-node}
RENAMED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/node
|
|
1
2
|
const process = require('process');
|
|
2
3
|
const fs = require('fs');
|
|
3
4
|
const { SSHTunnelProxy, KeypairStorage } = require('./lib/index.js');
|
|
@@ -21,14 +22,6 @@ var service = null;
|
|
|
21
22
|
var account = null;
|
|
22
23
|
var shell = null;
|
|
23
24
|
|
|
24
|
-
function get_key_from_file(filename) {
|
|
25
|
-
try {
|
|
26
|
-
return fs.readFileSync(filename);
|
|
27
|
-
} catch (err) {
|
|
28
|
-
console.log('Error loading key:', err);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
25
|
function get_config(filename) {
|
|
33
26
|
try {
|
|
34
27
|
configs = JSON.parse(fs.readFileSync(filename), 'utf8');
|
|
@@ -41,7 +34,7 @@ function get_default_config() {
|
|
|
41
34
|
try {
|
|
42
35
|
configs = JSON.parse(fs.readFileSync(homedir + '/.config/ssh_tunnel_proxy/config.json'), 'utf8');
|
|
43
36
|
} catch (err) {
|
|
44
|
-
console.log('Error loading config file:', err);
|
|
37
|
+
//console.log('Error loading config file:', err);
|
|
45
38
|
}
|
|
46
39
|
}
|
|
47
40
|
|
|
@@ -114,15 +107,21 @@ function main(args) {
|
|
|
114
107
|
username = value;
|
|
115
108
|
break;
|
|
116
109
|
default:
|
|
117
|
-
if (i
|
|
110
|
+
if (i == 2) remote_host = cmd;
|
|
111
|
+
if (i > 2) console.log('Invalid argument:', arg);
|
|
118
112
|
}
|
|
119
113
|
}
|
|
120
114
|
|
|
121
115
|
// prevent process from exiting
|
|
122
|
-
process.stdin.resume();
|
|
116
|
+
//process.stdin.resume();
|
|
123
117
|
|
|
124
|
-
// if no config file specified
|
|
118
|
+
// if no config file specified attempt to load default config
|
|
125
119
|
if (!configs) {
|
|
120
|
+
get_default_config();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// no default config build config from args
|
|
124
|
+
if (!remote_host) {
|
|
126
125
|
var config = {
|
|
127
126
|
enabled: true,
|
|
128
127
|
username: username,
|
|
@@ -140,24 +139,28 @@ function main(args) {
|
|
|
140
139
|
}
|
|
141
140
|
|
|
142
141
|
// process each tunnel config in ssh proxy tunnel list
|
|
142
|
+
var remote_server = null;
|
|
143
143
|
configs.forEach(async config => {
|
|
144
144
|
|
|
145
145
|
// skip tunnel if not enabled
|
|
146
|
-
if (
|
|
146
|
+
if (config.disabled) return;
|
|
147
147
|
|
|
148
148
|
// if remote host specified, only select tunnel for remote host
|
|
149
|
-
if (remote_host && remote_host !== config.
|
|
149
|
+
if (remote_host && remote_host !== config.hostname) return;
|
|
150
|
+
config.server_name = config.hostname || remote_host || 'ssh_tunnel_proxy';
|
|
151
|
+
remote_server = remote_host;
|
|
150
152
|
|
|
151
153
|
// if system key storage name specified get key from system keychain
|
|
152
154
|
if (config.server_name) {
|
|
153
155
|
const service_name = config.service_name || 'ssh_tunnel_proxy';
|
|
154
156
|
config.private_key = await keypairStorage.get_keypair(service_name, config.server_name);
|
|
155
157
|
}
|
|
158
|
+
|
|
156
159
|
// if private key filename specified, read private key
|
|
157
|
-
|
|
158
|
-
config.
|
|
160
|
+
if (privateKey) {
|
|
161
|
+
config.private_key_filename = privateKey;
|
|
159
162
|
}
|
|
160
|
-
if (shell) {
|
|
163
|
+
if (shell || exec.length === 0) {
|
|
161
164
|
config.shell = true;
|
|
162
165
|
}
|
|
163
166
|
if (exec.length > 0) {
|
|
@@ -169,7 +172,9 @@ function main(args) {
|
|
|
169
172
|
sshTunnelProxy.debug_en = debug;
|
|
170
173
|
sshTunnelProxy.connectSSH(config);
|
|
171
174
|
});
|
|
172
|
-
|
|
175
|
+
if (remote_host && !remote_server) {
|
|
176
|
+
console.log(`Remote host ${remote_host} not found`);
|
|
177
|
+
}
|
|
173
178
|
|
|
174
179
|
}
|
|
175
180
|
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/*
|
|
2
|
+
test exec cmd on remote server, parse to json object stream
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
//const assert = require('assert');
|
|
6
|
+
//const { describe, it } = require('mocha');
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const process = require('node:process');
|
|
10
|
+
const { SSHTunnelProxy } = require('..');
|
|
11
|
+
const { PassThrough, pipeline } = require("stream");
|
|
12
|
+
const through = require('through');
|
|
13
|
+
const split = require('split');
|
|
14
|
+
|
|
15
|
+
// get config file containing api keys
|
|
16
|
+
const homedir = require('os').homedir();
|
|
17
|
+
var config = null;
|
|
18
|
+
try {
|
|
19
|
+
config = JSON.parse(fs.readFileSync(homedir + '/.config/ssh_tunnel_proxy/config.json'), 'utf8');
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.log('Error reading config:' + err);
|
|
22
|
+
process.exit();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!config && !config[1]) {
|
|
26
|
+
console.log('Specified config not found in configs');
|
|
27
|
+
process.exit();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const opts = config[1];
|
|
31
|
+
|
|
32
|
+
const sshTunnelProxy = new SSHTunnelProxy();
|
|
33
|
+
|
|
34
|
+
// parse shell command result into array of strings
|
|
35
|
+
function parse_cmd(str) {
|
|
36
|
+
|
|
37
|
+
let result = [];
|
|
38
|
+
|
|
39
|
+
let regex = /(([\w-/_~.\:\[\]]+)|("(.*?)")|('(.*?)'))/g;
|
|
40
|
+
let groups = [2, 4, 6];
|
|
41
|
+
let match;
|
|
42
|
+
|
|
43
|
+
while ((match = regex.exec(str)) !== null) {
|
|
44
|
+
// This is necessary to avoid infinite loops
|
|
45
|
+
// with zero-width matches
|
|
46
|
+
if (match.index === regex.lastIndex) {
|
|
47
|
+
regex.lastIndex++;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// For this to work the regex groups need to
|
|
51
|
+
// be mutually exclusive
|
|
52
|
+
groups.forEach(function (group) {
|
|
53
|
+
if (match[group]) {
|
|
54
|
+
result.push(match[group]);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
/*
|
|
58
|
+
let log_matches = false;
|
|
59
|
+
// show matches for debugging
|
|
60
|
+
log_matches && match.forEach(function (m, group) {
|
|
61
|
+
if (m) {
|
|
62
|
+
console.log(`Match '${m}' found in group: ${group}`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
*/
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// generate object from arrays of names/values
|
|
71
|
+
function toObj(names, values) {
|
|
72
|
+
const obj = {};
|
|
73
|
+
for (var i = 0; i < names.length; i++) {
|
|
74
|
+
obj[names[i]] = values[i];
|
|
75
|
+
}
|
|
76
|
+
return obj;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
// merge array items beginning at start into single item and append back to array
|
|
81
|
+
function mergeArray(ar, start) {
|
|
82
|
+
const ar2 = ar.slice(start);
|
|
83
|
+
const ar1 = ar.slice(0, start);
|
|
84
|
+
const item = ar2.join(' ');
|
|
85
|
+
ar1.push(item);
|
|
86
|
+
return ar1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/*
|
|
90
|
+
Feb 4 21:34:17 tim-ThinkPad-P50s systemd[1]: Started Run anacron jobs.
|
|
91
|
+
*/
|
|
92
|
+
function parseSyslog(line) {
|
|
93
|
+
const format = ['M', 'D', 'T', 'host', 'proc', 'msg'];
|
|
94
|
+
const values = parse_cmd(line);
|
|
95
|
+
const log = mergeArray(values, 5);
|
|
96
|
+
const obj = toObj(format, log);
|
|
97
|
+
if (obj.M && obj.D && obj.T) {
|
|
98
|
+
obj.date = obj.M + ' ' + obj.D + ' ' + obj.T;
|
|
99
|
+
delete obj.M;
|
|
100
|
+
delete obj.D;
|
|
101
|
+
delete obj.T;
|
|
102
|
+
}
|
|
103
|
+
return obj;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// parse ls long line output into json object
|
|
107
|
+
function processLSLong(line) {
|
|
108
|
+
const format10 = ['pm', 'links', 'user', 'group', 'size', 'M', 'D', 'H', 'MM', 'name'];
|
|
109
|
+
const format9 = ['pm', 'links', 'user', 'group', 'size', 'M', 'D', 'Y', 'name'];
|
|
110
|
+
const values = parse_cmd(line);
|
|
111
|
+
const obj = toObj((values.length < 10) ? format9 : format10, values);
|
|
112
|
+
if (obj.pm) {
|
|
113
|
+
obj.type = obj.pm.substring(0, 1);
|
|
114
|
+
obj.pm = obj.pm.substring(1);
|
|
115
|
+
}
|
|
116
|
+
if (obj.M && obj.D) {
|
|
117
|
+
obj.date = obj.M + ' ' + obj.D;
|
|
118
|
+
delete obj.M;
|
|
119
|
+
delete obj.D;
|
|
120
|
+
if (obj.Y) {
|
|
121
|
+
obj.date += ' ' + obj.Y;
|
|
122
|
+
delete obj.Y;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (obj.H && obj.MM) {
|
|
126
|
+
obj.time = obj.H + ':' + obj.MM;
|
|
127
|
+
delete obj.H;
|
|
128
|
+
delete obj.MM;
|
|
129
|
+
}
|
|
130
|
+
return obj;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function to_lsParse() {
|
|
134
|
+
return new through(function (data) {
|
|
135
|
+
const obj = processLSLong(data);
|
|
136
|
+
if (obj.name) this.queue(obj);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function to_syslogParse() {
|
|
141
|
+
return new through(function (data) {
|
|
142
|
+
const obj = parseSyslog(data);
|
|
143
|
+
if (obj.proc) this.queue(obj);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function to_JSONString() {
|
|
148
|
+
return new through(function (data) {
|
|
149
|
+
this.queue(JSON.stringify(data) + '\n');
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// lsLongShellProc conversion tests
|
|
154
|
+
async function lsTest(sshTunnelProxy) {
|
|
155
|
+
return new Promise(async (resolve) => {
|
|
156
|
+
|
|
157
|
+
// exec remote command and pipe data through tunnel until end of data
|
|
158
|
+
const tunnel = new PassThrough();
|
|
159
|
+
pipeline(tunnel,
|
|
160
|
+
split(),
|
|
161
|
+
to_lsParse(),
|
|
162
|
+
to_JSONString(),
|
|
163
|
+
process.stdout,
|
|
164
|
+
() => { }
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const lscmd = 'ls -all';
|
|
168
|
+
console.log('\ninvoking ' + lscmd + ' on remote host:\n');
|
|
169
|
+
await sshTunnelProxy.execCmd(lscmd, tunnel);
|
|
170
|
+
|
|
171
|
+
// stream processing complete
|
|
172
|
+
console.log('ls -all completed');
|
|
173
|
+
resolve();
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// lsLongShellProc conversion tests
|
|
178
|
+
async function syslogTest(sshTunnelProxy) {
|
|
179
|
+
return new Promise(async (resolve) => {
|
|
180
|
+
|
|
181
|
+
// exec remote command and pipe data through tunnel until end of data
|
|
182
|
+
const tunnel = new PassThrough();
|
|
183
|
+
pipeline(tunnel,
|
|
184
|
+
split(),
|
|
185
|
+
to_syslogParse(),
|
|
186
|
+
to_JSONString(),
|
|
187
|
+
process.stdout,
|
|
188
|
+
() => { }
|
|
189
|
+
);
|
|
190
|
+
const syslogcmd = 'tail /var/log/syslog';
|
|
191
|
+
console.log('\ninvoking ' + syslogcmd + ' on remote host:\n');
|
|
192
|
+
await sshTunnelProxy.execCmd(syslogcmd, tunnel);
|
|
193
|
+
|
|
194
|
+
// stream processing complete
|
|
195
|
+
console.log(syslogcmd+' completed');
|
|
196
|
+
resolve();
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function runTests() {
|
|
201
|
+
|
|
202
|
+
// connect to remote host
|
|
203
|
+
sshTunnelProxy.debug_en = true;
|
|
204
|
+
await sshTunnelProxy.connectSSH(opts);
|
|
205
|
+
|
|
206
|
+
// invoke ls -all on remote host and parse result to json object string
|
|
207
|
+
await lsTest(sshTunnelProxy);
|
|
208
|
+
|
|
209
|
+
// invoke tail /var/log/syslog on remote host and parse result to json object string
|
|
210
|
+
await syslogTest(sshTunnelProxy);
|
|
211
|
+
|
|
212
|
+
process.exit();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
runTests();
|
|
216
|
+
|
|
217
|
+
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
|
|
3
|
-
test remote exec streams
|
|
4
|
-
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
//const assert = require('assert');
|
|
8
|
-
//const { describe, it } = require('mocha');
|
|
9
|
-
const process = require('process');
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const { PassThrough } = require("stream");
|
|
12
|
-
const through = require('through');
|
|
13
|
-
const split = require('split');
|
|
14
|
-
|
|
15
|
-
const { SSHTunnelProxy } = require('../lib/index.js');
|
|
16
|
-
|
|
17
|
-
// get config file containing api keys
|
|
18
|
-
const homedir = require('os').homedir();
|
|
19
|
-
var config = null;
|
|
20
|
-
try {
|
|
21
|
-
config = JSON.parse(fs.readFileSync(homedir + '/.config/ssh_tunnel_proxy/config.json'), 'utf8');
|
|
22
|
-
} catch (err) {
|
|
23
|
-
console.log('Error reading config');
|
|
24
|
-
process.exit();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (!config && !config[1]) {
|
|
28
|
-
console.log('Specified config not found in configs');
|
|
29
|
-
process.exit();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const opts = config[1];
|
|
33
|
-
|
|
34
|
-
const sshTunnelProxy = new SSHTunnelProxy();
|
|
35
|
-
|
|
36
|
-
// parse shell command result into array of strings
|
|
37
|
-
function parse_cmd(str) {
|
|
38
|
-
|
|
39
|
-
let result = [];
|
|
40
|
-
let log_matches = false;
|
|
41
|
-
|
|
42
|
-
let regex = /(([\w-/_~]+)|("(.*?)")|('(.*?)'))/g;
|
|
43
|
-
let groups = [2, 4, 6];
|
|
44
|
-
let match;
|
|
45
|
-
|
|
46
|
-
while ((match = regex.exec(str)) !== null) {
|
|
47
|
-
// This is necessary to avoid infinite loops
|
|
48
|
-
// with zero-width matches
|
|
49
|
-
if (match.index === regex.lastIndex) {
|
|
50
|
-
regex.lastIndex++;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// For this to work the regex groups need to
|
|
54
|
-
// be mutually exclusive
|
|
55
|
-
groups.forEach(function (group) {
|
|
56
|
-
if (match[group]) {
|
|
57
|
-
result.push(match[group]);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// show matches for debugging
|
|
62
|
-
log_matches && match.forEach(function (m, group) {
|
|
63
|
-
if (m) {
|
|
64
|
-
console.log(`Match '${m}' found in group: ${group}`);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
return result;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// generate object from arrays of names/values
|
|
72
|
-
function toObj(names, values) {
|
|
73
|
-
const obj = {};
|
|
74
|
-
for (var i = 0; i < names.length; i++) {
|
|
75
|
-
obj[names[i]] = values[i];
|
|
76
|
-
}
|
|
77
|
-
return obj;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// stream that receives single line of ls long data and converts to json object
|
|
81
|
-
const to_ls_JSON = new through(function (data) {
|
|
82
|
-
const file = processLSLong(data);
|
|
83
|
-
if (file.name) this.queue(file);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// stream that receives objects and converts to JSON strings
|
|
87
|
-
const to_JSON_string = new through(function (data) {
|
|
88
|
-
this.queue(JSON.stringify(data) + '\n');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// parse ls long line output into json object
|
|
92
|
-
function processLSLong(line) {
|
|
93
|
-
const format10 = ['pm', 'links', 'user', 'group', 'size', 'M', 'D', 'H', 'MM', 'name'];
|
|
94
|
-
const format9 = ['pm', 'links', 'user', 'group', 'size', 'M', 'D', 'Y', 'name'];
|
|
95
|
-
const values = parse_cmd(line);
|
|
96
|
-
const obj = toObj((values.length < 10) ? format9 : format10, values);
|
|
97
|
-
if (obj.pm) {
|
|
98
|
-
obj.type = obj.pm.substring(0, 1);
|
|
99
|
-
obj.pm = obj.pm.substring(1);
|
|
100
|
-
}
|
|
101
|
-
if (obj.M && obj.D) {
|
|
102
|
-
obj.date = obj.M + ' ' + obj.D;
|
|
103
|
-
delete obj.M;
|
|
104
|
-
delete obj.D;
|
|
105
|
-
if (obj.Y) {
|
|
106
|
-
obj.date += ' ' + obj.Y;
|
|
107
|
-
delete obj.Y;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (obj.H && obj.MM) {
|
|
111
|
-
obj.time = obj.H + ':' + obj.MM;
|
|
112
|
-
delete obj.H;
|
|
113
|
-
delete obj.MM;
|
|
114
|
-
}
|
|
115
|
-
return obj;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function test_exec_stream() {
|
|
119
|
-
sshTunnelProxy.debug_en = true;
|
|
120
|
-
await sshTunnelProxy.connectSSH(opts);
|
|
121
|
-
|
|
122
|
-
// exec remote command and save result to string
|
|
123
|
-
const uptime_result = await sshTunnelProxy.execCmd('uptime');
|
|
124
|
-
console.log('uptime:' + uptime_result.toString());
|
|
125
|
-
|
|
126
|
-
// exec remote command and pipe data through tunnel until no data
|
|
127
|
-
const tunnel = new PassThrough();
|
|
128
|
-
tunnel.pipe(split())
|
|
129
|
-
.pipe(to_ls_JSON)
|
|
130
|
-
.pipe(to_JSON_string)
|
|
131
|
-
.pipe(process.stdout);
|
|
132
|
-
|
|
133
|
-
await sshTunnelProxy.execCmd('ls -all', tunnel);
|
|
134
|
-
|
|
135
|
-
// stream processing complete
|
|
136
|
-
console.log('done');
|
|
137
|
-
process.exit();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
test_exec_stream();
|