ssh_tunnel_proxy 1.2.8 → 1.2.10
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 +43 -0
- package/lib/index.js +65 -62
- package/package.json +4 -2
- package/test/test_remote_exec.js +219 -0
package/README.md
CHANGED
|
@@ -62,6 +62,49 @@ Start a terminal session on the remote host:
|
|
|
62
62
|
node main.js -c -S
|
|
63
63
|
```
|
|
64
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.js:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
// lsLongShellProc to json stream test
|
|
69
|
+
async function lsTest(sshTunnelProxy) {
|
|
70
|
+
return new Promise(async (resolve) => {
|
|
71
|
+
|
|
72
|
+
// exec remote command and pipe data through tunnel until end of data
|
|
73
|
+
const tunnel = new PassThrough();
|
|
74
|
+
pipeline(tunnel,
|
|
75
|
+
split(),
|
|
76
|
+
to_lsParse(),
|
|
77
|
+
to_JSONString(),
|
|
78
|
+
process.stdout,
|
|
79
|
+
() => { }
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const lscmd = 'ls -all';
|
|
83
|
+
console.log('\ninvoking ' + lscmd + ' on remote host:\n');
|
|
84
|
+
await sshTunnelProxy.execCmd(lscmd, tunnel);
|
|
85
|
+
|
|
86
|
+
// stream processing complete
|
|
87
|
+
console.log('ls -all completed');
|
|
88
|
+
resolve();
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function runTests() {
|
|
93
|
+
|
|
94
|
+
const sshTunnelProxy = new SSHTunnelProxy();
|
|
95
|
+
|
|
96
|
+
// connect to remote host
|
|
97
|
+
await sshTunnelProxy.connectSSH(opts);
|
|
98
|
+
|
|
99
|
+
// invoke ls -all on remote host and parse result to json object string
|
|
100
|
+
await lsTest(sshTunnelProxy);
|
|
101
|
+
|
|
102
|
+
process.exit();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
runTests();
|
|
106
|
+
```
|
|
107
|
+
|
|
65
108
|
The following code is an example of use of the api with electronjs.
|
|
66
109
|
|
|
67
110
|
main.js:
|
package/lib/index.js
CHANGED
|
@@ -40,7 +40,7 @@ class SSHTunnelProxy extends Client {
|
|
|
40
40
|
// function to close all active sockets for tunnel restart and exception handling
|
|
41
41
|
close_sockets(server_name, proxy_ports) {
|
|
42
42
|
|
|
43
|
-
if (!this.listener[server_name]) return;
|
|
43
|
+
if (!this.listener[server_name] || !proxy_ports) return;
|
|
44
44
|
|
|
45
45
|
proxy_ports.forEach(proxy_port => {
|
|
46
46
|
const server = this.listener[server_name][proxy_port];
|
|
@@ -102,63 +102,72 @@ class SSHTunnelProxy extends Client {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
// function to setup proxy forwards for ssh tunnel
|
|
105
|
-
|
|
105
|
+
setupProxyPorts(server_name, proxy_ports) {
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
|
|
109
|
+
if (!proxy_ports) {
|
|
110
|
+
resolve();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
110
113
|
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
var listeners = 0;
|
|
115
|
+
if (this.listener[server_name]) this.listener[server_name].isConnected = true;
|
|
116
|
+
const _this = this;
|
|
113
117
|
|
|
114
|
-
|
|
118
|
+
// iterate through a list of local forward ports and create a local proxy port
|
|
119
|
+
proxy_ports.forEach(proxy_port => {
|
|
115
120
|
|
|
116
|
-
|
|
117
|
-
const server = this.listener[server_name][proxy_port];
|
|
118
|
-
if (server && server.listening) {
|
|
119
|
-
this.debug_en && this.debug('SSH Server :: closing forward 2:', proxy_port)
|
|
120
|
-
server.close();
|
|
121
|
-
}
|
|
122
|
-
this.listener[server_name][proxy_port] = null;
|
|
123
|
-
this.listener[server_name][proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
|
|
121
|
+
const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
|
|
124
122
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
// create local socket server
|
|
124
|
+
const server = this.listener[server_name][proxy_port];
|
|
125
|
+
if (server && server.listening) {
|
|
126
|
+
this.debug_en && this.debug('SSH Server :: closing forward 2:', proxy_port)
|
|
127
|
+
server.close();
|
|
129
128
|
}
|
|
129
|
+
this.listener[server_name][proxy_port] = null;
|
|
130
|
+
this.listener[server_name][proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
if (this.debug_en) {
|
|
133
|
+
var debug_msg = 'SSH Server :: connection on ' + local_port + ' ' + socket.remotePort;
|
|
134
|
+
_this.emit('debug', debug_msg);
|
|
135
|
+
_this.debug(debug_msg);
|
|
136
|
+
}
|
|
134
137
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
_this.emit('debug', status_msg);
|
|
139
|
-
_this.listener[server_name][proxy_port].listen(local_port, () => {
|
|
138
|
+
// create a proxy forward between local and remote ports
|
|
139
|
+
_this.setup_ssh_forward(socket, remote_hostname, remote_port);
|
|
140
|
+
});
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
// start listening on port
|
|
143
|
+
try {
|
|
144
|
+
var status_msg = 'SSH Server :: before listen on ' + local_port;
|
|
143
145
|
_this.emit('debug', status_msg);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
146
|
+
_this.listener[server_name][proxy_port].listen(local_port, () => {
|
|
147
|
+
|
|
148
|
+
// emit server listening on port message
|
|
149
|
+
var status_msg = 'SSH Server :: bound on ' + local_port;
|
|
150
|
+
_this.emit('debug', status_msg);
|
|
151
|
+
|
|
152
|
+
// if all listeners have been successfully established, resolve setup connection
|
|
153
|
+
if (listeners++ >= proxy_ports.length - 1) {
|
|
154
|
+
_this.emit('ssh_tunnel_ready', {});
|
|
155
|
+
if (_this._tunnelReadyTimeout) {
|
|
156
|
+
clearTimeout(this._tunnelReadyTimeout);
|
|
157
|
+
_this._tunnelReadyTimeout = undefined
|
|
158
|
+
}
|
|
159
|
+
resolve();
|
|
151
160
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
161
|
+
});
|
|
162
|
+
} catch (err) {
|
|
163
|
+
_this.debug_en && _this.debug('listen err:', err);
|
|
164
|
+
reject(err);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
158
167
|
});
|
|
159
168
|
}
|
|
160
169
|
|
|
161
|
-
|
|
170
|
+
execCmd(cmd, dataStream, errStream) {
|
|
162
171
|
|
|
163
172
|
return new Promise((resolve, reject) => {
|
|
164
173
|
var bufs = [];
|
|
@@ -172,24 +181,19 @@ class SSHTunnelProxy extends Client {
|
|
|
172
181
|
stream.on('close', () => {
|
|
173
182
|
var buf = Buffer.concat(bufs);
|
|
174
183
|
var err = Buffer.concat(errs);
|
|
184
|
+
//stream.removeListener('exit', onExit);
|
|
175
185
|
resolve(buf, err);
|
|
176
186
|
})
|
|
177
187
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
bufs.push(data);
|
|
183
|
-
})
|
|
184
|
-
}
|
|
188
|
+
stream.on('data', (data) => {
|
|
189
|
+
if (dataStream) dataStream.write(data);
|
|
190
|
+
else bufs.push(data);
|
|
191
|
+
})
|
|
185
192
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
errs.push(data);
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
+
stream.stderr.on('data', (data) => {
|
|
194
|
+
if (errStream) errStream.write(data);
|
|
195
|
+
else errs.push(data);
|
|
196
|
+
});
|
|
193
197
|
|
|
194
198
|
}
|
|
195
199
|
this.exec(cmd, remote_exec);
|
|
@@ -261,7 +265,7 @@ class SSHTunnelProxy extends Client {
|
|
|
261
265
|
for (var i = 0; i < opts.exec.length; i++) {
|
|
262
266
|
var cmd = opts.exec[i];
|
|
263
267
|
console.log(opts.username + '@' + opts.hostname + ':' + cmd);
|
|
264
|
-
var result = await _this.
|
|
268
|
+
var result = await _this.execCmd(cmd);
|
|
265
269
|
console.log(result.toString());
|
|
266
270
|
}
|
|
267
271
|
process.exit();
|
|
@@ -270,10 +274,9 @@ class SSHTunnelProxy extends Client {
|
|
|
270
274
|
|
|
271
275
|
// if local forward requested, setup local forwarding ports to remote host
|
|
272
276
|
if (opts.proxy_ports) {
|
|
273
|
-
_this.
|
|
274
|
-
} else {
|
|
275
|
-
resolve();
|
|
277
|
+
await _this.setupProxyPorts(opts.server_name, opts.proxy_ports);
|
|
276
278
|
}
|
|
279
|
+
resolve();
|
|
277
280
|
});
|
|
278
281
|
|
|
279
282
|
_this.on('end', () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ssh_tunnel_proxy",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.10",
|
|
4
4
|
"description": "obtain ngrok host port from api and establish a ssh connection",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"net": "^1.0.2",
|
|
19
19
|
"ngrok": "^4.3.3",
|
|
20
20
|
"socksv5": "^0.0.6",
|
|
21
|
-
"
|
|
21
|
+
"split": "^1.0.1",
|
|
22
|
+
"sshpk": "^1.17.0",
|
|
23
|
+
"through": "^2.3.8"
|
|
22
24
|
},
|
|
23
25
|
"devDependencies": {
|
|
24
26
|
"eslint": "^8.33.0",
|
|
@@ -0,0 +1,219 @@
|
|
|
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
|
+
//const lscmd = 'ls -all | column --table --table-columns pm,links,user,group,size,month,day,time,name -J';
|
|
169
|
+
console.log('\ninvoking ' + lscmd + ' on remote host:\n');
|
|
170
|
+
await sshTunnelProxy.execCmd(lscmd, tunnel);
|
|
171
|
+
|
|
172
|
+
// stream processing complete
|
|
173
|
+
console.log('ls -all completed');
|
|
174
|
+
resolve();
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// lsLongShellProc conversion tests
|
|
179
|
+
async function syslogTest(sshTunnelProxy) {
|
|
180
|
+
return new Promise(async (resolve) => {
|
|
181
|
+
|
|
182
|
+
// exec remote command and pipe data through tunnel until end of data
|
|
183
|
+
const tunnel = new PassThrough();
|
|
184
|
+
pipeline(tunnel,
|
|
185
|
+
split(),
|
|
186
|
+
to_syslogParse(),
|
|
187
|
+
to_JSONString(),
|
|
188
|
+
process.stdout,
|
|
189
|
+
() => { }
|
|
190
|
+
);
|
|
191
|
+
//const syslogcmd = 'tail /var/log/syslog | column --table --table-columns-limit 6 --table-columns month,day,time,host,proc,msg -J';
|
|
192
|
+
const syslogcmd = 'tail /var/log/syslog';
|
|
193
|
+
console.log('\ninvoking ' + syslogcmd + ' on remote host:\n');
|
|
194
|
+
await sshTunnelProxy.execCmd(syslogcmd, tunnel);
|
|
195
|
+
|
|
196
|
+
// stream processing complete
|
|
197
|
+
console.log(syslogcmd+' completed');
|
|
198
|
+
resolve();
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function runTests() {
|
|
203
|
+
|
|
204
|
+
// connect to remote host
|
|
205
|
+
sshTunnelProxy.debug_en = true;
|
|
206
|
+
await sshTunnelProxy.connectSSH(opts);
|
|
207
|
+
|
|
208
|
+
// invoke ls -all on remote host and parse result to json object string
|
|
209
|
+
await lsTest(sshTunnelProxy);
|
|
210
|
+
|
|
211
|
+
// invoke tail /var/log/syslog on remote host and parse result to json object string
|
|
212
|
+
await syslogTest(sshTunnelProxy);
|
|
213
|
+
|
|
214
|
+
process.exit();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
runTests();
|
|
218
|
+
|
|
219
|
+
|