More work on web based SSH support.
Ylian Saint-Hilaire committed
Apr 29, 2021 at 15:51 UTC
abbb6be431acce71544200663aa99778361b9f42
4 files changed
+132
-124
ssh.js
+68
-94
@@ -21,13 +21,12 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) {
21
var obj = {};
22
obj.domain = domain;
23
obj.ws = ws;
24
- obj.wsClient = null;
25
- obj.tcpServer = null;
26
- obj.tcpServerPort = 0;
24
obj.relaySocket = null;
25
obj.relayActive = false;
26
obj.infos = null;
30
- var sshClient = null;
27
+ obj.sshClient = null;
28
+ obj.sshShell = null;
29
+ obj.termSize = null;
30
31
parent.parent.debug('relay', 'SSH: Request for SSH relay (' + req.clientIp + ')');
32
@@ -35,102 +34,75 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) {
34
obj.close = function (arg) {
35
if ((arg == 1) || (arg == null)) { try { ws.close(); } catch (e) { console.log(e); } } // Soft close, close the websocket
36
if (arg == 2) { try { ws._socket._parent.end(); } catch (e) { console.log(e); } } // Hard close, close the TCP socket
38
- if (obj.wsClient) { obj.wsClient.close(); obj.wsClient = null; }
39
- if (obj.tcpServer) { obj.tcpServer.close(); obj.tcpServer = null; }
40
- if (sshClient) { sshClient.close(); sshClient = null; }
37
+ //if (obj.wsClient) { obj.wsClient.close(); obj.wsClient = null; }
38
+ //if (obj.tcpServer) { obj.tcpServer.close(); obj.tcpServer = null; }
39
+ //if (sshClient) { sshClient.close(); sshClient = null; }
40
+
41
+ if (obj.sshClient != null) {
42
+ try { obj.sshClient.end(); } catch (ex) { console.log(ex); }
43
+ delete obj.sshClient;
44
+ }
45
+ if (obj.sshShell != null) {
46
+ try { obj.sshShell.end(); } catch (ex) { console.log(ex); }
47
+ delete obj.sshShell;
48
+ }
49
+
50
delete obj.domain;
51
delete obj.ws;
52
};
53
45
- // Start the looppback server
46
- function startTcpServer() {
47
- obj.tcpServer = new Net.Server();
48
- obj.tcpServer.listen(0, '127.0.0.1', function () { obj.tcpServerPort = obj.tcpServer.address().port; startSSH(obj.tcpServerPort); });
49
- obj.tcpServer.on('connection', function (socket) {
50
- if (obj.relaySocket != null) {
51
- socket.close();
52
- } else {
53
- obj.relaySocket = socket;
54
- obj.relaySocket.pause();
55
- obj.relaySocket.on('data', function (chunk) { // Make sure to handle flow control.
56
- if (obj.relayActive == true) { obj.relaySocket.pause(); obj.wsClient.send(chunk, function () { obj.relaySocket.resume(); }); }
57
- });
58
- obj.relaySocket.on('end', function () { obj.close(); });
59
- obj.relaySocket.on('error', function (err) { obj.close(); });
60
-
61
- // Decode the authentication cookie
62
- var cookie = parent.parent.decodeCookie(obj.infos.ip, parent.parent.loginCookieEncryptionKey);
63
- if (cookie == null) return;
64
-
65
- // Setup the correct URL with domain and use TLS only if needed.
66
- var options = { rejectUnauthorized: false };
67
- if (domain.dns != null) { options.servername = domain.dns; }
68
- var protocol = 'wss';
69
- if (args.tlsoffload) { protocol = 'ws'; }
70
- var domainadd = '';
71
- if ((domain.dns == null) && (domain.id != '')) { domainadd = domain.id + '/' }
72
- var url = protocol + '://127.0.0.1:' + args.port + '/' + domainadd + ((cookie.lc == 1)?'local':'mesh') + 'relay.ashx?noping=1&auth=' + obj.infos.ip;
73
- parent.parent.debug('relay', 'SSH: Connection websocket to ' + url);
74
- obj.wsClient = new WebSocket(url, options);
75
- obj.wsClient.on('open', function () { parent.parent.debug('relay', 'SSH: Relay websocket open'); });
76
- obj.wsClient.on('message', function (data) { // Make sure to handle flow control.
77
- if ((obj.relayActive == false) && (data == 'c')) {
78
- obj.relayActive = true; obj.relaySocket.resume();
79
- } else {
80
- obj.wsClient._socket.pause();
81
- obj.relaySocket.write(data, function () { obj.wsClient._socket.resume(); });
82
- }
83
- });
84
- obj.wsClient.on('close', function () { parent.parent.debug('relay', 'SSH: Relay websocket closed'); obj.close(); });
85
- obj.wsClient.on('error', function (err) { parent.parent.debug('relay', 'SSH: Relay websocket error: ' + err); obj.close(); });
86
- obj.tcpServer.close();
87
- obj.tcpServer = null;
88
- }
89
- });
90
- }
91
-
92
- // Start the SSH client
93
- function startSSH(port) {
94
- parent.parent.debug('relay', 'SSH: Starting SSH client on loopback port ' + port);
95
- try {
96
- sshClient = require('node-rdpjs-2').createClient({
97
- logLevel: 'ERROR',
98
- domain: obj.infos.domain,
99
- userName: obj.infos.username,
100
- password: obj.infos.password,
101
- enablePerf: true,
102
- autoLogin: true,
103
- screen: obj.infos.screen,
104
- locale: obj.infos.locale
105
- }).on('connect', function () {
106
- send(['rdp-connect']);
107
- }).on('bitmap', function (bitmap) {
108
- try { ws.send(bitmap.data); } catch (ex) { } // Send the bitmap data as binary
109
- delete bitmap.data;
110
- send(['rdp-bitmap', bitmap]); // Send the bitmap metadata seperately, without bitmap data.
111
- }).on('close', function () {
112
- send(['rdp-close']);
113
- }).on('error', function (err) {
114
- send(['rdp-error', err]);
115
- }).connect('127.0.0.1', obj.tcpServerPort);
116
- } catch (ex) {
117
- console.log('startSshException', ex);
118
- obj.close();
119
- }
120
- }
121
-
54
// When data is received from the web socket
55
// SSH default port is 22
56
ws.on('message', function (msg) {
57
try {
126
- msg = JSON.parse(msg);
127
- switch (msg[0]) {
128
- case 'infos': { obj.infos = msg[1]; startTcpServer(); break; }
129
- case 'mouse': { if (sshClient) { sshClient.sendPointerEvent(msg[1], msg[2], msg[3], msg[4]); } break; }
130
- case 'wheel': { if (sshClient) { sshClient.sendWheelEvent(msg[1], msg[2], msg[3], msg[4]); } break; }
131
- case 'scancode': { if (sshClient) { sshClient.sendKeyEventScancode(msg[1], msg[2]); } break; }
132
- case 'unicode': { if (sshClient) { sshClient.sendKeyEventUnicode(msg[1], msg[2]); } break; }
133
- case 'disconnect': { obj.close(); break; }
58
+ if (typeof msg != 'string') return;
59
+ if (msg[0] == '{') {
60
+ // Control data
61
+ msg = JSON.parse(msg);
62
+ if (typeof msg.action != 'string') return;
63
+ switch (msg.action) {
64
+ case 'connect': {
65
+ obj.termSize = msg;
66
+ const Client = require('ssh2').Client;
67
+ obj.sshClient = new Client();
68
+
69
+ obj.sshClient.on('ready', function () { // Authentication was successful.
70
+ obj.sshClient.shell(function (err, stream) {
71
+ if (err) { obj.close(); return; }
72
+ obj.sshShell = stream;
73
+ obj.sshShell.setWindow(obj.termSize.rows, obj.termSize.cols, obj.termSize.height, obj.termSize.width);
74
+ obj.sshShell.on('close', function () { obj.close(); });
75
+ obj.sshShell.on('data', function (data) { obj.ws.send('~' + data); });
76
+ });
77
+ obj.ws.send(JSON.stringify({ action: 'connected' }));
78
+ });
79
+ obj.sshClient.on('error', function (err) {
80
+ if (err.level == 'client-authentication') { obj.ws.send(JSON.stringify({ action: 'autherror' })); }
81
+ obj.close();
82
+ });
83
+
84
+ var connectionOptions = {
85
+ //debug: function (msg) { console.log(msg); },
86
+ // sock: // TODO
87
+ host: '192.168.2.205',
88
+ port: 22
89
+ }
90
+
91
+ if (typeof msg.username == 'string') { connectionOptions.username = msg.username; }
92
+ if (typeof msg.password == 'string') { connectionOptions.password = msg.password; }
93
+
94
+ obj.sshClient.connect(connectionOptions);
95
+ break;
96
+ }
97
+ case 'resize': {
98
+ obj.termSize = msg;
99
+ if (obj.sshShell != null) { obj.sshShell.setWindow(obj.termSize.rows, obj.termSize.cols, obj.termSize.height, obj.termSize.width); }
100
+ break;
101
+ }
102
+ }
103
+ } else if (msg[0] == '~') {
104
+ // Terminal data
105
+ if (obj.sshShell != null) { obj.sshShell.write(msg.substring(1)); }
106
}
107
} catch (ex) {
108
console.log('SSHMessageException', msg, ex);
@@ -146,8 +118,10 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) {
118
119
// Send an object with flow control
120
function send(obj) {
149
- try { sshClient.bufferLayer.socket.pause(); } catch (ex) { }
150
- try { ws.send(JSON.stringify(obj), function () { try { sshClient.bufferLayer.socket.resume(); } catch (ex) { } }); } catch (ex) { }
121
+ //try { sshClient.bufferLayer.socket.pause(); } catch (ex) { }
122
+ //try { ws.send(JSON.stringify(obj), function () { try { sshClient.bufferLayer.socket.resume(); } catch (ex) { } }); } catch (ex) { }
123
+
124
+ try { ws.send(JSON.stringify(obj), function () { }); } catch (ex) { }
125
}
126
127
// We are all set, start receiving data
translate/translate.json
+8
-15
@@ -653,7 +653,6 @@
653
"sharing.handlebars->11->19",
654
"sharing.handlebars->11->27",
655
"sharing.handlebars->11->44",
656
- "ssh.handlebars->9->6",
656
"terminal.handlebars->3->9",
657
"xterm.handlebars->9->6"
658
]
@@ -4363,7 +4362,6 @@
4362
"zh-cht": "管理員PowerShell",
4363
"xloc": [
4364
"default.handlebars->termShellContextMenu->3",
4366
- "ssh.handlebars->termShellContextMenu->cxtermps",
4365
"xterm.handlebars->termShellContextMenu->cxtermps"
4366
]
4367
},
@@ -4405,7 +4403,6 @@
4403
"zh-cht": "管理控制台",
4404
"xloc": [
4405
"default.handlebars->termShellContextMenu->1->0",
4408
- "ssh.handlebars->termShellContextMenu->cxtermnorm->0",
4406
"xterm.handlebars->termShellContextMenu->cxtermnorm->0"
4407
]
4408
},
@@ -9704,7 +9701,7 @@
9701
"default.handlebars->31->11",
9702
"desktop.handlebars->3->4",
9703
"sharing.handlebars->11->4",
9707
- "ssh.handlebars->9->4",
9704
+ "ssh.handlebars->3->4",
9705
"terminal.handlebars->3->4",
9706
"xterm.handlebars->9->4"
9707
]
@@ -9818,7 +9815,7 @@
9815
"desktop.handlebars->3->2",
9816
"sharing.handlebars->11->2",
9817
"sharing.handlebars->11->93",
9821
- "ssh.handlebars->9->2",
9818
+ "ssh.handlebars->3->2",
9819
"terminal.handlebars->3->2",
9820
"xterm.handlebars->9->2"
9821
]
@@ -13384,7 +13381,7 @@
13381
"sharing.handlebars->p11->deskarea0->deskarea1->3->deskstatus",
13382
"sharing.handlebars->p12->5->3->termstatus",
13383
"sharing.handlebars->p13->p13toolbar->1->3->p13Status",
13387
- "ssh.handlebars->9->1",
13384
+ "ssh.handlebars->3->1",
13385
"terminal.handlebars->3->1",
13386
"terminal.handlebars->p12->5->3->termstatus",
13387
"xterm.handlebars->9->1"
@@ -17987,6 +17984,9 @@
17984
"default.handlebars->31->106"
17985
]
17986
},
17987
+ {
17988
+ "en": "Geen Intel® AMT apparaten in deze apparaatgroep"
17989
+ },
17990
{
17991
"cs": "Obecné",
17992
"de": "Allgemein",
@@ -20275,7 +20275,6 @@
20275
"default.handlebars->31->12",
20276
"desktop.handlebars->3->5",
20277
"sharing.handlebars->11->5",
20278
- "ssh.handlebars->9->5",
20278
"terminal.handlebars->3->5",
20279
"xterm.handlebars->9->5"
20280
]
@@ -23925,7 +23924,6 @@
23924
"zh-cht": "登入控制台",
23925
"xloc": [
23926
"default.handlebars->termShellContextMenuLinux->5",
23928
- "ssh.handlebars->termShellContextMenuLinux->cxtermps",
23927
"xterm.handlebars->termShellContextMenuLinux->cxtermps"
23928
]
23929
},
@@ -27216,7 +27214,6 @@
27214
},
27215
{
27216
"en": "No Intel® AMT devices in this device group",
27219
- "en": "Geen Intel® AMT apparaten in deze apparaatgroep",
27217
"xloc": [
27218
"default.handlebars->31->262"
27219
]
@@ -33820,7 +33817,6 @@
33817
"zh-cht": "Root Shell",
33818
"xloc": [
33819
"default.handlebars->termShellContextMenuLinux->1->0",
33823
- "ssh.handlebars->termShellContextMenuLinux->cxtermnorm->0",
33820
"xterm.handlebars->termShellContextMenuLinux->cxtermnorm->0"
33821
]
33822
},
@@ -36413,7 +36409,7 @@
36409
"default.handlebars->31->331",
36410
"desktop.handlebars->3->3",
36411
"sharing.handlebars->11->3",
36416
- "ssh.handlebars->9->3",
36412
+ "ssh.handlebars->3->3",
36413
"terminal.handlebars->3->3",
36414
"xterm.handlebars->9->3"
36415
]
@@ -42715,7 +42711,6 @@
42711
"zh-cht": "用戶PowerShell",
42712
"xloc": [
42713
"default.handlebars->termShellContextMenu->7",
42718
- "ssh.handlebars->termShellContextMenu->cxtermups",
42714
"xterm.handlebars->termShellContextMenu->cxtermups"
42715
]
42716
},
@@ -42758,8 +42753,6 @@
42753
"xloc": [
42754
"default.handlebars->termShellContextMenu->5",
42755
"default.handlebars->termShellContextMenuLinux->3",
42761
- "ssh.handlebars->termShellContextMenu->cxtermunorm",
42762
- "ssh.handlebars->termShellContextMenuLinux->cxtermps",
42756
"xterm.handlebars->termShellContextMenu->cxtermunorm",
42757
"xterm.handlebars->termShellContextMenuLinux->cxtermps"
42758
]
@@ -47962,4 +47955,4 @@
47955
]
47956
}
47957
]
47965
-}
47958
+}
\ No newline at end of file
views/ssh.handlebars
+55
-14
@@ -80,6 +80,8 @@
80
var StatusStrs = ["Disconnected", "Connecting...", "Setup...", "Connected"];
81
var state = 0;
82
var socket = null;
83
+ var user = '';
84
+ var pass = '';
85
86
function start() {
87
// When the user resizes the window, re-fit
@@ -94,9 +96,7 @@
96
term = new Terminal();
97
if (termfit) { term.loadAddon(termfit); }
98
term.open(Q('terminal'));
97
- term.onData(function (data) {
98
- //if (tunnel != null) { tunnel.sendText(data); }
99
- });
99
+ term.onData(function (data) { if (state == 3) { socket.send('~' + data); } });
100
if (termfit) { termfit.fit(); }
101
term.onResize(function (size) {
102
// Despam resize
@@ -109,27 +109,67 @@
109
// Send the new terminal size to the agent
110
function sendResize() {
111
resizeTimer = null;
112
- //if ((term != null) && (tunnel != null)) { tunnel.sendCtrlMsg(JSON.stringify({ ctrlChannel: '102938', type: 'termsize', cols: term.cols, rows: term.rows })); }
112
+ if (socket != null) { socket.send(JSON.stringify({ action: 'resize', cols: term.cols, rows: term.rows, width: Q('terminal').offsetWidth, height: Q('terminal').offsetHeight })); }
113
}
114
115
function connectButton() {
116
if (state == 0) {
117
- state = 1;
118
- var url = window.location.protocol.replace('http', 'ws') + '//' + window.location.host + domainurl + 'ssh/relay.ashx?auth=' + cookie + (urlargs.key ? ('&key=' + urlargs.key) : '');
119
- console.log('Connecting to ' + url);
120
- socket = new WebSocket(url);
121
- socket.onopen = function (e) { console.log('open'); state = 2; updateState(); }
122
- socket.onmessage = function (e) { console.log('message'); }
123
- socket.onclose = function (e) { console.log('close'); disconnect(); }
124
- socket.onerror = function (e) { console.log('error'); disconnect(); }
125
- updateState();
117
+ var x = '';
118
+ x += addHtmlValue("Username", '<input id=dp2user style=width:230px maxlength=64 autocomplete=off onkeyup=authKeyUp(event) />');
119
+ x += addHtmlValue("Password", '<input type=password id=dp2pass style=width:230px maxlength=64 autocomplete=off onkeyup=authKeyUp(event) />');
120
+ setDialogMode(2, "Authentication", 3, connectEx, x);
121
+ Q('dp2user').value = user;
122
+ Q('dp2pass').value = pass;
123
+ if (user == '') { Q('dp2user').focus(); } else { Q('dp2pass').focus(); }
124
+ setTimeout(authKeyUp, 50);
125
} else {
126
disconnect();
127
}
128
}
129
130
+ function authKeyUp(e) { QE('idx_dlgOkButton', (Q('dp2user').value.length > 0) && (Q('dp2pass').value.length > 0)); }
131
+
132
+ function connectEx() {
133
+ user = Q('dp2user').value;
134
+ pass = Q('dp2pass').value;
135
+ state = 1;
136
+ var url = window.location.protocol.replace('http', 'ws') + '//' + window.location.host + domainurl + 'ssh/relay.ashx?auth=' + cookie + (urlargs.key ? ('&key=' + urlargs.key) : '');
137
+ socket = new WebSocket(url);
138
+ socket.onopen = function (e) {
139
+ state = 2;
140
+ updateState();
141
+ term.reset();
142
+
143
+ // Send username and terminal width and height
144
+ socket.send(JSON.stringify({ action: 'connect', username: user, password: pass, cols: term.cols, rows: term.rows, width: Q('terminal').offsetWidth, height: Q('terminal').offsetHeight }));
145
+ pass = '';
146
+ }
147
+ socket.onmessage = function (data) {
148
+ if (typeof data.data != 'string') return;
149
+ if (data.data[0] == '{') {
150
+ var json = JSON.parse(data.data);
151
+ switch (json.action) {
152
+ case 'connected': {
153
+ state = 3;
154
+ updateState();
155
+ term.focus();
156
+ break;
157
+ }
158
+ case 'autherror': {
159
+ setDialogMode(2, "Authentication", 1, null, "Unable to authenticate.");
160
+ break;
161
+ }
162
+ }
163
+ } else if (data.data[0] == '~') {
164
+ term.writeUtf8(data.data.substring(1));
165
+ }
166
+ }
167
+ socket.onclose = function (e) { disconnect(); }
168
+ socket.onerror = function (e) { disconnect(); }
169
+ updateState();
170
+ }
171
+
172
function disconnect() {
132
- console.log('disconnect');
173
if (socket != null) { socket.close(); socket = null; }
174
state = 0;
175
updateState();
@@ -186,6 +226,7 @@
226
function format(format) { var args = Array.prototype.slice.call(arguments, 1); return format.replace(/{(\d+)}/g, function (match, number) { return typeof args[number] != 'undefined' ? args[number] : match; }); };
227
function isAlphaNumeric(str) { return (str.match(/^[A-Za-z0-9]+$/) != null); };
228
function isSafeString(str) { return ((typeof str == 'string') && (str.indexOf('<') == -1) && (str.indexOf('>') == -1) && (str.indexOf('&') == -1) && (str.indexOf('"') == -1) && (str.indexOf('\'') == -1) && (str.indexOf('+') == -1) && (str.indexOf('(') == -1) && (str.indexOf(')') == -1) && (str.indexOf('#') == -1) && (str.indexOf('%') == -1) && (str.indexOf(':') == -1)) };
229
+ function addHtmlValue(t, v) { return '<table><td style=width:120px>' + t + '<td><b>' + v + '</b></table>'; }
230
231
// Parse URL arguments, only keep safe values
232
function parseUriArgs() {
webserver.js
+1
-1
@@ -5576,7 +5576,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
5576
}
5577
5578
// Setup SSH if needed
5579
- if (domain.mstsc === true) {
5579
+ if (domain.ssh === true) {
5580
obj.app.get(url + 'ssh.html', function (req, res) { handleMSTSCRequest(req, res, 'ssh'); });
5581
obj.app.ws(url + 'ssh/relay.ashx', function (ws, req) {
5582
const domain = getDomain(req);