Start work in desktop multiplexor.

Ylian Saint-Hilaire committed Apr 23, 2020 at 23:19 UTC 658392bd1e0ce38acceb6c04a2e22ea10f752ee0
3 files changed +624 -1
meshcentral.js
+3
@@ -2570,6 +2570,9 @@ function mainStart() {
2570 if (allsspi == false) { modules.push('otplib@10.2.3'); } // Google Authenticator support (v10 supports older NodeJS versions).
2571 }
2572
2573 + // Desktop multiplexor support
2574 + if (config.settings.desktopmultiplex === true) { modules.push('image-size'); }
2575 +
2576 // SMS support
2577 if ((config.sms != null) && (config.sms.provider == 'twilio')) { modules.push('twilio'); }
2578 if ((config.sms != null) && (config.sms.provider == 'plivo')) {
meshdesktopmultiplex.js new
+611
@@ -0,0 +1,611 @@
1 +/**
2 +* @description MeshCentral remote desktop multiplexor
3 +* @author Ylian Saint-Hilaire
4 +* @copyright Intel Corporation 2018-2020
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 +function CreateDesktopDecoder() {
17 + var obj = {};
18 + obj.width = 0;
19 + obj.height = 0;
20 + obj.swidth = 0;
21 + obj.sheight = 0;
22 + obj.screen = null;
23 + obj.counter = 1;
24 + obj.imagesCount = 0;
25 + obj.imagesCounters = {};
26 + obj.images = {};
27 + obj.lastScreenSizeCmd = null;
28 + obj.lastScreenSizeCounter = 0;
29 +
30 + obj.processAgentData = function (data) {
31 + if ((typeof data != 'object') || (data.length < 4)) return;
32 + var command = data.readUInt16BE(0);
33 + var cmdsize = data.readUInt16BE(2);
34 + if ((command == 27) && (cmdsize == 8)) {
35 + // Jumbo packet
36 + if (data.length >= 12) {
37 + command = data.readUInt16BE(8);
38 + cmdsize = data.readUInt32BE(4);
39 + if (data.length == (cmdsize + 8)) {
40 + data = data.slice(8, block.data.length);
41 + } else {
42 + console.log('TODO-PARTIAL-JUMBO', command, cmdsize, data.length);
43 + return; // TODO
44 + }
45 + }
46 + }
47 +
48 + switch (command) {
49 + case 3: // Tile, check dimentions and store
50 + var x = data.readUInt16BE(4);
51 + var y = data.readUInt16BE(6);
52 + var dimensions = require('image-size')(data.slice(8));
53 + obj.counter++;
54 + console.log("Tile", x, y, dimensions.width, dimensions.height);
55 +
56 + // Update the screen with the correct pointers.
57 + var sx = (x / 16), sy = (y / 16), sw = (dimensions.width / 16), sh = (dimensions.height / 16);
58 + for (var i = 0; i < sw; i++) {
59 + for (var j = 0; j < sh; j++) {
60 + var k = ((obj.swidth * (j + sy)) + (i + sx)), oi = obj.screen[k];
61 + if (--obj.imagesCounters[oi] == 0) { obj.imagesCount--; delete obj.images[oi]; delete obj.imagesCounters[oi]; }
62 + obj.screen[k] = obj.counter;
63 + }
64 + }
65 +
66 + // Keep a reference to this image & how many tiles it covers
67 + obj.images[obj.counter] = data;
68 + obj.imagesCounters[obj.counter] = (sw * sh);
69 + obj.imagesCount++;
70 + console.log('images', obj.imagesCount);
71 +
72 + break;
73 + case 7:// Screen Size, clear the screen state and compute the tile count
74 + obj.counter++;
75 + obj.lastScreenSizeCmd = data;
76 + obj.lastScreenSizeCounter = obj.counter;
77 + obj.width = data.readUInt16BE(4);
78 + obj.height = data.readUInt16BE(6);
79 + obj.swidth = obj.width / 16;
80 + obj.sheight = obj.height / 16;
81 + if (Math.floor(obj.swidth) != obj.swidth) { obj.swidth = Math.floor(obj.swidth) + 1; }
82 + if (Math.floor(obj.sheight) != obj.sheight) { obj.sheight = Math.floor(obj.sheight) + 1; }
83 +
84 + // Reset the display
85 + obj.screen = new Array(obj.swidth * obj.sheight);
86 + obj.imagesCount = 0;
87 + obj.imagesCounters = {};
88 + obj.images = {};
89 +
90 + console.log("ScreenSize", obj.width, obj.height, obj.swidth, obj.sheight, obj.swidth * obj.sheight);
91 + break;
92 + }
93 + }
94 +
95 + return obj;
96 +}
97 +
98 +module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie) {
99 + var obj = {};
100 + obj.ws = ws;
101 + obj.ws.me = obj;
102 + obj.id = req.query.id;
103 + obj.user = user;
104 + obj.ruserid = null;
105 + obj.req = req; // Used in multi-server.js
106 +
107 + // Check relay authentication
108 + if ((user == null) && (obj.req.query != null) && (obj.req.query.rauth != null)) {
109 + const rcookie = parent.parent.decodeCookie(obj.req.query.rauth, parent.parent.loginCookieEncryptionKey, 240); // Cookie with 4 hour timeout
110 + if (rcookie.ruserid != null) { obj.ruserid = rcookie.ruserid; }
111 + }
112 +
113 + // If there is no authentication, drop this connection
114 + if ((obj.id != null) && (obj.id.startsWith('meshmessenger/') == false) && (obj.user == null) && (obj.ruserid == null)) { try { ws.close(); parent.parent.debug('relay', 'Relay: Connection with no authentication (' + cleanRemoteAddr(obj.req.ip) + ')'); } catch (e) { console.log(e); } return; }
115 +
116 + // Relay session count (we may remove this in the future)
117 + obj.relaySessionCounted = true;
118 + parent.relaySessionCount++;
119 +
120 + // Mesh Rights
121 + const MESHRIGHT_EDITMESH = 1;
122 + const MESHRIGHT_MANAGEUSERS = 2;
123 + const MESHRIGHT_MANAGECOMPUTERS = 4;
124 + const MESHRIGHT_REMOTECONTROL = 8;
125 + const MESHRIGHT_AGENTCONSOLE = 16;
126 + const MESHRIGHT_SERVERFILES = 32;
127 + const MESHRIGHT_WAKEDEVICE = 64;
128 + const MESHRIGHT_SETNOTES = 128;
129 + const MESHRIGHT_REMOTEVIEW = 256;
130 +
131 + // Site rights
132 + const SITERIGHT_SERVERBACKUP = 1;
133 + const SITERIGHT_MANAGEUSERS = 2;
134 + const SITERIGHT_SERVERRESTORE = 4;
135 + const SITERIGHT_FILEACCESS = 8;
136 + const SITERIGHT_SERVERUPDATE = 16;
137 + const SITERIGHT_LOCKED = 32;
138 +
139 + // Clean a IPv6 address that encodes a IPv4 address
140 + function cleanRemoteAddr(addr) { if (addr.startsWith('::ffff:')) { return addr.substring(7); } else { return addr; } }
141 +
142 + // Disconnect this agent
143 + obj.close = function (arg) {
144 + if ((arg == 1) || (arg == null)) { try { ws.close(); parent.parent.debug('relay', 'Relay: Soft disconnect (' + cleanRemoteAddr(obj.req.ip) + ')'); } catch (e) { console.log(e); } } // Soft close, close the websocket
145 + if (arg == 2) { try { ws._socket._parent.end(); parent.parent.debug('relay', 'Relay: Hard disconnect (' + cleanRemoteAddr(obj.req.ip) + ')'); } catch (e) { console.log(e); } } // Hard close, close the TCP socket
146 +
147 + // Aggressive cleanup
148 + delete obj.id;
149 + delete obj.ws;
150 + delete obj.peer;
151 + };
152 +
153 + obj.sendAgentMessage = function (command, userid, domainid) {
154 + var rights, mesh;
155 + if (command.nodeid == null) return false;
156 + var user = parent.users[userid];
157 + if (user == null) return false;
158 + var splitnodeid = command.nodeid.split('/');
159 + // Check that we are in the same domain and the user has rights over this node.
160 + if ((splitnodeid[0] == 'node') && (splitnodeid[1] == domainid)) {
161 + // Get the user object
162 + // See if the node is connected
163 + var agent = parent.wsagents[command.nodeid];
164 + if (agent != null) {
165 + // Check if we have permission to send a message to that node
166 + rights = parent.GetNodeRights(user, agent.dbMeshKey, agent.dbNodeKey);
167 + mesh = parent.meshes[agent.dbMeshKey];
168 + if ((rights != null) && (mesh != null) || ((rights & 16) != 0)) { // TODO: 16 is console permission, may need more gradular permission checking
169 + if (ws.sessionId) { command.sessionid = ws.sessionId; } // Set the session id, required for responses.
170 + command.rights = rights.rights; // Add user rights flags to the message
171 + command.consent = mesh.consent; // Add user consent
172 + if (typeof domain.userconsentflags == 'number') { command.consent |= domain.userconsentflags; } // Add server required consent flags
173 + command.username = user.name; // Add user name
174 + if (typeof domain.desktopprivacybartext == 'string') { command.privacybartext = domain.desktopprivacybartext; } // Privacy bar text
175 + delete command.nodeid; // Remove the nodeid since it's implyed.
176 + agent.send(JSON.stringify(command));
177 + return true;
178 + }
179 + } else {
180 + // Check if a peer server is connected to this agent
181 + var routing = parent.parent.GetRoutingServerId(command.nodeid, 1); // 1 = MeshAgent routing type
182 + if (routing != null) {
183 + // Check if we have permission to send a message to that node
184 + rights = parent.GetNodeRights(user, routing.meshid, command.nodeid);
185 + mesh = parent.meshes[routing.meshid];
186 + if (rights != null || ((rights & 16) != 0)) { // TODO: 16 is console permission, may need more gradular permission checking
187 + if (ws.sessionId) { command.fromSessionid = ws.sessionId; } // Set the session id, required for responses.
188 + command.rights = rights.rights; // Add user rights flags to the message
189 + command.consent = mesh.consent; // Add user consent
190 + if (typeof domain.userconsentflags == 'number') { command.consent |= domain.userconsentflags; } // Add server required consent flags
191 + command.username = user.name; // Add user name
192 + if (typeof domain.desktopprivacybartext == 'string') { command.privacybartext = domain.desktopprivacybartext; } // Privacy bar text
193 + parent.parent.multiServer.DispatchMessageSingleServer(command, routing.serverid);
194 + return true;
195 + }
196 + }
197 + }
198 + }
199 + return false;
200 + };
201 +
202 + // Send a PING/PONG message
203 + function sendPing() {
204 + try { obj.ws.send('{"ctrlChannel":"102938","type":"ping"}'); } catch (ex) { }
205 + try { if (obj.peer != null) { obj.peer.ws.send('{"ctrlChannel":"102938","type":"ping"}'); } } catch (ex) { }
206 + }
207 + function sendPong() {
208 + try { obj.ws.send('{"ctrlChannel":"102938","type":"pong"}'); } catch (ex) { }
209 + try { if (obj.peer != null) { obj.peer.ws.send('{"ctrlChannel":"102938","type":"pong"}'); } } catch (ex) { }
210 + }
211 +
212 + function performRelay() {
213 + if (obj.id == null) { try { obj.close(); } catch (e) { } return null; } // Attempt to connect without id, drop this.
214 + ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
215 +
216 + // If this is a MeshMessenger session, the ID is the two userid's and authentication must match one of them.
217 + if (obj.id.startsWith('meshmessenger/')) {
218 + if ((obj.id.startsWith('meshmessenger/user/') == true) && (user == null)) { try { obj.close(); } catch (e) { } return null; } // If user-to-user, both sides need to be authenticated.
219 + var x = obj.id.split('/'), user1 = x[1] + '/' + x[2] + '/' + x[3], user2 = x[4] + '/' + x[5] + '/' + x[6];
220 + if ((x[1] != 'user') && (x[4] != 'user')) { try { obj.close(); } catch (e) { } return null; } // MeshMessenger session must have at least one authenticated user
221 + if ((x[1] == 'user') && (x[4] == 'user')) {
222 + // If this is a user-to-user session, you must be authenticated to join.
223 + if ((user._id != user1) && (user._id != user2)) { try { obj.close(); } catch (e) { } return null; }
224 + } else {
225 + // If only one side of the session is a user
226 + // !!!!! TODO: Need to make sure that one of the two sides is the correct user. !!!!!
227 + }
228 + }
229 +
230 + // Validate that the id is valid, we only need to do this on non-authenticated sessions.
231 + // TODO: Figure out when this needs to be done.
232 + /*
233 + if (!parent.args.notls) {
234 + // Check the identifier, if running without TLS, skip this.
235 + var ids = obj.id.split(':');
236 + if (ids.length != 3) { ws.close(); delete obj.id; return null; } // Invalid ID, drop this.
237 + if (parent.crypto.createHmac('SHA384', parent.relayRandom).update(ids[0] + ':' + ids[1]).digest('hex') != ids[2]) { ws.close(); delete obj.id; return null; } // Invalid HMAC, drop this.
238 + if ((Date.now() - parseInt(ids[1])) > 120000) { ws.close(); delete obj.id; return null; } // Expired time, drop this.
239 + obj.id = ids[0];
240 + }
241 + */
242 +
243 + // Check the peer connection status
244 + {
245 + var relayinfo = parent.wsrelays[obj.id];
246 + if (relayinfo) {
247 + if (relayinfo.state == 1) {
248 + // Check that at least one connection is authenticated
249 + if ((obj.authenticated != true) && (relayinfo.peer1.authenticated != true)) {
250 + ws.close();
251 + parent.parent.debug('relay', 'Relay without-auth: ' + obj.id + ' (' + cleanRemoteAddr(obj.req.ip) + ')');
252 + delete obj.id;
253 + delete obj.ws;
254 + delete obj.peer;
255 + return null;
256 + }
257 +
258 + // Check that both connection are for the same user
259 + if (!obj.id.startsWith('meshmessenger/')) {
260 + var u1 = obj.user ? obj.user._id : obj.ruserid;
261 + var u2 = relayinfo.peer1.user ? relayinfo.peer1.user._id : relayinfo.peer1.ruserid;
262 + if (parent.args.user != null) { // If the server is setup with a default user, correct the userid now.
263 + if (u1 != null) { u1 = 'user/' + domain.id + '/' + parent.args.user.toLowerCase(); }
264 + if (u2 != null) { u2 = 'user/' + domain.id + '/' + parent.args.user.toLowerCase(); }
265 + }
266 + if (u1 != u2) {
267 + ws.close();
268 + parent.parent.debug('relay', 'Relay auth mismatch (' + u1 + ' != ' + u2 + '): ' + obj.id + ' (' + cleanRemoteAddr(obj.req.ip) + ')');
269 + delete obj.id;
270 + delete obj.ws;
271 + delete obj.peer;
272 + return null;
273 + }
274 + }
275 +
276 + // Connect to peer
277 + obj.peer = relayinfo.peer1;
278 + obj.peer.peer = obj;
279 + relayinfo.peer2 = obj;
280 + relayinfo.state = 2;
281 + relayinfo.peer1.ws._socket.resume(); // Release the traffic
282 + relayinfo.peer2.ws._socket.resume(); // Release the traffic
283 + ws.time = relayinfo.peer1.ws.time = Date.now();
284 +
285 + relayinfo.peer1.ws.peer = relayinfo.peer2.ws;
286 + relayinfo.peer2.ws.peer = relayinfo.peer1.ws;
287 +
288 + // Remove the timeout
289 + if (relayinfo.timeout) { clearTimeout(relayinfo.timeout); delete relayinfo.timeout; }
290 +
291 + // Setup the agent PING/PONG timers
292 + if ((typeof parent.parent.args.agentping == 'number') && (obj.pingtimer == null)) { obj.pingtimer = setInterval(sendPing, parent.parent.args.agentping * 1000); }
293 + else if ((typeof parent.parent.args.agentpong == 'number') && (obj.pongtimer == null)) { obj.pongtimer = setInterval(sendPong, parent.parent.args.agentpong * 1000); }
294 +
295 + // Setup the desktop decoder
296 + var agentPeer = null;
297 + if (obj.req.query.browser == null) { agentPeer = obj; }
298 + else if (obj.peer.req.query.browser == null) { agentPeer = obj.peer; }
299 + if (agentPeer != null) { agentPeer.deskDecoder = CreateDesktopDecoder(); }
300 +
301 + // Setup session recording
302 + var sessionUser = obj.user;
303 + if (sessionUser == null) { sessionUser = obj.peer.user; }
304 + if ((sessionUser != null) && (domain.sessionrecording == true || ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.protocols == null) || (domain.sessionrecording.protocols.indexOf(parseInt(obj.req.query.p)) >= 0))))) {
305 + // Get the computer name
306 + parent.db.Get(obj.req.query.nodeid, function (err, nodes) {
307 + var xusername = '', xdevicename = '', xdevicename2 = null;
308 + if ((nodes != null) && (nodes.length == 1)) { xdevicename2 = nodes[0].name; xdevicename = '-' + parent.common.makeFilename(nodes[0].name); }
309 +
310 + // Get the username and make it acceptable as a filename
311 + if (sessionUser._id) { xusername = '-' + parent.common.makeFilename(sessionUser._id.split('/')[2]); }
312 +
313 + var now = new Date(Date.now());
314 + var recFilename = 'relaysession' + ((domain.id == '') ? '' : '-') + domain.id + '-' + now.getUTCFullYear() + '-' + parent.common.zeroPad(now.getUTCMonth(), 2) + '-' + parent.common.zeroPad(now.getUTCDate(), 2) + '-' + parent.common.zeroPad(now.getUTCHours(), 2) + '-' + parent.common.zeroPad(now.getUTCMinutes(), 2) + '-' + parent.common.zeroPad(now.getUTCSeconds(), 2) + xusername + xdevicename + '-' + obj.id + '.mcrec'
315 + var recFullFilename = null;
316 + if (domain.sessionrecording.filepath) {
317 + try { parent.parent.fs.mkdirSync(domain.sessionrecording.filepath); } catch (e) { }
318 + recFullFilename = parent.parent.path.join(domain.sessionrecording.filepath, recFilename);
319 + } else {
320 + try { parent.parent.fs.mkdirSync(parent.parent.recordpath); } catch (e) { }
321 + recFullFilename = parent.parent.path.join(parent.parent.recordpath, recFilename);
322 + }
323 + parent.parent.fs.open(recFullFilename, 'w', function (err, fd) {
324 + if (err != null) {
325 + // Unable to record
326 + try { ws.send('c'); } catch (ex) { } // Send connect to both peers
327 + try { relayinfo.peer1.ws.send('c'); } catch (ex) { }
328 + } else {
329 + // Write the recording file header
330 + var metadata = { magic: 'MeshCentralRelaySession', ver: 1, userid: sessionUser._id, username: sessionUser.name, sessionid: obj.id, ipaddr1: cleanRemoteAddr(obj.req.ip), ipaddr2: cleanRemoteAddr(obj.peer.req.ip), time: new Date().toLocaleString(), protocol: (((obj.req == null) || (obj.req.query == null)) ? null : obj.req.query.p), nodeid: (((obj.req == null) || (obj.req.query == null)) ? null : obj.req.query.nodeid ) };
331 + if (xdevicename2 != null) { metadata.devicename = xdevicename2; }
332 + var firstBlock = JSON.stringify(metadata);
333 + recordingEntry(fd, 1, 0, firstBlock, function () {
334 + try { relayinfo.peer1.ws.logfile = ws.logfile = { fd: fd, lock: false, filename: recFullFilename }; } catch (ex) {
335 + try { ws.send('c'); } catch (ex) { } // Send connect to both peers, 'cr' indicates the session is being recorded.
336 + try { relayinfo.peer1.ws.send('c'); } catch (ex) { }
337 + return;
338 + }
339 + try { ws.send('cr'); } catch (ex) { } // Send connect to both peers, 'cr' indicates the session is being recorded.
340 + try { relayinfo.peer1.ws.send('cr'); } catch (ex) { }
341 + });
342 + }
343 + });
344 + });
345 + } else {
346 + // Send session start
347 + try { ws.send('c'); } catch (ex) { } // Send connect to both peers
348 + try { relayinfo.peer1.ws.send('c'); } catch (ex) { }
349 + }
350 +
351 + parent.parent.debug('relay', 'Relay connected: ' + obj.id + ' (' + cleanRemoteAddr(obj.req.ip) + ' --> ' + cleanRemoteAddr(obj.peer.req.ip) + ')');
352 +
353 + // Log the connection
354 + if (sessionUser != null) {
355 + var msg = 'Started relay session';
356 + if (obj.req.query.p == 1) { msg = 'Started terminal session'; }
357 + else if (obj.req.query.p == 2) { msg = 'Started desktop session'; }
358 + else if (obj.req.query.p == 5) { msg = 'Started file management session'; }
359 + var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: sessionUser._id, username: sessionUser.name, msg: msg + ' \"' + obj.id + '\" from ' + cleanRemoteAddr(obj.peer.req.ip) + ' to ' + cleanRemoteAddr(req.ip), protocol: req.query.p, nodeid: req.query.nodeid };
360 + parent.parent.DispatchEvent(['*', sessionUser._id], obj, event);
361 + }
362 + } else {
363 + // Connected already, drop (TODO: maybe we should re-connect?)
364 + ws.close();
365 + parent.parent.debug('relay', 'Relay duplicate: ' + obj.id + ' (' + cleanRemoteAddr(obj.req.ip) + ')');
366 + delete obj.id;
367 + delete obj.ws;
368 + delete obj.peer;
369 + return null;
370 + }
371 + } else {
372 + // Wait for other relay connection
373 + ws._socket.pause(); // Hold traffic until the other connection
374 + parent.wsrelays[obj.id] = { peer1: obj, state: 1, timeout: setTimeout(function () { closeBothSides(); }, 30000) };
375 + parent.parent.debug('relay', 'Relay holding: ' + obj.id + ' (' + cleanRemoteAddr(obj.req.ip) + ') ' + (obj.authenticated ? 'Authenticated' : ''));
376 +
377 + // Check if a peer server has this connection
378 + if (parent.parent.multiServer != null) {
379 + var rsession = parent.wsPeerRelays[obj.id];
380 + if ((rsession != null) && (rsession.serverId > parent.parent.serverId)) {
381 + // We must initiate the connection to the peer
382 + parent.parent.multiServer.createPeerRelay(ws, req, rsession.serverId, obj.req.session.userid);
383 + delete parent.wsrelays[obj.id];
384 + } else {
385 + // Send message to other peers that we have this connection
386 + parent.parent.multiServer.DispatchMessage(JSON.stringify({ action: 'relay', id: obj.id }));
387 + }
388 + }
389 + }
390 + }
391 + }
392 +
393 + ws.flushSink = function () { try { ws._socket.resume(); } catch (ex) { console.log(ex); } };
394 +
395 + // When data is received from the mesh relay web socket
396 + ws.on('message', function (data) {
397 + // If this data was received by the agent, decode it.
398 + if (this.me.deskDecoder != null) { this.me.deskDecoder.processAgentData(data); }
399 +
400 + //console.log(typeof data, data.length);
401 + if (this.peer != null) {
402 + //if (typeof data == 'string') { console.log('Relay: ' + data); } else { console.log('Relay:' + data.length + ' byte(s)'); }
403 + try {
404 + this._socket.pause();
405 + if (this.logfile != null) {
406 + // Write data to log file then perform relay
407 + var xthis = this;
408 + recordingEntry(this.logfile.fd, 2, ((obj.req.query.browser) ? 2 : 0), data, function () { xthis.peer.send(data, ws.flushSink); });
409 + } else {
410 + // Perform relay
411 + this.peer.send(data, ws.flushSink);
412 + }
413 + } catch (ex) { console.log(ex); }
414 + }
415 + });
416 +
417 + // If error, close both sides of the relay.
418 + ws.on('error', function (err) {
419 + parent.relaySessionErrorCount++;
420 + if (obj.relaySessionCounted) { parent.relaySessionCount--; delete obj.relaySessionCounted; }
421 + console.log('Relay error from ' + cleanRemoteAddr(obj.req.ip) + ', ' + err.toString().split('\r')[0] + '.');
422 + closeBothSides();
423 + });
424 +
425 + // If the relay web socket is closed, close both sides.
426 + ws.on('close', function (req) {
427 + if (obj.relaySessionCounted) { parent.relaySessionCount--; delete obj.relaySessionCounted; }
428 + closeBothSides();
429 + });
430 +
431 + // Close both our side and the peer side.
432 + function closeBothSides() {
433 + if (obj.id != null) {
434 + var relayinfo = parent.wsrelays[obj.id];
435 + if (relayinfo != null) {
436 + if (relayinfo.state == 2) {
437 + var peer = (relayinfo.peer1 == obj) ? relayinfo.peer2 : relayinfo.peer1;
438 +
439 + // Disconnect the peer
440 + try { if (peer.relaySessionCounted) { parent.relaySessionCount--; delete peer.relaySessionCounted; } } catch (ex) { console.log(ex); }
441 + parent.parent.debug('relay', 'Relay disconnect: ' + obj.id + ' (' + cleanRemoteAddr(obj.req.ip) + ' --> ' + cleanRemoteAddr(peer.req.ip) + ')');
442 + try { peer.ws.close(); } catch (e) { } // Soft disconnect
443 + try { peer.ws._socket._parent.end(); } catch (e) { } // Hard disconnect
444 +
445 + // Log the disconnection
446 + if (ws.time) {
447 + var msg = 'Ended relay session';
448 + if (obj.req.query.p == 1) { msg = 'Ended terminal session'; }
449 + else if (obj.req.query.p == 2) { msg = 'Ended desktop session'; }
450 + else if (obj.req.query.p == 5) { msg = 'Ended file management session'; }
451 + if (user) {
452 + var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: user._id, username: user.name, msg: msg + ' \"' + obj.id + '\" from ' + cleanRemoteAddr(obj.peer.req.ip) + ' to ' + cleanRemoteAddr(obj.req.ip) + ', ' + Math.floor((Date.now() - ws.time) / 1000) + ' second(s)', protocol: obj.req.query.p, nodeid: obj.req.query.nodeid };
453 + parent.parent.DispatchEvent(['*', user._id], obj, event);
454 + } else if (peer.user) {
455 + var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: peer.user._id, username: peer.user.name, msg: msg + ' \"' + obj.id + '\" from ' + cleanRemoteAddr(obj.peer.req.ip) + ' to ' + cleanRemoteAddr(obj.req.ip) + ', ' + Math.floor((Date.now() - ws.time) / 1000) + ' second(s)', protocol: obj.req.query.p, nodeid: obj.req.query.nodeid };
456 + parent.parent.DispatchEvent(['*', peer.user._id], obj, event);
457 + }
458 + }
459 +
460 + // Aggressive peer cleanup
461 + delete peer.id;
462 + delete peer.ws;
463 + delete peer.peer;
464 + if (peer.pingtimer != null) { clearInterval(peer.pingtimer); delete peer.pingtimer; }
465 + if (peer.pongtimer != null) { clearInterval(peer.pongtimer); delete peer.pongtimer; }
466 + } else {
467 + parent.parent.debug('relay', 'Relay disconnect: ' + obj.id + ' (' + cleanRemoteAddr(obj.req.ip) + ')');
468 + }
469 +
470 + // Close the recording file if needed
471 + if (ws.logfile != null) {
472 + var logfile = ws.logfile;
473 + delete ws.logfile;
474 + if (peer.ws) { delete peer.ws.logfile; }
475 + recordingEntry(logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd, tag) {
476 + parent.parent.fs.close(fd);
477 + // Now that the recording file is closed, check if we need to index this file.
478 + if (domain.sessionrecording.index !== false) { parent.parent.certificateOperations.acceleratorPerformOperation('indexMcRec', tag.logfile.filename); }
479 + }, { ws: ws, pws: peer.ws, logfile: logfile });
480 + }
481 +
482 + try { ws.close(); } catch (ex) { }
483 + delete parent.wsrelays[obj.id];
484 + }
485 + }
486 +
487 + // Aggressive cleanup
488 + delete obj.id;
489 + delete obj.ws;
490 + delete obj.peer;
491 + if (obj.pingtimer != null) { clearInterval(obj.pingtimer); delete obj.pingtimer; }
492 + if (obj.pongtimer != null) { clearInterval(obj.pongtimer); delete obj.pongtimer; }
493 + }
494 +
495 + // Record a new entry in a recording log
496 + function recordingEntry(fd, type, flags, data, func, tag) {
497 + try {
498 + if (typeof data == 'string') {
499 + // String write
500 + var blockData = Buffer.from(data), header = Buffer.alloc(16); // Header: Type (2) + Flags (2) + Size(4) + Time(8)
501 + header.writeInt16BE(type, 0); // Type (1 = Header, 2 = Network Data)
502 + header.writeInt16BE(flags, 2); // Flags (1 = Binary, 2 = User)
503 + header.writeInt32BE(blockData.length, 4); // Size
504 + header.writeIntBE(new Date(), 10, 6); // Time
505 + var block = Buffer.concat([header, blockData]);
506 + parent.parent.fs.write(fd, block, 0, block.length, function () { func(fd, tag); });
507 + } else {
508 + // Binary write
509 + var header = Buffer.alloc(16); // Header: Type (2) + Flags (2) + Size(4) + Time(8)
510 + header.writeInt16BE(type, 0); // Type (1 = Header, 2 = Network Data)
511 + header.writeInt16BE(flags | 1, 2); // Flags (1 = Binary, 2 = User)
512 + header.writeInt32BE(data.length, 4); // Size
513 + header.writeIntBE(new Date(), 10, 6); // Time
514 + var block = Buffer.concat([header, data]);
515 + parent.parent.fs.write(fd, block, 0, block.length, function () { func(fd, tag); });
516 + }
517 + } catch (ex) { console.log(ex); func(fd, tag); }
518 + }
519 +
520 + // Mark this relay session as authenticated if this is the user end.
521 + obj.authenticated = (user != null);
522 + if (obj.authenticated) {
523 + // Kick off the routing, if we have agent routing instructions, process them here.
524 + // Routing instructions can only be given by a authenticated user
525 + if ((cookie != null) && (cookie.nodeid != null) && (cookie.tcpport != null) && (cookie.domainid != null)) {
526 + // We have routing instructions in the cookie, but first, check user access for this node.
527 + parent.db.Get(cookie.nodeid, function (err, docs) {
528 + if (docs.length == 0) { console.log('ERR: Node not found'); try { obj.close(); } catch (e) { } return; } // Disconnect websocket
529 + const node = docs[0];
530 +
531 + // Check if this user has permission to manage this computer
532 + if ((parent.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { console.log('ERR: Access denied (1)'); try { obj.close(); } catch (e) { } return; }
533 +
534 + // Send connection request to agent
535 + const rcookie = parent.parent.encodeCookie({ ruserid: user._id }, parent.parent.loginCookieEncryptionKey);
536 + if (obj.id == undefined) { obj.id = ('' + Math.random()).substring(2); } // If there is no connection id, generate one.
537 + const command = { nodeid: cookie.nodeid, action: 'msg', type: 'tunnel', value: '*/meshrelay.ashx?id=' + obj.id + '&rauth=' + rcookie, tcpport: cookie.tcpport, tcpaddr: cookie.tcpaddr };
538 + parent.parent.debug('relay', 'Relay: Sending agent tunnel command: ' + JSON.stringify(command));
539 + if (obj.sendAgentMessage(command, user._id, cookie.domainid) == false) { delete obj.id; parent.parent.debug('relay', 'Relay: Unable to contact this agent (' + cleanRemoteAddr(obj.req.ip) + ')'); }
540 + performRelay();
541 + });
542 + return obj;
543 + } else if ((obj.req.query.nodeid != null) && ((obj.req.query.tcpport != null) || (obj.req.query.udpport != null))) {
544 + // We have routing instructions in the URL arguments, but first, check user access for this node.
545 + parent.db.Get(obj.req.query.nodeid, function (err, docs) {
546 + if (docs.length == 0) { console.log('ERR: Node not found'); try { obj.close(); } catch (e) { } return; } // Disconnect websocket
547 + const node = docs[0];
548 +
549 + // Check if this user has permission to manage this computer
550 + if ((parent.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (e) { } return; }
551 +
552 + // Send connection request to agent
553 + if (obj.id == null) { obj.id = ('' + Math.random()).substring(2); } // If there is no connection id, generate one.
554 + const rcookie = parent.parent.encodeCookie({ ruserid: user._id }, parent.parent.loginCookieEncryptionKey);
555 +
556 + if (obj.req.query.tcpport != null) {
557 + const command = { nodeid: obj.req.query.nodeid, action: 'msg', type: 'tunnel', value: '*/meshrelay.ashx?id=' + obj.id + '&rauth=' + rcookie, tcpport: obj.req.query.tcpport, tcpaddr: ((obj.req.query.tcpaddr == null) ? '127.0.0.1' : obj.req.query.tcpaddr) };
558 + parent.parent.debug('relay', 'Relay: Sending agent TCP tunnel command: ' + JSON.stringify(command));
559 + if (obj.sendAgentMessage(command, user._id, domain.id) == false) { delete obj.id; parent.parent.debug('relay', 'Relay: Unable to contact this agent (' + cleanRemoteAddr(obj.req.ip) + ')'); }
560 + } else if (obj.req.query.udpport != null) {
561 + const command = { nodeid: obj.req.query.nodeid, action: 'msg', type: 'tunnel', value: '*/meshrelay.ashx?id=' + obj.id + '&rauth=' + rcookie, udpport: obj.req.query.udpport, udpaddr: ((obj.req.query.udpaddr == null) ? '127.0.0.1' : obj.req.query.udpaddr) };
562 + parent.parent.debug('relay', 'Relay: Sending agent UDP tunnel command: ' + JSON.stringify(command));
563 + if (obj.sendAgentMessage(command, user._id, domain.id) == false) { delete obj.id; parent.parent.debug('relay', 'Relay: Unable to contact this agent (' + cleanRemoteAddr(obj.req.ip) + ')'); }
564 + }
565 + performRelay();
566 + });
567 + return obj;
568 + }
569 + }
570 +
571 + // If this is not an authenticated session, or the session does not have routing instructions, just go ahead an connect to existing session.
572 + performRelay();
573 + return obj;
574 +};
575 +
576 +/*
577 +Relay session recording required that "SessionRecording":true be set in the domain section of the config.json.
578 +Once done, a folder "meshcentral-recordings" will be created next to "meshcentral-data" that will contain all
579 +of the recording files with the .mcrec extension.
580 +
581 +The recording files are binary and contain a set of:
582 +
583 + <HEADER><DATABLOCK><HEADER><DATABLOCK><HEADER><DATABLOCK><HEADER><DATABLOCK>...
584 +
585 +The header is always 16 bytes long and is encoded like this:
586 +
587 + TYPE 2 bytes, 1 = Header, 2 = Network Data, 3 = EndBlock
588 + FLAGS 2 bytes, 0x0001 = Binary, 0x0002 = User
589 + SIZE 4 bytes, Size of the data following this header.
590 + TIME 8 bytes, Time this record was written, number of milliseconds since 1 January, 1970 UTC.
591 +
592 +All values are BigEndian encoded. The first data block is of TYPE 1 and contains a JSON string with information
593 +about this recording. It looks something like this:
594 +
595 +{
596 + magic: 'MeshCentralRelaySession',
597 + ver: 1,
598 + userid: "user\domain\userid",
599 + username: "username",
600 + sessionid: "RandomValue",
601 + ipaddr1: 1.2.3.4,
602 + ipaddr2: 1.2.3.5,
603 + time: new Date().toLocaleString()
604 +}
605 +
606 +The rest of the data blocks are all network traffic that was relayed thru the server. They are of TYPE 2 and have
607 +a given size and timestamp. When looking at network traffic the flags are important:
608 +
609 +- If traffic has the first (0x0001) flag set, the data is binary otherwise it's a string.
610 +- If the traffic has the second (0x0002) flag set, traffic is coming from the user's browser, if not, it's coming from the MeshAgent.
611 +*/
\ No newline at end of file
webserver.js
+10 -1
@@ -57,6 +57,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
57 obj.express = require('express');
58 obj.meshAgentHandler = require('./meshagent.js');
59 obj.meshRelayHandler = require('./meshrelay.js');
60 + obj.meshDesktopMultiplexHandler = require('./meshdesktopmultiplex.js');
61 obj.meshIderHandler = require('./amt/amt-ider.js');
62 obj.meshUserHandler = require('./meshuser.js');
63 obj.interceptor = require('./interceptor');
@@ -3877,7 +3878,6 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
3878 obj.app.get(url + 'userfiles/*', handleDownloadUserFiles);
3879 obj.app.ws(url + 'echo.ashx', handleEchoWebSocket);
3880 obj.app.ws(url + 'apf.ashx', function (ws, req) { obj.parent.apfserver.onConnection(ws); })
3880 - obj.app.ws(url + 'meshrelay.ashx', function (ws, req) { PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie) { obj.meshRelayHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); }); });
3881 obj.app.get(url + 'webrelay.ashx', function (req, res) { res.send('Websocket connection expected'); });
3882 obj.app.get(url + 'health.ashx', function (req, res) { res.send('ok'); }); // TODO: Perform more server checking.
3883 obj.app.ws(url + 'webrelay.ashx', function (ws, req) { PerformWSSessionAuth(ws, req, false, handleRelayWebSocket); });
@@ -3889,6 +3889,15 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
3889 obj.app.get(url + 'player.htm', handlePlayerRequest);
3890 obj.app.get(url + 'player', handlePlayerRequest);
3891 obj.app.ws(url + 'amtactivate', handleAmtActivateWebSocket);
3892 + obj.app.ws(url + 'meshrelay.ashx', function (ws, req) {
3893 + PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie) {
3894 + if ((parent.config.settings.desktopmultiplex === true) && (req.query.p == 2)) {
3895 + obj.meshDesktopMultiplexHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Desktop multiplexor 1-to-n
3896 + } else {
3897 + obj.meshRelayHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Normal relay 1-to-1
3898 + }
3899 + });
3900 + });
3901 if (parent.config.domains[i].agentinvitecodes == true) {
3902 obj.app.get(url + 'invite', handleInviteRequest);
3903 obj.app.post(url + 'invite', handleInviteRequest);