mcrec.js 16 KB

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