Session recording viewer can now stream.

This commit is contained in:
Ylian Saint-Hilaire 2021-11-07 18:07:45 -08:00
parent 9226bd78b5
commit 7974b43b3d
5 changed files with 297 additions and 37 deletions

BIN
public/images/link7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

View File

@ -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;

View File

@ -14986,7 +14986,11 @@
if (rec.protocol == 200) { sessionName += ' - ' + "Messenger"; }
var actions = '', icon = 'm0';
if (rec.present == 1) { icon = 'm1'; actions = '<div style=cursor:pointer;float:right><a onclick=downloadFile("recordings.ashx?file=' + encodeURIComponentEx(rec.filename) + '")><img src=images/link4.png height=10 width=10 title="Download Recording"></a>&nbsp;</div>'; }
if (rec.present == 1) {
icon = 'm1';
actions = '<div style=cursor:pointer;float:right><a onclick=downloadFile("recordings.ashx?file=' + encodeURIComponentEx(rec.filename) + '")><img src=images/link4.png height=10 width=10 title="Download Recording"></a>&nbsp;</div>';
actions += '<div style=cursor:pointer;float:right><a href="player.htm?stream=' + encodeURIComponentEx(rec.filename) + '")><img src=images/link7.png height=10 width=10 title="Play Recording"></a>&nbsp;</div>';
}
var x = '<tr tabindex=0 onmouseover=userMouseHover2(this,1) onmouseout=userMouseHover2(this,0) onkeypress="if (event.key==\'Enter\') showRecordingDialog(event,\'' + i + '\')"><td style=cursor:pointer>';
x += '<div class=bar style=width:100%>';
//x += '<div class=baricon><input class=RecordingCheckbox value="' + encodeURIComponentEx(rec.filename) + '" onclick=p52updateInfo() type=checkbox></div>';

View File

@ -29,7 +29,7 @@
<input id="ConvertAsWebM" style="display:none" type=button value="Convert to WebM" onclick="saveAsWebMfile()">&nbsp;
</div>
<div>
<input id="OpenFileButton" type=button value="Open File..." onclick="openfile()">
<input id="OpenFileButton" type=button value="Open File..." onclick="openfile()" style="display:none">
<span id="deskstatus"></span>
</div>
</div>
@ -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', '<span style=\"font-family:Arial,Helvetica Neue,Helvetica,sans-serif;font-size:28px\">MeshCentral Session Player</span><br /><br /><span style=color:gray>' + "Drag & drop a .mcrec file or click \"Open File...\"" + '</span>');
if (urlargs.stream == null) {
QH('metadatadiv', '<span style=\"font-family:Arial,Helvetica Neue,Helvetica,sans-serif;font-size:28px\">MeshCentral Session Player</span><br /><br /><span style=color:gray>' + "Drag & drop a .mcrec file or click \"Open File...\"" + '</span>');
} 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();
</script>
</body>

View File

@ -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);