diff --git a/MeshCentralServer.njsproj b/MeshCentralServer.njsproj index 794ef46d..a4ae82ff 100644 --- a/MeshCentralServer.njsproj +++ b/MeshCentralServer.njsproj @@ -104,6 +104,7 @@ + diff --git a/mcrec.js b/mcrec.js new file mode 100644 index 00000000..8f0da50a --- /dev/null +++ b/mcrec.js @@ -0,0 +1,294 @@ +/** +* @description MeshCentral MeshAgent +* @author Ylian Saint-Hilaire +* @copyright Intel Corporation 2019-2020 +* @license Apache-2.0 +* @version v0.0.1 +*/ + +var fs = require('fs'); +var path = require('path'); + +var worker = null; +const NodeJSVer = Number(process.version.match(/^v(\d+\.\d+)/)[1]); +var directRun = (require.main === module); +function log() { if (directRun) { console.log(...arguments); } else { if (worker != null) { worker.parentPort.postMessage({ msg: arguments[0] }); } } } +if (directRun && (NodeJSVer >= 12)) { const xworker = require('worker_threads'); try { if (xworker.isMainThread == false) { worker = xworker; } } catch (ex) { log(ex); } } +function start() { startEx(process.argv); } +if (directRun) { setup(); } + +function setup() { InstallModules(['image-size'], start); } +function start() { startEx(process.argv); } + +function startEx(argv) { + var state = { recFileName: null, recFile: null, recFileSize: 0, recFilePtr: 0 }; + var infile = null; + if (argv.length > 2) { infile = argv[2]; } else { + log('MeshCentral Session Recodings Processor'); + log('This tool will index a .mcrec file so that the player can seek thru the file.'); + log(''); + log(' Usage: node mcrec [file]'); + return; + } + if (fs.existsSync(infile) == false) { log("Missing file: " + infile); return; } + state.recFileName = infile; + state.recFileSize = fs.statSync(infile).size; + if (state.recFileSize < 32) { log("Invalid file: " + infile); return; } + log("Processing file: " + infile + ", " + state.recFileSize + " bytes."); + state.recFile = fs.openSync(infile, 'r'); + state.indexTime = 10; // Interval between indexes in seconds + state.lastIndex = 0; // Last time an index was writen in seconds + state.indexes = []; + state.width = 0; + state.height = 0; + state.basePtr = null; + readLastBlock(state, function (state, result) { + if (result == false) { log("Invalid file: " + infile); return; } + readNextBlock(state, processBlock); + }); +} + +function createIndex(state, ptr) { + var index = []; + for (var i in state.screen) { if (index.indexOf(state.screen[i]) == -1) { index.push(state.screen[i]); } } + index.sort(function (a, b) { return a - b }); + index.unshift(state.height); + index.unshift(state.width); + index.unshift(ptr - state.basePtr); + state.indexes.push(index); // Index = [ Ptr, Width, Height, Block Pointers... ] + //log('Index', state.lastIndex, index.length); + //log('Index', index); + state.lastIndex += 10; +} + +function processBlock(state, block) { + if (block == null) { writeIndexedFile(state, function () { log("Done."); }); return; } + var elapseMilliSeconds = 0; + if (state.startTime != null) { elapseMilliSeconds = (block.time - state.startTime); } + var flagBinary = (block.flags & 1) != 0; + var flagUser = (block.flags & 2) != 0; + + // Start indexing at the first type 2 block + if ((state.basePtr == null) && (block.type == 2)) { state.basePtr = block.ptr; state.startTime = block.time; } + + // Check if we need to create one or more indexes + while (((state.lastIndex + state.indexTime) * 1000) < elapseMilliSeconds) { createIndex(state, block.ptr); } + + if (block.type == 1) { + // Metadata + state.metadata = JSON.parse(block.data.toString()); + if (state.metadata.indexInterval != null) { log("This file is already indexed."); return; } + if (state.metadata.protocol != 2) { log("Only remote desktop sessions can currently be indexed."); return; } + state.metadataFlags = block.flags; + state.metadataTime = block.time; + state.recFileProtocol = state.metadata.protocol; + state.dataStartPtr = state.recFilePtr; + if (typeof state.recFileProtocol == 'string') { state.recFileProtocol = parseInt(state.recFileProtocol); } + } else if ((block.type == 2) && flagBinary && !flagUser) { + // Device --> User data + if (state.recFileProtocol == 1) { + // MeshCentral Terminal + // TODO + log('Terminal'); + } else if (state.recFileProtocol == 2) { + // MeshCentral Remote Desktop + // TODO + if (block.data.length >= 4) { + var command = block.data.readInt16BE(0); + var cmdsize = block.data.readInt16BE(2); + if ((command == 27) && (cmdsize == 8)) { + // Jumbo packet + if (block.data.length >= 12) { + command = block.data.readInt16BE(8); + cmdsize = block.data.readInt32BE(4); + if (block.data.length == (cmdsize + 8)) { + block.data = block.data.slice(8, block.data.length); + } else { + console.log('TODO-PARTIAL-JUMBO', command, cmdsize, block.data.length); + return; // TODO + } + } + } + + switch (command) { + case 3: // Tile + var x = block.data.readInt16BE(4); + var y = block.data.readInt16BE(6); + var dimensions = require('image-size')(block.data.slice(8)); + //log("Tile", x, y, dimensions.width, dimensions.height, block.ptr); + //console.log(elapseSeconds); + + // Update the screen with the correct pointers. + var sx = x/16, sy = y/16, sw = dimensions.width/16, sh = dimensions.height/16; + for (var i = 0; i < sw; i++) { + for (var j = 0; j < sh; j++) { + var k = ((state.swidth * (j + sy)) + (i + sx)); + state.screen[k] = (block.ptr - state.basePtr); + } + } + + break; + case 4: // Tile copy + var x = block.data.readInt16BE(4); + var y = block.data.readInt16BE(6); + //log("TileCopy", x, y); + break; + case 7: // Screen Size, clear the screen state and computer the tile count + state.width = block.data.readInt16BE(4); + state.height = block.data.readInt16BE(6); + state.swidth = state.width / 16; + state.sheight = state.height / 16; + if (Math.floor(state.swidth) != state.swidth) { state.swidth = Math.floor(state.swidth) + 1; } + if (Math.floor(state.sheight) != state.sheight) { state.sheight = Math.floor(state.sheight) + 1; } + state.screen = {}; + //log("ScreenSize", state.width, state.height, state.swidth, state.sheight, state.swidth * state.sheight); + break; + } + + //log('Desktop', command, cmdsize); + } + } else if (state.recFileProtocol == 101) { + // Intel AMT KVM + // TODO + log('AMTKVM'); + } + } else if ((block.type == 2) && flagBinary && flagUser) { + // User --> Device data + if (state.recFileProtocol == 101) { + // Intel AMT KVM + //if (rstr2hex(data) == '0000000008080001000700070003050200000000') { amtDesktop.bpp = 1; } // Switch to 1 byte per pixel. + } + } + + //console.log(block); + readNextBlock(state, processBlock); +} + +function writeIndexedFile(state, func) { + var outfile = state.recFileName; + if (outfile.endsWith('.mcrec')) { outfile = outfile.substring(0, outfile.length - 6) + '-ndx.mcrec'; } else { outfile += '-ndx.mcrec'; } + if (fs.existsSync(outfile)) { log("File already exists: " + outfile); return; } + log("Writing file: " + outfile); + state.writeFile = fs.openSync(outfile, 'w'); + state.metadata.indexInterval = state.indexTime; + state.metadata.indexStartTime = state.startTime; + state.metadata.indexes = state.indexes; + var firstBlock = JSON.stringify(state.metadata); + recordingEntry(state.writeFile, 1, state.metadataFlags, state.metadataTime, firstBlock, function (state) { + var len = 0, buffer = Buffer.alloc(4096), ptr = state.dataStartPtr; + while (ptr < state.recFileSize) { + len = fs.readSync(state.recFile, buffer, 0, 4096, ptr); + fs.writeSync(state.writeFile, buffer, 0, len); + ptr += len; + } + func(state); + }, state); +} + +// Record a new entry in a recording log +function recordingEntry(fd, type, flags, time, data, func, tag) { + try { + if (typeof data == 'string') { + // String write + var blockData = Buffer.from(data), header = Buffer.alloc(16); // Header: Type (2) + Flags (2) + Size(4) + Time(8) + header.writeInt16BE(type, 0); // Type (1 = Header, 2 = Network Data) + header.writeInt16BE(flags, 2); // Flags (1 = Binary, 2 = User) + header.writeInt32BE(blockData.length, 4); // Size + header.writeIntBE(time, 10, 6); // Time + var block = Buffer.concat([header, blockData]); + fs.write(fd, block, 0, block.length, function () { func(tag); }); + } else { + // Binary write + var header = Buffer.alloc(16); // Header: Type (2) + Flags (2) + Size(4) + Time(8) + header.writeInt16BE(type, 0); // Type (1 = Header, 2 = Network Data) + header.writeInt16BE(flags | 1, 2); // Flags (1 = Binary, 2 = User) + header.writeInt32BE(data.length, 4); // Size + header.writeIntBE(time, 10, 6); // Time + var block = Buffer.concat([header, data]); + fs.write(fd, block, 0, block.length, function () { func(tag); }); + } + } catch (ex) { console.log(ex); func(state, tag); } +} + +function readLastBlock(state, func) { + var buf = Buffer.alloc(32); + fs.read(state.recFile, buf, 0, 32, state.recFileSize - 32, function (err, bytesRead, buf) { + var type = buf.readInt16BE(0); + var flags = buf.readInt16BE(2); + var size = buf.readInt32BE(4); + var time = (buf.readInt32BE(8) << 32) + buf.readInt32BE(12); + var magic = buf.toString('utf8', 16, 32); + func(state, (type == 3) && (size == 16) && (magic == 'MeshCentralMCREC')); + }); +} + +function readNextBlock(state, func) { + if ((state.recFilePtr + 16) > state.recFileSize) { func(state, null); return; } + var r = {}, buf = Buffer.alloc(16); + fs.read(state.recFile, buf, 0, 16, state.recFilePtr, function (err, bytesRead, buf) { + r.type = buf.readInt16BE(0); + r.flags = buf.readInt16BE(2); + r.size = buf.readInt32BE(4); + r.time = buf.readIntBE(8, 8); + r.date = new Date(r.time); + r.ptr = state.recFilePtr; + if ((state.recFilePtr + 16 + r.size) > state.recFileSize) { func(state, null); return; } + if (r.size == 0) { + r.data = null; + func(state, r); + } else { + r.data = Buffer.alloc(r.size); + fs.read(state.recFile, r.data, 0, r.size, state.recFilePtr + 16, function (err, bytesRead, buf) { + state.recFilePtr += (16 + r.size); + func(state, r); + }); + } + }); +} + +function isNumber(x) { return (('' + parseInt(x)) === x) || (('' + parseFloat(x)) === x); } +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; }); }; + +// Check if a list of modules are present and install any missing ones +var InstallModuleChildProcess = null; +var previouslyInstalledModules = {}; +function InstallModules(modules, func) { + var missingModules = []; + if (previouslyInstalledModules == null) { previouslyInstalledModules = {}; } + if (modules.length > 0) { + for (var i in modules) { + try { + var xxmodule = require(modules[i]); + } catch (e) { + if (previouslyInstalledModules[modules[i]] !== true) { missingModules.push(modules[i]); } + } + } + if (missingModules.length > 0) { InstallModule(missingModules.shift(), InstallModules, modules, func); } else { func(); } + } +} + +// Check if a module is present and install it if missing +function InstallModule(modulename, func, tag1, tag2) { + log('Installing ' + modulename + '...'); + var child_process = require('child_process'); + var parentpath = __dirname; + + // Get the working directory + 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, '../..'); } + + // Looks like we need to keep a global reference to the child process object for this to work correctly. + InstallModuleChildProcess = child_process.exec('npm install --no-optional --save ' + modulename, { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { + InstallModuleChildProcess = null; + if ((error != null) && (error != '')) { + 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'); + process.exit(); + return; + } + previouslyInstalledModules[modulename] = true; + func(tag1, tag2); + return; + }); +} + +// Export table +module.exports.startEx = startEx; \ No newline at end of file diff --git a/package.json b/package.json index 073533cf..ecb528a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meshcentral", - "version": "0.4.9-b", + "version": "0.4.9-c", "keywords": [ "Remote Management", "Intel AMT", @@ -37,6 +37,7 @@ "express": "^4.17.0", "express-handlebars": "^3.1.0", "express-ws": "^4.0.0", + "image-size": "^0.8.3", "ipcheck": "^0.1.0", "minimist": "^1.2.0", "multiparty": "^4.2.1", diff --git a/translate/translate.json b/translate/translate.json index 7e1aa982..c6baafb3 100644 --- a/translate/translate.json +++ b/translate/translate.json @@ -1256,6 +1256,12 @@ "agentinvite.handlebars->3->4" ] }, + { + "en": "<<", + "xloc": [ + "player.handlebars->p11->deskarea0->deskarea4->3" + ] + }, { "cs": "Hardwarové klíče jsou použity jako druhý faktor ověřování.", "de": "Hardware-Schlüssel werden für Zweifaktor-Anmeldung verwendet.", @@ -1321,6 +1327,12 @@ "default-mobile.handlebars->9->23" ] }, + { + "en": ">>", + "xloc": [ + "player.handlebars->p11->deskarea0->deskarea4->3" + ] + }, { "cs": "ACM", "de": "ACM", @@ -6282,7 +6294,7 @@ "pt": "Arraste e solte um arquivo .mcrec ou clique em \\\"Abrir arquivo...\\\"", "ru": "Перетащите .mcrec файл или нажмите \\\"Открыть файл ... \\\"", "xloc": [ - "player.handlebars->3->18" + "player.handlebars->3->20" ] }, { @@ -8449,6 +8461,12 @@ "default.handlebars->container->dialog->dialogBody->dialog7->d7amtkvm->3->1" ] }, + { + "en": "Indexed every {0} seconds", + "xloc": [ + "player.handlebars->3->17" + ] + }, { "cs": "indonézština", "de": "Indonesisch", @@ -12920,7 +12938,7 @@ "pt": "Abrir arquivo...", "ru": "Открыть файл...", "xloc": [ - "player.handlebars->3->19", + "player.handlebars->3->21", "player.handlebars->p11->deskarea0->deskarea1->3" ] }, @@ -13882,8 +13900,8 @@ "pt": "Pressione [espaço] para reproduzir / pausar.", "ru": "Нажмите [пробел] для проигрывания/паузы.", "xloc": [ - "player.handlebars->3->16", - "player.handlebars->3->17" + "player.handlebars->3->18", + "player.handlebars->3->19" ] }, { @@ -15317,6 +15335,12 @@ "default.handlebars->25->1354" ] }, + { + "en": "Seeking", + "xloc": [ + "player.handlebars->3->16" + ] + }, { "cs": "Vybrat vše", "de": "Alle auswählen", diff --git a/views/player.handlebars b/views/player.handlebars index 41b9e70d..9c7da1e5 100644 --- a/views/player.handlebars +++ b/views/player.handlebars @@ -29,9 +29,9 @@
-
+
-
+
@@ -203,8 +203,13 @@ if (recFileMetadata.indexInterval) { recFileIndexBasePtr = recFilePtr; x += addInfoNoEsc("Seeking", format("Indexed every {0} seconds", recFileMetadata.indexInterval)); + QV('SeekBackwardButton', true); + QV('SeekForwardButton', true); QE('SeekBackwardButton', true); QE('SeekForwardButton', true); + } else { + QV('SeekBackwardButton', false); + QV('SeekForwardButton', false); } QV('DeskParent', true); QV('TermParent', false); @@ -247,6 +252,7 @@ QV('metadatadiv', true); QH('metadatadiv', x); QH('deskstatus', recFile.name); + QS('progressbar').width = '0px'; } function processBlock(type, flags, time, data) { @@ -256,7 +262,7 @@ processBlockEx(type, flags, time, data); } else { waitTimerArgs = [type, flags, time, data] - waitTimer = setTimeout(function () { waitTimer = null; processBlockEx(waitTimerArgs[0], waitTimerArgs[1], waitTimerArgs[2], waitTimerArgs[3]); }, waitTime); + waitTimer = setTimeout(function () { waitTimer = null; if (waitTimerArgs) { processBlockEx(waitTimerArgs[0], waitTimerArgs[1], waitTimerArgs[2], waitTimerArgs[3]); } }, waitTime); } } @@ -267,13 +273,12 @@ // Update the clock var deltaTimeTotalSec = Math.floor((time - recFileStartTime) / 1000); if (currentDeltaTimeTotalSec != deltaTimeTotalSec) { + // Hours, minutes and seconds currentDeltaTimeTotalSec = deltaTimeTotalSec; - var deltaTimeHours = Math.floor(deltaTimeTotalSec / 3600); - deltaTimeTotalSec -= (deltaTimeHours * 3600) - var deltaTimeMinutes = Math.floor(deltaTimeTotalSec / 60); - deltaTimeTotalSec -= (deltaTimeHours * 60) - var deltaTimeSeconds = Math.floor(deltaTimeTotalSec); - QH('timespan', pad2(deltaTimeHours) + ':' + pad2(deltaTimeMinutes) + ':' + pad2(deltaTimeSeconds)) + var hrs = Math.floor(deltaTimeTotalSec / 3600); + var mins = Math.floor((deltaTimeTotalSec % 3600) / 60); + var secs = Math.floor(deltaTimeTotalSec % 60); + QH('timespan', pad2(hrs) + ':' + pad2(mins) + ':' + pad2(secs)) } if ((type == 2) && flagBinary && !flagUser) { @@ -519,38 +524,66 @@ } function seekBackward() { - //console.log('seekBackward'); - seek(5); + var ndxNumber = Math.round(currentDeltaTimeTotalSec / recFileMetadata.indexInterval); + if (ndxNumber < 2) { + pause(); restart(); + } else { + if (recFileMetadata.indexes[ndxNumber - 2] != null) { seek(ndxNumber - 2); } + } } function seekForward() { - //console.log('seekForward'); - seek(10); + var ndxNumber = Math.round(currentDeltaTimeTotalSec / recFileMetadata.indexInterval); + if (recFileMetadata.indexes[ndxNumber] != null) { seek(ndxNumber); } } + function progressBarSeek(event) { + var ndxNumber = Math.round((event.clientX / document.body.offsetWidth) * (recFileMetadata.indexes.length + 1)) - 1; + if (ndxNumber == -1) { pause(); restart(); } else { seek(ndxNumber); } + } var SeekIndex; var SeekIndexPtr; + var SeekIndexTime; + var SeekPlayState; function seek(indexNumber) { //console.log('seek', indexNumber); if ((recFileMetadata.indexes == null) || (recFileMetadata.indexes[indexNumber] == null)) return null; + SeekPlayState = playing; + pause(); restart(); - //pause(); - if (agentDesktop) { agentDesktop.Canvas.clearRect(0, 0, agentDesktop.CanvasId.width, agentDesktop.CanvasId.height); } SeekIndex = recFileMetadata.indexes[indexNumber]; SeekIndexPtr = 3; - var ptr = SeekIndex[0]; + recFileLastTime = SeekIndexTime = recFileStartTime + ((1 + indexNumber) * recFileMetadata.indexInterval * 1000); + recFilePtr = recFileIndexBasePtr + SeekIndex[0]; var width = SeekIndex[1]; var height = SeekIndex[2]; - seekFetchNext(); + + if (recFileEndTime == 0) { + // File pointer progress bar + QS('progressbar').width = Math.floor(100 * (recFilePtr / recFile.size)) + '%'; + } else { + // Time progress bar + QS('progressbar').width = Math.floor(((recFileLastTime - recFileStartTime) / (recFileEndTime - recFileStartTime)) * 100) + '%'; + } + + if (agentDesktop) { + agentDesktop.Canvas.clearRect(0, 0, agentDesktop.CanvasId.width, agentDesktop.CanvasId.height); + agentDesktop.ProcessScreenMsg(width, height); + } + + QV('metadatadiv', false); + QV('Desk', false); + + seekFetchNext(function () { QV('Desk', true); if (SeekPlayState) { play(); } }); } - function seekFetchNext() { - if (SeekIndex[SeekIndexPtr] == null) { return; } + function seekFetchNext(func) { + if (SeekIndex[SeekIndexPtr] == null) { func(); return; } readBlockAt(recFileIndexBasePtr + SeekIndex[SeekIndexPtr], function (type, flags, time, data) { SeekIndexPtr++; - processBlockEx(type, flags, time, data, true); - seekFetchNext(); + processBlockEx(type, flags, SeekIndexTime, data, true); + seekFetchNext(func); }); }