diff --git a/public/images/link7.png b/public/images/link7.png new file mode 100644 index 00000000..93f27ac2 Binary files /dev/null and b/public/images/link7.png differ diff --git a/public/scripts/common-0.0.1.js b/public/scripts/common-0.0.1.js index c8bc207a..f25c3bb5 100644 --- a/public/scripts/common-0.0.1.js +++ b/public/scripts/common-0.0.1.js @@ -115,7 +115,7 @@ function isSafeString(str) { return ((typeof str == 'string') && (str.indexOf('< function isSafeString2(str) { return ((typeof str == 'string') && (str.indexOf('<') == -1) && (str.indexOf('>') == -1) && (str.indexOf('&') == -1) && (str.indexOf('"') == -1) && (str.indexOf('\'') == -1) && (str.indexOf('+') == -1) && (str.indexOf('(') == -1) && (str.indexOf(')') == -1) && (str.indexOf('#') == -1) && (str.indexOf('%') == -1)) }; // Parse URL arguments, only keep safe values -function parseUriArgs() { +function parseUriArgs(decodeUrl) { var href = window.document.location.href; if (href.endsWith('#')) { href = href.substring(0, href.length - 1); } var name, r = {}, parsedUri = href.split(/[\?&|]/); @@ -124,6 +124,7 @@ function parseUriArgs() { var arg = parsedUri[j], i = arg.indexOf('='); name = arg.substring(0, i); r[name] = arg.substring(i + 1); + if (decodeUrl) { r[name] = decodeURIComponent(arg.substring(i + 1)); } if (!isSafeString(r[name])) { delete r[name]; } else { var x = parseInt(r[name]); if (x == r[name]) { r[name] = x; } } } return r; diff --git a/views/default.handlebars b/views/default.handlebars index 3b525a9e..da973214 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -14986,7 +14986,11 @@ if (rec.protocol == 200) { sessionName += ' - ' + "Messenger"; } var actions = '', icon = 'm0'; - if (rec.present == 1) { icon = 'm1'; actions = '
 
'; } + if (rec.present == 1) { + icon = 'm1'; + actions = '
 
'; + actions += '
 
'; + } var x = ''; x += '
'; //x += '
'; diff --git a/views/player.handlebars b/views/player.handlebars index ab76dd1e..7389eda0 100644 --- a/views/player.handlebars +++ b/views/player.handlebars @@ -29,7 +29,7 @@  
- +
@@ -113,6 +113,13 @@ var videoWriterCurrentFrame = null; var videoFrameDuration = 100; var browser = null; + var domainUrl = '{{{domainurl}}}'; + var urlargs; + + // Streaming values + var ws = null; + var streamingBlockSize = 102400; // 100k block + var streamingBlockCache = {}; function start() { // Detect what browser is in use @@ -129,6 +136,7 @@ } })(window.navigator.userAgent.toLowerCase()); + urlargs = parseUriArgs(true); window.onresize = deskAdjust; document.ondrop = ondrop; document.ondragover = ondragover; @@ -139,19 +147,127 @@ // Make the dialog box movable dialogBoxDrag(); + + // Check if we need to stream a session + if (urlargs.stream != null) { + QV('metadatadiv', true); + QH('metadatadiv', "Connecting to server..."); + ws = new WebSocket(window.location.protocol.replace('http', 'ws') + '//' + window.location.host + domainUrl + 'recordings.ashx?file=' + urlargs.stream + (urlargs.key ? ('&key=' + urlargs.key) : '')); + ws.binaryType = 'arraybuffer'; + ws.onopen = function (e) { console.log('Session Streaming - Connected'); } + ws.onmessage = function (msg) { + if (typeof msg.data != 'string') { + var uint8View = new Uint8Array(msg.data); + var blocknum = (((uint8View[4] << 24) + (uint8View[5] << 16) + (uint8View[6] << 8) + uint8View[7]) / streamingBlockSize); + //console.log('Session Streaming - Got block: ' + blocknum); + streamingBlockCache[blocknum] = msg.data; + var pendingFetchStreamingData2 = [], pendingFetchStreamingData3 = []; + for (var i in pendingFetchStreamingData) { + var j = pendingFetchStreamingData[i].missingBlocks.indexOf(blocknum); + if (j >= 0) { pendingFetchStreamingData[i].missingBlocks.splice(i, 1); } + if (pendingFetchStreamingData[i].missingBlocks.length == 0) { + pendingFetchStreamingData3.push(pendingFetchStreamingData[i]); + } else { + pendingFetchStreamingData2.push(pendingFetchStreamingData[i]); + } + } + pendingFetchStreamingData = pendingFetchStreamingData2; + for (var i in pendingFetchStreamingData3) { + fetchStreamingData(pendingFetchStreamingData3[i].fr, pendingFetchStreamingData3[i].start, pendingFetchStreamingData3[i].end); + } + return; + } else { + var command = null; + try { command = JSON.parse(msg.data); } catch (ex) { console.log(ex); return; } + if ((command == null) || (typeof command.action != 'string')) return; + switch (command.action) { + case 'info': { + console.log('Session Streaming - Session file size: ' + command.size); + if ((typeof command.name != 'string') || (typeof command.size != 'number')) break; + recFile = { name: command.name, size: command.size, streaming: true }; + readLastBlock(function (type, flags, time, extras) { + if (type == 3) { + // File is ok + recFileEndTime = time; + recFileExtras = extras; + readNextBlock(processFirstBlock); + } else { + // This is not a good file + recFileEndTime = 0; + } + }); + break; + } + } + + } + } + ws.onclose = function (e) { console.log('Session Streaming - Disconnected'); restart(); } + } else { + QV('OpenFileButton', true); + } + } + + // Pending fetch requests + var pendingFetchStreamingData = []; + + // Get a section of the recorded file + function fetchStreamingData(fr, start, end) { + // Start by looking at what blocks are required + var firstBlock = Math.floor(start / streamingBlockSize); + var lastBlock = Math.floor(end / streamingBlockSize); + var missingBlocks = []; + for (var i = firstBlock; i <= lastBlock; i++) { + if ((streamingBlockCache[i] == null) || (streamingBlockCache[i] === 1)) { missingBlocks.push(i); fetchStreamingBlock(i); } + fetchStreamingBlock(i + 1); // Pre-fetch block + fetchStreamingBlock(i + 2); // Pre-fetch block + } + if (missingBlocks.length == 0) { + // We have all the blocks we need, assemble the data now + var outputptr = 0; + var output = new ArrayBuffer(end - start); + var outputBytes = new Uint8Array(output); + for (var i = firstBlock; i <= lastBlock; i++) { + var block = streamingBlockCache[i]; // Get a block with data we need + var blockstart = (i * streamingBlockSize); // Compute the block starting data pointer + var blockend = blockstart + (block.byteLength - 8); // Compute the block ending data pointer + var r1 = Math.max(start, blockstart); // Compute where we need to start data copy + var r2 = Math.min(end, blockend); // Compute where we need to end data copy + var p1 = r1 - blockstart; // Compute where in the block to start data copy + var p2 = r2 - r1; // Computer how many byte to copy from the block + var subblock = block.slice(8 + p1, 8 + p1 + p2); // Get the sub-block of data we need + outputBytes.set(new Uint8Array(subblock), outputptr); // Copy the sub-block into the main block + outputptr += p2; // Move the pointer forward + } + fr.onload({ target: { result: ArrayBufferToString(output) } } ); // Event the block of data + } else { + pendingFetchStreamingData.push({ fr: fr, start: start, end: end, missingBlocks: missingBlocks }); + } + } + + // Request a block of data from the server + function fetchStreamingBlock(n) { + if (streamingBlockCache[n] != null) return; + streamingBlockCache[n] = 1; // Mark the block as being requested + if ((n * streamingBlockSize) >= recFile.size) return; + var len = streamingBlockSize; + if (((n + 1) * streamingBlockSize) >= recFile.size) { len = (recFile.size - (n * streamingBlockSize)); } + ws.send('{"action":"get","ptr":' + (n * streamingBlockSize) + ',"size":' + len + '}'); } function readNextBlock(func) { if ((recFilePtr + 16) > recFile.size) { QS('progressbar').width = '100%'; func(-1); } else { var fr = new FileReader(); - fr.onload = function () { - var type = ReadShort(this.result, 0); - var flags = ReadShort(this.result, 2); - var size = ReadInt(this.result, 4); - var time = (ReadInt(this.result, 8) << 32) + ReadInt(this.result, 12); + fr.onload = function (r) { + var result = r.target.result; + var type = ReadShort(result, 0); + var flags = ReadShort(result, 2); + var size = ReadInt(result, 4); + var time = (ReadInt(result, 8) << 32) + ReadInt(result, 12); if ((recFilePtr + 16 + size) > recFile.size) { QS('progressbar').width = '100%'; func(-1); } else { var fr2 = new FileReader(); - fr2.onload = function () { + fr2.onload = function (r) { + var result = r.target.result; recFilePtr += (16 + size); if (recFileEndTime == 0) { // File pointer progress bar @@ -160,59 +276,89 @@ // Time progress bar QS('progressbar').width = Math.floor(((recFileLastTime - recFileStartTime) / (recFileEndTime - recFileStartTime)) * 100) + '%'; } - func(type, flags, time, this.result); + func(type, flags, time, result); }; - fr2.readAsBinaryString(recFile.slice(recFilePtr + 16, recFilePtr + 16 + size)); + if (ws == null) { + fr2.readAsBinaryString(recFile.slice(recFilePtr + 16, recFilePtr + 16 + size)); + } else { + fetchStreamingData(fr2, recFilePtr + 16, recFilePtr + 16 + size); + } } }; - fr.readAsBinaryString(recFile.slice(recFilePtr, recFilePtr + 16)); + if (ws == null) { + fr.readAsBinaryString(recFile.slice(recFilePtr, recFilePtr + 16)); + } else { + fetchStreamingData(fr, recFilePtr, recFilePtr + 16); + } } } function readBlockAt(ptr, func) { var fr = new FileReader(); - fr.onload = function () { - var type = ReadShort(this.result, 0); - var flags = ReadShort(this.result, 2); - var size = ReadInt(this.result, 4); - var time = (ReadInt(this.result, 8) << 32) + ReadInt(this.result, 12); + fr.onload = function (r) { + var result = r.target.result; + var type = ReadShort(result, 0); + var flags = ReadShort(result, 2); + var size = ReadInt(result, 4); + var time = (ReadInt(result, 8) << 32) + ReadInt(result, 12); if ((ptr + 16 + size) > recFile.size) { func(-1); } else { var fr2 = new FileReader(); - fr2.onload = function () { func(type, flags, time, this.result); }; - fr2.readAsBinaryString(recFile.slice(ptr + 16, ptr + 16 + size)); + fr2.onload = function (r) { + var result = r.target.result; + func(type, flags, time, result); + }; + if (ws == null) { + fr2.readAsBinaryString(recFile.slice(ptr + 16, ptr + 16 + size)); + } else { + fetchStreamingData(fr2, ptr + 16, ptr + 16 + size); + } } }; - fr.readAsBinaryString(recFile.slice(ptr, ptr + 16)); + if (ws == null) { + fr.readAsBinaryString(recFile.slice(ptr, ptr + 16)); + } else { + fetchStreamingData(fr, ptr, ptr + 16); + } } function readLastBlock(func) { if (recFile.size < 32) { func(-1); } else { var fr = new FileReader(); - fr.onload = function () { - var type = ReadShort(this.result, 0); - var flags = ReadShort(this.result, 2); - var size = ReadInt(this.result, 4); - var time = (ReadInt(this.result, 8) << 32) + ReadInt(this.result, 12); - var magic = this.result.substring(16, 32); + fr.onload = function (r) { + var result = r.target.result; + var type = ReadShort(result, 0); + var flags = ReadShort(result, 2); + var size = ReadInt(result, 4); + var time = (ReadInt(result, 8) << 32) + ReadInt(result, 12); + var magic = result.substring(16, 32); if ((type == 3) && (size == 16) && (magic == 'MeshCentralMCNDX')) { // Extra metadata present, lets read it. var fr2 = new FileReader(); - fr2.onload = function () { - var xtype = ReadShort(this.result, 0); - var xflags = ReadShort(this.result, 2); - var xsize = ReadInt(this.result, 4); - var xtime = (ReadInt(this.result, 8) << 32) + ReadInt(this.result, 12); - var extras = JSON.parse(this.result.substring(16)); + fr2.onload = function (r) { + var result = r.target.result; + var xtype = ReadShort(result, 0); + var xflags = ReadShort(result, 2); + var xsize = ReadInt(result, 4); + var xtime = (ReadInt(result, 8) << 32) + ReadInt(result, 12); + var extras = JSON.parse(result.substring(16)); func(type, flags, xtime, extras); // Include extra metadata } - fr2.readAsBinaryString(recFile.slice(time, recFile.size - 32)); + if (ws == null) { + fr2.readAsBinaryString(recFile.slice(time, recFile.size - 32)); + } else { + fetchStreamingData(fr2, time, recFile.size - 32); + } } else if ((type == 3) && (size == 16) && (magic == 'MeshCentralMCREC')) { func(type, flags, time); // No extra metadata } else { func(-1); // Fail } }; - fr.readAsBinaryString(recFile.slice(recFile.size - 32, recFile.size)); + if (ws == null) { + fr.readAsBinaryString(recFile.slice(recFile.size - 32, recFile.size)); + } else { + fetchStreamingData(fr, recFile.size - 32, recFile.size); + } } } @@ -451,7 +597,11 @@ QS('progressbar').width = '0px'; QH('timespan', '00:00:00'); QV('metadatadiv', true); - QH('metadatadiv', 'MeshCentral Session Player

' + "Drag & drop a .mcrec file or click \"Open File...\"" + ''); + if (urlargs.stream == null) { + QH('metadatadiv', 'MeshCentral Session Player

' + "Drag & drop a .mcrec file or click \"Open File...\"" + ''); + } else { + QH('metadatadiv', ''); + } QV('DeskParent', true); QV('TermParent', false); } @@ -845,6 +995,50 @@ } } + function ArrayBufferToString(buffer) { + return BinaryToString(String.fromCharCode.apply(null, Array.prototype.slice.apply(new Uint8Array(buffer)))); + } + + function StringToArrayBuffer(string) { + return StringToUint8Array(string).buffer; + } + + function BinaryToString(binary) { + var error; + try { + return decodeURIComponent(escape(binary)); + } catch (_error) { + error = _error; + if (error instanceof URIError) { return binary; } else { throw error; } + } + } + + function StringToBinary(string) { + var chars, code, i, isUCS2, len, _i; + len = string.length; + chars = []; + isUCS2 = false; + for (i = _i = 0; 0 <= len ? _i < len : _i > len; i = 0 <= len ? ++_i : --_i) { + code = String.prototype.charCodeAt.call(string, i); + if (code > 255) { isUCS2 = true; chars = null; break; } else { chars.push(code); } + } + if (isUCS2 === true) { + return unescape(encodeURIComponent(string)); + } else { + return String.fromCharCode.apply(null, Array.prototype.slice.apply(chars)); + } + } + + function StringToUint8Array(string) { + var binary, binLen, buffer, chars, i, _i; + binary = StringToBinary(string); + binLen = binary.length; + buffer = new ArrayBuffer(binLen); + chars = new Uint8Array(buffer); + for (i = _i = 0; 0 <= binLen ? _i < binLen : _i > binLen; i = 0 <= binLen ? ++_i : --_i) { chars[i] = String.prototype.charCodeAt.call(binary, i); } + return chars; + } + start(); diff --git a/webserver.js b/webserver.js index c4ba6dbe..6926474a 100644 --- a/webserver.js +++ b/webserver.js @@ -3456,13 +3456,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } } - // Download a desktop recording + // Download a session recording function handleGetRecordings(req, res) { const domain = checkUserIpAddress(req, res); if (domain == null) return; // Check the query - if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true)) { res.sendStatus(401); return; } + if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true) || (req.query.file.endsWith('.mcrec') == false)) { res.sendStatus(401); return; } // Get the recording path var recordingsPath = null; @@ -3482,6 +3482,66 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { try { res.sendFile(obj.path.join(recordingsPath, req.query.file)); } catch (ex) { res.sendStatus(404); } } + // Stream a session recording + function handleGetRecordingsWebSocket(ws, req) { + var domain = checkAgentIpAddress(ws, req); + if (domain == null) { parent.debug('web', 'Got recordings file transfer connection with bad domain or blocked IP address ' + req.clientIp + ', dropping.'); try { ws.close(); } catch (ex) { } return; } + + // Check the query + if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true) || (req.query.file.endsWith('.mcrec') == false)) { try { ws.close(); } catch (ex) { } return; } + + // Get the recording path + var recordingsPath = null; + if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; } + if (recordingsPath == null) { try { ws.close(); } catch (ex) { } return; } + + // Get the user and check user rights + var authUserid = null; + if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; } + if (authUserid == null) { try { ws.close(); } catch (ex) { } return; } + const user = obj.users[authUserid]; + if (user == null) { try { ws.close(); } catch (ex) { } return; } + if ((user.siteadmin & 512) == 0) { try { ws.close(); } catch (ex) { } return; } // Check if we have right to get recordings + const filefullpath = obj.path.join(recordingsPath, req.query.file); + + obj.fs.stat(filefullpath, function(err, stats) { + if (err) { + try { ws.close(); } catch (ex) { } // File does not exist + } else { + obj.fs.open(filefullpath, function (err, fd) { + if (err == null) { + // When data is received from the web socket + ws.on('message', function (msg) { + if (typeof msg != 'string') return; + var command; + try { command = JSON.parse(msg); } catch (e) { return; } + if ((command == null) || (typeof command.action != 'string')) return; + switch (command.action) { + case 'get': { + const buffer = Buffer.alloc(8 + command.size); + //buffer.writeUInt32BE((command.ptr >> 32), 0); + buffer.writeUInt32BE((command.ptr & 0xFFFFFFFF), 4); + obj.fs.read(fd, buffer, 8, command.size, command.ptr, function (err, bytesRead, buffer) { if (bytesRead > (buffer.length - 8)) { buffer = buffer.slice(0, bytesRead + 8); } ws.send(buffer); }); + break; + } + } + }); + + // If error, do nothing + ws.on('error', function (err) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); }); + + // If the web socket is closed + ws.on('close', function (req) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); }); + + ws.send(JSON.stringify({ "action": "info", "name": req.query.file, "size": stats.size })); + } else { + try { ws.close(); } catch (ex) { } + } + }); + } + }); + } + // Serve the player page function handlePlayerRequest(req, res) { const domain = checkUserIpAddress(req, res); @@ -5738,6 +5798,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { obj.app.get(url + 'welcome.jpg', handleWelcomeImageRequest); obj.app.get(url + 'welcome.png', handleWelcomeImageRequest); obj.app.get(url + 'recordings.ashx', handleGetRecordings); + obj.app.ws(url + 'recordings.ashx', handleGetRecordingsWebSocket); obj.app.get(url + 'player.htm', handlePlayerRequest); obj.app.get(url + 'player', handlePlayerRequest); obj.app.get(url + 'sharing', handleSharingRequest);