| 1 | /** |
| 2 | * @description MeshCentral remote desktop multiplexor |
| 3 | * @author Ylian Saint-Hilaire |
| 4 | * @copyright Intel Corporation 2018-2022 |
| 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 | |
| 17 | /* |
| 18 | --- KVM Commands --- |
| 19 | MNG_KVM_NOP = 0, |
| 20 | MNG_KVM_KEY = 1, |
| 21 | MNG_KVM_MOUSE = 2, |
| 22 | MNG_KVM_MOUSE_CURSOR = 88, |
| 23 | MNG_KVM_MOUSE_MOVE = 89, |
| 24 | MNG_KVM_PICTURE = 3, |
| 25 | MNG_KVM_COPY = 4, |
| 26 | MNG_KVM_COMPRESSION = 5, |
| 27 | MNG_KVM_REFRESH = 6, |
| 28 | MNG_KVM_SCREEN = 7, |
| 29 | MNG_KVM_PAUSE = 8, |
| 30 | MNG_TERMTEXT = 9, |
| 31 | MNG_CTRLALTDEL = 10, |
| 32 | MNG_KVM_GET_DISPLAYS = 11, |
| 33 | MNG_KVM_SET_DISPLAY = 12, |
| 34 | MNG_KVM_FRAME_RATE_TIMER = 13, |
| 35 | MNG_KVM_INIT_TOUCH = 14, |
| 36 | MNG_KVM_TOUCH = 15, |
| 37 | MNG_KVM_CONNECTCOUNT = 16, |
| 38 | MNG_KVM_MESSAGE = 17, |
| 39 | MNG_KVM_KEYSTATE = 18, |
| 40 | MNG_ECHO = 21, |
| 41 | MNG_JUMBO = 27, |
| 42 | MNG_GETDIR = 50, |
| 43 | MNG_FILEMOVE = 51, |
| 44 | MNG_FILEDELETE = 52, |
| 45 | MNG_FILECOPY = 53, |
| 46 | MNG_FILECREATEDIR = 54, |
| 47 | MNG_FILETRANSFER = 55, |
| 48 | MNG_FILEUPLOAD = 56, |
| 49 | MNG_FILESEARCH = 57, |
| 50 | MNG_FILETRANSFER2 = 58, |
| 51 | MNG_KVM_DISCONNECT = 59, |
| 52 | MNG_GETDIR2 = 60, // Same as MNG_GETDIR but with date/time. |
| 53 | MNG_FILEUPLOAD2 = 61, // Used for slot based fast upload. |
| 54 | MNG_FILEDELETEREC = 62, // Same as MNG_FILEDELETE but recursive |
| 55 | MNG_USERCONSENT = 63, // Used to notify management console of user consent state |
| 56 | MNG_DEBUG = 64, // Debug/Logging Message for ILibRemoteLogging |
| 57 | MNG_ERROR = 65, |
| 58 | MNG_ENCAPSULATE_AGENT_COMMAND = 70, |
| 59 | MNG_KVM_DISPLAY_INFO = 82 |
| 60 | */ |
| 61 | |
| 62 | function CreateDesktopMultiplexor(parent, domain, nodeid, id, func) { |
| 63 | var obj = {}; |
| 64 | obj.id = id; // Unique identifier for this session |
| 65 | obj.nodeid = nodeid; // Remote device nodeid for this session |
| 66 | obj.parent = parent; // Parent web server instance |
| 67 | obj.agent = null; // Reference to the connection object that is the agent. |
| 68 | obj.viewers = []; // Array of references to all viewers. |
| 69 | obj.viewersOverflowCount = 0; // Number of viewers currently in overflow state. |
| 70 | obj.width = 0; // Current width of the display in pixels. |
| 71 | obj.height = 0; // Current height of the display in pixels. |
| 72 | obj.swidth = 0; // Current width of the display in tiles. |
| 73 | obj.sheight = 0; // Current height of the display in tiles. |
| 74 | obj.screen = null; // The main screen, (x * y) --> tile index. Indicates this image is covering each tile on the screen. |
| 75 | obj.counter = 1; // The main counter, used as index for the obj.images table when now images come in. |
| 76 | obj.imagesCount = 0; // Total number of images in the obj.images table. |
| 77 | obj.imagesCounters = {}; // Main table of indexes --> tile count, the number of tiles still using this image. |
| 78 | obj.images = {}; // Main table of indexes --> image data object. |
| 79 | obj.lastScreenSizeCmd = null; // Pointer to the last screen size command from the agent. |
| 80 | obj.lastScreenSizeCounter = 0; // Index into the image table of the screen size command, this is generally also the first command. |
| 81 | obj.lastConsoleMessage = null; // Last agent console message. |
| 82 | obj.firstData = null; // Index in the image table of the first image in the table, generally this points to the display resolution command. |
| 83 | obj.lastData = null; // Index in the images table of the last image in the table. |
| 84 | obj.lastDisplayInfoData = null; // Pointer to the last display information command from the agent (Number of displays). |
| 85 | obj.lastDisplayLocationData = null; // Pointer to the last display location and size command from the agent. |
| 86 | obj.lastKeyState = null; // Pointer to the last key state command from the agent. |
| 87 | obj.desktopPaused = true; // Current desktop pause state, it's true if all viewers are paused. |
| 88 | obj.imageType = 1; // Current image type, 1 = JPEG, 2 = PNG, 3 = TIFF, 4 = WebP |
| 89 | obj.imageCompression = 50; // Current image compression, this is the highest value of all viewers. |
| 90 | obj.imageScaling = 1024; // Current image scaling, this is the highest value of all viewers. |
| 91 | obj.imageFrameRate = 50; // Current framerate setting, this is the lowest values of all viewers. |
| 92 | obj.protocolOptions = null; // Set to the protocol options of the first viewer that connected. |
| 93 | obj.viewerConnected = false; // Set to true if one viewer attempted to connect to the agent. |
| 94 | obj.recordingFile = null; // Present if we are recording to file. |
| 95 | obj.recordingFileSize = 0; // Current size of the recording file. |
| 96 | obj.recordingFileWriting = false; // Set to true is we are in the process if writing to the recording file. |
| 97 | obj.startTime = null; // Starting time of the multiplex session. |
| 98 | obj.userIds = []; // List of userid's that have intertracted with this session. |
| 99 | //obj.autoLock = false; // Automatically lock the remote device once disconnected |
| 100 | |
| 101 | // Accounting |
| 102 | parent.trafficStats.desktopMultiplex.sessions++; |
| 103 | |
| 104 | // Add an agent or viewer |
| 105 | obj.addPeer = function (peer) { |
| 106 | if (obj.viewers == null) { parent.parent.debug('relay', 'DesktopRelay: Error, addingPeer on disposed session'); return; } |
| 107 | if (peer.req == null) return; // This peer is already disposed, don't add it. |
| 108 | if (peer.req.query.browser) { |
| 109 | //console.log('addPeer-viewer', obj.nodeid); |
| 110 | |
| 111 | // Setup the viewer |
| 112 | if (obj.viewers.indexOf(peer) >= 0) return true; |
| 113 | obj.viewers.push(peer); |
| 114 | peer.desktopPaused = true; |
| 115 | peer.imageType = 1; |
| 116 | peer.imageCompression = 30; |
| 117 | peer.imageScaling = 1024; |
| 118 | peer.imageFrameRate = 100; |
| 119 | peer.lastImageNumberSent = null; |
| 120 | peer.dataPtr = obj.firstData; |
| 121 | peer.sending = false; |
| 122 | peer.overflow = false; |
| 123 | peer.sendQueue = []; |
| 124 | peer.paused = false; |
| 125 | peer.startTime = Date.now(); |
| 126 | |
| 127 | // Add the user to the userids list if needed |
| 128 | if ((peer.user != null) && (obj.userIds.indexOf(peer.user._id) == -1)) { obj.userIds.push(peer.user._id); } |
| 129 | |
| 130 | // Setup slow relay is requested. This will show down sending any data to this viewer. |
| 131 | if ((peer.req.query.slowrelay != null)) { |
| 132 | var sr = null; |
| 133 | try { sr = parseInt(peer.req.query.slowrelay); } catch (ex) { } |
| 134 | if ((typeof sr == 'number') && (sr > 0) && (sr < 1000)) { peer.slowRelay = sr; } |
| 135 | } |
| 136 | |
| 137 | // Update user last access time |
| 138 | if ((peer.user != null) && (peer.guestName == null)) { |
| 139 | const user = parent.users[peer.user._id]; |
| 140 | if (user != null) { |
| 141 | const timeNow = Math.floor(Date.now() / 1000); |
| 142 | if (user.access < (timeNow - 300)) { // Only update user access time if longer than 5 minutes |
| 143 | user.access = timeNow; |
| 144 | parent.db.SetUser(user); |
| 145 | |
| 146 | // Event the change |
| 147 | var message = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', domain: domain.id, nolog: 1 }; |
| 148 | if (parent.db.changeStream) { message.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come. |
| 149 | var targets = ['*', 'server-users', user._id]; |
| 150 | if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } } |
| 151 | parent.parent.DispatchEvent(targets, obj, message); |
| 152 | } |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | // Check session recording |
| 157 | var startRecord = false; |
| 158 | if (typeof domain.sessionrecording == 'object') { |
| 159 | // Check if this user is set to record all sessions |
| 160 | if ((domain.sessionrecording.onlyselectedusers === true) && (peer.user != null) && (peer.user.flags != null) && ((peer.user.flags & 2) != 0)) { startRecord = true; } |
| 161 | else if (domain.sessionrecording.onlyselectedusergroups === true) { |
| 162 | // Check if there is a usergroup that requires recording of the session |
| 163 | var user = null; |
| 164 | if (peer.user != null) { user = parent.users[peer.user._id]; } |
| 165 | if ((user != null) && (user.links != null) && (user.links[obj.meshid] == null) && (user.links[obj.nodeid] == null)) { |
| 166 | // This user does not have a direct link to the device group or device. Find all user groups the would cause the link. |
| 167 | for (var i in user.links) { |
| 168 | var ugrp = parent.userGroups[i]; |
| 169 | if ((ugrp != null) && (typeof ugrp.flags == 'number') && ((ugrp.flags & 2) != 0) && (ugrp.links != null) && ((ugrp.links[obj.meshid] != null) || (ugrp.links[obj.nodeid] != null))) { startRecord = true; } |
| 170 | } |
| 171 | } |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | startRecording(domain, startRecord, function () { |
| 176 | // Indicated we are connected |
| 177 | obj.sendToViewer(peer, obj.recordingFile ? 'cr' : 'c'); |
| 178 | |
| 179 | // If the agent sent display information or console message, send it to the viewer |
| 180 | if (obj.lastDisplayInfoData != null) { obj.sendToViewer(peer, obj.lastDisplayInfoData); } |
| 181 | if (obj.lastDisplayLocationData != null) { obj.sendToViewer(peer, obj.lastDisplayLocationData); } |
| 182 | if (obj.lastConsoleMessage != null) { obj.sendToViewer(peer, obj.lastConsoleMessage); } |
| 183 | if (obj.lastKeyState != null) { obj.sendToViewer(peer, obj.lastKeyState); } |
| 184 | |
| 185 | // Log joining the multiplex session |
| 186 | if (obj.startTime != null) { |
| 187 | var event = { etype: 'relay', action: 'relaylog', domain: domain.id, nodeid: obj.nodeid, userid: peer.user ? peer.user._id : null, username: peer.user.name, msgid: 143, msgArgs: [obj.id], msg: "Joined desktop multiplex session \"" + obj.id + "\"", protocol: 2 }; |
| 188 | parent.parent.DispatchEvent(['*', obj.nodeid, peer.user._id, obj.meshid], obj, event); |
| 189 | } |
| 190 | |
| 191 | // Send an updated list of all peers to all viewers |
| 192 | obj.sendSessionMetadata(); |
| 193 | }); |
| 194 | } else { |
| 195 | //console.log('addPeer-agent', obj.nodeid); |
| 196 | if (obj.agent != null) { parent.parent.debug('relay', 'DesktopRelay: Error, duplicate agent connection'); return false; } |
| 197 | |
| 198 | // Setup the agent |
| 199 | obj.agent = peer; |
| 200 | peer.sending = false; |
| 201 | peer.overflow = false; |
| 202 | peer.sendQueue = []; |
| 203 | peer.paused = false; |
| 204 | |
| 205 | // Indicated we are connected and send connection options and protocol if needed |
| 206 | obj.sendToAgent(obj.recordingFile?'cr':'c'); |
| 207 | if (obj.viewerConnected == true) { |
| 208 | if (obj.protocolOptions != null) { obj.sendToAgent(JSON.stringify(obj.protocolOptions)); } // Send connection options |
| 209 | obj.sendToAgent('2'); // Send remote desktop connect |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | // Log multiplex session start |
| 214 | if ((obj.agent != null) && (obj.viewers.length > 0) && (obj.startTime == null)) { |
| 215 | var event = { etype: 'relay', action: 'relaylog', domain: domain.id, nodeid: obj.nodeid, msgid: 145, msgArgs: [obj.id], msg: "Started desktop multiplex session \"" + obj.id + "\"", protocol: 2 }; |
| 216 | if (obj.viewers[0].user != null) { event.userid = obj.viewers[0].user._id; event.username = obj.viewers[0].user.name; } |
| 217 | const targets = ['*', obj.nodeid, obj.meshid]; |
| 218 | if (obj.viewers[0].user != null) { targets.push(obj.viewers[0].user._id); } |
| 219 | parent.parent.DispatchEvent(targets, obj, event); |
| 220 | obj.startTime = Date.now(); |
| 221 | } |
| 222 | return true; |
| 223 | } |
| 224 | |
| 225 | // Remove an agent or viewer |
| 226 | // Return true if this multiplexor is no longer needed. |
| 227 | obj.removePeer = function (peer) { |
| 228 | if (obj.viewers == null) return; |
| 229 | if (peer == obj.agent) { |
| 230 | //console.log('removePeer-agent', obj.nodeid); |
| 231 | // Agent has disconnected, disconnect everyone. |
| 232 | if (obj.viewers != null) { for (var i in obj.viewers) { obj.viewers[i].close(); } } |
| 233 | |
| 234 | // Clean up the agent |
| 235 | obj.agent = null; |
| 236 | |
| 237 | dispose(); |
| 238 | return true; |
| 239 | } else { |
| 240 | //console.log('removePeer-viewer', obj.nodeid); |
| 241 | // Remove a viewer |
| 242 | if (obj.viewers != null) { |
| 243 | var i = obj.viewers.indexOf(peer); |
| 244 | if (i == -1) return false; |
| 245 | obj.viewers.splice(i, 1); |
| 246 | } |
| 247 | |
| 248 | // Resume flow control if this was the peer that was limiting traffic (because it was the fastest one). |
| 249 | if (peer.overflow == true) { |
| 250 | obj.viewersOverflowCount--; |
| 251 | peer.overflow = false; |
| 252 | if ((obj.viewersOverflowCount < obj.viewers.length) && (obj.recordingFileWriting == false) && obj.agent && (obj.agent.paused == true)) { obj.agent.paused = false; obj.agent.ws._socket.resume(); } |
| 253 | } |
| 254 | |
| 255 | // Log leaving the multiplex session |
| 256 | if (obj.startTime != null) { // Used to check if the agent has connected. If not, don't log this event since the session never really started. |
| 257 | // In this code, we want to compute the share of in/out traffic that belongs to this viewer. It includes all of the viewers in/out traffic + all or a portion of the agents in/out traffic. |
| 258 | // The agent traffic needs to get divided out between the viewers fairly. For the time that multiple viewers are present, the agent traffic is divided between the viewers. |
| 259 | |
| 260 | // Compute traffic to and from the browser |
| 261 | var inTraffc, outTraffc; |
| 262 | try { inTraffc = peer.ws._socket.bytesRead; } catch (ex) { } |
| 263 | try { outTraffc = peer.ws._socket.bytesWritten; } catch (ex) { } |
| 264 | |
| 265 | // Add any previous agent traffic accounting |
| 266 | if (peer.agentInTraffic) { inTraffc += peer.agentInTraffic; } |
| 267 | if (peer.outTraffc) { inTraffc += peer.agentOutTraffic; } |
| 268 | |
| 269 | // Compute traffic to and from the agent |
| 270 | if (obj.agent != null) { |
| 271 | // Get unaccounted bytes from/to the agent |
| 272 | var agentInTraffc, agentOutTraffc, agentInTraffc2, agentOutTraffc2; |
| 273 | try { agentInTraffc = agentInTraffc2 = obj.agent.ws._socket.bytesRead; } catch (ex) { } |
| 274 | try { agentOutTraffc = agentOutTraffc2 = obj.agent.ws._socket.bytesWritten; } catch (ex) { } |
| 275 | if (obj.agent.accountedBytesRead) { agentInTraffc -= obj.agent.accountedBytesRead; } |
| 276 | if (obj.agent.accountedBytesWritten) { agentOutTraffc -= obj.agent.accountedBytesWritten; } |
| 277 | obj.agent.accountedBytesRead = agentInTraffc2; |
| 278 | obj.agent.accountedBytesWritten = agentOutTraffc2; |
| 279 | |
| 280 | // Devide up the agent traffic amoung the viewers |
| 281 | var viewerPartIn = Math.floor(agentInTraffc / (obj.viewers.length + 1)); |
| 282 | var viewerPartOut = Math.floor(agentOutTraffc / (obj.viewers.length + 1)); |
| 283 | |
| 284 | // Add the portion to this viewer and all other viewer |
| 285 | inTraffc += viewerPartIn; |
| 286 | outTraffc += viewerPartOut; |
| 287 | for (var i in obj.viewers) { |
| 288 | if (obj.viewers[i].agentInTraffic) { obj.viewers[i].agentInTraffic += viewerPartIn; } else { obj.viewers[i].agentInTraffic = viewerPartIn; } |
| 289 | if (obj.viewers[i].agentOutTraffic) { obj.viewers[i].agentOutTraffic += viewerPartOut; } else { obj.viewers[i].agentOutTraffic = viewerPartOut; } |
| 290 | } |
| 291 | } |
| 292 | |
| 293 | //var event = { etype: 'relay', action: 'relaylog', domain: domain.id, nodeid: obj.nodeid, userid: peer.user._id, username: peer.user.name, msgid: 5, msg: "Left the desktop multiplex session", protocol: 2 }; |
| 294 | const sessionSeconds = Math.floor((Date.now() - peer.startTime) / 1000); |
| 295 | var event = { etype: 'relay', action: 'relaylog', domain: domain.id, nodeid: obj.nodeid, msgid: 144, msgArgs: [obj.id, sessionSeconds], msg: "Left the desktop multiplex session \"" + obj.id + "\" after " + sessionSeconds + " second(s).", protocol: 2, bytesin: inTraffc, bytesout: outTraffc }; |
| 296 | if (peer.user != null) { event.userid = peer.user._id; event.username = peer.user.name; } |
| 297 | if (peer.guestName) { event.guestname = peer.guestName; } |
| 298 | const targets = ['*', obj.nodeid, obj.meshid]; |
| 299 | if (peer.user != null) { targets.push(peer.user._id); } |
| 300 | parent.parent.DispatchEvent(targets, obj, event); |
| 301 | } |
| 302 | |
| 303 | // Aggressive clean up of the viewer |
| 304 | delete peer.desktopPaused; |
| 305 | delete peer.imageType; |
| 306 | delete peer.imageCompression; |
| 307 | delete peer.imageScaling; |
| 308 | delete peer.imageFrameRate; |
| 309 | delete peer.lastImageNumberSent; |
| 310 | delete peer.dataPtr; |
| 311 | delete peer.sending; |
| 312 | delete peer.overflow; |
| 313 | delete peer.sendQueue; |
| 314 | delete peer.startTime; |
| 315 | |
| 316 | // If this is the last viewer, disconnect the agent |
| 317 | if ((obj.viewers != null) && (obj.viewers.length == 0) && (obj.agent != null)) { obj.agent.close(); dispose(); return true; } |
| 318 | |
| 319 | // Send an updated list of all peers to all viewers |
| 320 | obj.sendSessionMetadata(); |
| 321 | } |
| 322 | return false; |
| 323 | } |
| 324 | |
| 325 | // Clean up ourselves |
| 326 | function dispose() { |
| 327 | if (obj.viewers == null) return; |
| 328 | //console.log('dispose', obj.nodeid); |
| 329 | delete obj.viewers; |
| 330 | delete obj.imagesCounters; |
| 331 | delete obj.images; |
| 332 | |
| 333 | // Close the recording file if needed |
| 334 | if (obj.recordingFile != null) { |
| 335 | // Compute session length |
| 336 | if (obj.startTime != null) { obj.sessionStart = obj.startTime; obj.sessionLength = Math.round((Date.now() - obj.startTime) / 1000); } |
| 337 | |
| 338 | // Write the last record of the recording file |
| 339 | var rf = obj.recordingFile; |
| 340 | delete obj.recordingFile; |
| 341 | recordingEntry(rf.fd, 3, 0, 'MeshCentralMCREC', function (fd, filename) { |
| 342 | parent.parent.fs.close(fd); |
| 343 | |
| 344 | // Now that the recording file is closed, check if we need to index this file. |
| 345 | if (domain.sessionrecording.index && domain.sessionrecording.index !== false) { parent.parent.certificateOperations.acceleratorPerformOperation('indexMcRec', filename); } |
| 346 | |
| 347 | // Add a event entry about this recording |
| 348 | var basefile = parent.parent.path.basename(filename); |
| 349 | var event = { etype: 'relay', action: 'recording', domain: domain.id, nodeid: obj.nodeid, msgid: 146, msgArgs: [obj.id, obj.sessionLength], msg: "Finished recording session \"" + obj.id + "\", " + obj.sessionLength + " second(s)", filename: basefile, size: obj.recordingFileSize, protocol: 2, icon: obj.icon, name: obj.name, meshid: obj.meshid, userids: obj.userIds, multiplex: true }; |
| 350 | var mesh = parent.meshes[obj.meshid]; |
| 351 | if (mesh != null) { event.meshname = mesh.name; } |
| 352 | if (obj.sessionStart) { event.startTime = obj.sessionStart; event.lengthTime = obj.sessionLength; } |
| 353 | parent.parent.DispatchEvent(['*', 'recording', obj.nodeid, obj.meshid], obj, event); |
| 354 | |
| 355 | cleanUpRecordings(); |
| 356 | }, rf.filename); |
| 357 | } |
| 358 | |
| 359 | // Log end of multiplex session |
| 360 | if (obj.startTime != null) { |
| 361 | var event = { etype: 'relay', action: 'relaylog', domain: domain.id, nodeid: obj.nodeid, msgid: 147, msgArgs: [obj.id, Math.floor((Date.now() - obj.startTime) / 1000)], msg: "Closed desktop multiplex session \"" + obj.id + "\", " + Math.floor((Date.now() - obj.startTime) / 1000) + ' second(s)', protocol: 2 }; |
| 362 | parent.parent.DispatchEvent(['*', obj.nodeid, obj.meshid], obj, event); |
| 363 | obj.startTime = null; |
| 364 | } |
| 365 | |
| 366 | // Send an updated list of all peers to all viewers |
| 367 | obj.sendSessionMetadata(); |
| 368 | |
| 369 | parent.parent.debug('relay', 'DesktopRelay: Disposing desktop multiplexor'); |
| 370 | } |
| 371 | |
| 372 | // Send data to the agent or queue it up for sending |
| 373 | obj.sendToAgent = function (data) { |
| 374 | if ((obj.viewers == null) || (obj.agent == null)) return; |
| 375 | //console.log('SendToAgent', data.length); |
| 376 | if (obj.agent.sending) { |
| 377 | obj.agent.sendQueue.push(data); |
| 378 | |
| 379 | // Flow control, pause all viewers is the queue is backing up |
| 380 | if (obj.agent.sendQueue > 10) { |
| 381 | obj.agent.overflow = true; |
| 382 | for (var i in obj.viewers) { |
| 383 | var v = obj.viewers[i]; |
| 384 | if (v.paused == false) { v.paused = true; v.ws._socket.pause(); } |
| 385 | } |
| 386 | } |
| 387 | } else { |
| 388 | obj.agent.ws.send(data, sendAgentNext); |
| 389 | } |
| 390 | } |
| 391 | |
| 392 | // Send more data to the agent |
| 393 | function sendAgentNext() { |
| 394 | if ((obj.viewers == null) || (obj.agent == null)) return; |
| 395 | if (obj.agent.sendQueue.length > 0) { |
| 396 | // Send from the pending send queue |
| 397 | obj.agent.ws.send(obj.agent.sendQueue.shift(), sendAgentNext); |
| 398 | } else { |
| 399 | // Nothing to send |
| 400 | obj.agent.sending = false; |
| 401 | |
| 402 | // Flow control, resume all viewers |
| 403 | if (obj.agent.overflow == true) { |
| 404 | obj.agent.overflow = false; |
| 405 | for (var i in obj.viewers) { |
| 406 | var v = obj.viewers[i]; |
| 407 | if (v.paused == true) { v.paused = false; v.ws._socket.resume(); } |
| 408 | } |
| 409 | } |
| 410 | } |
| 411 | } |
| 412 | |
| 413 | // Send the list of all users currently vieweing this session to all viewers and servers |
| 414 | obj.sendSessionMetadata = function () { |
| 415 | var allUsers = {}; |
| 416 | if (obj.viewers != null) { |
| 417 | for (var i in obj.viewers) { |
| 418 | var v = obj.viewers[i]; |
| 419 | if ((v.user != null) && (v.user._id != null)) { |
| 420 | var id = v.user._id; |
| 421 | if (v.guestName) { id += '/guest:' + Buffer.from(v.guestName).toString('base64'); } // If this is a guest connect, add the Base64 guest name. |
| 422 | if (allUsers[id] == null) { allUsers[id] = 1; } else { allUsers[id]++; } |
| 423 | } |
| 424 | } |
| 425 | obj.sendToAllViewers(JSON.stringify({ type: 'metadata', 'ctrlChannel': '102938', users: allUsers, startTime: obj.startTime })); |
| 426 | } |
| 427 | |
| 428 | // Update the sessions attached the to agent |
| 429 | if (obj.nodeid != null) { |
| 430 | const xagent = parent.wsagents[obj.nodeid]; |
| 431 | if (xagent != null) { |
| 432 | if (xagent.sessions == null) { xagent.sessions = {}; } |
| 433 | xagent.sessions.multidesk = allUsers; |
| 434 | xagent.updateSessions(); |
| 435 | } |
| 436 | } |
| 437 | } |
| 438 | |
| 439 | // Send this command to all viewers |
| 440 | obj.sendToAllViewers = function (data) { |
| 441 | if (obj.viewers == null) return; |
| 442 | for (var i in obj.viewers) { obj.sendToViewer(obj.viewers[i], data); } |
| 443 | } |
| 444 | |
| 445 | // Send this command to all viewers |
| 446 | obj.sendToAllInputViewers = function (data) { |
| 447 | if (obj.viewers == null) return; |
| 448 | for (var i in obj.viewers) { if (obj.viewers[i].viewOnly != true) { obj.sendToViewer(obj.viewers[i], data); } } |
| 449 | } |
| 450 | |
| 451 | // Send data to the viewer or queue it up for sending |
| 452 | obj.sendToViewer = function (viewer, data) { |
| 453 | if ((viewer == null) || (obj.viewers == null)) return; |
| 454 | //console.log('SendToViewer', data.length); |
| 455 | if (viewer.sending) { |
| 456 | viewer.sendQueue.push(data); |
| 457 | } else { |
| 458 | viewer.sending = true; |
| 459 | if (viewer.slowRelay) { |
| 460 | setTimeout(function () { try { viewer.ws.send(data, function () { sendViewerNext(viewer); }); } catch (ex) { } }, viewer.slowRelay); |
| 461 | } else { |
| 462 | try { viewer.ws.send(data, function () { sendViewerNext(viewer); }); } catch (ex) { } |
| 463 | } |
| 464 | |
| 465 | // Flow control, pause the agent if needed |
| 466 | checkViewerOverflow(viewer); |
| 467 | } |
| 468 | } |
| 469 | |
| 470 | // Check if a viewer is in overflow situation |
| 471 | function checkViewerOverflow(viewer) { |
| 472 | if ((viewer.overflow == true) || (obj.viewers == null)) return; |
| 473 | if ((viewer.sendQueue.length > 5) || ((viewer.dataPtr != null) && (viewer.dataPtr != obj.lastData))) { |
| 474 | viewer.overflow = true; |
| 475 | obj.viewersOverflowCount++; |
| 476 | if ((obj.viewersOverflowCount >= obj.viewers.length) && obj.agent && (obj.agent.paused == false)) { obj.agent.paused = true; obj.agent.ws._socket.pause(); } |
| 477 | } |
| 478 | } |
| 479 | |
| 480 | // Check if a viewer is in underflow situation |
| 481 | function checkViewerUnderflow(viewer) { |
| 482 | if ((viewer.overflow == false) || (obj.viewers == null)) return; |
| 483 | if ((viewer.sendQueue.length <= 5) && ((viewer.dataPtr == null) || (viewer.dataPtr == obj.lastData))) { |
| 484 | viewer.overflow = false; |
| 485 | obj.viewersOverflowCount--; |
| 486 | if ((obj.viewersOverflowCount < obj.viewers.length) && (obj.recordingFileWriting == false) && obj.agent && (obj.agent.paused == true)) { obj.agent.paused = false; obj.agent.ws._socket.resume(); } |
| 487 | } |
| 488 | } |
| 489 | |
| 490 | // Send more data to the viewer |
| 491 | function sendViewerNext(viewer) { |
| 492 | if ((viewer.sendQueue == null) || (obj.viewers == null)) return; |
| 493 | if (viewer.sendQueue.length > 0) { |
| 494 | // Send from the pending send queue |
| 495 | if (viewer.sending == false) { viewer.sending = true; } |
| 496 | if (viewer.slowRelay) { |
| 497 | setTimeout(function () { try { viewer.ws.send(viewer.sendQueue.shift(), function () { sendViewerNext(viewer); }); } catch (ex) { } }, viewer.slowRelay); |
| 498 | } else { |
| 499 | try { viewer.ws.send(viewer.sendQueue.shift(), function () { sendViewerNext(viewer); }); } catch (ex) { } |
| 500 | } |
| 501 | checkViewerOverflow(viewer); |
| 502 | } else { |
| 503 | if (viewer.dataPtr != null) { |
| 504 | // Send the next image |
| 505 | //if ((viewer.lastImageNumberSent != null) && ((viewer.lastImageNumberSent + 1) != (viewer.dataPtr))) { console.log('SVIEW-S1', viewer.lastImageNumberSent, viewer.dataPtr); } // DEBUG |
| 506 | var image = obj.images[viewer.dataPtr]; |
| 507 | viewer.lastImageNumberSent = viewer.dataPtr; |
| 508 | //if ((image.next != null) && ((viewer.dataPtr + 1) != image.next)) { console.log('SVIEW-S2', viewer.dataPtr, image.next); } // DEBUG |
| 509 | viewer.dataPtr = image.next; |
| 510 | if (viewer.slowRelay) { |
| 511 | setTimeout(function () { try { viewer.ws.send(image.data, function () { sendViewerNext(viewer); }); } catch (ex) { } }, viewer.slowRelay); |
| 512 | } else { |
| 513 | try { viewer.ws.send(image.data, function () { sendViewerNext(viewer); }); } catch (ex) { } |
| 514 | } |
| 515 | |
| 516 | // Flow control, pause the agent if needed |
| 517 | if (viewer.sending == false) { viewer.sending = true; } |
| 518 | checkViewerOverflow(viewer); |
| 519 | } else { |
| 520 | // Nothing to send |
| 521 | viewer.sending = false; |
| 522 | |
| 523 | // Flow control, resume agent if needed |
| 524 | checkViewerUnderflow(viewer); |
| 525 | } |
| 526 | } |
| 527 | } |
| 528 | |
| 529 | // Process data coming from the agent or any viewers |
| 530 | obj.processData = function (peer, data) { |
| 531 | if (obj.viewers == null) return; |
| 532 | if (peer == obj.agent) { |
| 533 | obj.recordingFileWriting = true; |
| 534 | recordData(true, data, function () { |
| 535 | if (obj.viewers == null) return; |
| 536 | obj.recordingFileWriting = false; |
| 537 | if ((obj.viewersOverflowCount < obj.viewers.length) && obj.agent && (obj.agent.paused == true)) { obj.agent.paused = false; obj.agent.ws._socket.resume(); } |
| 538 | obj.processAgentData(data); |
| 539 | }); |
| 540 | } else { |
| 541 | obj.processViewerData(peer, data); |
| 542 | } |
| 543 | } |
| 544 | |
| 545 | // Process incoming viewer data |
| 546 | obj.processViewerData = function (viewer, data) { |
| 547 | if (typeof data == 'string') { |
| 548 | if (data == '2') { |
| 549 | if (obj.viewerConnected == false) { |
| 550 | if (obj.agent != null) { |
| 551 | if (obj.protocolOptions != null) { obj.sendToAgent(JSON.stringify(obj.protocolOptions)); } // Send connection options |
| 552 | obj.sendToAgent('2'); // Send remote desktop connect |
| 553 | } |
| 554 | obj.viewerConnected = true; |
| 555 | } |
| 556 | return; |
| 557 | } |
| 558 | var json = null; |
| 559 | try { json = JSON.parse(data); } catch (ex) { } |
| 560 | if (json == null) return; |
| 561 | if ((json.type == 'options') && (obj.protocolOptions == null)) { obj.protocolOptions = json; } |
| 562 | if (json.ctrlChannel == '102938') { |
| 563 | if ((json.type == 'lock') && (viewer.viewOnly == false)) { obj.sendToAgent('{"ctrlChannel":"102938","type":"lock"}'); } // Account lock support |
| 564 | if ((json.type == 'autolock') && (viewer.viewOnly == false) && (typeof json.value == 'boolean')) { obj.sendToAgent('{"ctrlChannel":"102938","type":"autolock","value":' + json.value + '}'); } // Lock on disconnect |
| 565 | } |
| 566 | return; |
| 567 | } |
| 568 | |
| 569 | //console.log('ViewerData', data.length, typeof data, data); |
| 570 | if ((typeof data != 'object') || (data.length < 4)) return; // Ignore all control traffic for now (WebRTC) |
| 571 | var command = data.readUInt16BE(0); |
| 572 | var cmdsize = data.readUInt16BE(2); |
| 573 | if (data.length != cmdsize) return; // Invalid command length |
| 574 | |
| 575 | //console.log('ViewerData', data.length, command, cmdsize); |
| 576 | switch (command) { |
| 577 | case 1: // Key Events, forward to agent |
| 578 | if (viewer.viewOnly == false) { obj.sendToAgent(data); } |
| 579 | break; |
| 580 | case 2: // Mouse events, forward to agent |
| 581 | if (viewer.viewOnly == false) { obj.sendToAgent(data); } |
| 582 | break; |
| 583 | case 5: // Compression |
| 584 | if (data.length < 10) return; |
| 585 | viewer.imageType = data[4]; // Image type: 1 = JPEG, 2 = PNG, 3 = TIFF, 4 = WebP |
| 586 | viewer.imageCompression = data[5]; |
| 587 | viewer.imageScaling = data.readUInt16BE(6); |
| 588 | viewer.imageFrameRate = data.readUInt16BE(8); |
| 589 | //console.log('Viewer-Compression', viewer.imageType, viewer.imageCompression, viewer.imageScaling, viewer.imageFrameRate); |
| 590 | |
| 591 | // See if this changes anything |
| 592 | var viewersimageType = null; |
| 593 | var viewersimageCompression = null; |
| 594 | var viewersimageScaling = null; |
| 595 | var viewersimageFrameRate = null; |
| 596 | for (var i in obj.viewers) { |
| 597 | if (viewersimageType == null) { viewersimageType = obj.viewers[i].imageType; } else if (obj.viewers[i].imageType != viewersimageType) { viewersimageType = 1; }; // Default to JPEG if viewers has different image formats |
| 598 | if ((viewersimageCompression == null) || (obj.viewers[i].imageCompression > viewersimageCompression)) { viewersimageCompression = obj.viewers[i].imageCompression; }; |
| 599 | if ((viewersimageScaling == null) || (obj.viewers[i].imageScaling > viewersimageScaling)) { viewersimageScaling = obj.viewers[i].imageScaling; }; |
| 600 | if ((viewersimageFrameRate == null) || (obj.viewers[i].imageFrameRate < viewersimageFrameRate)) { viewersimageFrameRate = obj.viewers[i].imageFrameRate; }; |
| 601 | } |
| 602 | if ((obj.imageCompression != viewersimageCompression) || (obj.imageScaling != viewersimageScaling) || (obj.imageFrameRate != viewersimageFrameRate)) { |
| 603 | // Update and send to agent new compression settings |
| 604 | obj.imageType = viewersimageType; |
| 605 | obj.imageCompression = viewersimageCompression; |
| 606 | obj.imageScaling = viewersimageScaling; |
| 607 | obj.imageFrameRate = viewersimageFrameRate |
| 608 | //console.log('Send-Agent-Compression', obj.imageType, obj.imageCompression, obj.imageScaling, obj.imageFrameRate); |
| 609 | var cmd = Buffer.alloc(10); |
| 610 | cmd.writeUInt16BE(5, 0); // Command 5, compression |
| 611 | cmd.writeUInt16BE(10, 2); // Command size, 10 bytes long |
| 612 | cmd[4] = obj.imageType; // Image type: 1 = JPEG, 2 = PNG, 3 = TIFF, 4 = WebP |
| 613 | cmd[5] = obj.imageCompression; // Image compression level |
| 614 | cmd.writeUInt16BE(obj.imageScaling, 6); // Scaling level |
| 615 | cmd.writeUInt16BE(obj.imageFrameRate, 8); // Frame rate timer |
| 616 | obj.sendToAgent(cmd); |
| 617 | } |
| 618 | break; |
| 619 | case 6: // Refresh, handle this on the server |
| 620 | //console.log('Viewer-Refresh'); |
| 621 | viewer.dataPtr = obj.firstData; // Start over |
| 622 | if (viewer.sending == false) { sendViewerNext(viewer); } |
| 623 | break; |
| 624 | case 8: // Pause and unpause |
| 625 | if (data.length != 5) break; |
| 626 | var pause = data[4]; // 0 = Unpause, 1 = Pause |
| 627 | if (viewer.desktopPaused == (pause == 1)) break; |
| 628 | viewer.desktopPaused = (pause == 1); |
| 629 | //console.log('Viewer-' + ((pause == 1)?'Pause':'UnPause')); |
| 630 | var viewersPaused = true; |
| 631 | for (var i in obj.viewers) { if (obj.viewers[i].desktopPaused == false) { viewersPaused = false; }; } |
| 632 | if (viewersPaused != obj.desktopPaused) { |
| 633 | obj.desktopPaused = viewersPaused; |
| 634 | //console.log('Send-Agent-' + ((viewersPaused == true) ? 'Pause' : 'UnPause')); |
| 635 | data[4] = (viewersPaused == true) ? 1 : 0; |
| 636 | obj.sendToAgent(data); |
| 637 | } |
| 638 | break; |
| 639 | case 10: // CTRL-ALT-DEL, forward to agent |
| 640 | if (viewer.viewOnly == false) { obj.sendToAgent(data); } |
| 641 | break; |
| 642 | case 12: // SET DISPLAY, forward to agent |
| 643 | if (viewer.viewOnly == false) { obj.sendToAgent(data); } |
| 644 | break; |
| 645 | case 14: // Touch setup |
| 646 | break; |
| 647 | case 82: // Request display information |
| 648 | if (obj.lastDisplayLocationData != null) { obj.sendToAgent(obj.lastDisplayLocationData); } |
| 649 | break; |
| 650 | case 85: // Unicode Key Events, forward to agent |
| 651 | if (viewer.viewOnly == false) { obj.sendToAgent(data); } |
| 652 | break; |
| 653 | case 87: // Remote input lock, forward to agent |
| 654 | if (viewer.viewOnly == false) { obj.sendToAgent(data); } |
| 655 | break; |
| 656 | default: |
| 657 | console.log('Un-handled viewer command: ' + command); |
| 658 | break; |
| 659 | } |
| 660 | } |
| 661 | |
| 662 | // Process incoming agent data |
| 663 | obj.processAgentData = function (data) { |
| 664 | if ((typeof data != 'object') || (data.length < 4)) { |
| 665 | if (typeof data == 'string') { |
| 666 | var json = null; |
| 667 | try { json = JSON.parse(data); } catch (ex) { } |
| 668 | if (json == null) return; |
| 669 | if (json.type == 'console') { |
| 670 | // This is a console message, store it and forward this to all viewers |
| 671 | if (json.msg != null) { obj.lastConsoleMessage = data; } else { obj.lastConsoleMessage = null; } |
| 672 | obj.sendToAllViewers(data); |
| 673 | } |
| 674 | // All other control messages (notably WebRTC), are ignored for now. |
| 675 | } |
| 676 | return; // Ignore all other traffic |
| 677 | } |
| 678 | const jumboData = data; |
| 679 | var command = data.readUInt16BE(0); |
| 680 | var cmdsize = data.readUInt16BE(2); |
| 681 | //console.log('AgentData', data.length, command, cmdsize); |
| 682 | if ((command == 27) && (cmdsize == 8)) { |
| 683 | // Jumbo packet |
| 684 | if (data.length >= 12) { |
| 685 | command = data.readUInt16BE(8); |
| 686 | cmdsize = data.readUInt32BE(4); |
| 687 | if (data.length == (cmdsize + 8)) { |
| 688 | data = data.slice(8, data.length); |
| 689 | } else { |
| 690 | console.log('TODO-PARTIAL-JUMBO', command, cmdsize, data.length); |
| 691 | return; // TODO |
| 692 | } |
| 693 | } |
| 694 | } |
| 695 | |
| 696 | switch (command) { |
| 697 | case 3: // Tile, check dimentions and store |
| 698 | if ((data.length < 10) || (obj.lastData == null)) break; |
| 699 | var x = data.readUInt16BE(4), y = data.readUInt16BE(6); |
| 700 | var dimensions = require('image-size').imageSize(data.slice(8)); |
| 701 | var sx = (x / 16), sy = (y / 16), sw = (dimensions.width / 16), sh = (dimensions.height / 16); |
| 702 | obj.counter++; |
| 703 | |
| 704 | // Keep a reference to this image & how many tiles it covers |
| 705 | obj.images[obj.counter] = { next: null, prev: obj.lastData, data: jumboData }; |
| 706 | obj.images[obj.lastData].next = obj.counter; |
| 707 | obj.lastData = obj.counter; |
| 708 | obj.imagesCounters[obj.counter] = (sw * sh); |
| 709 | obj.imagesCount++; |
| 710 | if (obj.imagesCount == 2000000000) { obj.imagesCount = 1; } // Loop the counter if needed |
| 711 | |
| 712 | //console.log('Adding Image ' + obj.counter, x, y, dimensions.width, dimensions.height); |
| 713 | |
| 714 | // Update the screen with the correct pointers. |
| 715 | for (var i = 0; i < sw; i++) { |
| 716 | for (var j = 0; j < sh; j++) { |
| 717 | var k = ((obj.swidth * (j + sy)) + (i + sx)); |
| 718 | const oi = obj.screen[k]; |
| 719 | obj.screen[k] = obj.counter; |
| 720 | if ((oi != null) && (--obj.imagesCounters[oi] == 0)) { |
| 721 | // Remove data from the link list |
| 722 | obj.imagesCount--; |
| 723 | var d = obj.images[oi]; |
| 724 | //console.log('Removing Image', oi, obj.images[oi].prev, obj.images[oi].next); |
| 725 | obj.images[d.prev].next = d.next; |
| 726 | obj.images[d.next].prev = d.prev; |
| 727 | delete obj.images[oi]; |
| 728 | delete obj.imagesCounters[oi]; |
| 729 | |
| 730 | // If any viewers are currently on image "oi" must be moved to "d.next" |
| 731 | for (var l in obj.viewers) { const v = obj.viewers[l]; if (v.dataPtr == oi) { v.dataPtr = d.next; } } |
| 732 | } |
| 733 | } |
| 734 | } |
| 735 | |
| 736 | // Any viewer on dataPtr null, change to this image |
| 737 | for (var i in obj.viewers) { |
| 738 | const v = obj.viewers[i]; |
| 739 | if (v.dataPtr == null) { v.dataPtr = obj.counter; if (v.sending == false) { sendViewerNext(v); } } |
| 740 | } |
| 741 | |
| 742 | // Debug, display the link list |
| 743 | //var xx = '', xptr = obj.firstData; |
| 744 | //while (xptr != null) { xx += '>' + xptr; xptr = obj.images[xptr].next; } |
| 745 | //console.log('list', xx); |
| 746 | //console.log('images', obj.imagesCount); |
| 747 | |
| 748 | break; |
| 749 | case 4: // Tile Copy, do nothing. |
| 750 | break; |
| 751 | case 7: // Screen Size, clear the screen state and compute the tile count |
| 752 | if (data.length < 8) break; |
| 753 | if ((obj.width === data.readUInt16BE(4)) && (obj.height === data.readUInt16BE(6))) break; // Same screen size as before, skip this. |
| 754 | obj.counter++; |
| 755 | obj.lastScreenSizeCmd = data; |
| 756 | obj.lastScreenSizeCounter = obj.counter; |
| 757 | obj.width = data.readUInt16BE(4); |
| 758 | obj.height = data.readUInt16BE(6); |
| 759 | obj.swidth = obj.width / 16; |
| 760 | obj.sheight = obj.height / 16; |
| 761 | if (Math.floor(obj.swidth) != obj.swidth) { obj.swidth = Math.floor(obj.swidth) + 1; } |
| 762 | if (Math.floor(obj.sheight) != obj.sheight) { obj.sheight = Math.floor(obj.sheight) + 1; } |
| 763 | |
| 764 | // Reset the display |
| 765 | obj.screen = new Array(obj.swidth * obj.sheight); |
| 766 | obj.imagesCount = 0; |
| 767 | obj.imagesCounters = {}; |
| 768 | obj.images = {}; |
| 769 | obj.images[obj.counter] = { next: null, prev: null, data: data }; |
| 770 | obj.firstData = obj.counter; |
| 771 | obj.lastData = obj.counter; |
| 772 | |
| 773 | // Add viewers must be set to start at "obj.counter" |
| 774 | for (var i in obj.viewers) { |
| 775 | const v = obj.viewers[i]; |
| 776 | v.dataPtr = obj.counter; |
| 777 | if (v.sending == false) { sendViewerNext(v); } |
| 778 | } |
| 779 | |
| 780 | //console.log("ScreenSize", obj.width, obj.height, obj.swidth, obj.sheight, obj.swidth * obj.sheight); |
| 781 | break; |
| 782 | case 11: // GetDisplays |
| 783 | // Store and send this to all viewers right away |
| 784 | obj.lastDisplayInfoData = data; |
| 785 | obj.sendToAllInputViewers(data); |
| 786 | break; |
| 787 | case 12: // SetDisplay |
| 788 | obj.sendToAllInputViewers(data); |
| 789 | break; |
| 790 | case 14: // KVM_INIT_TOUCH |
| 791 | break; |
| 792 | case 15: // KVM_TOUCH |
| 793 | break; |
| 794 | case 17: // MNG_KVM_MESSAGE |
| 795 | // Send this to all viewers right away |
| 796 | obj.sendToAllViewers(data); |
| 797 | break; |
| 798 | case 18: // MNG_KVM_KEYSTATE |
| 799 | // Store and send this to all viewers right away |
| 800 | obj.lastKeyState = data; |
| 801 | obj.sendToAllInputViewers(data); |
| 802 | break; |
| 803 | case 65: // Alert |
| 804 | // Send this to all viewers right away |
| 805 | obj.sendToAllViewers(data); |
| 806 | break; |
| 807 | case 82: |
| 808 | // Display information |
| 809 | if ((data.length < 14) || (((data.length - 4) % 10) != 0)) break; // Command must be 14 bytes and have header + 10 byte for each display. |
| 810 | obj.lastDisplayLocationData = data; |
| 811 | obj.sendToAllInputViewers(data); |
| 812 | break; |
| 813 | case 87: // MNG_KVM_INPUT_LOCK |
| 814 | // Send this to all viewers right away |
| 815 | // This will update all views on the current state of the input lock |
| 816 | obj.sendToAllInputViewers(data); |
| 817 | break; |
| 818 | case 88: // MNG_KVM_MOUSE_CURSOR |
| 819 | // Send this to all viewers right away |
| 820 | obj.sendToAllInputViewers(data); |
| 821 | break; |
| 822 | default: |
| 823 | console.log('Un-handled agent command: ' + command + ', length: ' + cmdsize); |
| 824 | break; |
| 825 | } |
| 826 | } |
| 827 | |
| 828 | function startRecording(domain, start, func) { |
| 829 | if ((obj.pendingRecording == 1) || (obj.recordingFile != null)) { func(true); return; } // Check if already recording |
| 830 | if (start == false) { func(false); return; } // Just skip this |
| 831 | obj.pendingRecording = 1; |
| 832 | var now = new Date(Date.now()); |
| 833 | var recFilename = 'desktopSession' + ((domain.id == '') ? '' : '-') + domain.id + '-' + now.getUTCFullYear() + '-' + parent.common.zeroPad(now.getUTCMonth() + 1, 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) + '-' + obj.nodeid.split('/')[2] + '.mcrec' |
| 834 | var recFullFilename = null; |
| 835 | if (domain.sessionrecording.filepath) { |
| 836 | try { parent.parent.fs.mkdirSync(domain.sessionrecording.filepath); } catch (e) { } |
| 837 | recFullFilename = parent.parent.path.join(domain.sessionrecording.filepath, recFilename); |
| 838 | } else { |
| 839 | try { parent.parent.fs.mkdirSync(parent.parent.recordpath); } catch (e) { } |
| 840 | recFullFilename = parent.parent.path.join(parent.parent.recordpath, recFilename); |
| 841 | } |
| 842 | parent.parent.fs.open(recFullFilename, 'w', function (err, fd) { |
| 843 | delete obj.pendingRecording; |
| 844 | if (err != null) { |
| 845 | parent.parent.debug('relay', 'Relay: Unable to record to file: ' + recFullFilename); |
| 846 | func(false); |
| 847 | return; |
| 848 | } |
| 849 | // Write the recording file header |
| 850 | parent.parent.debug('relay', 'Relay: Started recording to file: ' + recFullFilename); |
| 851 | var metadata = { magic: 'MeshCentralRelaySession', ver: 1, nodeid: obj.nodeid, meshid: obj.meshid, time: new Date().toLocaleString(), protocol: 2, devicename: obj.name, devicegroup: obj.meshname }; |
| 852 | var firstBlock = JSON.stringify(metadata); |
| 853 | recordingEntry(fd, 1, 0, firstBlock, function () { |
| 854 | obj.recordingFile = { fd: fd, filename: recFullFilename }; |
| 855 | obj.recordingFileWriting = false; |
| 856 | func(true); |
| 857 | }); |
| 858 | }); |
| 859 | } |
| 860 | |
| 861 | // Here, we check if we have to record the device, regardless of what user is looking at it. |
| 862 | function recordingSetup(domain, func) { |
| 863 | var record = false; |
| 864 | |
| 865 | // Setup session recording |
| 866 | if (((domain.sessionrecording == true) || ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.protocols == null) || (domain.sessionrecording.protocols.indexOf(2) >= 0))))) { |
| 867 | record = true; |
| 868 | |
| 869 | // Check again to make sure we need to start recording |
| 870 | if ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.onlyselecteddevicegroups === true) || (domain.sessionrecording.onlyselectedusergroups === true) || (domain.sessionrecording.onlyselectedusers === true))) { |
| 871 | record = false; |
| 872 | |
| 873 | // Check device group recording |
| 874 | if (domain.sessionrecording.onlyselecteddevicegroups === true) { |
| 875 | var mesh = parent.meshes[obj.meshid]; |
| 876 | if ((mesh.flags != null) && ((mesh.flags & 4) != 0)) { record = true; } |
| 877 | } |
| 878 | } |
| 879 | } |
| 880 | startRecording(domain, record, func); |
| 881 | } |
| 882 | |
| 883 | // Record data to the recording file |
| 884 | function recordData(isAgent, data, func) { |
| 885 | try { |
| 886 | if (obj.recordingFile != null) { |
| 887 | // Write data to recording file |
| 888 | recordingEntry(obj.recordingFile.fd, 2, (isAgent ? 0 : 2), data, function () { func(data); }); |
| 889 | } else { |
| 890 | func(data); |
| 891 | } |
| 892 | } catch (ex) { console.log(ex); } |
| 893 | } |
| 894 | |
| 895 | // Record a new entry in a recording log |
| 896 | function recordingEntry(fd, type, flags, data, func, tag) { |
| 897 | try { |
| 898 | if (typeof data == 'string') { |
| 899 | // String write |
| 900 | var blockData = Buffer.from(data), header = Buffer.alloc(16); // Header: Type (2) + Flags (2) + Size(4) + Time(8) |
| 901 | header.writeInt16BE(type, 0); // Type (1 = Header, 2 = Network Data) |
| 902 | header.writeInt16BE(flags, 2); // Flags (1 = Binary, 2 = User) |
| 903 | header.writeInt32BE(blockData.length, 4); // Size |
| 904 | header.writeIntBE(new Date(), 10, 6); // Time |
| 905 | var block = Buffer.concat([header, blockData]); |
| 906 | parent.parent.fs.write(fd, block, 0, block.length, function () { func(fd, tag); }); |
| 907 | obj.recordingFileSize += block.length; |
| 908 | } else { |
| 909 | // Binary write |
| 910 | var header = Buffer.alloc(16); // Header: Type (2) + Flags (2) + Size(4) + Time(8) |
| 911 | header.writeInt16BE(type, 0); // Type (1 = Header, 2 = Network Data) |
| 912 | header.writeInt16BE(flags | 1, 2); // Flags (1 = Binary, 2 = User) |
| 913 | header.writeInt32BE(data.length, 4); // Size |
| 914 | header.writeIntBE(new Date(), 10, 6); // Time |
| 915 | var block = Buffer.concat([header, data]); |
| 916 | parent.parent.fs.write(fd, block, 0, block.length, function () { func(fd, tag); }); |
| 917 | obj.recordingFileSize += block.length; |
| 918 | } |
| 919 | } catch (ex) { console.log(ex); func(fd, tag); } |
| 920 | } |
| 921 | |
| 922 | // If there is a recording quota, remove any old recordings if needed |
| 923 | function cleanUpRecordings() { |
| 924 | if ((parent.cleanUpRecordingsActive !== true) && domain.sessionrecording && ((typeof domain.sessionrecording.maxrecordings == 'number') || (typeof domain.sessionrecording.maxrecordingsizemegabytes == 'number') || (typeof domain.sessionrecording.maxrecordingdays == 'number'))) { |
| 925 | parent.cleanUpRecordingsActive = true; |
| 926 | setTimeout(function () { |
| 927 | var recPath = null, fs = require('fs'), now = Date.now(); |
| 928 | if (domain.sessionrecording.filepath) { recPath = domain.sessionrecording.filepath; } else { recPath = parent.parent.recordpath; } |
| 929 | fs.readdir(recPath, function (err, files) { |
| 930 | if ((err != null) || (files == null)) { delete parent.cleanUpRecordingsActive; return; } |
| 931 | var recfiles = []; |
| 932 | for (var i in files) { |
| 933 | if (files[i].endsWith('.mcrec')) { |
| 934 | var j = files[i].indexOf('-'); |
| 935 | if (j > 0) { |
| 936 | var stats = null; |
| 937 | try { stats = fs.statSync(parent.parent.path.join(recPath, files[i])); } catch (ex) { } |
| 938 | if (stats != null) { recfiles.push({ n: files[i], r: files[i].substring(j + 1), s: stats.size, t: stats.mtimeMs }); } |
| 939 | } |
| 940 | } |
| 941 | } |
| 942 | recfiles.sort(function (a, b) { if (a.r < b.r) return 1; if (a.r > b.r) return -1; return 0; }); |
| 943 | var totalFiles = 0, totalSize = 0; |
| 944 | for (var i in recfiles) { |
| 945 | var overQuota = false; |
| 946 | if ((typeof domain.sessionrecording.maxrecordings == 'number') && (domain.sessionrecording.maxrecordings > 0) && (totalFiles >= domain.sessionrecording.maxrecordings)) { overQuota = true; } |
| 947 | else if ((typeof domain.sessionrecording.maxrecordingsizemegabytes == 'number') && (domain.sessionrecording.maxrecordingsizemegabytes > 0) && (totalSize >= (domain.sessionrecording.maxrecordingsizemegabytes * 1048576))) { overQuota = true; } |
| 948 | else if ((typeof domain.sessionrecording.maxrecordingdays == 'number') && (domain.sessionrecording.maxrecordingdays > 0) && (((now - recfiles[i].t) / 1000 / 60 / 60 / 24) >= domain.sessionrecording.maxrecordingdays)) { overQuota = true; } |
| 949 | if (overQuota) { fs.unlinkSync(parent.parent.path.join(recPath, recfiles[i].n)); } |
| 950 | totalFiles++; |
| 951 | totalSize += recfiles[i].s; |
| 952 | } |
| 953 | delete parent.cleanUpRecordingsActive; |
| 954 | }); |
| 955 | }, 500); |
| 956 | } |
| 957 | } |
| 958 | |
| 959 | // Get node information |
| 960 | parent.db.Get(nodeid, function (err, nodes) { |
| 961 | if ((err != null) || (nodes.length != 1)) { func(null); return; } |
| 962 | obj.meshid = nodes[0].meshid; |
| 963 | obj.icon = nodes[0].icon; |
| 964 | obj.name = nodes[0].name; |
| 965 | recordingSetup(domain, function () { func(obj); }); |
| 966 | }); |
| 967 | return obj; |
| 968 | } |
| 969 | |
| 970 | function checkDeviceSharePublicIdentifier(parent, domain, nodeid, pid, extraKey, func) { |
| 971 | // Check the public id |
| 972 | parent.db.GetAllTypeNodeFiltered([nodeid], domain.id, 'deviceshare', null, function (err, docs) { |
| 973 | if ((err != null) || (docs.length == 0)) { func(false); return; } |
| 974 | |
| 975 | // Search for the device share public identifier |
| 976 | var found = false; |
| 977 | for (var i = 0; i < docs.length; i++) { if ((docs[i].publicid == pid) && ((docs[i].extrakey == null) || (docs[i].extrakey === extraKey))) { found = true; } } |
| 978 | func(found); |
| 979 | }); |
| 980 | } |
| 981 | |
| 982 | module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie) { |
| 983 | if ((cookie != null) && (typeof cookie.nid == 'string') && (typeof cookie.pid == 'string')) { |
| 984 | checkDeviceSharePublicIdentifier(parent, domain, cookie.nid, cookie.pid, cookie.k, function (result) { |
| 985 | // If the identifier if not found, close the connection |
| 986 | if (result == false) { try { ws.close(); } catch (e) { } return; } |
| 987 | // Public device sharing identifier found, continue as normal. |
| 988 | CreateMeshRelayEx(parent, ws, req, domain, user, cookie); |
| 989 | }); |
| 990 | } else { |
| 991 | CreateMeshRelayEx(parent, ws, req, domain, user, cookie); |
| 992 | } |
| 993 | } |
| 994 | |
| 995 | // If we are in multi-server mode, the desktop multiplexor needs to be created on the server with the agent connected to it. |
| 996 | // So, if the agent is connected to a different server, just relay the connection to that server |
| 997 | function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { |
| 998 | // Do validation work |
| 999 | if (cookie) { |
| 1000 | if ((typeof cookie.expire == 'number') && (cookie.expire <= Date.now())) { delete req.query.nodeid; } |
| 1001 | else if (typeof cookie.nid == 'string') { req.query.nodeid = cookie.nid; } |
| 1002 | } |
| 1003 | if ((req.query.nodeid == null) || (req.query.p != '2') || (req.query.id == null) || (domain == null)) { try { ws.close(); } catch (e) { } return; } // Not is not a valid remote desktop connection. |
| 1004 | |
| 1005 | // Check routing if in multi-server mode |
| 1006 | var nodeid = req.query.nodeid; |
| 1007 | if (parent.parent.multiServer != null) { |
| 1008 | const routing = parent.parent.GetRoutingServerIdNotSelf(nodeid, 1); // 1 = MeshAgent routing type |
| 1009 | if (routing == null) { |
| 1010 | // No need to relay the connection to a different server |
| 1011 | return CreateMeshRelayEx2(parent, ws, req, domain, user, cookie); |
| 1012 | } else { |
| 1013 | // We must relay the connection to a different server |
| 1014 | return parent.parent.multiServer.createPeerRelay(ws, req, routing.serverid, req.session.userid); |
| 1015 | } |
| 1016 | } else { |
| 1017 | // No need to relay the connection to a different server |
| 1018 | return CreateMeshRelayEx2(parent, ws, req, domain, user, cookie); |
| 1019 | } |
| 1020 | } |
| 1021 | |
| 1022 | function CreateMeshRelayEx2(parent, ws, req, domain, user, cookie) { |
| 1023 | const currentTime = Date.now(); |
| 1024 | var obj = {}; |
| 1025 | obj.ws = ws; |
| 1026 | obj.ws.me = obj; |
| 1027 | obj.id = req.query.id; |
| 1028 | obj.nodeid = req.query.nodeid; |
| 1029 | obj.user = user; |
| 1030 | obj.ruserid = null; |
| 1031 | obj.req = req; // Used in multi-server.js |
| 1032 | obj.viewOnly = ((cookie != null) && (cookie.vo == 1)); // set view only mode |
| 1033 | if ((cookie != null) && (cookie.nouser == 1)) { obj.nouser = true; } // This is a relay without user authentication |
| 1034 | |
| 1035 | // If the domain has remote desktop viewonly set, force everyone to be in viewonly mode. |
| 1036 | if ((typeof domain.desktop == 'object') && (domain.desktop.viewonly == true)) { obj.viewOnly = true; } |
| 1037 | |
| 1038 | // Setup traffic accounting |
| 1039 | if (parent.trafficStats.desktopMultiplex == null) { parent.trafficStats.desktopMultiplex = { connections: 1, sessions: 0, in: 0, out: 0 }; } else { parent.trafficStats.desktopMultiplex.connections++; } |
| 1040 | ws._socket.bytesReadEx = 0; |
| 1041 | ws._socket.bytesWrittenEx = 0; |
| 1042 | |
| 1043 | // Setup subscription for desktop sharing public identifier |
| 1044 | // If the identifier is removed, drop the connection |
| 1045 | if ((cookie != null) && (typeof cookie.pid == 'string')) { |
| 1046 | obj.pid = cookie.pid; |
| 1047 | obj.guestName = cookie.gn; |
| 1048 | parent.parent.AddEventDispatch([obj.nodeid], obj); |
| 1049 | obj.HandleEvent = function (source, event, ids, id) { if ((event.action == 'removedDeviceShare') && (obj.pid == event.publicid)) { obj.close(); } } |
| 1050 | } |
| 1051 | |
| 1052 | // Check relay authentication |
| 1053 | if ((user == null) && (obj.req.query != null) && (obj.req.query.rauth != null)) { |
| 1054 | const rcookie = parent.parent.decodeCookie(obj.req.query.rauth, parent.parent.loginCookieEncryptionKey, 240); // Cookie with 4 hour timeout |
| 1055 | if (rcookie.ruserid != null) { obj.ruserid = rcookie.ruserid; } else if (rcookie.nouser === 1) { obj.rnouser = true; } |
| 1056 | if (rcookie.nodeid != null) { obj.nodeid = rcookie.nodeid; } |
| 1057 | } |
| 1058 | |
| 1059 | // If there is no authentication, drop this connection |
| 1060 | if ((obj.id != null) && (obj.user == null) && (obj.ruserid == null) && (obj.nouser !== true) && (obj.rnouser !== true)) { try { ws.close(); parent.parent.debug('relay', 'DesktopRelay: Connection with no authentication (' + obj.req.clientIp + ')'); } catch (e) { console.log(e); } return; } |
| 1061 | |
| 1062 | // Relay session count (we may remove this in the future) |
| 1063 | obj.relaySessionCounted = true; |
| 1064 | parent.relaySessionCount++; |
| 1065 | |
| 1066 | // Mesh Rights |
| 1067 | const MESHRIGHT_EDITMESH = 1; |
| 1068 | const MESHRIGHT_MANAGEUSERS = 2; |
| 1069 | const MESHRIGHT_MANAGECOMPUTERS = 4; |
| 1070 | const MESHRIGHT_REMOTECONTROL = 8; |
| 1071 | const MESHRIGHT_AGENTCONSOLE = 16; |
| 1072 | const MESHRIGHT_SERVERFILES = 32; |
| 1073 | const MESHRIGHT_WAKEDEVICE = 64; |
| 1074 | const MESHRIGHT_SETNOTES = 128; |
| 1075 | const MESHRIGHT_REMOTEVIEW = 256; |
| 1076 | |
| 1077 | // Site rights |
| 1078 | const SITERIGHT_SERVERBACKUP = 1; |
| 1079 | const SITERIGHT_MANAGEUSERS = 2; |
| 1080 | const SITERIGHT_SERVERRESTORE = 4; |
| 1081 | const SITERIGHT_FILEACCESS = 8; |
| 1082 | const SITERIGHT_SERVERUPDATE = 16; |
| 1083 | const SITERIGHT_LOCKED = 32; |
| 1084 | |
| 1085 | // Clean a IPv6 address that encodes a IPv4 address |
| 1086 | function cleanRemoteAddr(addr) { if (addr.startsWith('::ffff:')) { return addr.substring(7); } else { return addr; } } |
| 1087 | |
| 1088 | // Disconnect this agent |
| 1089 | obj.close = function (arg) { |
| 1090 | if (obj.ws == null) return; // Already closed. |
| 1091 | |
| 1092 | // Close the connection |
| 1093 | if ((arg == 1) || (arg == null)) { try { ws.close(); parent.parent.debug('relay', 'DesktopRelay: Soft disconnect (' + obj.req.clientIp + ')'); } catch (e) { console.log(e); } } // Soft close, close the websocket |
| 1094 | if (arg == 2) { try { ws._socket._parent.end(); parent.parent.debug('relay', 'DesktopRelay: Hard disconnect (' + obj.req.clientIp + ')'); } catch (e) { console.log(e); } } // Hard close, close the TCP socket |
| 1095 | if (obj.relaySessionCounted) { parent.relaySessionCount--; delete obj.relaySessionCounted; } |
| 1096 | if ((obj.deskMultiplexor != null) && (typeof obj.deskMultiplexor == 'object') && (obj.deskMultiplexor.removePeer(obj) == true)) { delete parent.desktoprelays[obj.nodeid]; } |
| 1097 | |
| 1098 | // Aggressive cleanup |
| 1099 | delete obj.id; |
| 1100 | delete obj.ws; |
| 1101 | delete obj.req; |
| 1102 | delete obj.user; |
| 1103 | delete obj.nodeid; |
| 1104 | delete obj.ruserid; |
| 1105 | delete obj.expireTimer; |
| 1106 | delete obj.deskMultiplexor; |
| 1107 | |
| 1108 | // Clear timers if present |
| 1109 | if (obj.pingtimer != null) { clearInterval(obj.pingtimer); delete obj.pingtimer; } |
| 1110 | if (obj.pongtimer != null) { clearInterval(obj.pongtimer); delete obj.pongtimer; } |
| 1111 | |
| 1112 | // Unsubscribe |
| 1113 | if (obj.pid != null) { parent.parent.RemoveAllEventDispatch(obj); } |
| 1114 | }; |
| 1115 | |
| 1116 | obj.sendAgentMessage = function (command, userid, domainid) { |
| 1117 | var rights, mesh; |
| 1118 | if (command.nodeid == null) return false; |
| 1119 | var user = null; |
| 1120 | if (userid != null) { user = parent.users[userid]; if (user == null) return false; } |
| 1121 | var splitnodeid = command.nodeid.split('/'); |
| 1122 | // Check that we are in the same domain and the user has rights over this node. |
| 1123 | if ((splitnodeid[0] == 'node') && (splitnodeid[1] == domainid)) { |
| 1124 | // Get the user object |
| 1125 | // See if the node is connected |
| 1126 | var agent = parent.wsagents[command.nodeid]; |
| 1127 | if (agent != null) { |
| 1128 | // Check if we have permission to send a message to that node |
| 1129 | if (userid == null) { rights = MESHRIGHT_REMOTECONTROL; } else { rights = parent.GetNodeRights(user, agent.dbMeshKey, agent.dbNodeKey); } |
| 1130 | mesh = parent.meshes[agent.dbMeshKey]; |
| 1131 | if ((rights != null) && (mesh != null) || ((rights & 16) != 0)) { // TODO: 16 is console permission, may need more gradular permission checking |
| 1132 | if (ws.sessionId) { command.sessionid = ws.sessionId; } // Set the session id, required for responses. |
| 1133 | command.rights = rights; // Add user rights flags to the message |
| 1134 | if ((command.rights != 0xFFFFFFFF) && ((command.rights & 0x100) != 0)) { command.rights -= 0x100; } // Since the multiplexor will enforce view-only, remove MESHRIGHT_REMOTEVIEWONLY |
| 1135 | if (typeof command.consent == 'number') { command.consent = command.consent | mesh.consent; } else { command.consent = mesh.consent; } // Add user consent |
| 1136 | if (typeof domain.userconsentflags == 'number') { command.consent |= domain.userconsentflags; } // Add server required consent flags |
| 1137 | if (user != null) { |
| 1138 | command.username = user.name; // Add user name |
| 1139 | command.realname = user.realname; // Add real name |
| 1140 | } |
| 1141 | if (typeof domain.desktopprivacybartext == 'string') { command.privacybartext = domain.desktopprivacybartext; } // Privacy bar text |
| 1142 | delete command.nodeid; // Remove the nodeid since it's implyed. |
| 1143 | agent.send(JSON.stringify(command)); |
| 1144 | return true; |
| 1145 | } |
| 1146 | } else { |
| 1147 | // Check if a peer server is connected to this agent |
| 1148 | var routing = parent.parent.GetRoutingServerIdNotSelf(command.nodeid, 1); // 1 = MeshAgent routing type |
| 1149 | if (routing != null) { |
| 1150 | // Check if we have permission to send a message to that node |
| 1151 | if (userid == null) { rights = MESHRIGHT_REMOTECONTROL; } else { rights = parent.GetNodeRights(user, routing.meshid, command.nodeid); } |
| 1152 | mesh = parent.meshes[routing.meshid]; |
| 1153 | if (rights != null || ((rights & 16) != 0)) { // TODO: 16 is console permission, may need more gradular permission checking |
| 1154 | if (ws.sessionId) { command.fromSessionid = ws.sessionId; } // Set the session id, required for responses. |
| 1155 | command.rights = rights; // Add user rights flags to the message |
| 1156 | if ((command.rights != 0xFFFFFFFF) && ((command.rights & 0x00000100) != 0)) { command.rights -= 0x00000100; } // Since the multiplexor will enforce view-only, remove MESHRIGHT_REMOTEVIEWONLY |
| 1157 | if (typeof command.consent == 'number') { command.consent = command.consent | mesh.consent; } else { command.consent = mesh.consent; } // Add user consent |
| 1158 | if (typeof domain.userconsentflags == 'number') { command.consent |= domain.userconsentflags; } // Add server required consent flags |
| 1159 | if (user != null) { |
| 1160 | command.username = user.name; // Add user name |
| 1161 | command.realname = user.realname; // Add real name |
| 1162 | } |
| 1163 | if (typeof domain.desktopprivacybartext == 'string') { command.privacybartext = domain.desktopprivacybartext; } // Privacy bar text |
| 1164 | parent.parent.multiServer.DispatchMessageSingleServer(command, routing.serverid); |
| 1165 | return true; |
| 1166 | } |
| 1167 | } |
| 1168 | } |
| 1169 | } |
| 1170 | return false; |
| 1171 | }; |
| 1172 | |
| 1173 | // Send a PING/PONG message |
| 1174 | function sendPing() { |
| 1175 | try { obj.ws.send('{"ctrlChannel":"102938","type":"ping"}'); } catch (ex) { } |
| 1176 | try { if (obj.peer != null) { obj.peer.ws.send('{"ctrlChannel":"102938","type":"ping"}'); } } catch (ex) { } |
| 1177 | } |
| 1178 | function sendPong() { |
| 1179 | try { obj.ws.send('{"ctrlChannel":"102938","type":"pong"}'); } catch (ex) { } |
| 1180 | try { if (obj.peer != null) { obj.peer.ws.send('{"ctrlChannel":"102938","type":"pong"}'); } } catch (ex) { } |
| 1181 | } |
| 1182 | |
| 1183 | function performRelay(retryCount) { |
| 1184 | if ((obj.id == null) || (retryCount > 20)) { try { obj.close(); } catch (e) { } return null; } // Attempt to connect without id, drop this. |
| 1185 | if (retryCount == 0) { ws._socket.setKeepAlive(true, 240000); } // Set TCP keep alive |
| 1186 | |
| 1187 | /* |
| 1188 | // Validate that the id is valid, we only need to do this on non-authenticated sessions. |
| 1189 | // TODO: Figure out when this needs to be done. |
| 1190 | if (user == null) { |
| 1191 | // Check the identifier, if running without TLS, skip this. |
| 1192 | var ids = obj.id.split(':'); |
| 1193 | if (ids.length != 3) { ws.close(); delete obj.id; return null; } // Invalid ID, drop this. |
| 1194 | 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. |
| 1195 | if ((Date.now() - parseInt(ids[1])) > 120000) { ws.close(); delete obj.id; return null; } // Expired time, drop this. |
| 1196 | obj.id = ids[0]; |
| 1197 | } |
| 1198 | */ |
| 1199 | |
| 1200 | if (retryCount == 0) { |
| 1201 | // Setup the agent PING/PONG timers |
| 1202 | if ((typeof parent.parent.args.agentping == 'number') && (obj.pingtimer == null)) { obj.pingtimer = setInterval(sendPing, parent.parent.args.agentping * 1000); } |
| 1203 | else if ((typeof parent.parent.args.agentpong == 'number') && (obj.pongtimer == null)) { obj.pongtimer = setInterval(sendPong, parent.parent.args.agentpong * 1000); } |
| 1204 | |
| 1205 | parent.parent.debug('relay', 'DesktopRelay: Connection (' + obj.req.clientIp + ')'); |
| 1206 | } |
| 1207 | |
| 1208 | // Create if needed and add this peer to the desktop multiplexor |
| 1209 | obj.deskMultiplexor = parent.desktoprelays[obj.nodeid]; |
| 1210 | if (obj.deskMultiplexor == null) { |
| 1211 | parent.desktoprelays[obj.nodeid] = 1; // Indicate that the creating of the desktop multiplexor is pending. |
| 1212 | parent.parent.debug('relay', 'DesktopRelay: Creating new desktop multiplexor'); |
| 1213 | CreateDesktopMultiplexor(parent, domain, obj.nodeid, obj.id, function (deskMultiplexor) { |
| 1214 | if (deskMultiplexor != null) { |
| 1215 | // Desktop multiplexor was created, use it. |
| 1216 | obj.deskMultiplexor = deskMultiplexor; |
| 1217 | parent.desktoprelays[obj.nodeid] = obj.deskMultiplexor; |
| 1218 | obj.deskMultiplexor.addPeer(obj); |
| 1219 | ws._socket.resume(); // Release the traffic |
| 1220 | } else { |
| 1221 | // An error has occured, close this connection |
| 1222 | delete parent.desktoprelays[obj.nodeid]; |
| 1223 | ws.close(); |
| 1224 | } |
| 1225 | }); |
| 1226 | } else { |
| 1227 | if (obj.deskMultiplexor == 1) { |
| 1228 | // The multiplexor is being created, hold a little and try again. This is to prevent a possible race condition. |
| 1229 | setTimeout(function () { performRelay(++retryCount); }, 50); |
| 1230 | } else { |
| 1231 | // Hook up this peer to the multiplexor and release the traffic |
| 1232 | obj.deskMultiplexor.addPeer(obj); |
| 1233 | ws._socket.resume(); |
| 1234 | } |
| 1235 | } |
| 1236 | } |
| 1237 | |
| 1238 | // When data is received from the mesh relay web socket |
| 1239 | ws.on('message', function (data) { |
| 1240 | // Data accounting |
| 1241 | parent.trafficStats.desktopMultiplex.in += (this._socket.bytesRead - this._socket.bytesReadEx); |
| 1242 | parent.trafficStats.desktopMultiplex.out += (this._socket.bytesWritten - this._socket.bytesWrittenEx); |
| 1243 | this._socket.bytesReadEx = this._socket.bytesRead; |
| 1244 | this._socket.bytesWrittenEx = this._socket.bytesWritten; |
| 1245 | |
| 1246 | // If this data was received by the agent, decode it. |
| 1247 | if (this.me.deskMultiplexor != null) { this.me.deskMultiplexor.processData(this.me, data); } |
| 1248 | }); |
| 1249 | |
| 1250 | // If error, close both sides of the relay. |
| 1251 | ws.on('error', function (err) { |
| 1252 | //console.log('ws-error', err); |
| 1253 | parent.relaySessionErrorCount++; |
| 1254 | console.log('Relay error from ' + obj.req.clientIp + ', ' + err.toString().split('\r')[0] + '.'); |
| 1255 | obj.close(); |
| 1256 | }); |
| 1257 | |
| 1258 | // If the relay web socket is closed, close both sides. |
| 1259 | ws.on('close', function (req) { |
| 1260 | // Data accounting |
| 1261 | parent.trafficStats.desktopMultiplex.in += (this._socket.bytesRead - this._socket.bytesReadEx); |
| 1262 | parent.trafficStats.desktopMultiplex.out += (this._socket.bytesWritten - this._socket.bytesWrittenEx); |
| 1263 | this._socket.bytesReadEx = this._socket.bytesRead; |
| 1264 | this._socket.bytesWrittenEx = this._socket.bytesWritten; |
| 1265 | |
| 1266 | //console.log('ws-close', req); |
| 1267 | obj.close(); |
| 1268 | }); |
| 1269 | |
| 1270 | // If this session has a expire time, setup the expire timer now. |
| 1271 | setExpireTimer(); |
| 1272 | |
| 1273 | // Mark this relay session as authenticated if this is the user end. |
| 1274 | obj.authenticated = ((user != null) || (obj.nouser === true)); |
| 1275 | if (obj.authenticated) { |
| 1276 | // Kick off the routing, if we have agent routing instructions, process them here. |
| 1277 | // Routing instructions can only be given by a authenticated user |
| 1278 | if ((cookie != null) && (cookie.nodeid != null) && (cookie.tcpport != null) && (cookie.domainid != null)) { |
| 1279 | // We have routing instructions in the cookie, but first, check user access for this node. |
| 1280 | parent.db.Get(cookie.nodeid, function (err, docs) { |
| 1281 | if (obj.req == null) return; // This connection was closed. |
| 1282 | if (docs.length == 0) { console.log('ERR: Node not found'); try { obj.close(); } catch (e) { } return; } // Disconnect websocket |
| 1283 | const node = docs[0]; |
| 1284 | |
| 1285 | // Check if this user has permission to manage this computer |
| 1286 | if ((obj.nouser !== true) && ((parent.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0)) { console.log('ERR: Access denied (1)'); try { obj.close(); } catch (e) { } return; } |
| 1287 | |
| 1288 | // Send connection request to agent |
| 1289 | const rcookieData = { nodeid: node._id }; |
| 1290 | if (user != null) { rcookieData.ruserid = user._id; } else if (obj.nouser === true) { rcookieData.nouser = 1; } |
| 1291 | const rcookie = parent.parent.encodeCookie(rcookieData, parent.parent.loginCookieEncryptionKey); |
| 1292 | if (obj.id == undefined) { obj.id = ('' + Math.random()).substring(2); } // If there is no connection id, generate one. |
| 1293 | const command = { nodeid: node._id, action: 'msg', type: 'tunnel', value: '*/meshrelay.ashx?id=' + obj.id + '&rauth=' + rcookie, tcpport: cookie.tcpport, tcpaddr: cookie.tcpaddr }; |
| 1294 | parent.parent.debug('relay', 'Relay: Sending agent tunnel command: ' + JSON.stringify(command)); |
| 1295 | if (obj.sendAgentMessage(command, user ? user._id : null, cookie.domainid) == false) { delete obj.id; parent.parent.debug('relay', 'Relay: Unable to contact this agent (' + obj.req.clientIp + ')'); } |
| 1296 | performRelay(0); |
| 1297 | }); |
| 1298 | return obj; |
| 1299 | } else if ((obj.req.query.nodeid != null) && ((obj.req.query.tcpport != null) || (obj.req.query.udpport != null))) { |
| 1300 | // We have routing instructions in the URL arguments, but first, check user access for this node. |
| 1301 | parent.db.Get(obj.req.query.nodeid, function (err, docs) { |
| 1302 | if (obj.req == null) return; // This connection was closed. |
| 1303 | if (docs.length == 0) { console.log('ERR: Node not found'); try { obj.close(); } catch (e) { } return; } // Disconnect websocket |
| 1304 | const node = docs[0]; |
| 1305 | |
| 1306 | // Check if this user has permission to manage this computer |
| 1307 | if ((obj.nouser !== true) && ((parent.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0)) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (e) { } return; } |
| 1308 | |
| 1309 | // Send connection request to agent |
| 1310 | if (obj.id == null) { obj.id = ('' + Math.random()).substring(2); } // If there is no connection id, generate one. |
| 1311 | const rcookieData = { nodeid: node._id }; |
| 1312 | if (user != null) { rcookieData.ruserid = user._id; } else if (obj.nouser === true) { rcookieData.nouser = 1; } |
| 1313 | const rcookie = parent.parent.encodeCookie(rcookieData, parent.parent.loginCookieEncryptionKey); |
| 1314 | |
| 1315 | if (obj.req.query.tcpport != null) { |
| 1316 | const command = { nodeid: node._id, 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) }; |
| 1317 | parent.parent.debug('relay', 'Relay: Sending agent TCP tunnel command: ' + JSON.stringify(command)); |
| 1318 | if (obj.sendAgentMessage(command, user ? user._id : null, domain.id) == false) { delete obj.id; parent.parent.debug('relay', 'Relay: Unable to contact this agent (' + obj.req.clientIp + ')'); } |
| 1319 | } else if (obj.req.query.udpport != null) { |
| 1320 | const command = { nodeid: node._id, 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) }; |
| 1321 | parent.parent.debug('relay', 'Relay: Sending agent UDP tunnel command: ' + JSON.stringify(command)); |
| 1322 | if (obj.sendAgentMessage(command, user ? user._id : null, domain.id) == false) { delete obj.id; parent.parent.debug('relay', 'Relay: Unable to contact this agent (' + obj.req.clientIp + ')'); } |
| 1323 | } |
| 1324 | performRelay(0); |
| 1325 | }); |
| 1326 | return obj; |
| 1327 | } else if ((cookie != null) && (cookie.nid != null) && (typeof cookie.r == 'number') && (typeof cookie.cf == 'number') && (typeof cookie.gn == 'string')) { |
| 1328 | // We have routing instructions in the cookie, but first, check user access for this node. |
| 1329 | parent.db.Get(cookie.nid, function (err, docs) { |
| 1330 | if (obj.req == null) return; // This connection was closed. |
| 1331 | if (docs.length == 0) { console.log('ERR: Node not found'); try { obj.close(); } catch (e) { } return; } // Disconnect websocket |
| 1332 | const node = docs[0]; |
| 1333 | |
| 1334 | // Check if this user has permission to manage this computer |
| 1335 | if ((obj.nouser !== true) && ((parent.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0)) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (e) { } return; } |
| 1336 | |
| 1337 | // Send connection request to agent |
| 1338 | if (obj.id == null) { obj.id = ('' + Math.random()).substring(2); } |
| 1339 | const rcookieData = { nodeid: node._id }; |
| 1340 | if (user != null) { rcookieData.ruserid = user._id; } else if (obj.nouser === true) { rcookieData.nouser = 1; } |
| 1341 | const rcookie = parent.parent.encodeCookie(rcookieData, parent.parent.loginCookieEncryptionKey); |
| 1342 | const command = { nodeid: node._id, action: 'msg', type: 'tunnel', value: '*/meshrelay.ashx?p=2&id=' + obj.id + '&rauth=' + rcookie + '&nodeid=' + node._id, soptions: {}, usage: 2, rights: cookie.r, guestuserid: user._id, guestname: cookie.gn, consent: cookie.cf, remoteaddr: cleanRemoteAddr(obj.req.clientIp) }; |
| 1343 | if (typeof domain.terminaluservariable == 'string') { command.soptions.terminalUserVariable = domain.terminaluservariable; } |
| 1344 | if (typeof domain.consentmessages == 'object') { |
| 1345 | if (typeof domain.consentmessages.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; } |
| 1346 | if (typeof domain.consentmessages.desktop == 'string') { command.soptions.consentMsgDesktop = domain.consentmessages.desktop; } |
| 1347 | if (typeof domain.consentmessages.terminal == 'string') { command.soptions.consentMsgTerminal = domain.consentmessages.terminal; } |
| 1348 | if (typeof domain.consentmessages.files == 'string') { command.soptions.consentMsgFiles = domain.consentmessages.files; } |
| 1349 | if ((typeof domain.consentmessages.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; } |
| 1350 | if (domain.consentmessages.autoacceptontimeout === true) { command.soptions.consentAutoAccept = true; } |
| 1351 | if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; } |
| 1352 | if (domain.consentmessages.autoacceptifdesktopnouser === true) { command.soptions.consentAutoAcceptIfDesktopNoUser = true; } |
| 1353 | if (domain.consentmessages.autoacceptifterminalnouser === true) { command.soptions.consentAutoAcceptIfTerminalNoUser = true; } |
| 1354 | if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAutoAcceptIfFileNoUser = true; } |
| 1355 | if (domain.consentmessages.autoacceptiflocked === true) { command.soptions.consentAutoAcceptIfLocked = true; } |
| 1356 | if (domain.consentmessages.autoacceptifdesktoplocked === true) { command.soptions.consentAutoAcceptIfDesktopLocked = true; } |
| 1357 | if (domain.consentmessages.autoacceptifterminallocked === true) { command.soptions.consentAutoAcceptIfTerminalLocked = true; } |
| 1358 | if (domain.consentmessages.autoacceptiffilelocked === true) { command.soptions.consentAutoAcceptIfFileLocked = true; } |
| 1359 | if (domain.consentmessages.oldstyle === true) { command.soptions.oldStyle = true; } |
| 1360 | } |
| 1361 | if (typeof domain.notificationmessages == 'object') { |
| 1362 | if (typeof domain.notificationmessages.title == 'string') { command.soptions.notifyTitle = domain.notificationmessages.title; } |
| 1363 | if (typeof domain.notificationmessages.desktop == 'string') { command.soptions.notifyMsgDesktop = domain.notificationmessages.desktop; } |
| 1364 | if (typeof domain.notificationmessages.terminal == 'string') { command.soptions.notifyMsgTerminal = domain.notificationmessages.terminal; } |
| 1365 | if (typeof domain.notificationmessages.files == 'string') { command.soptions.notifyMsgFiles = domain.notificationmessages.files; } |
| 1366 | } |
| 1367 | parent.parent.debug('relay', 'Relay: Sending agent tunnel command: ' + JSON.stringify(command)); |
| 1368 | if (obj.sendAgentMessage(command, user ? user._id : null, domain.id) == false) { delete obj.id; parent.parent.debug('relay', 'Relay: Unable to contact this agent (' + obj.req.clientIp + ')'); } |
| 1369 | |
| 1370 | performRelay(0); |
| 1371 | }); |
| 1372 | return obj; |
| 1373 | } |
| 1374 | } |
| 1375 | |
| 1376 | // Set the session expire timer |
| 1377 | function setExpireTimer() { |
| 1378 | if (obj.expireTimer != null) { clearTimeout(obj.expireTimer); delete obj.expireTimer; } |
| 1379 | if (cookie && (typeof cookie.expire == 'number')) { |
| 1380 | const timeToExpire = (cookie.expire - Date.now()); |
| 1381 | if (timeToExpire < 1) { |
| 1382 | obj.close(); |
| 1383 | } else if (timeToExpire >= 0x7FFFFFFF) { |
| 1384 | obj.expireTimer = setTimeout(setExpireTimer, 0x7FFFFFFF); // Since expire timer can't be larger than 0x7FFFFFFF, reset timer after that time. |
| 1385 | } else { |
| 1386 | obj.expireTimer = setTimeout(obj.close, timeToExpire); |
| 1387 | } |
| 1388 | } |
| 1389 | } |
| 1390 | |
| 1391 | |
| 1392 | // Check if this user has input access on the device |
| 1393 | if ((obj.user != null) && (obj.viewOnly == false)) { |
| 1394 | obj.viewOnly = true; // Set a view only for now until we figure out otherwise |
| 1395 | parent.db.Get(obj.nodeid, function (err, docs) { |
| 1396 | if (obj.req == null) return; // This connection was closed. |
| 1397 | if (docs.length == 0) { console.log('ERR: Node not found'); try { obj.close(); } catch (e) { } return; } // Disconnect websocket |
| 1398 | const node = docs[0]; |
| 1399 | |
| 1400 | // Check if this user has permission to manage this computer |
| 1401 | const rights = parent.GetNodeRights(obj.user, node.meshid, node._id); |
| 1402 | if ((rights & 0x00000008) == 0) { try { obj.close(); } catch (e) { } return; } // Check MESHRIGHT_ADMIN or MESHRIGHT_REMOTECONTROL |
| 1403 | if ((rights != 0xFFFFFFFF) && ((rights & 0x00010000) != 0)) { try { obj.close(); } catch (e) { } return; } // Check MESHRIGHT_NODESKTOP |
| 1404 | if ((rights == 0xFFFFFFFF) || ((rights & 0x00000100) == 0)) { obj.viewOnly = false; } // Check MESHRIGHT_REMOTEVIEWONLY |
| 1405 | performRelay(0); |
| 1406 | }); |
| 1407 | } else { |
| 1408 | // If this is not an authenticated session, or the session does not have routing instructions, just go ahead an connect to existing session. |
| 1409 | performRelay(0); |
| 1410 | } |
| 1411 | return obj; |
| 1412 | }; |
| 1413 | |
| 1414 | /* |
| 1415 | Relay session recording required that "SessionRecording":true be set in the domain section of the config.json. |
| 1416 | Once done, a folder "meshcentral-recordings" will be created next to "meshcentral-data" that will contain all |
| 1417 | of the recording files with the .mcrec extension. |
| 1418 | |
| 1419 | The recording files are binary and contain a set of: |
| 1420 | |
| 1421 | <HEADER><DATABLOCK><HEADER><DATABLOCK><HEADER><DATABLOCK><HEADER><DATABLOCK>... |
| 1422 | |
| 1423 | The header is always 16 bytes long and is encoded like this: |
| 1424 | |
| 1425 | TYPE 2 bytes, 1 = Header, 2 = Network Data, 3 = EndBlock |
| 1426 | FLAGS 2 bytes, 0x0001 = Binary, 0x0002 = User |
| 1427 | SIZE 4 bytes, Size of the data following this header. |
| 1428 | TIME 8 bytes, Time this record was written, number of milliseconds since 1 January, 1970 UTC. |
| 1429 | |
| 1430 | All values are BigEndian encoded. The first data block is of TYPE 1 and contains a JSON string with information |
| 1431 | about this recording. It looks something like this: |
| 1432 | |
| 1433 | { |
| 1434 | magic: 'MeshCentralRelaySession', |
| 1435 | ver: 1, |
| 1436 | userid: "user\domain\userid", |
| 1437 | username: "username", |
| 1438 | sessionid: "RandomValue", |
| 1439 | ipaddr1: 1.2.3.4, |
| 1440 | ipaddr2: 1.2.3.5, |
| 1441 | time: new Date().toLocaleString() |
| 1442 | } |
| 1443 | |
| 1444 | The rest of the data blocks are all network traffic that was relayed thru the server. They are of TYPE 2 and have |
| 1445 | a given size and timestamp. When looking at network traffic the flags are important: |
| 1446 | |
| 1447 | - If traffic has the first (0x0001) flag set, the data is binary otherwise it's a string. |
| 1448 | - 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. |
| 1449 | */ |