From d7045452b2c7ed9f9883a5f9b5dcdc1062776f7b Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Sun, 11 Apr 2021 17:36:22 -0700 Subject: [PATCH] Added Messenger session recording. --- meshcentral-config-schema.json | 2 +- meshrelay.js | 109 +++++++++++++++++++++++++-------- sample-config-advanced.json | 2 +- views/default.handlebars | 11 +++- views/messenger.handlebars | 24 +++++--- 5 files changed, 111 insertions(+), 37 deletions(-) diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index 329689a0..7b885500 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -588,7 +588,7 @@ "index": { "type": "boolean", "default": false }, "maxRecordings": { "type": "integer" }, "maxRecordingSizeMegabytes": { "type": "integer" }, - "protocols": { "type": "array", "uniqueItems": true, "items": { "type": "integer" } } + "protocols": { "type": "array", "uniqueItems": true, "items": { "type": "integer" }, "description": "This is an array: 1 = Terminal, 2 = Desktop, 5 = Files, 100 = Intel AMT WSMAN, 101 = Intel AMT Redirection, 200 = Messenger" } }, "required": [ "protocols" ] }, diff --git a/meshrelay.js b/meshrelay.js index 6c94488a..90d9e9a7 100644 --- a/meshrelay.js +++ b/meshrelay.js @@ -308,16 +308,29 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { // Setup session recording var sessionUser = obj.user; if (sessionUser == null) { sessionUser = obj.peer.user; } - if ((obj.req.query.p != null) && (obj.req.query.nodeid != null) && (sessionUser != null) && (domain.sessionrecording == true || ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.protocols == null) || (domain.sessionrecording.protocols.indexOf(parseInt(obj.req.query.p)) >= 0))))) { + + // If this is a MeshMessenger session, set the protocol to 200. + var xtextSession = 0; + var recordSession = false; + if ((obj.id.startsWith('meshmessenger/node/') == true) && (sessionUser != null) && (domain.sessionrecording == true || ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.protocols == null) || (domain.sessionrecording.protocols.indexOf(parseInt(200)) >= 0))))) { + var split = obj.id.split('/'); + obj.req.query.nodeid = split[1] + '/' + split[2] + '/' + split[3]; + recordSession = true; + xtextSession = 2; // 1 = Raw recording of all strings, 2 = Record chat session messages only. + } + if ((obj.req.query.p != null) && (obj.req.query.nodeid != null) && (sessionUser != null) && (domain.sessionrecording == true || ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.protocols == null) || (domain.sessionrecording.protocols.indexOf(parseInt(obj.req.query.p)) >= 0))))) { recordSession = true; } + + if (recordSession) { // Get the computer name parent.db.Get(obj.req.query.nodeid, function (err, nodes) { var xusername = '', xdevicename = '', xdevicename2 = null, node = null; if ((nodes != null) && (nodes.length == 1)) { node = nodes[0]; xdevicename2 = node.name; xdevicename = '-' + parent.common.makeFilename(node.name); } // Check again if we need to do recording - if (domain.sessionrecording.onlyselecteddevicegroups === true) { - var mesh = parent.meshes[node.meshid]; - if ((mesh.flags == null) || ((mesh.flags & 4) == 0)) { + if ((node == null) || (domain.sessionrecording.onlyselecteddevicegroups === true)) { + var mesh = null; + if (node != null) { mesh = parent.meshes[node.meshid]; } + if ((node == null) || (mesh == null) || (mesh.flags == null) || ((mesh.flags & 4) == 0)) { // Do not record the session, just send session start try { ws.send('c'); } catch (ex) { } // Send connect to both peers try { relayinfo.peer1.ws.send('c'); } catch (ex) { } @@ -337,7 +350,9 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { if (sessionUser._id) { xusername = '-' + parent.common.makeFilename(sessionUser._id.split('/')[2]); } var now = new Date(Date.now()); - var recFilename = 'relaysession' + ((domain.id == '') ? '' : '-') + domain.id + '-' + now.getUTCFullYear() + '-' + parent.common.zeroPad(now.getUTCMonth(), 2) + '-' + parent.common.zeroPad(now.getUTCDate(), 2) + '-' + parent.common.zeroPad(now.getUTCHours(), 2) + '-' + parent.common.zeroPad(now.getUTCMinutes(), 2) + '-' + parent.common.zeroPad(now.getUTCSeconds(), 2) + xusername + xdevicename + '-' + obj.id + '.mcrec' + var xsessionid = obj.id; + if ((typeof xsessionid == 'string') && (xsessionid.startsWith('meshmessenger/node/') == true)) { xsessionid = 'Messenger' } + var recFilename = 'relaysession' + ((domain.id == '') ? '' : '-') + domain.id + '-' + now.getUTCFullYear() + '-' + parent.common.zeroPad(now.getUTCMonth(), 2) + '-' + parent.common.zeroPad(now.getUTCDate(), 2) + '-' + parent.common.zeroPad(now.getUTCHours(), 2) + '-' + parent.common.zeroPad(now.getUTCMinutes(), 2) + '-' + parent.common.zeroPad(now.getUTCSeconds(), 2) + xusername + xdevicename + '-' + xsessionid + (xtextSession ? '.txt' : '.mcrec'); var recFullFilename = null; if (domain.sessionrecording.filepath) { try { parent.parent.fs.mkdirSync(domain.sessionrecording.filepath); } catch (e) { } @@ -375,9 +390,10 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { protocol: (((obj.req == null) || (obj.req.query == null)) ? null : obj.req.query.p), nodeid: (((obj.req == null) || (obj.req.query == null)) ? null : obj.req.query.nodeid) }; + if (xdevicename2 != null) { metadata.devicename = xdevicename2; } var firstBlock = JSON.stringify(metadata); - var logfile = { fd: fd, lock: false, filename: recFullFilename, startTime: Date.now(), size: 0 }; + var logfile = { fd: fd, lock: false, filename: recFullFilename, startTime: Date.now(), size: 0, text: xtextSession }; if (node != null) { logfile.nodeid = node._id; logfile.meshid = node.meshid; logfile.name = node.name; logfile.icon = node.icon; } recordingEntry(logfile, 1, 0, firstBlock, function () { try { relayinfo.peer1.ws.logfile = ws.logfile = logfile; } catch (ex) { @@ -441,6 +457,9 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { return null; } } else { + // Set authenticated side as browser side for messenger sessions + if ((obj.id.startsWith('meshmessenger/node/') == true) && obj.authenticated) { obj.req.query.browser = 1; } + // Wait for other relay connection if ((obj.id.startsWith('meshmessenger/node/') == true) && obj.authenticated && (parent.parent.firebase != null)) { // This is an authenticated messenger session, push messaging may be allowed. Don't hold traffic. @@ -578,6 +597,7 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { if (obj.req.query.p == 1) { msg = 'Ended terminal session', msgid = 10; } else if (obj.req.query.p == 2) { msg = 'Ended desktop session', msgid = 11; } else if (obj.req.query.p == 5) { msg = 'Ended file management session', msgid = 12; } + else if (obj.req.query.p == 200) { msg = 'Ended messenger session', msgid = 112; } if (user) { var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: user._id, username: user.name, msgid: msgid, msgArgs: [obj.id, obj.req.clientIp, obj.peer.req.clientIp, Math.floor((Date.now() - ws.time) / 1000)], msg: msg + ' \"' + obj.id + '\" from ' + obj.req.clientIp + ' to ' + obj.peer.req.clientIp + ', ' + Math.floor((Date.now() - ws.time) / 1000) + ' second(s)', protocol: obj.req.query.p, nodeid: obj.req.query.nodeid }; parent.parent.DispatchEvent(['*', user._id], obj, event); @@ -617,6 +637,7 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { var event = { etype: 'relay', action: 'recording', domain: domain.id, nodeid: tag.logfile.nodeid, msg: "Finished recording session" + (sessionLength ? (', ' + sessionLength + ' second(s)') : ''), filename: basefile, size: tag.logfile.size }; if (user) { event.userids = [user._id]; } else if (peer.user) { event.userids = [peer.user._id]; } var xprotocol = (((obj.req == null) || (obj.req.query == null)) ? null : obj.req.query.p); + if ((xprotocol == null) && (logfile.text == 2)) { xprotocol = 200; } if (xprotocol != null) { event.protocol = parseInt(xprotocol); } var mesh = parent.meshes[tag.logfile.meshid]; if (mesh != null) { event.meshname = mesh.name; event.meshid = mesh._id; } @@ -648,26 +669,64 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) { // Record a new entry in a recording log function recordingEntry(logfile, type, flags, 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(new Date(), 10, 6); // Time - var block = Buffer.concat([header, blockData]); - parent.parent.fs.write(logfile.fd, block, 0, block.length, function () { func(logfile, tag); }); - logfile.size += block.length; + if (logfile.text) { + // Text recording format + var out = ''; + const utcDate = new Date(Date.now()); + if (type == 1) { + // End of start + out = data + '\r\n' + utcDate.toUTCString() + ', ' + "<<>>" + '\r\n'; + } else if (type == 3) { + // End of log + out = utcDate.toUTCString() + ', ' + "<<>>" + '\r\n'; + } else if (typeof data == 'string') { + // Log message + if (logfile.text == 1) { + out = utcDate.toUTCString() + ', ' + data + '\r\n'; + } else if (logfile.text == 2) { + try { + var x = JSON.parse(data); + if (typeof x.action == 'string') { + if ((x.action == 'chat') && (typeof x.msg == 'string')) { out = utcDate.toUTCString() + ', ' + (((flags & 2) ? '--> ' : '<-- ') + x.msg + '\r\n'); } + else if ((x.action == 'file') && (typeof x.name == 'string') && (typeof x.size == 'number')) { out = utcDate.toUTCString() + ', ' + (((flags & 2) ? '--> ' : '<-- ') + "File Transfer" + ', \"' + x.name + '\" (' + x.size + ' ' + "bytes" + ')\r\n'); } + } else { out = utcDate.toUTCString() + ', ' + data + '\r\n'; } + } catch (ex) { + out = utcDate.toUTCString() + ', ' + data + '\r\n'; + } + } + } + if (out != null) { + // Log this event + const block = Buffer.from(out); + parent.parent.fs.write(logfile.fd, block, 0, block.length, function () { func(logfile, tag); }); + logfile.size += block.length; + } else { + // Skip logging this. + func(logfile, 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(new Date(), 10, 6); // Time - var block = Buffer.concat([header, data]); - parent.parent.fs.write(logfile.fd, block, 0, block.length, function () { func(logfile, tag); }); - logfile.size += block.length; + // Binary recording format + 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(new Date(), 10, 6); // Time + var block = Buffer.concat([header, blockData]); + parent.parent.fs.write(logfile.fd, block, 0, block.length, function () { func(logfile, tag); }); + logfile.size += block.length; + } 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(new Date(), 10, 6); // Time + var block = Buffer.concat([header, data]); + parent.parent.fs.write(logfile.fd, block, 0, block.length, function () { func(logfile, tag); }); + logfile.size += block.length; + } } } catch (ex) { console.log(ex); func(logfile, tag); } } diff --git a/sample-config-advanced.json b/sample-config-advanced.json index acb47ecc..de09d78b 100644 --- a/sample-config-advanced.json +++ b/sample-config-advanced.json @@ -292,7 +292,7 @@ "_index": true, "_maxRecordings": 10, "_maxRecordingSizeMegabytes": 3, - "__protocols__": "Is an array: 1 = Terminal, 2 = Desktop, 5 = Files, 100 = Intel AMT WSMAN, 101 = Intel AMT Redirection", + "__protocols__": "Is an array: 1 = Terminal, 2 = Desktop, 5 = Files, 100 = Intel AMT WSMAN, 101 = Intel AMT Redirection, 200 = Messenger", "protocols": [ 1, 2, 101 ] }, "_authStrategies": { diff --git a/views/default.handlebars b/views/default.handlebars index 4b9192be..e50eecf5 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -11943,7 +11943,8 @@ 108: "User login attempt with incorrect 2nd factor from {0}, {1}, {2}", 109: "User login attempt on locked account from {0}, {1}, {2}", 110: "Invalid user login attempt from {0}, {1}, {2}", - 111: "Device requested Intel(R) AMT ACM TLS activation, FQDN: {0}" + 111: "Device requested Intel(R) AMT ACM TLS activation, FQDN: {0}", + 112: "Ended messenger session \"{0}\" from {1} to {2}, {3} second(s)" }; // Highlights the device being hovered @@ -13702,6 +13703,14 @@ } } } + + if (rec.protocol == 1) { sessionName += ' - ' + "Terminal Session"; } + if (rec.protocol == 2) { sessionName += ' - ' + "Desktop Session"; } + if (rec.protocol == 5) { sessionName += ' - ' + "File Transfer"; } + if (rec.protocol == 100) { sessionName += ' - ' + "Intel® AMT WSMAN"; } + if (rec.protocol == 101) { sessionName += ' - ' + "Intel® AMT Redirection"; } + if (rec.protocol == 200) { sessionName += ' - ' + "Messenger"; } + var actions = '', icon = 'm0'; if (rec.present == 1) { icon = 'm1'; actions = '
 
'; } var x = ''; diff --git a/views/messenger.handlebars b/views/messenger.handlebars index 56ce218a..85098f07 100644 --- a/views/messenger.handlebars +++ b/views/messenger.handlebars @@ -23,6 +23,7 @@ +
@@ -88,6 +89,7 @@ var localOutText = false; var remoteOutText = false; var remoteImage = false; + var serverRecording = false; // File transfer state var fileUploads = []; @@ -279,7 +281,7 @@ //console.log('ondatachannel'); webchannel = ev.channel; webchannel.onmessage = function (event) { processMessage(event.data, 2); }; - webchannel.onopen = function () { webchannel.ok = true; updateControls(); sendws({ action: 'rtcSwitch', v: 0 }); }; + webchannel.onopen = function () { webchannel.ok = true; updateControls(); if (serverRecording == false) { sendws({ action: 'rtcSwitch', v: 0 }); } }; webchannel.onclose = function (event) { if (webchannel && webchannel.ok) { disconnect(); } else { hangUpButtonClick(0); } } } webrtc.onnegotiationneeded = function (event) { @@ -316,7 +318,7 @@ if (startDataChannel == true) { webchannel = webrtc.createDataChannel('DataChannel', {}); // { ordered: false, maxRetransmits: 2 } webchannel.onmessage = function (event) { processMessage(event.data, 2); }; - webchannel.onopen = function () { webchannel.ok = true; updateControls(); sendws({ action: 'rtcSwitch', v: 0 }); }; + webchannel.onopen = function () { webchannel.ok = true; updateControls(); if (serverRecording == false) { sendws({ action: 'rtcSwitch', v: 0 }); } }; webchannel.onclose = function (event) { if (webchannel && webchannel.ok) { disconnect(); } else { hangUpButtonClick(0); } } } @@ -344,11 +346,13 @@ // Indicate to peer that data traffic will no longer be sent over websocket and start holding traffic. function performWebRtcSwitch() { - if (webchannel && webchannel.ok) { sendws({ action: 'rtcSwitch', v: 1 }); webchannel.xoutBuffer = []; } + if (!serverRecording && webchannel && webchannel.ok) { sendws({ action: 'rtcSwitch', v: 1 }); webchannel.xoutBuffer = []; } } // Disconnect everything function disconnect() { + serverRecording = false; + QV('recordIcon', false); if (state > 0) { displayControl("Connection closed."); } if (state > 1) { setTimeout(start, 500); } cancelAllFileTransfers(); @@ -368,7 +372,7 @@ function send(data) { if ((state != 2) && (pushMessaging == false)) return; // If not in connected state, ignore this. if (typeof data == 'object') { data = JSON.stringify(data); } // If this is an object, convert it to a string. - if (webchannel && webchannel.ok) { if (webchannel.xoutBuffer != null) { webchannel.xoutBuffer.push(data); } else { webchannel.send(data); } } // If WebRTC channel is possible, use it or hold until we can use it. + if (!serverRecording && webchannel && webchannel.ok) { if (webchannel.xoutBuffer != null) { webchannel.xoutBuffer.push(data); } else { webchannel.send(data); } } // If WebRTC channel is possible, use it or hold until we can use it. else { if (socket != null) { try { socket.send(data); } catch (ex) { } } } // If a websocket channel is present, use that. } @@ -520,7 +524,7 @@ file.id = Math.random(); fileUploads.push(file); chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Upload" + '> ' + file.name + ' (' + file.size + ' ' + "bytes" + ')\r\n'); - QA('xmsg', '
' + file.name + '
 
'); + QA('xmsg', '
' + file.name + '
 
'); Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight; send({ action: 'file', size: file.size, id: file.id, type: file.type, name: file.name }); if (currentFileUpload == null) continueFileUpload(); @@ -530,7 +534,7 @@ if (state != 2) return; fileDownloads[file.id] = file; chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Download" + '> ' + file.name + ' (' + file.size + ' ' + "bytes" + ')\r\n'); - QA('xmsg', '
' + file.name + '
 
'); + QA('xmsg', '
' + file.name + '
 
'); Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight; } @@ -717,6 +721,8 @@ socket.onclose = function () { disconnect(); } socket.onmessage = function (msg) { if ((state < 2) && (typeof msg.data == 'string') && ((msg.data == 'c') || (msg.data == 'cr'))) { + serverRecording = (msg.data == 'cr'); + QV('recordIcon', serverRecording); hangUpButtonClick(0, true); hangUpButtonClick(1, true); hangUpButtonClick(2, true); @@ -742,9 +748,9 @@ start(); function onUnLoad() { - for (var i = 0; i < 3; i++) { if (webrtcSessions[i]) { webrtcSessions[i].close(); delete webrtcSessions[i]; } } - if (webchannel != null) { try { webchannel.close(); } catch (e) { } webchannel = null; } - if (socket != null) { try { socket.close(); } catch (e) { } socket = null; } + for (var i = 0; i < 3; i++) { if (webrtcSessions[i]) { try { webrtcSessions[i].close(); delete webrtcSessions[i]; } catch (ex) { } } } + if (webchannel != null) { try { webchannel.close(); } catch (ex) { } webchannel = null; } + if (socket != null) { try { socket.close(); } catch (ex) { } socket = null; } } function isSafeString3(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)) };