First working web-based SSH.

Ylian Saint-Hilaire committed Apr 29, 2021 at 22:31 UTC 1b32966c760e2fb1fccdf6182b836722b00e8459
8 files changed +338 -306
MeshCentralServer.njsproj
+1 -2
@@ -119,7 +119,7 @@
119 <Compile Include="meshuser.js" />
120 <Compile Include="mpsserver.js" />
121 <Compile Include="mqttbroker.js" />
122 - <Compile Include="mstsc.js" />
122 + <Compile Include="apprelays.js" />
123 <Compile Include="pluginHandler.js" />
124 <Compile Include="public\mstsc\client.js" />
125 <Compile Include="public\mstsc\js\keyboard.js" />
@@ -178,7 +178,6 @@
178 <Compile Include="public\novnc\vendor\promise.js" />
179 <Compile Include="public\scripts\agent-redir-rtc-0.1.0.js" />
180 <Compile Include="public\serviceworker.js" />
181 - <Compile Include="ssh.js" />
181 <Compile Include="swarmserver.js" />
182 <Compile Include="multiserver.js" />
183 <Compile Include="pass.js" />
apprelays.js new
+325
@@ -0,0 +1,325 @@
1 +/**
2 +* @description MeshCentral MSTSC & SSH relay
3 +* @author Ylian Saint-Hilaire & Bryan Roe
4 +* @copyright Intel Corporation 2018-2021
5 +* @license Apache-2.0
6 +* @version v0.0.1
7 +*/
8 +
9 +/*jslint node: true */
10 +/*jshint node: true */
11 +/*jshint strict:false */
12 +/*jshint -W097 */
13 +/*jshint esversion: 6 */
14 +"use strict";
15 +
16 +// Construct a MSTSC Relay object, called upon connection
17 +// This is a bit of a hack as we are going to run the RDP connection thru a loopback connection.
18 +// If the "node-rdpjs-2" module supported passing a socket, we would do something different.
19 +module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
20 + const Net = require('net');
21 + const WebSocket = require('ws');
22 +
23 + var obj = {};
24 + obj.domain = domain;
25 + obj.ws = ws;
26 + obj.wsClient = null;
27 + obj.tcpServer = null;
28 + obj.tcpServerPort = 0;
29 + obj.relaySocket = null;
30 + obj.relayActive = false;
31 + obj.infos = null;
32 + var rdpClient = null;
33 +
34 + parent.parent.debug('relay', 'RDP: Request for RDP relay (' + req.clientIp + ')');
35 +
36 + // Disconnect
37 + obj.close = function (arg) {
38 + if ((arg == 1) || (arg == null)) { try { ws.close(); } catch (e) { console.log(e); } } // Soft close, close the websocket
39 + if (arg == 2) { try { ws._socket._parent.end(); } catch (e) { console.log(e); } } // Hard close, close the TCP socket
40 + if (obj.wsClient) { obj.wsClient.close(); obj.wsClient = null; }
41 + if (obj.tcpServer) { obj.tcpServer.close(); obj.tcpServer = null; }
42 + if (rdpClient) { rdpClient.close(); rdpClient = null; }
43 + delete obj.domain;
44 + delete obj.ws;
45 + };
46 +
47 + // Start the looppback server
48 + function startTcpServer() {
49 + obj.tcpServer = new Net.Server();
50 + obj.tcpServer.listen(0, '127.0.0.1', function () { obj.tcpServerPort = obj.tcpServer.address().port; startRdp(obj.tcpServerPort); });
51 + obj.tcpServer.on('connection', function (socket) {
52 + if (obj.relaySocket != null) {
53 + socket.close();
54 + } else {
55 + obj.relaySocket = socket;
56 + obj.relaySocket.pause();
57 + obj.relaySocket.on('data', function (chunk) { // Make sure to handle flow control.
58 + if (obj.relayActive == true) { obj.relaySocket.pause(); obj.wsClient.send(chunk, function () { obj.relaySocket.resume(); }); }
59 + });
60 + obj.relaySocket.on('end', function () { obj.close(); });
61 + obj.relaySocket.on('error', function (err) { obj.close(); });
62 +
63 + // Decode the authentication cookie
64 + var cookie = parent.parent.decodeCookie(obj.infos.ip, parent.parent.loginCookieEncryptionKey);
65 + if (cookie == null) return;
66 +
67 + // Setup the correct URL with domain and use TLS only if needed.
68 + var options = { rejectUnauthorized: false };
69 + if (domain.dns != null) { options.servername = domain.dns; }
70 + var protocol = 'wss';
71 + if (args.tlsoffload) { protocol = 'ws'; }
72 + var domainadd = '';
73 + if ((domain.dns == null) && (domain.id != '')) { domainadd = domain.id + '/' }
74 + var url = protocol + '://127.0.0.1:' + args.port + '/' + domainadd + ((cookie.lc == 1)?'local':'mesh') + 'relay.ashx?noping=1&auth=' + obj.infos.ip;
75 + parent.parent.debug('relay', 'RDP: Connection websocket to ' + url);
76 + obj.wsClient = new WebSocket(url, options);
77 + obj.wsClient.on('open', function () { parent.parent.debug('relay', 'RDP: Relay websocket open'); });
78 + obj.wsClient.on('message', function (data) { // Make sure to handle flow control.
79 + if ((obj.relayActive == false) && (data == 'c')) {
80 + obj.relayActive = true; obj.relaySocket.resume();
81 + } else {
82 + obj.wsClient._socket.pause();
83 + obj.relaySocket.write(data, function () { obj.wsClient._socket.resume(); });
84 + }
85 + });
86 + obj.wsClient.on('close', function () { parent.parent.debug('relay', 'RDP: Relay websocket closed'); obj.close(); });
87 + obj.wsClient.on('error', function (err) { parent.parent.debug('relay', 'RDP: Relay websocket error: ' + err); obj.close(); });
88 + obj.tcpServer.close();
89 + obj.tcpServer = null;
90 + }
91 + });
92 + }
93 +
94 + // Start the RDP client
95 + function startRdp(port) {
96 + parent.parent.debug('relay', 'RDP: Starting RDP client on loopback port ' + port);
97 + try {
98 + rdpClient = require('node-rdpjs-2').createClient({
99 + logLevel: 'ERROR',
100 + domain: obj.infos.domain,
101 + userName: obj.infos.username,
102 + password: obj.infos.password,
103 + enablePerf: true,
104 + autoLogin: true,
105 + screen: obj.infos.screen,
106 + locale: obj.infos.locale
107 + }).on('connect', function () {
108 + send(['rdp-connect']);
109 + }).on('bitmap', function (bitmap) {
110 + try { ws.send(bitmap.data); } catch (ex) { } // Send the bitmap data as binary
111 + delete bitmap.data;
112 + send(['rdp-bitmap', bitmap]); // Send the bitmap metadata seperately, without bitmap data.
113 + }).on('close', function () {
114 + send(['rdp-close']);
115 + }).on('error', function (err) {
116 + send(['rdp-error', err]);
117 + }).connect('127.0.0.1', obj.tcpServerPort);
118 + } catch (ex) {
119 + console.log('startRdpException', ex);
120 + obj.close();
121 + }
122 + }
123 +
124 + // When data is received from the web socket
125 + // RDP default port is 3389
126 + ws.on('message', function (msg) {
127 + try {
128 + msg = JSON.parse(msg);
129 + switch (msg[0]) {
130 + case 'infos': { obj.infos = msg[1]; startTcpServer(); break; }
131 + case 'mouse': { if (rdpClient) { rdpClient.sendPointerEvent(msg[1], msg[2], msg[3], msg[4]); } break; }
132 + case 'wheel': { if (rdpClient) { rdpClient.sendWheelEvent(msg[1], msg[2], msg[3], msg[4]); } break; }
133 + case 'scancode': { if (rdpClient) { rdpClient.sendKeyEventScancode(msg[1], msg[2]); } break; }
134 + case 'unicode': { if (rdpClient) { rdpClient.sendKeyEventUnicode(msg[1], msg[2]); } break; }
135 + case 'disconnect': { obj.close(); break; }
136 + }
137 + } catch (ex) {
138 + console.log('RdpMessageException', msg, ex);
139 + obj.close();
140 + }
141 + });
142 +
143 + // If error, do nothing
144 + ws.on('error', function (err) { parent.parent.debug('relay', 'RDP: Browser websocket error: ' + err); obj.close(); });
145 +
146 + // If the web socket is closed
147 + ws.on('close', function (req) { parent.parent.debug('relay', 'RDP: Browser websocket closed'); obj.close(); });
148 +
149 + // Send an object with flow control
150 + function send(obj) {
151 + try { rdpClient.bufferLayer.socket.pause(); } catch (ex) { }
152 + try { ws.send(JSON.stringify(obj), function () { try { rdpClient.bufferLayer.socket.resume(); } catch (ex) { } }); } catch (ex) { }
153 + }
154 +
155 + // We are all set, start receiving data
156 + ws._socket.resume();
157 +
158 + return obj;
159 +};
160 +
161 +
162 +
163 +// Construct a SSH Relay object, called upon connection
164 +module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) {
165 + const Net = require('net');
166 + const WebSocket = require('ws');
167 +
168 + // SerialTunnel object is used to embed SSH within another connection.
169 + function SerialTunnel(options) {
170 + var obj = new require('stream').Duplex(options);
171 + obj.forwardwrite = null;
172 + obj.updateBuffer = function (chunk) { this.push(chunk); };
173 + obj._write = function (chunk, encoding, callback) { if (obj.forwardwrite != null) { obj.forwardwrite(chunk); } else { console.err("Failed to fwd _write."); } if (callback) callback(); }; // Pass data written to forward
174 + obj._read = function (size) { }; // Push nothing, anything to read should be pushed from updateBuffer()
175 + return obj;
176 + }
177 +
178 + const obj = {};
179 + obj.domain = domain;
180 + obj.ws = ws;
181 + obj.relayActive = false;
182 + obj.sshClient = null;
183 + obj.sshShell = null;
184 + obj.termSize = null;
185 + obj.relayActive = false;
186 + obj.wsClient = null;
187 +
188 + parent.parent.debug('relay', 'SSH: Request for SSH relay (' + req.clientIp + ')');
189 +
190 + // Disconnect
191 + obj.close = function (arg) {
192 + if ((arg == 1) || (arg == null)) { try { ws.close(); } catch (e) { console.log(e); } } // Soft close, close the websocket
193 + if (arg == 2) { try { ws._socket._parent.end(); } catch (e) { console.log(e); } } // Hard close, close the TCP socket
194 + //if (obj.wsClient) { obj.wsClient.close(); obj.wsClient = null; }
195 + //if (obj.tcpServer) { obj.tcpServer.close(); obj.tcpServer = null; }
196 + //if (sshClient) { sshClient.close(); sshClient = null; }
197 +
198 + if (obj.wsClient != null) {
199 + try { obj.wsClient.close(); } catch (ex) { console.log(ex); }
200 + delete obj.wsClient;
201 + }
202 + if (obj.sshClient != null) {
203 + try { obj.sshClient.end(); } catch (ex) { console.log(ex); }
204 + delete obj.sshClient;
205 + }
206 + if (obj.sshShell != null) {
207 + try { obj.sshShell.end(); } catch (ex) { console.log(ex); }
208 + delete obj.sshShell;
209 + }
210 +
211 + obj.relayActive = false;
212 + delete obj.termSize;
213 + delete obj.cookie;
214 + delete obj.domain;
215 + delete obj.ws;
216 + };
217 +
218 + // Decode the authentication cookie
219 + obj.cookie = parent.parent.decodeCookie(req.query.auth, parent.parent.loginCookieEncryptionKey);
220 + if (obj.cookie == null) { obj.ws.send(JSON.stringify({ action: 'sessionerror' })); obj.close(); return; }
221 +
222 + // Start the looppback server
223 + function startRelayConnection() {
224 + try {
225 + // Setup the correct URL with domain and use TLS only if needed.
226 + var options = { rejectUnauthorized: false };
227 + if (domain.dns != null) { options.servername = domain.dns; }
228 + var protocol = 'wss';
229 + if (args.tlsoffload) { protocol = 'ws'; }
230 + var domainadd = '';
231 + if ((domain.dns == null) && (domain.id != '')) { domainadd = domain.id + '/' }
232 + var url = protocol + '://127.0.0.1:' + args.port + '/' + domainadd + ((obj.cookie.lc == 1) ? 'local' : 'mesh') + 'relay.ashx?noping=1&auth=' + req.query.auth;
233 + parent.parent.debug('relay', 'SSH: Connection websocket to ' + url);
234 + obj.wsClient = new WebSocket(url, options);
235 + obj.wsClient.on('open', function () { parent.parent.debug('relay', 'SSH: Relay websocket open'); });
236 + obj.wsClient.on('message', function (data) { // Make sure to handle flow control.
237 + if ((obj.relayActive == false) && (data == 'c')) {
238 + obj.relayActive = true;
239 +
240 + // Create a serial tunnel && SSH module
241 + obj.ser = new SerialTunnel();
242 + const Client = require('ssh2').Client;
243 + obj.sshClient = new Client();
244 + obj.sshClient.on('ready', function () { // Authentication was successful.
245 + obj.sshClient.shell(function (err, stream) { // Start a remote shell
246 + if (err) { obj.close(); return; }
247 + obj.sshShell = stream;
248 + obj.sshShell.setWindow(obj.termSize.rows, obj.termSize.cols, obj.termSize.height, obj.termSize.width);
249 + obj.sshShell.on('close', function () { obj.close(); });
250 + obj.sshShell.on('data', function (data) { obj.ws.send('~' + data); });
251 + });
252 + obj.ws.send(JSON.stringify({ action: 'connected' }));
253 + });
254 + obj.sshClient.on('error', function (err) {
255 + if (err.level == 'client-authentication') { obj.ws.send(JSON.stringify({ action: 'autherror' })); }
256 + obj.close();
257 + });
258 +
259 + // Setup the serial tunnel, SSH ---> Relay WS
260 + obj.ser.forwardwrite = function (data) { if ((data.length > 0) && (obj.wsClient != null)) { try { obj.wsClient.send(data); } catch (ex) { } } };
261 +
262 + // Connect the SSH module to the serial tunnel
263 + var connectionOptions = { sock: obj.ser }
264 + if (typeof obj.username == 'string') { connectionOptions.username = obj.username; delete obj.username; }
265 + if (typeof obj.password == 'string') { connectionOptions.password = obj.password; delete obj.password; }
266 + obj.sshClient.connect(connectionOptions);
267 +
268 + // We are all set, start receiving data
269 + ws._socket.resume();
270 + } else {
271 + // Relay WS --> SSH
272 + if ((data.length > 0) && (obj.ser != null)) { try { obj.ser.updateBuffer(data); } catch (ex) { console.log(ex); } }
273 + }
274 + });
275 + obj.wsClient.on('close', function () { parent.parent.debug('relay', 'SSH: Relay websocket closed'); obj.close(); });
276 + obj.wsClient.on('error', function (err) { parent.parent.debug('relay', 'SSH: Relay websocket error: ' + err); obj.close(); });
277 + } catch (ex) {
278 + console.log(ex);
279 + }
280 + }
281 +
282 + // When data is received from the web socket
283 + // SSH default port is 22
284 + ws.on('message', function (msg) {
285 + try {
286 + if (typeof msg != 'string') return;
287 + if (msg[0] == '{') {
288 + // Control data
289 + msg = JSON.parse(msg);
290 + if (typeof msg.action != 'string') return;
291 + switch (msg.action) {
292 + case 'connect': {
293 + obj.termSize = msg;
294 + obj.username = msg.username;
295 + obj.password = msg.password;
296 + startRelayConnection();
297 + break;
298 + }
299 + case 'resize': {
300 + obj.termSize = msg;
301 + if (obj.sshShell != null) { obj.sshShell.setWindow(obj.termSize.rows, obj.termSize.cols, obj.termSize.height, obj.termSize.width); }
302 + break;
303 + }
304 + }
305 + } else if (msg[0] == '~') {
306 + // Terminal data
307 + if (obj.sshShell != null) { obj.sshShell.write(msg.substring(1)); }
308 + }
309 + } catch (ex) {
310 + console.log('SSHMessageException', msg, ex);
311 + obj.close();
312 + }
313 + });
314 +
315 + // If error, do nothing
316 + ws.on('error', function (err) { parent.parent.debug('relay', 'SSH: Browser websocket error: ' + err); obj.close(); });
317 +
318 + // If the web socket is closed
319 + ws.on('close', function (req) { parent.parent.debug('relay', 'SSH: Browser websocket closed'); obj.close(); });
320 +
321 + // Send data on the web socket
322 + //function send(obj) { try { ws.send(JSON.stringify(obj), function () { }); } catch (ex) { } }
323 +
324 + return obj;
325 +};
\ No newline at end of file
meshcentral-config-schema.json
+2 -1
@@ -373,8 +373,9 @@
373 }
374 },
375 "geoLocation": { "type": "boolean", "default": false, "description": "Enables the geo-location feature and device location map in the user interface, this feature is not being worked on." },
376 - "novnc": { "type": "boolean", "default": true, "description": "When enabled, activates the built-in web-based noVNC client." },
376 + "novnc": { "type": "boolean", "default": true, "description": "When enabled, activates the built-in web-based VNC client." },
377 "mstsc": { "type": "boolean", "default": false, "description": "When enabled, activates the built-in web-based RDP client." },
378 + "ssh": { "type": "boolean", "default": false, "description": "When enabled, activates the built-in web-based SSH client." },
379 "webEmailsPath": { "type": "string", "description": "Path where to find custom email templates for this domain." },
380 "customUI": { "type": "object" },
381 "consentMessages": {
mstsc.js deleted
-159
@@ -1,159 +0,0 @@
1 -/**
2 -* @description MeshCentral MSTSC relay
3 -* @author Ylian Saint-Hilaire & Bryan Roe
4 -* @copyright Intel Corporation 2018-2021
5 -* @license Apache-2.0
6 -* @version v0.0.1
7 -*/
8 -
9 -/*jslint node: true */
10 -/*jshint node: true */
11 -/*jshint strict:false */
12 -/*jshint -W097 */
13 -/*jshint esversion: 6 */
14 -"use strict";
15 -
16 -// Construct a MSTSC Relay object, called upon connection
17 -// This is a bit of a hack as we are going to run the RDP connection thru a loopback connection.
18 -// If the "node-rdpjs-2" module supported passing a socket, we would do something different.
19 -module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
20 - const Net = require('net');
21 - const WebSocket = require('ws');
22 -
23 - var obj = {};
24 - obj.domain = domain;
25 - obj.ws = ws;
26 - obj.wsClient = null;
27 - obj.tcpServer = null;
28 - obj.tcpServerPort = 0;
29 - obj.relaySocket = null;
30 - obj.relayActive = false;
31 - obj.infos = null;
32 - var rdpClient = null;
33 -
34 - parent.parent.debug('relay', 'RDP: Request for RDP relay (' + req.clientIp + ')');
35 -
36 - // Disconnect
37 - obj.close = function (arg) {
38 - if ((arg == 1) || (arg == null)) { try { ws.close(); } catch (e) { console.log(e); } } // Soft close, close the websocket
39 - if (arg == 2) { try { ws._socket._parent.end(); } catch (e) { console.log(e); } } // Hard close, close the TCP socket
40 - if (obj.wsClient) { obj.wsClient.close(); obj.wsClient = null; }
41 - if (obj.tcpServer) { obj.tcpServer.close(); obj.tcpServer = null; }
42 - if (rdpClient) { rdpClient.close(); rdpClient = null; }
43 - delete obj.domain;
44 - delete obj.ws;
45 - };
46 -
47 - // Start the looppback server
48 - function startTcpServer() {
49 - obj.tcpServer = new Net.Server();
50 - obj.tcpServer.listen(0, '127.0.0.1', function () { obj.tcpServerPort = obj.tcpServer.address().port; startRdp(obj.tcpServerPort); });
51 - obj.tcpServer.on('connection', function (socket) {
52 - if (obj.relaySocket != null) {
53 - socket.close();
54 - } else {
55 - obj.relaySocket = socket;
56 - obj.relaySocket.pause();
57 - obj.relaySocket.on('data', function (chunk) { // Make sure to handle flow control.
58 - if (obj.relayActive == true) { obj.relaySocket.pause(); obj.wsClient.send(chunk, function () { obj.relaySocket.resume(); }); }
59 - });
60 - obj.relaySocket.on('end', function () { obj.close(); });
61 - obj.relaySocket.on('error', function (err) { obj.close(); });
62 -
63 - // Decode the authentication cookie
64 - var cookie = parent.parent.decodeCookie(obj.infos.ip, parent.parent.loginCookieEncryptionKey);
65 - if (cookie == null) return;
66 -
67 - // Setup the correct URL with domain and use TLS only if needed.
68 - var options = { rejectUnauthorized: false };
69 - if (domain.dns != null) { options.servername = domain.dns; }
70 - var protocol = 'wss';
71 - if (args.tlsoffload) { protocol = 'ws'; }
72 - var domainadd = '';
73 - if ((domain.dns == null) && (domain.id != '')) { domainadd = domain.id + '/' }
74 - var url = protocol + '://127.0.0.1:' + args.port + '/' + domainadd + ((cookie.lc == 1)?'local':'mesh') + 'relay.ashx?noping=1&auth=' + obj.infos.ip;
75 - parent.parent.debug('relay', 'RDP: Connection websocket to ' + url);
76 - obj.wsClient = new WebSocket(url, options);
77 - obj.wsClient.on('open', function () { parent.parent.debug('relay', 'RDP: Relay websocket open'); });
78 - obj.wsClient.on('message', function (data) { // Make sure to handle flow control.
79 - if ((obj.relayActive == false) && (data == 'c')) {
80 - obj.relayActive = true; obj.relaySocket.resume();
81 - } else {
82 - obj.wsClient._socket.pause();
83 - obj.relaySocket.write(data, function () { obj.wsClient._socket.resume(); });
84 - }
85 - });
86 - obj.wsClient.on('close', function () { parent.parent.debug('relay', 'RDP: Relay websocket closed'); obj.close(); });
87 - obj.wsClient.on('error', function (err) { parent.parent.debug('relay', 'RDP: Relay websocket error: ' + err); obj.close(); });
88 - obj.tcpServer.close();
89 - obj.tcpServer = null;
90 - }
91 - });
92 - }
93 -
94 - // Start the RDP client
95 - function startRdp(port) {
96 - parent.parent.debug('relay', 'RDP: Starting RDP client on loopback port ' + port);
97 - try {
98 - rdpClient = require('node-rdpjs-2').createClient({
99 - logLevel: 'ERROR',
100 - domain: obj.infos.domain,
101 - userName: obj.infos.username,
102 - password: obj.infos.password,
103 - enablePerf: true,
104 - autoLogin: true,
105 - screen: obj.infos.screen,
106 - locale: obj.infos.locale
107 - }).on('connect', function () {
108 - send(['rdp-connect']);
109 - }).on('bitmap', function (bitmap) {
110 - try { ws.send(bitmap.data); } catch (ex) { } // Send the bitmap data as binary
111 - delete bitmap.data;
112 - send(['rdp-bitmap', bitmap]); // Send the bitmap metadata seperately, without bitmap data.
113 - }).on('close', function () {
114 - send(['rdp-close']);
115 - }).on('error', function (err) {
116 - send(['rdp-error', err]);
117 - }).connect('127.0.0.1', obj.tcpServerPort);
118 - } catch (ex) {
119 - console.log('startRdpException', ex);
120 - obj.close();
121 - }
122 - }
123 -
124 - // When data is received from the web socket
125 - // RDP default port is 3389
126 - ws.on('message', function (msg) {
127 - try {
128 - msg = JSON.parse(msg);
129 - switch (msg[0]) {
130 - case 'infos': { obj.infos = msg[1]; startTcpServer(); break; }
131 - case 'mouse': { if (rdpClient) { rdpClient.sendPointerEvent(msg[1], msg[2], msg[3], msg[4]); } break; }
132 - case 'wheel': { if (rdpClient) { rdpClient.sendWheelEvent(msg[1], msg[2], msg[3], msg[4]); } break; }
133 - case 'scancode': { if (rdpClient) { rdpClient.sendKeyEventScancode(msg[1], msg[2]); } break; }
134 - case 'unicode': { if (rdpClient) { rdpClient.sendKeyEventUnicode(msg[1], msg[2]); } break; }
135 - case 'disconnect': { obj.close(); break; }
136 - }
137 - } catch (ex) {
138 - console.log('RdpMessageException', msg, ex);
139 - obj.close();
140 - }
141 - });
142 -
143 - // If error, do nothing
144 - ws.on('error', function (err) { parent.parent.debug('relay', 'RDP: Browser websocket error: ' + err); obj.close(); });
145 -
146 - // If the web socket is closed
147 - ws.on('close', function (req) { parent.parent.debug('relay', 'RDP: Browser websocket closed'); obj.close(); });
148 -
149 - // Send an object with flow control
150 - function send(obj) {
151 - try { rdpClient.bufferLayer.socket.pause(); } catch (ex) { }
152 - try { ws.send(JSON.stringify(obj), function () { try { rdpClient.bufferLayer.socket.resume(); } catch (ex) { } }); } catch (ex) { }
153 - }
154 -
155 - // We are all set, start receiving data
156 - ws._socket.resume();
157 -
158 - return obj;
159 -};
\ No newline at end of file
ssh.js deleted
-131
@@ -1,131 +0,0 @@
1 -/**
2 -* @description MeshCentral SSH relay
3 -* @author Ylian Saint-Hilaire
4 -* @copyright Intel Corporation 2018-2021
5 -* @license Apache-2.0
6 -* @version v0.0.1
7 -*/
8 -
9 -/*jslint node: true */
10 -/*jshint node: true */
11 -/*jshint strict:false */
12 -/*jshint -W097 */
13 -/*jshint esversion: 6 */
14 -"use strict";
15 -
16 -// Construct a SSH Relay object, called upon connection
17 -module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) {
18 - const Net = require('net');
19 - const WebSocket = require('ws');
20 -
21 - var obj = {};
22 - obj.domain = domain;
23 - obj.ws = ws;
24 - obj.relaySocket = null;
25 - obj.relayActive = false;
26 - obj.infos = 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 -
33 - // Disconnect
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
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 -
54 - // When data is received from the web socket
55 - // SSH default port is 22
56 - ws.on('message', function (msg) {
57 - try {
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);
109 - obj.close();
110 - }
111 - });
112 -
113 - // If error, do nothing
114 - ws.on('error', function (err) { parent.parent.debug('relay', 'SSH: Browser websocket error: ' + err); obj.close(); });
115 -
116 - // If the web socket is closed
117 - ws.on('close', function (req) { parent.parent.debug('relay', 'SSH: Browser websocket closed'); obj.close(); });
118 -
119 - // Send an object with flow control
120 - function send(obj) {
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
128 - ws._socket.resume();
129 -
130 - return obj;
131 -};
\ No newline at end of file
views/default.handlebars
+1 -1
@@ -6458,7 +6458,7 @@
6458
6459 // SSH link
6460 if ((((connectivity & 1) != 0) || (node.mtype == 3)) && (node.agent) && ((meshrights & 8) != 0) && ((features & 0x40000000) == 0) && (node.agent.id != 14)) {
6461 - x += '<a href=# cmenu=altPortContextMenu id=sshLink onclick=p10ssh("' + node._id + '") title="' + "Launch web-based SSH session to this device" + '.">' + "Web-SSH" + '</a>&nbsp;';
6461 + x += '<a href=# id=sshLink onclick=p10ssh("' + node._id + '") title="' + "Launch web-based SSH session to this device" + '.">' + "Web-SSH" + '</a>&nbsp;';
6462 }
6463
6464 // MQTT options
views/ssh.handlebars
+5 -10
@@ -104,6 +104,8 @@
104 resizeTimer = setTimeout(sendResize, 200);
105 });
106 //term.setOption('convertEol', true); // Consider \n to be \r\n, this should be taken care of by "termios"
107 +
108 + connectButton();
109 }
110
111 // Send the new terminal size to the agent
@@ -149,16 +151,9 @@
151 if (data.data[0] == '{') {
152 var json = JSON.parse(data.data);
153 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 - }
154 + case 'connected': { state = 3; updateState(); term.focus(); break; }
155 + case 'autherror': { setDialogMode(2, "Authentication", 1, null, "Unable to authenticate."); break; }
156 + case 'sessionerror': { setDialogMode(2, "Session", 1, null, "Session expired."); break; }
157 }
158 } else if (data.data[0] == '~') {
159 term.writeUtf8(data.data.substring(1));
webserver.js
+4 -2
@@ -5571,7 +5571,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
5571 obj.app.ws(url + 'mstsc/relay.ashx', function (ws, req) {
5572 const domain = getDomain(req);
5573 if (domain == null) { parent.debug('web', 'mstsc: failed checks.'); try { ws.close(); } catch (e) { } return; }
5574 - require('./mstsc.js').CreateMstscRelay(obj, obj.db, ws, req, obj.args, domain);
5574 + require('./apprelays.js').CreateMstscRelay(obj, obj.db, ws, req, obj.args, domain);
5575 });
5576 }
5577
@@ -5581,7 +5581,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
5581 obj.app.ws(url + 'ssh/relay.ashx', function (ws, req) {
5582 const domain = getDomain(req);
5583 if (domain == null) { parent.debug('web', 'ssh: failed checks.'); try { ws.close(); } catch (e) { } return; }
5584 - require('./ssh.js').CreateSshRelay(obj, obj.db, ws, req, obj.args, domain);
5584 + try {
5585 + require('./apprelays.js').CreateSshRelay(obj, obj.db, ws, req, obj.args, domain);
5586 + } catch (ex) { console.log(ex); }
5587 });
5588 }
5589