ssh_tunnel_proxy 1.2.7 → 1.2.9
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 +1 -1
- package/README.md +30 -0
- package/lib/index.js +96 -67
- package/main.js +8 -0
- package/package.json +4 -2
- package/test/test_remote_exec_streams.js +140 -0
package/.vscode/launch.json
CHANGED
package/README.md
CHANGED
|
@@ -55,7 +55,37 @@ node main.js -c
|
|
|
55
55
|
],
|
|
56
56
|
"ngrok_api": "<ngrok api key>"
|
|
57
57
|
}]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Start a terminal session on the remote host:
|
|
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:
|
|
58
66
|
|
|
67
|
+
```js
|
|
68
|
+
async function test_exec_stream(opts) {
|
|
69
|
+
sshTunnelProxy.debug_en = true;
|
|
70
|
+
await sshTunnelProxy.connectSSH(opts);
|
|
71
|
+
|
|
72
|
+
// exec remote command and save result to string
|
|
73
|
+
const uptime_result = await sshTunnelProxy.execCmd('uptime');
|
|
74
|
+
console.log('uptime:' + uptime_result.toString());
|
|
75
|
+
|
|
76
|
+
// exec remote command and pipe data through tunnel until end of data
|
|
77
|
+
const tunnel = new PassThrough();
|
|
78
|
+
tunnel.pipe(split())
|
|
79
|
+
.pipe(to_ls_JSON)
|
|
80
|
+
.pipe(to_JSON_string)
|
|
81
|
+
.pipe(process.stdout);
|
|
82
|
+
|
|
83
|
+
await sshTunnelProxy.execCmd('ls -all', tunnel);
|
|
84
|
+
|
|
85
|
+
// stream processing complete
|
|
86
|
+
console.log('done');
|
|
87
|
+
process.exit();
|
|
88
|
+
}
|
|
59
89
|
```
|
|
60
90
|
|
|
61
91
|
The following code is an example of use of the api with electronjs.
|
package/lib/index.js
CHANGED
|
@@ -18,6 +18,7 @@ License: MIT
|
|
|
18
18
|
|
|
19
19
|
const net = require('net');
|
|
20
20
|
const process = require('process');
|
|
21
|
+
const { Buffer } = require('node:buffer');
|
|
21
22
|
const { Client } = require('electron-ssh2');
|
|
22
23
|
|
|
23
24
|
const KeypairStorage = require('./keypair_storage');
|
|
@@ -101,83 +102,102 @@ class SSHTunnelProxy extends Client {
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
// function to setup proxy forwards for ssh tunnel
|
|
104
|
-
|
|
105
|
+
setupProxyPorts(server_name, proxy_ports) {
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
if (this.listener[server_name]) this.listener[server_name].isConnected = true;
|
|
108
|
-
const _this = this;
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
109
108
|
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
var listeners = 0;
|
|
110
|
+
if (this.listener[server_name]) this.listener[server_name].isConnected = true;
|
|
111
|
+
const _this = this;
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
// iterate through a list of local forward ports and create a local proxy port
|
|
114
|
+
proxy_ports.forEach(proxy_port => {
|
|
114
115
|
|
|
115
|
-
|
|
116
|
-
const server = this.listener[server_name][proxy_port];
|
|
117
|
-
if (server && server.listening) {
|
|
118
|
-
this.debug_en && this.debug('SSH Server :: closing forward 2:', proxy_port)
|
|
119
|
-
server.close();
|
|
120
|
-
}
|
|
121
|
-
this.listener[server_name][proxy_port] = null;
|
|
122
|
-
this.listener[server_name][proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
|
|
116
|
+
const [local_port, remote_hostname, remote_port] = proxy_port.split(':');
|
|
123
117
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
118
|
+
// create local socket server
|
|
119
|
+
const server = this.listener[server_name][proxy_port];
|
|
120
|
+
if (server && server.listening) {
|
|
121
|
+
this.debug_en && this.debug('SSH Server :: closing forward 2:', proxy_port)
|
|
122
|
+
server.close();
|
|
128
123
|
}
|
|
124
|
+
this.listener[server_name][proxy_port] = null;
|
|
125
|
+
this.listener[server_name][proxy_port] = net.createServer({ keepAlive: true, allowHalfOpen: false }, socket => {
|
|
129
126
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
if (this.debug_en) {
|
|
128
|
+
var debug_msg = 'SSH Server :: connection on ' + local_port + ' ' + socket.remotePort;
|
|
129
|
+
_this.emit('debug', debug_msg);
|
|
130
|
+
_this.debug(debug_msg);
|
|
131
|
+
}
|
|
133
132
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
_this.emit('debug', status_msg);
|
|
138
|
-
_this.listener[server_name][proxy_port].listen(local_port, () => {
|
|
133
|
+
// create a proxy forward between local and remote ports
|
|
134
|
+
_this.setup_ssh_forward(socket, remote_hostname, remote_port);
|
|
135
|
+
});
|
|
139
136
|
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
// start listening on port
|
|
138
|
+
try {
|
|
139
|
+
var status_msg = 'SSH Server :: before listen on ' + local_port;
|
|
142
140
|
_this.emit('debug', status_msg);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
141
|
+
_this.listener[server_name][proxy_port].listen(local_port, () => {
|
|
142
|
+
|
|
143
|
+
// emit server listening on port message
|
|
144
|
+
var status_msg = 'SSH Server :: bound on ' + local_port;
|
|
145
|
+
_this.emit('debug', status_msg);
|
|
146
|
+
|
|
147
|
+
// if all listeners have been successfully established, resolve setup connection
|
|
148
|
+
if (listeners++ >= proxy_ports.length - 1) {
|
|
149
|
+
_this.emit('ssh_tunnel_ready', {});
|
|
150
|
+
if (_this._tunnelReadyTimeout) {
|
|
151
|
+
clearTimeout(this._tunnelReadyTimeout);
|
|
152
|
+
_this._tunnelReadyTimeout = undefined
|
|
153
|
+
}
|
|
154
|
+
resolve();
|
|
150
155
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
156
|
+
});
|
|
157
|
+
} catch (err) {
|
|
158
|
+
_this.debug_en && _this.debug('listen err:', err);
|
|
159
|
+
reject(err);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
157
162
|
});
|
|
158
163
|
}
|
|
159
164
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
165
|
+
execCmd(cmd, dataStream, errStream) {
|
|
166
|
+
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
var bufs = [];
|
|
169
|
+
var errs = [];
|
|
170
|
+
|
|
171
|
+
const remote_exec = (err, stream) => {
|
|
172
|
+
if (err) {
|
|
173
|
+
reject(err);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
stream.on('close', () => {
|
|
177
|
+
var buf = Buffer.concat(bufs);
|
|
178
|
+
var err = Buffer.concat(errs);
|
|
179
|
+
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
|
+
}
|
|
189
|
+
|
|
190
|
+
if (errStream) {
|
|
191
|
+
stream.stderr.pipe(errStream);
|
|
192
|
+
} else {
|
|
193
|
+
stream.stderr.on('data', (data) => {
|
|
194
|
+
errs.push(data);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
}
|
|
177
199
|
this.exec(cmd, remote_exec);
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
return false;
|
|
200
|
+
})
|
|
181
201
|
}
|
|
182
202
|
|
|
183
203
|
// handle remote shell stream processing
|
|
@@ -227,7 +247,7 @@ class SSHTunnelProxy extends Client {
|
|
|
227
247
|
_this.listener[opts.server_name] = {};
|
|
228
248
|
}
|
|
229
249
|
|
|
230
|
-
_this.on('ready', () => {
|
|
250
|
+
_this.on('ready', async () => {
|
|
231
251
|
|
|
232
252
|
// stop any pending retry timeouts
|
|
233
253
|
if (_this._tunnelReadyTimeout) {
|
|
@@ -239,18 +259,24 @@ class SSHTunnelProxy extends Client {
|
|
|
239
259
|
if (opts.shell) {
|
|
240
260
|
_this.shell(_this.remote_shell);
|
|
241
261
|
|
|
242
|
-
|
|
262
|
+
// if exec requested, exec series of cmds
|
|
243
263
|
} else {
|
|
244
264
|
if (opts.exec && opts.exec.length > 0) {
|
|
245
|
-
|
|
246
|
-
|
|
265
|
+
for (var i = 0; i < opts.exec.length; i++) {
|
|
266
|
+
var cmd = opts.exec[i];
|
|
267
|
+
console.log(opts.username + '@' + opts.hostname + ':' + cmd);
|
|
268
|
+
var result = await _this.execCmd(cmd);
|
|
269
|
+
console.log(result.toString());
|
|
270
|
+
}
|
|
271
|
+
process.exit();
|
|
247
272
|
}
|
|
248
273
|
}
|
|
249
274
|
|
|
250
275
|
// if local forward requested, setup local forwarding ports to remote host
|
|
251
276
|
if (opts.proxy_ports) {
|
|
252
|
-
_this.
|
|
277
|
+
await _this.setupProxyPorts(opts.server_name, opts.proxy_ports);
|
|
253
278
|
}
|
|
279
|
+
resolve();
|
|
254
280
|
});
|
|
255
281
|
|
|
256
282
|
_this.on('end', () => {
|
|
@@ -310,6 +336,9 @@ class SSHTunnelProxy extends Client {
|
|
|
310
336
|
|
|
311
337
|
// validate local forwards for correct format and valid ports
|
|
312
338
|
validate_local_forward(proxy_ports, whitelist) {
|
|
339
|
+
|
|
340
|
+
if (!proxy_ports) return;
|
|
341
|
+
|
|
313
342
|
const local_ports = [];
|
|
314
343
|
const remote_ports = [];
|
|
315
344
|
proxy_ports.forEach(proxy_port => {
|
package/main.js
CHANGED
|
@@ -6,6 +6,7 @@ const keypairStorage = new KeypairStorage();
|
|
|
6
6
|
const homedir = require('os').homedir();
|
|
7
7
|
var debug = false;
|
|
8
8
|
var exec = [];
|
|
9
|
+
var remote_host = null;
|
|
9
10
|
var proxies = [];
|
|
10
11
|
var configs = null;
|
|
11
12
|
var host = null;
|
|
@@ -92,6 +93,10 @@ function main(args) {
|
|
|
92
93
|
case '--ngrok':
|
|
93
94
|
ngrokAPIKey = value;
|
|
94
95
|
break;
|
|
96
|
+
case '-r':
|
|
97
|
+
case '--remote':
|
|
98
|
+
remote_host = value;
|
|
99
|
+
break;
|
|
95
100
|
case '-R':
|
|
96
101
|
case '--RemoteForward':
|
|
97
102
|
forwardIn.push(value);
|
|
@@ -140,6 +145,9 @@ function main(args) {
|
|
|
140
145
|
// skip tunnel if not enabled
|
|
141
146
|
if (!config.enabled) return;
|
|
142
147
|
|
|
148
|
+
// if remote host specified, only select tunnel for remote host
|
|
149
|
+
if (remote_host && remote_host !== config.alias) return;
|
|
150
|
+
|
|
143
151
|
// if system key storage name specified get key from system keychain
|
|
144
152
|
if (config.server_name) {
|
|
145
153
|
const service_name = config.service_name || 'ssh_tunnel_proxy';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ssh_tunnel_proxy",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
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,140 @@
|
|
|
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();
|