Added recodings manager and recording improvements.

Ylian Saint-Hilaire committed May 5, 2020 at 13:00 UTC efc378be6b81453d79964a77b8c47024df96a45a
7 files changed +410 -126
meshdesktopmultiplex.js
+53 -18
@@ -86,8 +86,10 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, func) {
86 obj.protocolOptions = null; // Set to the protocol options of the first viewer that connected.
87 obj.viewerConnected = false; // Set to true if one viewer attempted to connect to the agent.
88 obj.recordingFile = null; // Present if we are recording to file.
89 + obj.recordingFileSize = 0; // Current size of the recording file.
90 obj.recordingFileWriting = false; // Set to true is we are in the process if writing to the recording file.
91 obj.startTime = null; // Starting time of the multiplex session.
92 + obj.userIds = []; // List of userid's that have intertracted with this session.
93
94 // Add an agent or viewer
95 obj.addPeer = function (peer) {
@@ -108,6 +110,9 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, func) {
110 peer.sendQueue = [];
111 peer.paused = false;
112
113 + // Add the user to the userids list if needed
114 + if ((peer.user != null) && (obj.userIds.indexOf(peer.user._id) == -1)) { obj.userIds.push(peer.user._id); }
115 +
116 // Setup slow relay is requested. This will show down sending any data to this viewer.
117 if ((peer.req.query.slowrelay != null)) {
118 var sr = null;
@@ -125,7 +130,7 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, func) {
130 // Log joining the multiplex session
131 if (obj.startTime != null) {
132 var event = { etype: 'relay', action: 'relaylog', domain: domain.id, nodeid: obj.nodeid, userid: peer.user._id, username: peer.user.name, msg: "Joined desktop multiplex session", protocol: 2 };
128 - parent.parent.DispatchEvent(['*', obj.nodeid, peer.user._id], obj, event); // TODO: Add Node MeshID to targets
133 + parent.parent.DispatchEvent(['*', obj.nodeid, peer.user._id, obj.meshid], obj, event);
134 }
135 } else {
136 //console.log('addPeer-agent', obj.nodeid);
@@ -149,7 +154,7 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, func) {
154 // Log multiplex session start
155 if ((obj.agent != null) && (obj.viewers.length > 0) && (obj.startTime == null)) {
156 var event = { etype: 'relay', action: 'relaylog', domain: domain.id, nodeid: obj.nodeid, userid: obj.viewers[0].user._id, username: obj.viewers[0].user.name, msg: "Started desktop multiplex session", protocol: 2 };
152 - parent.parent.DispatchEvent(['*', obj.nodeid, obj.viewers[0].user._id], obj, event); // TODO: Add Node MeshID to targets
157 + parent.parent.DispatchEvent(['*', obj.nodeid, obj.viewers[0].user._id, obj.meshid], obj, event);
158 obj.startTime = Date.now();
159 }
160 return true;
@@ -195,7 +200,7 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, func) {
200 // Log leaving the multiplex session
201 if (obj.startTime != null) {
202 var event = { etype: 'relay', action: 'relaylog', domain: domain.id, nodeid: obj.nodeid, userid: peer.user._id, username: peer.user.name, msg: "Left the desktop multiplex session", protocol: 2 };
198 - parent.parent.DispatchEvent(['*', obj.nodeid, peer.user._id], obj, event); // TODO: Add Node MeshID to targets
203 + parent.parent.DispatchEvent(['*', obj.nodeid, peer.user._id, obj.meshid], obj, event);
204 }
205
206 // If this is the last viewer, disconnect the agent
@@ -214,12 +219,26 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, func) {
219
220 // Close the recording file if needed
221 if (obj.recordingFile != null) {
222 + // Compute session length
223 + if (obj.startTime != null) { obj.sessionStart = obj.startTime; obj.sessionLength = Math.round((Date.now() - obj.startTime) / 1000); }
224 +
225 + // Write the last record of the recording file
226 var rf = obj.recordingFile;
227 delete obj.recordingFile;
228 recordingEntry(rf.fd, 3, 0, 'MeshCentralMCREC', function (fd, filename) {
229 parent.parent.fs.close(fd);
230 +
231 // Now that the recording file is closed, check if we need to index this file.
232 if (domain.sessionrecording.index !== false) { parent.parent.certificateOperations.acceleratorPerformOperation('indexMcRec', filename); }
233 +
234 + // Add a event entry about this recording
235 + var basefile = parent.parent.path.basename(filename);
236 + var event = { etype: 'relay', action: 'recording', domain: domain.id, nodeid: obj.nodeid, msg: "Finished recording session" + (obj.sessionLength ? (', ' + obj.sessionLength + ' second(s)') : ''), filename: basefile, size: obj.recordingFileSize, protocol: 2, icon: obj.icon, name: obj.name, meshid: obj.meshid, userids: obj.userIds, multiplex: true };
237 + var mesh = parent.meshes[obj.meshid];
238 + if (mesh != null) { event.meshname = mesh.name; }
239 + if (obj.sessionStart) { event.startTime = obj.sessionStart; event.lengthTime = obj.sessionLength; }
240 + parent.parent.DispatchEvent(['*', 'recording', obj.nodeid, obj.meshid], obj, event);
241 +
242 cleanUpRecordings();
243 }, rf.filename);
244 }
@@ -227,7 +246,7 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, func) {
246 // Log end of multiplex session
247 if (obj.startTime != null) {
248 var event = { etype: 'relay', action: 'relaylog', domain: domain.id, nodeid: obj.nodeid, msg: "Closed desktop multiplex session" + ', ' + Math.floor((Date.now() - obj.startTime) / 1000) + ' second(s)', protocol: 2 };
230 - parent.parent.DispatchEvent(['*', obj.nodeid], obj, event); // TODO: Add Node MeshID to targets
249 + parent.parent.DispatchEvent(['*', obj.nodeid, obj.meshid], obj, event);
250 obj.startTime = null;
251 }
252
@@ -647,7 +666,7 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, func) {
666 }
667 // Write the recording file header
668 parent.parent.debug('relay', 'Relay: Started recoding to file: ' + recFullFilename);
650 - var metadata = { magic: 'MeshCentralRelaySession', ver: 1, nodeid: obj.nodeid, time: new Date().toLocaleString(), protocol: 2 };
669 + var metadata = { magic: 'MeshCentralRelaySession', ver: 1, nodeid: obj.nodeid, meshid: obj.meshid, time: new Date().toLocaleString(), protocol: 2, devicename: obj.name, devicegroup: obj.meshname };
670 var firstBlock = JSON.stringify(metadata);
671 recordingEntry(fd, 1, 0, firstBlock, function () {
672 obj.recordingFile = { fd: fd, filename: recFullFilename };
@@ -684,6 +703,7 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, func) {
703 header.writeIntBE(new Date(), 10, 6); // Time
704 var block = Buffer.concat([header, blockData]);
705 parent.parent.fs.write(fd, block, 0, block.length, function () { func(fd, tag); });
706 + obj.recordingFileSize += block.length;
707 } else {
708 // Binary write
709 var header = Buffer.alloc(16); // Header: Type (2) + Flags (2) + Size(4) + Time(8)
@@ -693,6 +713,7 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, func) {
713 header.writeIntBE(new Date(), 10, 6); // Time
714 var block = Buffer.concat([header, data]);
715 parent.parent.fs.write(fd, block, 0, block.length, function () { func(fd, tag); });
716 + obj.recordingFileSize += block.length;
717 }
718 } catch (ex) { console.log(ex); func(fd, tag); }
719 }
@@ -725,7 +746,14 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, func) {
746 }
747 }
748
728 - recordingSetup(domain, function () { func(obj); });
749 + // Get node information
750 + parent.db.Get(nodeid, function (err, nodes) {
751 + if ((err != null) || (nodes.length != 1)) { func(null); }
752 + obj.meshid = nodes[0].meshid;
753 + obj.icon = nodes[0].icon;
754 + obj.name = nodes[0].name;
755 + recordingSetup(domain, function () { func(obj); });
756 + });
757 return obj;
758 }
759
@@ -784,7 +812,7 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
812 if ((arg == 1) || (arg == null)) { try { ws.close(); parent.parent.debug('relay', 'DesktopRelay: Soft disconnect (' + cleanRemoteAddr(obj.req.ip) + ')'); } catch (e) { console.log(e); } } // Soft close, close the websocket
813 if (arg == 2) { try { ws._socket._parent.end(); parent.parent.debug('relay', 'DesktopRelay: Hard disconnect (' + cleanRemoteAddr(obj.req.ip) + ')'); } catch (e) { console.log(e); } } // Hard close, close the TCP socket
814 if (obj.relaySessionCounted) { parent.relaySessionCount--; delete obj.relaySessionCounted; }
787 - if (obj.deskDecoder != null) { if (obj.deskDecoder.removePeer(obj) == true) { delete parent.desktoprelays[obj.nodeid]; } }
815 + if (obj.deskMultiplexor != null) { if (obj.deskMultiplexor.removePeer(obj) == true) { delete parent.desktoprelays[obj.nodeid]; } }
816
817 // Aggressive cleanup
818 delete obj.id;
@@ -793,7 +821,7 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
821 delete obj.user;
822 delete obj.nodeid;
823 delete obj.ruserid;
796 - delete obj.deskDecoder;
824 + delete obj.deskMultiplexor;
825
826 // Clear timers if present
827 if (obj.pingtimer != null) { clearInterval(obj.pingtimer); delete obj.pingtimer; }
@@ -885,23 +913,30 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
913 }
914
915 // Create if needed and add this peer to the desktop multiplexor
888 - obj.deskDecoder = parent.desktoprelays[obj.nodeid];
889 - if (obj.deskDecoder == null) {
916 + obj.deskMultiplexor = parent.desktoprelays[obj.nodeid];
917 + if (obj.deskMultiplexor == null) {
918 parent.desktoprelays[obj.nodeid] = 1; // Indicate that the creating of the desktop multiplexor is pending.
919 parent.parent.debug('relay', 'DesktopRelay: Creating new desktop multiplexor');
892 - CreateDesktopMultiplexor(parent, domain, obj.nodeid, function (deskDecoder) {
893 - obj.deskDecoder = deskDecoder;
894 - parent.desktoprelays[obj.nodeid] = obj.deskDecoder;
895 - obj.deskDecoder.addPeer(obj);
896 - ws._socket.resume(); // Release the traffic
920 + CreateDesktopMultiplexor(parent, domain, obj.nodeid, function (deskMultiplexor) {
921 + if (deskMultiplexor != null) {
922 + // Desktop multiplexor was created, use it.
923 + obj.deskMultiplexor = deskMultiplexor;
924 + parent.desktoprelays[obj.nodeid] = obj.deskMultiplexor;
925 + obj.deskMultiplexor.addPeer(obj);
926 + ws._socket.resume(); // Release the traffic
927 + } else {
928 + // An error has occured, close this connection
929 + delete parent.desktoprelays[obj.nodeid];
930 + ws.close();
931 + }
932 });
933 } else {
899 - if (obj.deskDecoder == 1) {
934 + if (obj.deskMultiplexor == 1) {
935 // The multiplexor is being created, hold a little and try again. This is to prevent a possible race condition.
936 setTimeout(function () { performRelay(++retryCount); }, 50);
937 } else {
938 // Hook up this peer to the multiplexor and release the traffic
904 - obj.deskDecoder.addPeer(obj);
939 + obj.deskMultiplexor.addPeer(obj);
940 ws._socket.resume();
941 }
942 }
@@ -910,7 +945,7 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
945 // When data is received from the mesh relay web socket
946 ws.on('message', function (data) {
947 // If this data was received by the agent, decode it.
913 - if (this.me.deskDecoder != null) { this.me.deskDecoder.processData(this.me, data); }
948 + if (this.me.deskMultiplexor != null) { this.me.deskMultiplexor.processData(this.me, data); }
949 });
950
951 // If error, close both sides of the relay.
meshrelay.js
+34 -12
@@ -222,8 +222,8 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
222 if ((sessionUser != null) && (domain.sessionrecording == true || ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.protocols == null) || (domain.sessionrecording.protocols.indexOf(parseInt(obj.req.query.p)) >= 0))))) {
223 // Get the computer name
224 parent.db.Get(obj.req.query.nodeid, function (err, nodes) {
225 - var xusername = '', xdevicename = '', xdevicename2 = null;
226 - if ((nodes != null) && (nodes.length == 1)) { xdevicename2 = nodes[0].name; xdevicename = '-' + parent.common.makeFilename(nodes[0].name); }
225 + var xusername = '', xdevicename = '', xdevicename2 = null, node = null;
226 + if ((nodes != null) && (nodes.length == 1)) { node = nodes[0]; xdevicename2 = node.name; xdevicename = '-' + parent.common.makeFilename(node.name); }
227
228 // Get the username and make it acceptable as a filename
229 if (sessionUser._id) { xusername = '-' + parent.common.makeFilename(sessionUser._id.split('/')[2]); }
@@ -250,8 +250,9 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
250 var metadata = { magic: 'MeshCentralRelaySession', ver: 1, userid: sessionUser._id, username: sessionUser.name, sessionid: obj.id, ipaddr1: cleanRemoteAddr(obj.req.ip), ipaddr2: cleanRemoteAddr(obj.peer.req.ip), time: new Date().toLocaleString(), 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 ) };
251 if (xdevicename2 != null) { metadata.devicename = xdevicename2; }
252 var firstBlock = JSON.stringify(metadata);
253 - recordingEntry(fd, 1, 0, firstBlock, function () {
254 - try { relayinfo.peer1.ws.logfile = ws.logfile = { fd: fd, lock: false, filename: recFullFilename }; } catch (ex) {
253 + var logfile = { fd: fd, lock: false, filename: recFullFilename, startTime: Date.now(), size: 0, nodeid: node._id, meshid: node.meshid, name: node.name, icon: node.icon };
254 + recordingEntry(logfile, 1, 0, firstBlock, function () {
255 + try { relayinfo.peer1.ws.logfile = ws.logfile = logfile; } catch (ex) {
256 try { ws.send('c'); } catch (ex) { } // Send connect to both peers, 'cr' indicates the session is being recorded.
257 try { relayinfo.peer1.ws.send('c'); } catch (ex) { }
258 return;
@@ -323,7 +324,7 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
324 if (this.logfile != null) {
325 // Write data to log file then perform relay
326 var xthis = this;
326 - recordingEntry(this.logfile.fd, 2, ((obj.req.query.browser) ? 2 : 0), data, function () { xthis.peer.send(data, ws.flushSink); });
327 + recordingEntry(this.logfile, 2, ((obj.req.query.browser) ? 2 : 0), data, function () { xthis.peer.send(data, ws.flushSink); });
328 } else {
329 // Perform relay
330 this.peer.send(data, ws.flushSink);
@@ -335,7 +336,7 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
336 if (this.logfile != null) {
337 // Write data to log file then perform slow relay
338 var xthis = this;
338 - recordingEntry(this.logfile.fd, 2, ((obj.req.query.browser) ? 2 : 0), data, function () {
339 + recordingEntry(this.logfile, 2, ((obj.req.query.browser) ? 2 : 0), data, function () {
340 setTimeout(function () { xthis.peer.send(data, ws.flushSink); }, xthis.peer.slowRelay);
341 });
342 } else {
@@ -406,10 +407,29 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
407 var logfile = ws.logfile;
408 delete ws.logfile;
409 if (peer.ws) { delete peer.ws.logfile; }
409 - recordingEntry(logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd, tag) {
410 - parent.parent.fs.close(fd);
410 + recordingEntry(logfile, 3, 0, 'MeshCentralMCREC', function (logfile, tag) {
411 + parent.parent.fs.close(logfile.fd);
412 +
413 // Now that the recording file is closed, check if we need to index this file.
414 if (domain.sessionrecording.index !== false) { parent.parent.certificateOperations.acceleratorPerformOperation('indexMcRec', tag.logfile.filename); }
415 +
416 + // Compute session length
417 + var sessionLength = null;
418 + if (tag.logfile.startTime != null) { sessionLength = Math.round((Date.now() - tag.logfile.startTime) / 1000); }
419 +
420 + // Add a event entry about this recording
421 + var basefile = parent.parent.path.basename(tag.logfile.filename);
422 + 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 };
423 + if (user) { event.userids = [user._id]; } else if (peer.user) { event.userids = [peer.user._id]; }
424 + var xprotocol = (((obj.req == null) || (obj.req.query == null)) ? null : obj.req.query.p);
425 + if (xprotocol != null) { event.protocol = parseInt(xprotocol); }
426 + var mesh = parent.meshes[tag.logfile.meshid];
427 + if (mesh != null) { event.meshname = mesh.name; event.meshid = mesh._id; }
428 + if (tag.logfile.startTime) { event.startTime = tag.logfile.startTime; event.lengthTime = sessionLength; }
429 + if (tag.logfile.name) { event.name = tag.logfile.name; }
430 + if (tag.logfile.icon) { event.icon = tag.logfile.icon; }
431 + parent.parent.DispatchEvent(['*', 'recording', obj.nodeid, obj.meshid], obj, event);
432 +
433 cleanUpRecordings();
434 }, { ws: ws, pws: peer.ws, logfile: logfile });
435 }
@@ -428,7 +448,7 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
448 }
449
450 // Record a new entry in a recording log
431 - function recordingEntry(fd, type, flags, data, func, tag) {
451 + function recordingEntry(logfile, type, flags, data, func, tag) {
452 try {
453 if (typeof data == 'string') {
454 // String write
@@ -438,7 +458,8 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
458 header.writeInt32BE(blockData.length, 4); // Size
459 header.writeIntBE(new Date(), 10, 6); // Time
460 var block = Buffer.concat([header, blockData]);
441 - parent.parent.fs.write(fd, block, 0, block.length, function () { func(fd, tag); });
461 + parent.parent.fs.write(logfile.fd, block, 0, block.length, function () { func(logfile, tag); });
462 + logfile.size += block.length;
463 } else {
464 // Binary write
465 var header = Buffer.alloc(16); // Header: Type (2) + Flags (2) + Size(4) + Time(8)
@@ -447,9 +468,10 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
468 header.writeInt32BE(data.length, 4); // Size
469 header.writeIntBE(new Date(), 10, 6); // Time
470 var block = Buffer.concat([header, data]);
450 - parent.parent.fs.write(fd, block, 0, block.length, function () { func(fd, tag); });
471 + parent.parent.fs.write(logfile.fd, block, 0, block.length, function () { func(logfile, tag); });
472 + logfile.size += block.length;
473 }
452 - } catch (ex) { console.log(ex); func(fd, tag); }
474 + } catch (ex) { console.log(ex); func(logfile, tag); }
475 }
476
477 // Mark this relay session as authenticated if this is the user end.
meshuser.js
+31
@@ -1211,6 +1211,37 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
1211 }
1212 break;
1213 }
1214 + case 'recordings': {
1215 + if (((user.siteadmin & SITERIGHT_RECORDINGS) == 0) || (domain.sessionrecording == null)) return; // Check if recordings is enabled and we have rights to do this.
1216 + var recordingsPath = null;
1217 + if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.parent.recordpath; }
1218 + if (recordingsPath == null) return;
1219 + fs.readdir(recordingsPath, function (err, files) {
1220 + if (err != null) return;
1221 + if ((command.limit == null) || (typeof command.limit != 'number')) {
1222 + // Send the list of all recordings
1223 + db.GetEvents(['recording'], domain.id, function (err, docs) {
1224 + if (err != null) return;
1225 + for (var i in docs) {
1226 + delete docs[i].action; delete docs[i].etype; delete docs[i].msg; // TODO: We could make a more specific query in the DB and never have these.
1227 + if (files.indexOf(docs[i].filename) >= 0) { docs[i].present = 1; }
1228 + }
1229 + try { ws.send(JSON.stringify({ action: 'recordings', events: docs, tag: command.tag })); } catch (ex) { }
1230 + });
1231 + } else {
1232 + // Send the list of most recent recordings, up to 'limit' count
1233 + db.GetEventsWithLimit(['recording'], domain.id, command.limit, function (err, docs) {
1234 + if (err != null) return;
1235 + for (var i in docs) {
1236 + delete docs[i].action; delete docs[i].etype; delete docs[i].msg; // TODO: We could make a more specific query in the DB and never have these.
1237 + if (files.indexOf(docs[i].filename) >= 0) { docs[i].present = 1; }
1238 + }
1239 + try { ws.send(JSON.stringify({ action: 'recordings', events: docs, tag: command.tag })); } catch (ex) { }
1240 + });
1241 + }
1242 + });
1243 + break;
1244 + }
1245 case 'users':
1246 {
1247 // Request a list of all users
translate/translate.json
+166 -88
@@ -607,7 +607,7 @@
607 "default-mobile.handlebars->9->248",
608 "default-mobile.handlebars->9->77",
609 "default.handlebars->25->1339",
610 - "default.handlebars->25->1649",
610 + "default.handlebars->25->1676",
611 "default.handlebars->25->727"
612 ]
613 },
@@ -2956,7 +2956,7 @@
2956 "ru": "Счетчик ошибок агента",
2957 "zh-chs": "座席錯誤計數器",
2958 "xloc": [
2959 - "default.handlebars->25->1659"
2959 + "default.handlebars->25->1686"
2960 ]
2961 },
2962 {
@@ -3025,7 +3025,7 @@
3025 "ru": "Сессии агентов",
3026 "zh-chs": "座席會議",
3027 "xloc": [
3028 - "default.handlebars->25->1675"
3028 + "default.handlebars->25->1702"
3029 ]
3030 },
3031 {
@@ -3148,7 +3148,7 @@
3148 "ru": "Агенты",
3149 "zh-chs": "代理商",
3150 "xloc": [
3151 - "default.handlebars->25->1688"
3151 + "default.handlebars->25->1715"
3152 ]
3153 },
3154 {
@@ -3970,7 +3970,7 @@
3970 "ru": "Вы уверенны, что {0} плагин: {1}",
3971 "zh-chs": "您確定要{0}插件嗎:{1}",
3972 "xloc": [
3973 - "default.handlebars->25->1728"
3973 + "default.handlebars->25->1755"
3974 ]
3975 },
3976 {
@@ -4399,7 +4399,7 @@
4399 "ru": "Плохой ключ",
4400 "zh-chs": "錯誤的簽名",
4401 "xloc": [
4402 - "default.handlebars->25->1666"
4402 + "default.handlebars->25->1693"
4403 ]
4404 },
4405 {
@@ -4416,7 +4416,7 @@
4416 "ru": "Плохой веб-сертификат",
4417 "zh-chs": "錯誤的網絡證書",
4418 "xloc": [
4419 - "default.handlebars->25->1665"
4419 + "default.handlebars->25->1692"
4420 ]
4421 },
4422 {
@@ -4679,7 +4679,7 @@
4679 "ru": "CIRA Сервер",
4680 "zh-chs": "CIRA服務器",
4681 "xloc": [
4682 - "default.handlebars->25->1716"
4682 + "default.handlebars->25->1743"
4683 ]
4684 },
4685 {
@@ -4696,7 +4696,7 @@
4696 "ru": "CIRA Сервер команды",
4697 "zh-chs": "CIRA服務器命令",
4698 "xloc": [
4699 - "default.handlebars->25->1717"
4699 + "default.handlebars->25->1744"
4700 ]
4701 },
4702 {
@@ -4729,7 +4729,7 @@
4729 "ru": "Загрузка CPU",
4730 "zh-chs": "CPU負載",
4731 "xloc": [
4732 - "default.handlebars->25->1680"
4732 + "default.handlebars->25->1707"
4733 ]
4734 },
4735 {
@@ -4746,7 +4746,7 @@
4746 "ru": "Загрузка CPU за последние 15 минут",
4747 "zh-chs": "最近15分鐘的CPU負載",
4748 "xloc": [
4749 - "default.handlebars->25->1683"
4749 + "default.handlebars->25->1710"
4750 ]
4751 },
4752 {
@@ -4763,7 +4763,7 @@
4763 "ru": "Загрузка CPU за последние 5 минут",
4764 "zh-chs": "最近5分鐘的CPU負載",
4765 "xloc": [
4766 - "default.handlebars->25->1682"
4766 + "default.handlebars->25->1709"
4767 ]
4768 },
4769 {
@@ -4780,7 +4780,7 @@
4780 "ru": "Загрузка CPU за последнюю минуту",
4781 "zh-chs": "最後一分鐘的CPU負載",
4782 "xloc": [
4783 - "default.handlebars->25->1681"
4783 + "default.handlebars->25->1708"
4784 ]
4785 },
4786 {
@@ -4835,7 +4835,7 @@
4835 "ru": "Ошибка вызова",
4836 "zh-chs": "通話錯誤",
4837 "xloc": [
4838 - "default.handlebars->25->1729"
4838 + "default.handlebars->25->1756"
4839 ]
4840 },
4841 {
@@ -5302,7 +5302,7 @@
5302 "ru": "Проверка...",
5303 "zh-chs": "檢查...",
5304 "xloc": [
5305 - "default.handlebars->25->1723",
5305 + "default.handlebars->25->1750",
5306 "default.handlebars->25->865"
5307 ]
5308 },
@@ -5518,7 +5518,7 @@
5518 "nl": "Wis alle meldingen",
5519 "zh-chs": "全部清除",
5520 "xloc": [
5521 - "default.handlebars->25->1653"
5521 + "default.handlebars->25->1680"
5522 ]
5523 },
5524 {
@@ -5568,7 +5568,7 @@
5568 "ru": "Очистить это уведомление",
5569 "zh-chs": "清除此通知",
5570 "xloc": [
5571 - "default.handlebars->25->1652"
5571 + "default.handlebars->25->1679"
5572 ]
5573 },
5574 {
@@ -6380,7 +6380,7 @@
6380 "ru": "Подключено Intel® AMT",
6381 "zh-chs": "連接的英特爾®AMT",
6382 "xloc": [
6383 - "default.handlebars->25->1671"
6383 + "default.handlebars->25->1698"
6384 ]
6385 },
6386 {
@@ -6397,7 +6397,7 @@
6397 "ru": "Подключенные пользователи",
6398 "zh-chs": "關聯用戶",
6399 "xloc": [
6400 - "default.handlebars->25->1676"
6400 + "default.handlebars->25->1703"
6401 ]
6402 },
6403 {
@@ -6473,7 +6473,7 @@
6473 "ru": "Подключений ",
6474 "zh-chs": "連接數",
6475 "xloc": [
6476 - "default.handlebars->25->1687"
6476 + "default.handlebars->25->1714"
6477 ]
6478 },
6479 {
@@ -6490,7 +6490,7 @@
6490 "ru": "Ретранслятор подключения",
6491 "zh-chs": "連接繼電器",
6492 "xloc": [
6493 - "default.handlebars->25->1715"
6493 + "default.handlebars->25->1742"
6494 ]
6495 },
6496 {
@@ -6617,7 +6617,7 @@
6617 "ru": "Cookie-кодировщик",
6618 "zh-chs": "Cookie編碼器",
6619 "xloc": [
6620 - "default.handlebars->25->1701"
6620 + "default.handlebars->25->1728"
6621 ]
6622 },
6623 {
@@ -6922,7 +6922,7 @@
6922 "ru": "Основной сервер",
6923 "zh-chs": "核心服務器",
6924 "xloc": [
6925 - "default.handlebars->25->1700"
6925 + "default.handlebars->25->1727"
6926 ]
6927 },
6928 {
@@ -7872,6 +7872,7 @@
7872 "zh-chs": "桌面",
7873 "xloc": [
7874 "default.handlebars->25->1217",
7875 + "default.handlebars->25->1657",
7876 "default.handlebars->25->432",
7877 "default.handlebars->container->topbar->1->1->MainSubMenuSpan->MainSubMenu->1->0->MainDevDesktop",
7878 "default.handlebars->contextMenu->cxdesktop"
@@ -8092,7 +8093,8 @@
8093 "default.handlebars->25->1238",
8094 "default.handlebars->25->1512",
8095 "default.handlebars->25->1518",
8095 - "default.handlebars->25->1619"
8096 + "default.handlebars->25->1619",
8097 + "default.handlebars->25->1666"
8098 ]
8099 },
8100 {
@@ -8132,7 +8134,7 @@
8134 "default.handlebars->25->1477",
8135 "default.handlebars->25->1499",
8136 "default.handlebars->25->1563",
8135 - "default.handlebars->25->1674",
8137 + "default.handlebars->25->1701",
8138 "default.handlebars->container->column_l->p2->p2info->7"
8139 ]
8140 },
@@ -8185,6 +8187,7 @@
8187 "zh-chs": "設備名稱",
8188 "xloc": [
8189 "default-mobile.handlebars->9->232",
8190 + "default.handlebars->25->1665",
8191 "default.handlebars->25->231",
8192 "default.handlebars->25->669",
8193 "player.handlebars->3->9"
@@ -9185,7 +9188,7 @@
9188 "nl": "Dubbele agent",
9189 "zh-chs": "代理重复",
9190 "xloc": [
9188 - "default.handlebars->25->1670"
9191 + "default.handlebars->25->1697"
9192 ]
9193 },
9194 {
@@ -9250,6 +9253,8 @@
9253 "ru": "Длительность",
9254 "zh-chs": "持續時間",
9255 "xloc": [
9256 + "default.handlebars->25->1651",
9257 + "default.handlebars->25->1671",
9258 "player.handlebars->3->2"
9259 ]
9260 },
@@ -9999,7 +10004,7 @@
10004 "nl": "Email/SMS verkeer",
10005 "zh-chs": "电子邮件/短信流量",
10006 "xloc": [
10002 - "default.handlebars->25->1709"
10007 + "default.handlebars->25->1736"
10008 ]
10009 },
10010 {
@@ -10093,6 +10098,12 @@
10098 "default.handlebars->container->column_l->p2->p2info->p2AccountActions->3->accountEnableNotificationsSpan->0"
10099 ]
10100 },
10101 + {
10102 + "en": "Enabled",
10103 + "xloc": [
10104 + "default.handlebars->25->1673"
10105 + ]
10106 + },
10107 {
10108 "cs": "Kódování",
10109 "de": "Kodierung",
@@ -10110,6 +10121,12 @@
10121 "default-mobile.handlebars->dialog->3->dialog7->d7amtkvm->3->3"
10122 ]
10123 },
10124 + {
10125 + "en": "End Time",
10126 + "xloc": [
10127 + "default.handlebars->25->1670"
10128 + ]
10129 + },
10130 {
10131 "cs": "angličtina",
10132 "de": "Englisch",
@@ -10670,7 +10687,7 @@
10687 "ru": "Внешний",
10688 "zh-chs": "外部",
10689 "xloc": [
10673 - "default.handlebars->25->1694"
10690 + "default.handlebars->25->1721"
10691 ]
10692 },
10693 {
@@ -10874,6 +10891,7 @@
10891 "zh-chs": "檔案",
10892 "xloc": [
10893 "default.handlebars->25->1224",
10894 + "default.handlebars->25->1658",
10895 "default.handlebars->container->topbar->1->1->MainSubMenuSpan->MainSubMenu->1->0->MainDevFiles",
10896 "default.handlebars->contextMenu->cxfiles"
10897 ]
@@ -11176,8 +11194,8 @@
11194 "ru": "Свободно",
11195 "zh-chs": "自由",
11196 "xloc": [
11179 - "default.handlebars->25->1655",
11180 - "default.handlebars->25->1657"
11197 + "default.handlebars->25->1682",
11198 + "default.handlebars->25->1684"
11199 ]
11200 },
11201 {
@@ -12094,7 +12112,7 @@
12112 "ru": "Всего кучи",
12113 "zh-chs": "堆總數",
12114 "xloc": [
12097 - "default.handlebars->25->1696"
12115 + "default.handlebars->25->1723"
12116 ]
12117 },
12118 {
@@ -12111,7 +12129,7 @@
12129 "ru": "Куча используется",
12130 "zh-chs": "堆使用",
12131 "xloc": [
12114 - "default.handlebars->25->1695"
12132 + "default.handlebars->25->1722"
12133 ]
12134 },
12135 {
@@ -12897,8 +12915,8 @@
12915 "xloc": [
12916 "default.handlebars->25->1326",
12917 "default.handlebars->25->1334",
12900 - "default.handlebars->25->1692",
12901 - "default.handlebars->25->1714"
12918 + "default.handlebars->25->1719",
12919 + "default.handlebars->25->1741"
12920 ]
12921 },
12922 {
@@ -12935,6 +12953,18 @@
12953 "default.handlebars->25->150"
12954 ]
12955 },
12956 + {
12957 + "en": "Intel AMT Redirection",
12958 + "xloc": [
12959 + "default.handlebars->25->1660"
12960 + ]
12961 + },
12962 + {
12963 + "en": "Intel AMT WSMAN",
12964 + "xloc": [
12965 + "default.handlebars->25->1659"
12966 + ]
12967 + },
12968 {
12969 "cs": "Intel AMT zjištěno",
12970 "de": "Intel AMT erkannt",
@@ -13616,7 +13646,7 @@
13646 "ru": "Некорректный тип группы устройств",
13647 "zh-chs": "無效的設備組類型",
13648 "xloc": [
13619 - "default.handlebars->25->1669"
13649 + "default.handlebars->25->1696"
13650 ]
13651 },
13652 {
@@ -13633,7 +13663,7 @@
13663 "ru": "Некорректный JSON",
13664 "zh-chs": "無效的JSON",
13665 "xloc": [
13636 - "default.handlebars->25->1663"
13666 + "default.handlebars->25->1690"
13667 ]
13668 },
13669 {
@@ -13685,7 +13715,7 @@
13715 "ru": "Некорректная сигнатура PKCS",
13716 "zh-chs": "無效的PKCS簽名",
13717 "xloc": [
13688 - "default.handlebars->25->1661"
13718 + "default.handlebars->25->1688"
13719 ]
13720 },
13721 {
@@ -13702,7 +13732,7 @@
13732 "ru": "Некорректная сигнатура RSA",
13733 "zh-chs": "無效的RSA密碼",
13734 "xloc": [
13705 - "default.handlebars->25->1662"
13735 + "default.handlebars->25->1689"
13736 ]
13737 },
13738 {
@@ -14785,7 +14815,7 @@
14815 "ru": "Меньше",
14816 "zh-chs": "減",
14817 "xloc": [
14788 - "default.handlebars->25->1731"
14818 + "default.handlebars->25->1758"
14819 ]
14820 },
14821 {
@@ -15882,7 +15912,7 @@
15912 "ru": "Сообщения главного сервера",
15913 "zh-chs": "主服務器消息",
15914 "xloc": [
15885 - "default.handlebars->25->1703"
15915 + "default.handlebars->25->1730"
15916 ]
15917 },
15918 {
@@ -16308,7 +16338,7 @@
16338 "ru": "Достигнуто максимальное число сессий",
16339 "zh-chs": "達到的會話數上限",
16340 "xloc": [
16311 - "default.handlebars->25->1667"
16341 + "default.handlebars->25->1694"
16342 ]
16343 },
16344 {
@@ -16362,7 +16392,7 @@
16392 "ru": "Мегабайт",
16393 "zh-chs": "兆字節",
16394 "xloc": [
16365 - "default.handlebars->25->1693"
16395 + "default.handlebars->25->1720"
16396 ]
16397 },
16398 {
@@ -16379,7 +16409,7 @@
16409 "ru": "ОЗУ",
16410 "zh-chs": "記憶",
16411 "xloc": [
16382 - "default.handlebars->25->1684",
16412 + "default.handlebars->25->1711",
16413 "default.handlebars->25->819",
16414 "default.handlebars->container->column_l->p40->3->1->p40type->3"
16415 ]
@@ -16515,7 +16545,7 @@
16545 "ru": "Трафик MeshAgent",
16546 "zh-chs": "MeshAgent流量",
16547 "xloc": [
16518 - "default.handlebars->25->1705"
16548 + "default.handlebars->25->1732"
16549 ]
16550 },
16551 {
@@ -16532,7 +16562,7 @@
16562 "ru": "Обновление MeshAgent",
16563 "zh-chs": "MeshAgent更新",
16564 "xloc": [
16535 - "default.handlebars->25->1706"
16565 + "default.handlebars->25->1733"
16566 ]
16567 },
16568 {
@@ -16635,7 +16665,7 @@
16665 "ru": "Соединения сервера MeshCentral",
16666 "zh-chs": "MeshCentral服務器對等",
16667 "xloc": [
16638 - "default.handlebars->25->1704"
16668 + "default.handlebars->25->1731"
16669 ]
16670 },
16671 {
@@ -16898,7 +16928,7 @@
16928 "ru": "Диспетчер сообщения",
16929 "zh-chs": "郵件調度程序",
16930 "xloc": [
16901 - "default.handlebars->25->1702"
16931 + "default.handlebars->25->1729"
16932 ]
16933 },
16934 {
@@ -17046,7 +17076,7 @@
17076 "ru": "Еще",
17077 "zh-chs": "更多",
17078 "xloc": [
17049 - "default.handlebars->25->1730"
17079 + "default.handlebars->25->1757"
17080 ]
17081 },
17082 {
@@ -17117,6 +17147,12 @@
17147 "default.handlebars->contextMenu->cxmdesktop"
17148 ]
17149 },
17150 + {
17151 + "en": "Multiplexor",
17152 + "xloc": [
17153 + "default.handlebars->25->1672"
17154 + ]
17155 + },
17156 {
17157 "cs": "Můj účet",
17158 "de": "Mein Konto",
@@ -18638,6 +18674,12 @@
18674 "default.handlebars->25->802"
18675 ]
18676 },
18677 + {
18678 + "en": "Not on server",
18679 + "xloc": [
18680 + "default.handlebars->25->1664"
18681 + ]
18682 + },
18683 {
18684 "cs": "Nenastaveno",
18685 "de": "Nicht gesetzt",
@@ -18907,7 +18949,7 @@
18949 "ru": "Произошло в {0}",
18950 "zh-chs": "發生在{0}",
18951 "xloc": [
18910 - "default.handlebars->25->1651"
18952 + "default.handlebars->25->1678"
18953 ]
18954 },
18955 {
@@ -20011,7 +20053,7 @@
20053 "zh-chs": "插件動作",
20054 "xloc": [
20055 "default.handlebars->25->169",
20014 - "default.handlebars->25->1727"
20056 + "default.handlebars->25->1754"
20057 ]
20058 },
20059 {
@@ -20317,6 +20359,12 @@
20359 "default.handlebars->25->7"
20360 ]
20361 },
20362 + {
20363 + "en": "Present on server",
20364 + "xloc": [
20365 + "default.handlebars->25->1663"
20366 + ]
20367 + },
20368 {
20369 "cs": "Pro pokračování/pozastavení stiskněte [mezerník].",
20370 "de": "Für Abspielen/Pause [Leertaste] drücken,",
@@ -20421,6 +20469,7 @@
20469 "ru": "Протокол",
20470 "zh-chs": "協議",
20471 "xloc": [
20472 + "default.handlebars->25->1661",
20473 "player.handlebars->3->15"
20474 ]
20475 },
@@ -20727,7 +20776,7 @@
20776 "ru": "RSS",
20777 "zh-chs": "的RSS",
20778 "xloc": [
20730 - "default.handlebars->25->1697"
20779 + "default.handlebars->25->1724"
20780 ]
20781 },
20782 {
@@ -20818,6 +20867,12 @@
20867 "default.handlebars->container->column_l->p11->deskarea0->deskarea4->1"
20868 ]
20869 },
20870 + {
20871 + "en": "Recording Details",
20872 + "xloc": [
20873 + "default.handlebars->25->1675"
20874 + ]
20875 + },
20876 {
20877 "en": "Recordings",
20878 "nl": "Opnames",
@@ -20885,7 +20940,8 @@
20940 "default.handlebars->25->437",
20941 "default.handlebars->container->column_l->p11->deskarea0->deskarea3x->DeskTools->deskToolsAreaTop->DeskToolsRefreshButton",
20942 "default.handlebars->container->column_l->p13->p13toolbar->1->2->1->3",
20888 - "default.handlebars->container->column_l->p40->3->3"
20943 + "default.handlebars->container->column_l->p40->3->3",
20944 + "default.handlebars->container->column_l->p52->3->1->0->3->3"
20945 ]
20946 },
20947 {
@@ -20938,7 +20994,7 @@
20994 "ru": "Число ретрансляций",
20995 "zh-chs": "中繼計數",
20996 "xloc": [
20941 - "default.handlebars->25->1679"
20997 + "default.handlebars->25->1706"
20998 ]
20999 },
21000 {
@@ -20955,7 +21011,7 @@
21011 "ru": "Ошибки ретранслятора",
21012 "zh-chs": "中繼錯誤",
21013 "xloc": [
20958 - "default.handlebars->25->1672"
21014 + "default.handlebars->25->1699"
21015 ]
21016 },
21017 {
@@ -20972,8 +21028,8 @@
21028 "ru": "Сессии ретранслятора",
21029 "zh-chs": "接力會議",
21030 "xloc": [
20975 - "default.handlebars->25->1678",
20976 - "default.handlebars->25->1691"
21031 + "default.handlebars->25->1705",
21032 + "default.handlebars->25->1718"
21033 ]
21034 },
21035 {
@@ -23002,7 +23058,7 @@
23058 "ru": "Сертификат сервера",
23059 "zh-chs": "服務器證書",
23060 "xloc": [
23005 - "default.handlebars->25->1707"
23061 + "default.handlebars->25->1734"
23062 ]
23063 },
23064 {
@@ -23019,7 +23075,7 @@
23075 "ru": "База данных сервера",
23076 "zh-chs": "服務器數據庫",
23077 "xloc": [
23022 - "default.handlebars->25->1708"
23078 + "default.handlebars->25->1735"
23079 ]
23080 },
23081 {
@@ -23128,7 +23184,7 @@
23184 "ru": "Состояние сервера",
23185 "zh-chs": "服務器狀態",
23186 "xloc": [
23131 - "default.handlebars->25->1658"
23187 + "default.handlebars->25->1685"
23188 ]
23189 },
23190 {
@@ -23162,7 +23218,7 @@
23218 "ru": "Трассировка сервера",
23219 "zh-chs": "服務器跟踪",
23220 "xloc": [
23165 - "default.handlebars->25->1718"
23221 + "default.handlebars->25->1745"
23222 ]
23223 },
23224 {
@@ -23301,7 +23357,7 @@
23357 "ru": "ServerStats.csv",
23358 "zh-chs": "ServerStats.csv",
23359 "xloc": [
23304 - "default.handlebars->25->1699"
23360 + "default.handlebars->25->1726"
23361 ]
23362 },
23363 {
@@ -23338,6 +23394,12 @@
23394 "default.handlebars->container->column_l->p11->deskarea0->deskarea3x->DeskTools->deskToolsAreaTop->deskToolsTopTabService"
23395 ]
23396 },
23397 + {
23398 + "en": "Session",
23399 + "xloc": [
23400 + "default.handlebars->25->1649"
23401 + ]
23402 + },
23403 {
23404 "cs": "Čas spojení",
23405 "de": "Sitzungszeit",
@@ -23795,6 +23857,8 @@
23857 "ru": "Размер",
23858 "zh-chs": "尺寸",
23859 "xloc": [
23860 + "default.handlebars->25->1652",
23861 + "default.handlebars->25->1667",
23862 "default.handlebars->container->column_l->p1->devListToolbarSpan->1->0->9->devListToolbarSize"
23863 ]
23864 },
@@ -24602,6 +24666,13 @@
24666 "default.handlebars->25->705"
24667 ]
24668 },
24669 + {
24670 + "en": "Start Time",
24671 + "xloc": [
24672 + "default.handlebars->25->1650",
24673 + "default.handlebars->25->1669"
24674 + ]
24675 + },
24676 {
24677 "cs": "Stav",
24678 "de": "Zustand",
@@ -24652,6 +24723,7 @@
24723 "zh-chs": "狀態",
24724 "xloc": [
24725 "default.handlebars->25->1601",
24726 + "default.handlebars->25->1662",
24727 "default.handlebars->container->column_l->p42->p42tbl->1->0->7"
24728 ]
24729 },
@@ -25193,6 +25265,7 @@
25265 "zh-chs": "終奌站",
25266 "xloc": [
25267 "default.handlebars->25->1221",
25268 + "default.handlebars->25->1656",
25269 "default.handlebars->25->433",
25270 "default.handlebars->container->topbar->1->1->MainSubMenuSpan->MainSubMenu->1->0->MainDevTerminal",
25271 "default.handlebars->contextMenu->cxterminal"
@@ -25393,7 +25466,7 @@
25466 "ru": "На данный момент уведомлений нет",
25467 "zh-chs": "目前沒有任何通知",
25468 "xloc": [
25396 - "default.handlebars->25->1650"
25469 + "default.handlebars->25->1677"
25470 ]
25471 },
25472 {
@@ -26623,6 +26696,8 @@
26696 "default-mobile.handlebars->9->182",
26697 "default.handlebars->25->13",
26698 "default.handlebars->25->1641",
26699 + "default.handlebars->25->1654",
26700 + "default.handlebars->25->1655",
26701 "default.handlebars->25->379",
26702 "default.handlebars->25->41",
26703 "default.handlebars->25->42",
@@ -26667,7 +26742,7 @@
26742 "ru": "Неизвестное действие",
26743 "zh-chs": "未知動作",
26744 "xloc": [
26670 - "default.handlebars->25->1664"
26745 + "default.handlebars->25->1691"
26746 ]
26747 },
26748 {
@@ -26704,7 +26779,7 @@
26779 "xloc": [
26780 "default.handlebars->25->1510",
26781 "default.handlebars->25->1617",
26707 - "default.handlebars->25->1668"
26782 + "default.handlebars->25->1695"
26783 ]
26784 },
26785 {
@@ -26721,7 +26796,7 @@
26796 "ru": "Неизвестная группа",
26797 "zh-chs": "未知群組",
26798 "xloc": [
26724 - "default.handlebars->25->1660"
26799 + "default.handlebars->25->1687"
26800 ]
26801 },
26802 {
@@ -26845,7 +26920,7 @@
26920 "ru": "Актуально",
26921 "zh-chs": "最新",
26922 "xloc": [
26848 - "default.handlebars->25->1725"
26923 + "default.handlebars->25->1752"
26924 ]
26925 },
26926 {
@@ -27094,8 +27169,8 @@
27169 "ru": "Использовано",
27170 "zh-chs": "用過的",
27171 "xloc": [
27097 - "default.handlebars->25->1654",
27098 - "default.handlebars->25->1656"
27172 + "default.handlebars->25->1681",
27173 + "default.handlebars->25->1683"
27174 ]
27175 },
27176 {
@@ -27116,6 +27191,7 @@
27191 "default.handlebars->25->1179",
27192 "default.handlebars->25->1402",
27193 "default.handlebars->25->1506",
27194 + "default.handlebars->25->1674",
27195 "default.handlebars->25->193",
27196 "default.handlebars->25->562"
27197 ]
@@ -27171,7 +27247,7 @@
27247 "ru": "Учетные записи пользователей",
27248 "zh-chs": "用戶帳號",
27249 "xloc": [
27174 - "default.handlebars->25->1673"
27250 + "default.handlebars->25->1700"
27251 ]
27252 },
27253 {
@@ -27383,7 +27459,7 @@
27459 "ru": "Сессии пользователя",
27460 "zh-chs": "用戶會話",
27461 "xloc": [
27386 - "default.handlebars->25->1690"
27462 + "default.handlebars->25->1717"
27463 ]
27464 },
27465 {
@@ -27572,7 +27648,7 @@
27648 "xloc": [
27649 "default.handlebars->25->1476",
27650 "default.handlebars->25->1498",
27575 - "default.handlebars->25->1689",
27651 + "default.handlebars->25->1716",
27652 "default.handlebars->container->topbar->1->1->UsersSubMenuSpan->UsersSubMenu->1->0->UsersGeneral"
27653 ]
27654 },
@@ -27590,7 +27666,7 @@
27666 "ru": "Сессии пользователей",
27667 "zh-chs": "用戶會話",
27668 "xloc": [
27593 - "default.handlebars->25->1677"
27669 + "default.handlebars->25->1704"
27670 ]
27671 },
27672 {
@@ -27773,7 +27849,7 @@
27849 "ru": "Версия несовместима, пожалуйста, сначала обновите установку MeshCentral",
27850 "zh-chs": "版本不兼容,请先升级您的MeshCentral安装",
27851 "xloc": [
27776 - "default.handlebars->25->1721"
27852 + "default.handlebars->25->1748"
27853 ]
27854 },
27855 {
@@ -27841,8 +27917,8 @@
27917 "ru": "Просмотр журнала изменений",
27918 "zh-chs": "查看变更日志",
27919 "xloc": [
27844 - "default.handlebars->25->1724",
27845 - "default.handlebars->25->1726"
27920 + "default.handlebars->25->1751",
27921 + "default.handlebars->25->1753"
27922 ]
27923 },
27924 {
@@ -28100,8 +28176,8 @@
28176 "ru": "Веб-сервер",
28177 "zh-chs": "網絡服務器",
28178 "xloc": [
28103 - "default.handlebars->25->1710",
28104 - "default.handlebars->25->1711"
28179 + "default.handlebars->25->1737",
28180 + "default.handlebars->25->1738"
28181 ]
28182 },
28183 {
@@ -28118,7 +28194,7 @@
28194 "ru": "Запросы веб-сервера",
28195 "zh-chs": "Web服務器請求",
28196 "xloc": [
28121 - "default.handlebars->25->1712"
28197 + "default.handlebars->25->1739"
28198 ]
28199 },
28200 {
@@ -28135,7 +28211,7 @@
28211 "ru": "Ретранслятор Web Socket",
28212 "zh-chs": "Web套接字中繼",
28213 "xloc": [
28138 - "default.handlebars->25->1713"
28214 + "default.handlebars->25->1740"
28215 ]
28216 },
28217 {
@@ -29200,7 +29276,7 @@
29276 "ru": "\\\\'",
29277 "zh-chs": "\\\\'",
29278 "xloc": [
29203 - "default.handlebars->25->1722"
29279 + "default.handlebars->25->1749"
29280 ]
29281 },
29282 {
@@ -29475,7 +29551,7 @@
29551 "ru": "свободно",
29552 "zh-chs": "自由",
29553 "xloc": [
29478 - "default.handlebars->25->1685"
29554 + "default.handlebars->25->1712"
29555 ]
29556 },
29557 {
@@ -29839,7 +29915,7 @@
29915 "ru": "servertrace.csv",
29916 "zh-chs": "servertrace.csv",
29917 "xloc": [
29842 - "default.handlebars->25->1720"
29918 + "default.handlebars->25->1747"
29919 ]
29920 },
29921 {
@@ -29892,7 +29968,7 @@
29968 "ru": "time, conn.agent, conn.users, conn.usersessions, conn.relaysession, conn.intelamt, mem.external, mem.heapused, mem.heaptotal, mem.rss",
29969 "zh-chs": "時間,conn.agent,conn.users,conn.usersessions,conn.relaysession,conn.intelamt,mem.external,mem.heapused,mem.heaptotal,mem.rss",
29970 "xloc": [
29895 - "default.handlebars->25->1698"
29971 + "default.handlebars->25->1725"
29972 ]
29973 },
29974 {
@@ -29909,7 +29985,7 @@
29985 "ru": "time, source, message",
29986 "zh-chs": "時間,來源,訊息",
29987 "xloc": [
29912 - "default.handlebars->25->1719"
29988 + "default.handlebars->25->1746"
29989 ]
29990 },
29991 {
@@ -29940,7 +30016,7 @@
30016 "ru": "всего",
30017 "zh-chs": "總",
30018 "xloc": [
29943 - "default.handlebars->25->1686"
30019 + "default.handlebars->25->1713"
30020 ]
30021 },
30022 {
@@ -30094,7 +30170,8 @@
30170 "ru": "{0} Kб",
30171 "zh-chs": "{0} Kb",
30172 "xloc": [
30097 - "default.handlebars->25->1349"
30173 + "default.handlebars->25->1349",
30174 + "default.handlebars->25->1653"
30175 ]
30176 },
30177 {
@@ -30181,7 +30258,8 @@
30258 "zh-chs": "{0}個字節",
30259 "xloc": [
30260 "default-mobile.handlebars->9->88",
30184 - "default.handlebars->25->1359"
30261 + "default.handlebars->25->1359",
30262 + "default.handlebars->25->1668"
30263 ]
30264 },
30265 {
@@ -30796,4 +30874,4 @@
30874 ]
30875 }
30876 ]
30799 -}
30877 +}
\ No newline at end of file
views/default.handlebars
+98 -7
@@ -1047,12 +1047,13 @@
1047 <input type=button onclick=openRecodringPlayer() value="Open Player..." />
1048 </div>
1049 <div>
1050 + <input type=button onclick=refreshRecodings() value="Refresh" />
1051 </div>
1052 </td>
1053 <td class="h2"></td>
1054 </tr>
1055 </table>
1055 - <div id=p52recordings style=""></div>
1056 + <div id=p52recordings style="overflow-y:auto"></div>
1057 </div>
1058 <br id="column_l_bottomgap" />
1059 </div>
@@ -1489,6 +1490,8 @@
1490 QS('p31events')['max-height'] = 'calc(100vh - ' + (50 + xh + xh2) + 'px)';
1491 QS('p41events')['height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
1492 QS('p41events')['max-height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
1493 + QS('p52recordings')['height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
1494 + QS('p52recordings')['max-height'] = 'calc(100vh - ' + (48 + xh + xh2) + 'px)';
1495
1496 // We are looking at a single device, remove all the back buttons
1497 if ('{{currentNode}}'.toLowerCase() != '') {
@@ -2094,6 +2097,11 @@
2097 }
2098 break;
2099 }
2100 + case 'recordings': {
2101 + p52recordings = message.events;
2102 + updateRecordings();
2103 + break;
2104 + }
2105 case 'getcookie': {
2106 if (message.tag == 'clickonce') {
2107 if (message.trustedCert == true) {
@@ -2278,6 +2286,10 @@
2286 }
2287 if (message.event.noact) break; // Take no action on this event
2288 switch (message.event.action) {
2289 + case 'recording': {
2290 + if (p52recordings != null) { p52recordings.unshift(message.event); message.event.present = 1; updateRecordings(); }
2291 + break;
2292 + }
2293 case 'userWebState': {
2294 // New user web state, update the web page as needed
2295 try {
@@ -6595,7 +6607,7 @@
6607 function deskSaveImage() {
6608 if (xxdialogMode || desktop == null || desktop.State != 3) return;
6609 var d = new Date(), n = 'Desktop-' + currentNode.name + '-' + d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2) + '-' + ('0' + d.getHours()).slice(-2) + '-' + ('0' + d.getMinutes()).slice(-2);
6598 - Q('Desk')['toBlob'](function (blob) { saveAs(blob, n + '.jpg'); });
6610 + Q('Desk')['toBlob'](function (blob) { saveAs(blob, n + '.png'); });
6611 }
6612
6613 function deskDisplayInfo(sender, displays, selDisplay) {
@@ -11124,15 +11136,92 @@
11136
11137 var p52recordings = null;
11138 function updateRecordings() {
11127 - var x = 'Under construction';
11139 + // Display the users using the sorted list
11140 + var x = '<table class=p3usersTable cellpadding=0 cellspacing=0>', addHeader = true;
11141 + x += '<th>' + "Session" + '<th style=width:110px>' + nobreak("Start Time") + '<th style=width:110px>' + "Duration" + '<th style=width:110px>' + "Size";
11142 + if (p52recordings != null) {
11143 + var recdate = null;
11144 + for (var i in p52recordings) {
11145 + var rec = p52recordings[i], rect = new Date(rec.time), day = printDate(rect);
11146 + if (day != recdate) { recdate = day; x += '<tr><td class=userTableHeader colspan=4>' + day; }
11147 + x += addRecordingHtml(i, rec);
11148 + }
11149 + }
11150 + x += '</table>';
11151 QH('p52recordings', x);
11152 }
11153
11131 - function openRecodringPlayer() {
11132 - if (xxdialogMode) return;
11133 - window.open(window.location.origin + '{{{domainurl}}}player.htm', 'meshcentral-deskplayer');
11154 + function addRecordingHtml(i, rec) {
11155 + var sessionLengthStr = '';
11156 + if (rec.lengthTime) { sessionLengthStr = pad2(Math.floor(rec.lengthTime / 3600)) + ':' + pad2(Math.floor((rec.lengthTime % 3600) / 60)) + ':' + pad2(Math.floor(rec.lengthTime % 60)); }
11157 + var sessionStartStr = printTime(new Date(rec.time));
11158 + if (rec.sessionStart) { sessionStartStr = printTime(new Date(rec.sessionStart)); }
11159 + var sessionSize = '';
11160 + if (rec.size) { sessionSize = format("{0} Kb", Math.round(rec.size / 1024)); }
11161 + var sessionName = '<i>' + "Unknown" + '</i>';
11162 + if (rec.name && rec.meshname) {
11163 + var recmesh = meshes[rec.meshid];
11164 + if (recmesh != null) {
11165 + sessionName = '<a href=# onclick=\'gotoMesh("' + rec.meshid + '")\'>' + EscapeHtml(rec.meshname) + '</a> - <a href=# onclick=\'gotoDevice("' + rec.nodeid + '",10)\'>' + EscapeHtml(rec.name) + '</a>';
11166 + } else {
11167 + sessionName = EscapeHtml(rec.meshname) + ' - ' + EscapeHtml(rec.name);
11168 + }
11169 + }
11170 + if ((rec.userids != null) && (rec.userids.length > 0)) {
11171 + if (rec.userids.length > 1) {
11172 + sessionName += ' - ' + format('{0} users', rec.userids.length);
11173 + } else {
11174 + var ruser = null;
11175 + if (users != null) { ruser = users[rec.userids[0]]; }
11176 + if (ruser != null) {
11177 + sessionName += ' - <a href=# onclick=\'gotoUser("' + rec.userids[0] + '")\'>' + EscapeHtml(ruser.name) + '</a>';
11178 + } else {
11179 + sessionName += ' - ' + EscapeHtml(rec.userids[0].split('/')[2]);
11180 + }
11181 + }
11182 + }
11183 + var actions = '', icon = 'm0';
11184 + if (rec.present == 1) { icon = 'm1'; actions = '<div style=cursor:pointer;float:right><a href=recordings.ashx?file=' + encodeURIComponent(rec.filename) + ' download><img src=images/link4.png height=10 width=10 title="Download Recording" download></a>&nbsp;</div>'; }
11185 + 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>';
11186 + x += '<div class=bar style=width:100%>';
11187 + x += '<div class=baricon><input class=RecordingCheckbox value=' + i + ' onclick=p52updateInfo() type=checkbox></div>';
11188 + x += '<div class=baricon onclick=showRecordingDialog(event,"' + i + '")><div class=' + icon + '></div></div>';
11189 + x += '<div class=g1 onclick=showRecordingDialog(event,"' + i + '")></div><div class=g2 onclick=showRecordingDialog("' + i + '")></div>';
11190 + x += '<div onclick=showRecordingDialog(event,"' + i + '")>' + actions + '<div style=font-size:16px>' + sessionName + '</div></div></div><td style=text-align:center>' + sessionStartStr + '<td style=text-align:center>' + sessionLengthStr + '<td style=text-align:center>' + sessionSize;
11191 + return x;
11192 }
11193
11194 + function showRecordingDialog(event, i) {
11195 + if (xxdialogMode) return;
11196 + if ((event.target.tagName == 'IMG') || (event.target.tagName == 'A')) return;
11197 + var rec = p52recordings[i];
11198 + //console.log(rec);
11199 + var x = '';
11200 + if (rec.protocol) {
11201 + var protocolStr = "Unknown";
11202 + if (rec.protocol == 1) { protocolStr = "Terminal"; }
11203 + if (rec.protocol == 2) { protocolStr = "Desktop"; }
11204 + if (rec.protocol == 5) { protocolStr = "Files"; }
11205 + if (rec.protocol == 100) { protocolStr = "Intel AMT WSMAN"; }
11206 + if (rec.protocol == 101) { protocolStr = "Intel AMT Redirection"; }
11207 + x += addHtmlValue4("Protocol", protocolStr);
11208 + }
11209 + x += addHtmlValue4("Status", (rec.present == 1)?"Present on server":"Not on server");
11210 + if (rec.name) { x += addHtmlValue4("Device Name", EscapeHtml(rec.name)); }
11211 + if (rec.meshname) { x += addHtmlValue4("Device Group", EscapeHtml(rec.meshname)); }
11212 + if (rec.size) { x += addHtmlValue4("Size", format("{0} bytes", rec.size)); }
11213 + if (rec.startTime) { x += addHtmlValue4("Start Time", printTime(new Date(rec.startTime))); }
11214 + if (rec.time) { x += addHtmlValue4("End Time", printTime(new Date(rec.time))); }
11215 + if (rec.lengthTime) { x += addHtmlValue4("Duration", pad2(Math.floor(rec.lengthTime / 3600)) + ':' + pad2(Math.floor((rec.lengthTime % 3600) / 60)) + ':' + pad2(Math.floor(rec.lengthTime % 60))); }
11216 + if (rec.multiplex == true) { x += addHtmlValue4("Multiplexor", "Enabled"); }
11217 + if (rec.userids) { for (var i in rec.userids) { x += addHtmlValue4("User", rec.userids[i].split('/')[2]); } }
11218 + setDialogMode(2, "Recording Details", 9, null, x);
11219 + }
11220 +
11221 + function refreshRecodings() { meshserver.send({ action: 'recordings', limit: 1000 }); }
11222 + function openRecodringPlayer() { if (!xxdialogMode) window.open(window.location.origin + '{{{domainurl}}}player.htm', 'meshcentral-deskplayer'); }
11223 + function p52updateInfo() { }
11224 +
11225 //
11226 // FILE SELECTOR, DIALOG 3
11227 //
@@ -11856,7 +11945,7 @@
11945 if (x == 21) { p21updateMesh(); }
11946
11947 // Update Recordings
11859 - if (x == 52) { updateRecordings(); }
11948 + if (x == 52) { if (p52recordings == null) { refreshRecodings(); } updateRecordings(); }
11949
11950 // Update the web page title
11951 if ((currentNode) && (x >= 10) && (x < 20)) {
@@ -12223,6 +12312,7 @@
12312 function addHtmlValue(t, v) { return '<table><td style=width:120px>' + t + '<td><b>' + v + '</b></table>'; }
12313 function addHtmlValue2(t, v) { return '<div><div style=display:inline-block;float:right>' + v + '</div><div style=display:inline-block>' + t + '</div></div>'; }
12314 function addHtmlValue3(t, v) { return '<div><b>' + t + '</b></div><div style=margin-left:16px>' + v + '</div>'; }
12315 + function addHtmlValue4(t, v) { return '<table style=width:100%><td style=width:120px>' + t + '<td style=text-align:right><b>' + v + '</b></table>'; }
12316 function parseUriArgs() { var href = window.document.location.href; if (href.endsWith('#')) { href = href.substring(0, href.length - 1); } var name, r = {}, parsedUri = href.split(/[\?&|\=]/); parsedUri.splice(0, 1); for (x in parsedUri) { switch (x % 2) { case 0: { name = decodeURIComponent(parsedUri[x]); break; } case 1: { r[name] = decodeURIComponent(parsedUri[x]); var x = parseInt(r[name]); if (x == r[name]) { r[name] = x; } break; } default: { break; } } } return r; }
12317 function focusTextBox(x) { setTimeout(function(){ Q(x).selectionStart = Q(x).selectionEnd = 65535; Q(x).focus(); }, 0); }
12318 function validateEmail(v) { var emailReg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; return emailReg.test(v); } // New version
@@ -12241,6 +12331,7 @@
12331 function getOrderedList(objList, oname) { var r = []; for (var i in objList) { r.push(objList[i]); } r.sort(function(a, b) { var aa = a[oname].toLowerCase(), bb = b[oname].toLowerCase(); if (aa > bb) return 1; if (aa < bb) return -1; return 0; }); return r; }
12332 function capitalizeFirstLetter(string) { return string.charAt(0).toUpperCase() + string.slice(1); }
12333 function nobreak(x) { return x.split(' ').join('&nbsp;'); }
12334 + function pad2(num) { var s = '00' + num; return s.substr(s.length - 2); }
12335
12336 </script>
12337 </body>
views/player.handlebars
+1 -1
@@ -94,6 +94,7 @@
94 var recFileMetadata = null;
95 var recFileProtocol = 0;
96 var recFileIndexBasePtr = null;
97 + var recFileExtras = null;
98 var agentDesktop = null;
99 var amtDesktop = null;
100 var playing = false;
@@ -427,7 +428,6 @@
428 cleanup();
429 recFile = files[0];
430 recFilePtr = 0;
430 - readNextBlock(processFirstBlock);
431 readLastBlock(function (type, flags, time, extras) {
432 if (type == 3) {
433 // File is ok
webserver.js
+27
@@ -2437,6 +2437,32 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
2437 }
2438 }
2439
2440 + // Download a desktop recording
2441 + function handleGetRecordings(req, res) {
2442 + const domain = checkUserIpAddress(req, res);
2443 + if (domain == null) { return; }
2444 +
2445 + // Check the query
2446 + if (req.query.file == null) { res.sendStatus(401); return; }
2447 +
2448 + // Get the recording path
2449 + var recordingsPath = null;
2450 + if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; }
2451 + if (recordingsPath == null) { res.sendStatus(401); return; }
2452 +
2453 + // Get the user and check user rights
2454 + var authUserid = null;
2455 + if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
2456 + if (authUserid == null) { res.sendStatus(401); return; }
2457 + const user = obj.users[authUserid];
2458 + if (user == null) { res.sendStatus(401); return; }
2459 + if ((user.siteadmin & 512) == 0) { res.sendStatus(401); return; } // Check if we have right to get recordings
2460 +
2461 + // Send the recorded file
2462 + res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0', 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename=\"' + req.query.file + '\"' });
2463 + try { res.sendFile(obj.path.join(recordingsPath, req.query.file)); } catch (ex) { res.sendStatus(404); }
2464 + }
2465 +
2466 // Server the player page
2467 function handlePlayerRequest(req, res) {
2468 const domain = checkUserIpAddress(req, res);
@@ -3917,6 +3943,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
3943 obj.app.get(url + 'logo.png', handleLogoRequest);
3944 obj.app.post(url + 'translations', handleTranslationsRequest);
3945 obj.app.get(url + 'welcome.jpg', handleWelcomeImageRequest);
3946 + obj.app.get(url + 'recordings.ashx', handleGetRecordings);
3947 obj.app.get(url + 'player.htm', handlePlayerRequest);
3948 obj.app.get(url + 'player', handlePlayerRequest);
3949 obj.app.ws(url + 'amtactivate', handleAmtActivateWebSocket);