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);