| 1 | /** |
| 2 | * @description MeshCentral MeshAgent |
| 3 | * @author Ylian Saint-Hilaire |
| 4 | * @copyright Intel Corporation 2019-2022 |
| 5 | * @license Apache-2.0 |
| 6 | * @version v0.0.1 |
| 7 | */ |
| 8 | |
| 9 | var fs = require('fs'); |
| 10 | var path = require('path'); |
| 11 | |
| 12 | var worker = null; |
| 13 | const NodeJSVer = Number(process.version.match(/^v(\d+\.\d+)/)[1]); |
| 14 | var directRun = (require.main === module); |
| 15 | function log() { if (directRun) { console.log(...arguments); } /*else { if (worker != null) { worker.parentPort.postMessage({ msg: arguments[0] }); } } */ } |
| 16 | function log2() { if (directRun) { console.log(...arguments); } else { process.send(...arguments); } /*else { if (worker != null) { worker.parentPort.postMessage({ msg: arguments[0] }); } } */ } |
| 17 | if (directRun && (NodeJSVer >= 12)) { const xworker = require('worker_threads'); try { if (xworker.isMainThread == false) { worker = xworker; } } catch (ex) { log(ex); } } |
| 18 | |
| 19 | function indexFile(infile) { |
| 20 | var state = { recFileName: null, recFile: null, recFileSize: 0, recFilePtr: 0 }; |
| 21 | if (fs.existsSync(infile) == false) { log2("Missing file: " + infile); return; } |
| 22 | state.recFileName = infile; |
| 23 | state.recFileSize = fs.statSync(infile).size; |
| 24 | if (state.recFileSize < 32) { log2("Invalid file size: " + infile); return; } |
| 25 | log("Processing file: " + infile + ", " + state.recFileSize + " bytes."); |
| 26 | state.recFile = fs.openSync(infile, 'r+'); |
| 27 | state.indexTime = 10; // Interval between indexes in seconds |
| 28 | state.lastIndex = 0; // Last time an index was writen in seconds |
| 29 | state.indexes = []; |
| 30 | state.width = 0; |
| 31 | state.height = 0; |
| 32 | state.basePtr = null; |
| 33 | readLastBlock(state, function (state, result, time, extras) { |
| 34 | if (result == false) { log2("Invalid file: " + infile); return; } |
| 35 | if (extras != null) { log2("File already indexed: " + infile); return; } |
| 36 | state.lastTimeStamp = time; |
| 37 | readNextBlock(state, processBlock); |
| 38 | }); |
| 39 | } |
| 40 | |
| 41 | function createIndex(state, ptr) { |
| 42 | var index = []; |
| 43 | for (var i in state.screen) { if (index.indexOf(state.screen[i]) == -1) { index.push(state.screen[i]); } } |
| 44 | index.sort(function (a, b) { return a - b }); |
| 45 | index.unshift(state.height); |
| 46 | index.unshift(state.width); |
| 47 | index.unshift(ptr - state.basePtr); |
| 48 | state.indexes.push(index); // Index = [ Ptr, Width, Height, Block Pointers... ] |
| 49 | //log('Index', state.lastIndex, index.length); |
| 50 | //log('Index', index); |
| 51 | state.lastIndex += 10; |
| 52 | } |
| 53 | |
| 54 | function processBlock(state, block, err) { |
| 55 | if (err != null) { |
| 56 | // Error reading the next block, exit now. |
| 57 | fs.close(state.recFile, function () { |
| 58 | for (var i in state) { delete state[i]; } // Clear the state. |
| 59 | log2("Error."); |
| 60 | }); |
| 61 | return; |
| 62 | } |
| 63 | if (block == null) { |
| 64 | // We are done, close this file. |
| 65 | writeIndex(state, function () { |
| 66 | fs.close(state.recFile, function () { |
| 67 | for (var i in state) { delete state[i]; } // Clear the state. |
| 68 | log2("Done."); |
| 69 | }); |
| 70 | }); |
| 71 | return; |
| 72 | } |
| 73 | var elapseMilliSeconds = 0; |
| 74 | if (state.startTime != null) { elapseMilliSeconds = (block.time - state.startTime); } |
| 75 | var flagBinary = (block.flags & 1) != 0; |
| 76 | var flagUser = (block.flags & 2) != 0; |
| 77 | |
| 78 | // Start indexing at the first type 2 block |
| 79 | if ((state.basePtr == null) && (block.type == 2)) { state.basePtr = block.ptr; state.startTime = block.time; } |
| 80 | |
| 81 | // Check if we need to create one or more indexes |
| 82 | while (((state.lastIndex + state.indexTime) * 1000) < elapseMilliSeconds) { createIndex(state, block.ptr); } |
| 83 | |
| 84 | if (block.type == 1) { |
| 85 | // Metadata |
| 86 | state.metadata = JSON.parse(block.data.toString()); |
| 87 | if (state.metadata.indexInterval != null) { log2("This file is already indexed."); return; } |
| 88 | if (state.metadata.protocol != 2) { log2("Only remote desktop sessions can currently be indexed."); return; } |
| 89 | state.metadataFlags = block.flags; |
| 90 | state.metadataTime = block.time; |
| 91 | state.recFileProtocol = state.metadata.protocol; |
| 92 | state.dataStartPtr = state.recFilePtr; |
| 93 | if (typeof state.recFileProtocol == 'string') { state.recFileProtocol = parseInt(state.recFileProtocol); } |
| 94 | } else if ((block.type == 2) && flagBinary && !flagUser) { |
| 95 | // Device --> User data |
| 96 | if (state.recFileProtocol == 1) { |
| 97 | // MeshCentral Terminal |
| 98 | // TODO |
| 99 | log('Terminal'); |
| 100 | } else if (state.recFileProtocol == 2) { |
| 101 | // MeshCentral Remote Desktop |
| 102 | // TODO |
| 103 | if (block.data.length >= 4) { |
| 104 | var command = block.data.readUInt16BE(0); |
| 105 | var cmdsize = block.data.readUInt16BE(2); |
| 106 | if ((command == 27) && (cmdsize == 8)) { |
| 107 | // Jumbo packet |
| 108 | if (block.data.length >= 12) { |
| 109 | command = block.data.readUInt16BE(8); |
| 110 | cmdsize = block.data.readUInt32BE(4); |
| 111 | if (block.data.length == (cmdsize + 8)) { |
| 112 | block.data = block.data.slice(8, block.data.length); |
| 113 | } else { |
| 114 | console.log('TODO-PARTIAL-JUMBO', command, cmdsize, block.data.length); |
| 115 | return; // TODO |
| 116 | } |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | switch (command) { |
| 121 | case 3: // Tile |
| 122 | var x = block.data.readUInt16BE(4); |
| 123 | var y = block.data.readUInt16BE(6); |
| 124 | var dimensions = require('image-size').imageSize(block.data.slice(8)); |
| 125 | //log("Tile", x, y, dimensions.width, dimensions.height, block.ptr); |
| 126 | //console.log(elapseSeconds); |
| 127 | |
| 128 | // Update the screen with the correct pointers. |
| 129 | var sx = x/16, sy = y/16, sw = dimensions.width/16, sh = dimensions.height/16; |
| 130 | for (var i = 0; i < sw; i++) { |
| 131 | for (var j = 0; j < sh; j++) { |
| 132 | var k = ((state.swidth * (j + sy)) + (i + sx)); |
| 133 | state.screen[k] = (block.ptr - state.basePtr); |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | break; |
| 138 | case 4: // Tile copy |
| 139 | var x = block.data.readUInt16BE(4); |
| 140 | var y = block.data.readUInt16BE(6); |
| 141 | //log("TileCopy", x, y); |
| 142 | break; |
| 143 | case 7: // Screen Size, clear the screen state and computer the tile count |
| 144 | state.width = block.data.readUInt16BE(4); |
| 145 | state.height = block.data.readUInt16BE(6); |
| 146 | state.swidth = state.width / 16; |
| 147 | state.sheight = state.height / 16; |
| 148 | if (Math.floor(state.swidth) != state.swidth) { state.swidth = Math.floor(state.swidth) + 1; } |
| 149 | if (Math.floor(state.sheight) != state.sheight) { state.sheight = Math.floor(state.sheight) + 1; } |
| 150 | state.screen = {}; |
| 151 | //log("ScreenSize", state.width, state.height, state.swidth, state.sheight, state.swidth * state.sheight); |
| 152 | break; |
| 153 | } |
| 154 | |
| 155 | //log('Desktop', command, cmdsize); |
| 156 | } |
| 157 | } else if (state.recFileProtocol == 101) { |
| 158 | // Intel AMT KVM |
| 159 | // TODO |
| 160 | log('AMTKVM'); |
| 161 | } |
| 162 | } else if ((block.type == 2) && flagBinary && flagUser) { |
| 163 | // User --> Device data |
| 164 | if (state.recFileProtocol == 101) { |
| 165 | // Intel AMT KVM |
| 166 | //if (rstr2hex(data) == '0000000008080001000700070003050200000000') { amtDesktop.bpp = 1; } // Switch to 1 byte per pixel. |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | //console.log(block); |
| 171 | readNextBlock(state, processBlock); |
| 172 | } |
| 173 | |
| 174 | function writeIndex(state, func) { |
| 175 | // Add the new indexes in extra metadata at the end of the file. |
| 176 | var extraMetadata = {}; |
| 177 | extraMetadata.indexInterval = state.indexTime; |
| 178 | extraMetadata.indexStartTime = state.startTime; |
| 179 | extraMetadata.indexes = state.indexes; |
| 180 | recordingEntry(state.recFile, 4, 0, state.lastTimeStamp, JSON.stringify(extraMetadata), function (state, len) { |
| 181 | recordingEntry(state.recFile, 3, 0, state.recFileSize - 32, 'MeshCentralMCNDX', function (state) { |
| 182 | func(state); |
| 183 | }, state, state.recFileSize - 32 + len); |
| 184 | }, state, state.recFileSize - 32); |
| 185 | } |
| 186 | |
| 187 | // Record a new entry in a recording log |
| 188 | function recordingEntry(fd, type, flags, time, data, func, tag, position) { |
| 189 | try { |
| 190 | if (typeof data == 'string') { |
| 191 | // String write |
| 192 | var blockData = Buffer.from(data), header = Buffer.alloc(16); // Header: Type (2) + Flags (2) + Size(4) + Time(8) |
| 193 | header.writeInt16BE(type, 0); // Type (1 = Header, 2 = Network Data, 3 = End, 4 = Extra Metadata) |
| 194 | header.writeInt16BE(flags, 2); // Flags (1 = Binary, 2 = User) |
| 195 | header.writeInt32BE(blockData.length, 4); // Size |
| 196 | header.writeIntBE(time, 10, 6); // Time |
| 197 | var block = Buffer.concat([header, blockData]); |
| 198 | if (typeof position == 'number') { |
| 199 | fs.write(fd, block, 0, block.length, position, function () { func(tag, block.length); }); |
| 200 | } else { |
| 201 | fs.write(fd, block, 0, block.length, function () { func(tag, block.length); }); |
| 202 | } |
| 203 | } else { |
| 204 | // Binary write |
| 205 | var header = Buffer.alloc(16); // Header: Type (2) + Flags (2) + Size(4) + Time(8) |
| 206 | header.writeInt16BE(type, 0); // Type (1 = Header, 2 = Network Data, 3 = End, 4 = Extra Metadata) |
| 207 | header.writeInt16BE(flags | 1, 2); // Flags (1 = Binary, 2 = User) |
| 208 | header.writeInt32BE(data.length, 4); // Size |
| 209 | header.writeIntBE(time, 10, 6); // Time |
| 210 | var block = Buffer.concat([header, data]); |
| 211 | if (typeof position == 'number') { |
| 212 | fs.write(fd, block, 0, block.length, position, function () { func(tag, block.length); }); |
| 213 | } else { |
| 214 | fs.write(fd, block, 0, block.length, function () { func(tag, block.length); }); |
| 215 | } |
| 216 | } |
| 217 | } catch (ex) { console.log(ex); func(tag); } |
| 218 | } |
| 219 | |
| 220 | function readLastBlock(state, func) { |
| 221 | var buf = Buffer.alloc(32); |
| 222 | fs.read(state.recFile, buf, 0, 32, state.recFileSize - 32, function (err, bytesRead, buf) { |
| 223 | var type = buf.readUInt16BE(0); // Type (1 = Header, 2 = Network Data) |
| 224 | var flags = buf.readUInt16BE(2); // Flags (1 = Binary, 2 = User) |
| 225 | var size = buf.readUInt32BE(4); // Size |
| 226 | var time = buf.readUIntBE(10, 6); // Time |
| 227 | var magic = buf.toString('utf8', 16, 32); |
| 228 | if ((type == 3) && (size == 16) && (magic == 'MeshCentralMCNDX')) { |
| 229 | // Extra metadata present, lets read it. |
| 230 | extraMetadata = null; |
| 231 | var buf2 = Buffer.alloc(16); |
| 232 | fs.read(state.recFile, buf2, 0, 16, time, function (err, bytesRead, buf2) { |
| 233 | var xtype = buf2.readUInt16BE(0); // Type (1 = Header, 2 = Network Data, 3 = End, 4 = Extra Metadata) |
| 234 | var xflags = buf2.readUInt16BE(2); // Flags (1 = Binary, 2 = User) |
| 235 | var xsize = buf2.readUInt32BE(4); // Size |
| 236 | var xtime = buf.readUIntBE(10, 6); // Time |
| 237 | var buf3 = Buffer.alloc(xsize); |
| 238 | fs.read(state.recFile, buf3, 0, xsize, time + 16, function (err, bytesRead, buf3) { |
| 239 | func(state, true, xtime, JSON.parse(buf3.toString())); |
| 240 | }); |
| 241 | }); |
| 242 | } else { |
| 243 | // No extra metadata or fail |
| 244 | func(state, (type == 3) && (size == 16) && (magic == 'MeshCentralMCREC'), time, null); |
| 245 | } |
| 246 | }); |
| 247 | } |
| 248 | |
| 249 | function readNextBlock(state, func) { |
| 250 | if ((state.recFilePtr + 16) > state.recFileSize) { func(state, null); return; } |
| 251 | var r = {}, buf = Buffer.alloc(16); |
| 252 | fs.read(state.recFile, buf, 0, 16, state.recFilePtr, function (err, bytesRead, buf) { |
| 253 | if (bytesRead != 16) { func(state, null, true); return; } // Error |
| 254 | try { |
| 255 | r.type = buf.readUInt16BE(0); // Type (1 = Header, 2 = Network Data, 3 = End, 4 = Extra Metadata) |
| 256 | r.flags = buf.readUInt16BE(2); // Flags (1 = Binary, 2 = User) |
| 257 | r.size = buf.readUInt32BE(4); // Size |
| 258 | r.time = buf.readUIntBE(10, 6); // Time |
| 259 | r.date = new Date(r.time); |
| 260 | r.ptr = state.recFilePtr; |
| 261 | if ((state.recFilePtr + 16 + r.size) > state.recFileSize) { func(state, null, true); return; } // Error |
| 262 | if (r.size == 0) { |
| 263 | r.data = null; |
| 264 | func(state, r); |
| 265 | } else { |
| 266 | r.data = Buffer.alloc(r.size); |
| 267 | fs.read(state.recFile, r.data, 0, r.size, state.recFilePtr + 16, function (err, bytesRead, buf) { |
| 268 | state.recFilePtr += (16 + r.size); |
| 269 | func(state, r); |
| 270 | }); |
| 271 | } |
| 272 | } catch (ex) { func(state, null, true); return; } // Error |
| 273 | }); |
| 274 | } |
| 275 | |
| 276 | function isNumber(x) { return (('' + parseInt(x)) === x) || (('' + parseFloat(x)) === x); } |
| 277 | function format(format) { var args = Array.prototype.slice.call(arguments, 1); return format.replace(/{(\d+)}/g, function (match, number) { return typeof args[number] != 'undefined' ? args[number] : match; }); }; |
| 278 | |
| 279 | // Check if a list of modules are present and install any missing ones |
| 280 | var InstallModuleChildProcess = null; |
| 281 | var previouslyInstalledModules = {}; |
| 282 | function InstallModules(modules, func) { |
| 283 | var missingModules = []; |
| 284 | if (previouslyInstalledModules == null) { previouslyInstalledModules = {}; } |
| 285 | if (modules.length > 0) { |
| 286 | for (var i in modules) { |
| 287 | try { |
| 288 | var xxmodule = require(modules[i]); |
| 289 | } catch (e) { |
| 290 | if (previouslyInstalledModules[modules[i]] !== true) { missingModules.push(modules[i]); } |
| 291 | } |
| 292 | } |
| 293 | if (missingModules.length > 0) { InstallModule(missingModules.shift(), InstallModules, modules, func); } else { func(); } |
| 294 | } |
| 295 | } |
| 296 | |
| 297 | // Check if a module is present and install it if missing |
| 298 | function InstallModule(modulename, func, tag1, tag2) { |
| 299 | log('Installing ' + modulename + '...'); |
| 300 | var child_process = require('child_process'); |
| 301 | var parentpath = __dirname; |
| 302 | |
| 303 | // Get the working directory |
| 304 | if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); } |
| 305 | |
| 306 | // Looks like we need to keep a global reference to the child process object for this to work correctly. |
| 307 | InstallModuleChildProcess = child_process.exec('npm install --no-optional --save ' + modulename, { maxBuffer: 512000, timeout: 300000, cwd: parentpath }, function (error, stdout, stderr) { |
| 308 | InstallModuleChildProcess = null; |
| 309 | if ((error != null) && (error != '')) { |
| 310 | log('ERROR: Unable to install required module "' + modulename + '". May not have access to npm, or npm may not have suffisent rights to load the new module. Try "npm install ' + modulename + '" to manualy install this module.\r\n'); |
| 311 | process.exit(); |
| 312 | return; |
| 313 | } |
| 314 | previouslyInstalledModules[modulename] = true; |
| 315 | func(tag1, tag2); |
| 316 | return; |
| 317 | }); |
| 318 | } |
| 319 | |
| 320 | function setup() { InstallModules(['image-size'], start); } |
| 321 | function start() { startEx(process.argv); } |
| 322 | function startEx(argv) { |
| 323 | if (argv.length > 2) { indexFile(argv[2]); } else { |
| 324 | log("MeshCentral Session Recordings Processor"); |
| 325 | log("This tool will index a .mcrec file so that the player can seek thru the file."); |
| 326 | log(""); |
| 327 | log(" Usage: node mcrec [file]"); |
| 328 | } |
| 329 | } |
| 330 | if (directRun) { setup(); } |
| 331 | |
| 332 | // Export table |
| 333 | module.exports.startEx = startEx; |
| 334 | module.exports.indexFile = indexFile; |