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> </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(' '); }
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);