Reporting improvements.

Ylian Saint-Hilaire committed Sep 10, 2021 at 17:28 UTC ea027d73a16744bcac8174f169a5bb9e481434d8
3 files changed +143 -38
apprelays.js
+99 -32
@@ -22,6 +22,18 @@ Protocol numbers
22 13 - SSH-FILES
23 */
24
25 +// Protocol Numbers
26 +const PROTOCOL_TERMINAL = 1;
27 +const PROTOCOL_DESKTOP = 2;
28 +const PROTOCOL_FILES = 5;
29 +const PROTOCOL_AMTWSMAN = 100;
30 +const PROTOCOL_AMTREDIR = 101;
31 +const PROTOCOL_MESSENGER = 200;
32 +const PROTOCOL_WEBRDP = 201;
33 +const PROTOCOL_WEBSSH = 202;
34 +const PROTOCOL_WEBSFTP = 203;
35 +const PROTOCOL_WEBVNC = 204;
36 +
37
38 // Construct a MSTSC Relay object, called upon connection
39 // This is a bit of a hack as we are going to run the RDP connection thru a loopback connection.
@@ -42,11 +54,19 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
54 obj.close = function (arg) {
55 if (obj.ws == null) return;
56
45 - // Collect how many raw bytes where received and sent.
46 - // We sum both the websocket and TCP client in this case.
47 - //var inTraffc = obj.ws._socket.bytesRead, outTraffc = obj.ws._socket.bytesWritten;
48 - //if (obj.wsClient != null) { inTraffc += obj.wsClient._socket.bytesRead; outTraffc += obj.wsClient._socket.bytesWritten; }
49 - //console.log('WinRDP - in', inTraffc, 'out', outTraffc);
57 + // Event the session ending
58 + if ((obj.startTime) && (obj.meshid != null)) {
59 + // Collect how many raw bytes where received and sent.
60 + // We sum both the websocket and TCP client in this case.
61 + var inTraffc = obj.ws._socket.bytesRead, outTraffc = obj.ws._socket.bytesWritten;
62 + if (obj.wsClient != null) { inTraffc += obj.wsClient._socket.bytesRead; outTraffc += obj.wsClient._socket.bytesWritten; }
63 + const sessionSeconds = Math.round((Date.now() - obj.startTime) / 1000);
64 + var user = parent.users[obj.cookie.userid];
65 + var username = (user != null) ? user.name : null;
66 + var event = { etype: 'relay', action: 'relaylog', domain: domain.id, nodeid: obj.nodeid, userid: obj.cookie.userid, username: username, msgid: 125, msgArgs: [sessionSeconds], msg: "Left Web-RDP session after " + sessionSeconds + " second(s).", protocol: PROTOCOL_WEBRDP, bytesin: inTraffc, bytesout: outTraffc };
67 + parent.parent.DispatchEvent(['*', obj.nodeid, obj.cookie.userid, obj.meshid], obj, event);
68 + delete obj.startTime;
69 + }
70
71 if (obj.wsClient) { obj.wsClient.close(); delete obj.wsClient; }
72 if (obj.tcpServer) { obj.tcpServer.close(); delete obj.tcpServer; }
@@ -55,7 +75,11 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
75 if (arg == 2) { try { ws._socket._parent.end(); } catch (e) { console.log(e); } } // Hard close, close the TCP socket
76 obj.ws.removeAllListeners();
77 obj.relayActive = false;
78 +
79 delete obj.ws;
80 + delete obj.nodeid;
81 + delete obj.meshid;
82 + delete obj.userid;
83 };
84
85 // Start the looppback server
@@ -116,6 +140,7 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
140 }).on('connect', function () {
141 send(['rdp-connect']);
142 if ((typeof obj.infos.options == 'object') && (obj.infos.options.savepass == true)) { saveRdpCredentials(); } // Save the credentials if needed
143 + obj.startTime = Date.now();
144 }).on('bitmap', function (bitmap) {
145 try { ws.send(bitmap.data); } catch (ex) { } // Send the bitmap data as binary
146 delete bitmap.data;
@@ -148,7 +173,7 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
173 // Event node change if needed
174 if (changed) {
175 // Event the node change
151 - var event = { etype: 'node', action: 'changenode', nodeid: obj.nodeid, domain: domain.id, userid: obj.userid, node: parent.CloneSafeNode(node), msg: "Changed RDP credentials" };
176 + var event = { etype: 'node', action: 'changenode', nodeid: obj.nodeid, domain: domain.id, userid: obj.cookie.userid, node: parent.CloneSafeNode(node), msg: "Changed RDP credentials" };
177 if (parent.parent.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
178 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(node.meshid, [obj.nodeid]), obj, event);
179 }
@@ -166,16 +191,18 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
191
192 // Decode the authentication cookie
193 obj.cookie = parent.parent.decodeCookie(obj.infos.ip, parent.parent.loginCookieEncryptionKey);
169 - if ((obj.cookie == null) || (typeof obj.cookie.nodeid != 'string') || (typeof obj.cookie.userid != 'string')) return;
194 + if ((obj.cookie == null) || (typeof obj.cookie.nodeid != 'string') || (typeof obj.cookie.userid != 'string')) { obj.close(); return; }
195 obj.nodeid = obj.cookie.nodeid;
196 obj.userid = obj.cookie.userid;
197
173 - // Check is you need to load server stored credentials
174 - if ((typeof obj.infos.options == 'object') && (obj.infos.options.useServerCreds == true)) {
175 - parent.parent.db.Get(obj.nodeid, function (err, nodes) {
176 - if ((err != null) || (nodes == null) || (nodes.length != 1)) return;
177 - const node = nodes[0];
198 + // Get node
199 + parent.parent.db.Get(obj.nodeid, function (err, nodes) {
200 + if ((err != null) || (nodes == null) || (nodes.length != 1)) { obj.close(); return; }
201 + const node = nodes[0];
202 + obj.meshid = node.meshid;
203
204 + // Check if we need to load server stored credentials
205 + if ((typeof obj.infos.options == 'object') && (obj.infos.options.useServerCreds == true)) {
206 // Check if RDP credentials exist
207 if ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string')) {
208 obj.infos.domain = node.rdp.d;
@@ -189,10 +216,10 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
216 obj.infos.password = '';
217 startTcpServer();
218 }
192 - });
193 - } else {
194 - startTcpServer();
195 - }
219 + } else {
220 + startTcpServer();
221 + }
222 + });
223 break;
224 }
225 case 'mouse': { if (rdpClient) { rdpClient.sendPointerEvent(msg[1], msg[2], msg[3], msg[4]); } break; }
@@ -253,11 +280,19 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) {
280 obj.close = function (arg) {
281 if (obj.ws == null) return;
282
256 - // Collect how many raw bytes where received and sent.
257 - // We sum both the websocket and TCP client in this case.
258 - //var inTraffc = obj.ws._socket.bytesRead, outTraffc = obj.ws._socket.bytesWritten;
259 - //if (obj.wsClient != null) { inTraffc += obj.wsClient._socket.bytesRead; outTraffc += obj.wsClient._socket.bytesWritten; }
260 - //console.log('WinSSH - in', inTraffc, 'out', outTraffc);
283 + // Event the session ending
284 + if ((obj.startTime) && (obj.meshid != null)) {
285 + // Collect how many raw bytes where received and sent.
286 + // We sum both the websocket and TCP client in this case.
287 + var inTraffc = obj.ws._socket.bytesRead, outTraffc = obj.ws._socket.bytesWritten;
288 + if (obj.wsClient != null) { inTraffc += obj.wsClient._socket.bytesRead; outTraffc += obj.wsClient._socket.bytesWritten; }
289 + const sessionSeconds = Math.round((Date.now() - obj.startTime) / 1000);
290 + var user = parent.users[obj.cookie.userid];
291 + var username = (user != null) ? user.name : null;
292 + var event = { etype: 'relay', action: 'relaylog', domain: domain.id, nodeid: obj.nodeid, userid: obj.cookie.userid, username: username, msgid: 123, msgArgs: [sessionSeconds], msg: "Left Web-SSH session after " + sessionSeconds + " second(s).", protocol: PROTOCOL_WEBSSH, bytesin: inTraffc, bytesout: outTraffc };
293 + parent.parent.DispatchEvent(['*', obj.nodeid, obj.cookie.userid, obj.meshid], obj, event);
294 + delete obj.startTime;
295 + }
296
297 if (obj.sshShell) {
298 obj.sshShell.destroy();
@@ -287,6 +322,8 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) {
322 obj.relayActive = false;
323 delete obj.termSize;
324 delete obj.cookie;
325 + delete obj.nodeid;
326 + delete obj.meshid;
327 delete obj.ws;
328 };
329
@@ -347,6 +384,7 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) {
384 obj.sshClient.on('ready', function () { // Authentication was successful.
385 // If requested, save the credentials
386 if (obj.keep === true) saveSshCredentials();
387 + obj.startTime = Date.now();
388
389 obj.sshClient.shell(function (err, stream) { // Start a remote shell
390 if (err) { obj.close(); return; }
@@ -466,7 +504,15 @@ module.exports.CreateSshRelay = function (parent, db, ws, req, args, domain) {
504
505 // If the web socket is closed
506 ws.on('close', function (req) { parent.parent.debug('relay', 'SSH: Browser websocket closed'); obj.close(); });
469 -
507 +
508 + // Get the meshid for this device
509 + parent.parent.db.Get(obj.cookie.nodeid, function (err, nodes) {
510 + if ((err != null) || (nodes == null) || (nodes.length != 1)) { parent.parent.debug('relay', 'SSH: Invalid device'); obj.close(); }
511 + const node = nodes[0];
512 + obj.nodeid = node._id; // Store the NodeID
513 + obj.meshid = node.meshid; // Store the MeshID
514 + });
515 +
516 return obj;
517 };
518
@@ -497,11 +543,17 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
543 obj.close = function (arg) {
544 if (obj.ws == null) return;
545
500 - // Collect how many raw bytes where received and sent.
501 - // We sum both the websocket and TCP client in this case.
502 - //var inTraffc = obj.ws._socket.bytesRead, outTraffc = obj.ws._socket.bytesWritten;
503 - //if (obj.wsClient != null) { inTraffc += obj.wsClient._socket.bytesRead; outTraffc += obj.wsClient._socket.bytesWritten; }
504 - //console.log('WinSSH - in', inTraffc, 'out', outTraffc);
546 + // Event the session ending
547 + if (obj.startTime) {
548 + // Collect how many raw bytes where received and sent.
549 + // We sum both the websocket and TCP client in this case.
550 + var inTraffc = obj.ws._socket.bytesRead, outTraffc = obj.ws._socket.bytesWritten;
551 + if (obj.wsClient != null) { inTraffc += obj.wsClient._socket.bytesRead; outTraffc += obj.wsClient._socket.bytesWritten; }
552 + const sessionSeconds = Math.round((Date.now() - obj.startTime) / 1000);
553 + var event = { etype: 'relay', action: 'relaylog', domain: domain.id, nodeid: obj.nodeid, userid: user._id, username: user.name, msgid: 123, msgArgs: [sessionSeconds], msg: "Left Web-SSH session after " + sessionSeconds + " second(s).", protocol: PROTOCOL_WEBSSH, bytesin: inTraffc, bytesout: outTraffc };
554 + parent.parent.DispatchEvent(['*', obj.nodeid, user._id, obj.meshid], obj, event);
555 + delete obj.startTime;
556 + }
557
558 if (obj.sshShell) {
559 obj.sshShell.destroy();
@@ -531,6 +583,8 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
583 obj.relayActive = false;
584 delete obj.termSize;
585 delete obj.cookie;
586 + delete obj.nodeid;
587 + delete obj.meshid;
588 delete obj.ws;
589 };
590
@@ -587,6 +641,7 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
641 obj.sshClient.on('ready', function () { // Authentication was successful.
642 // If requested, save the credentials
643 if (obj.keep === true) saveSshCredentials();
644 + obj.startTime = Date.now();
645
646 obj.sshClient.shell(function (err, stream) { // Start a remote shell
647 if (err) { obj.close(); return; }
@@ -712,6 +767,7 @@ module.exports.CreateSshTerminalRelay = function (parent, db, ws, req, domain, u
767 if ((rights != 0xFFFFFFFF) && (rights & 0x00000200)) { obj.close(); return; } // MESHRIGHT_NOTERMINAL is set
768 obj.mtype = node.mtype; // Store the device group type
769 obj.nodeid = node._id; // Store the NodeID
770 + obj.meshid = node.meshid; // Store the MeshID
771
772 // Check the SSH port
773 obj.tcpport = 22;
@@ -776,11 +832,17 @@ module.exports.CreateSshFilesRelay = function (parent, db, ws, req, domain, user
832 obj.close = function (arg) {
833 if (obj.ws == null) return;
834
779 - // Collect how many raw bytes where received and sent.
780 - // We sum both the websocket and TCP client in this case.
781 - //var inTraffc = obj.ws._socket.bytesRead, outTraffc = obj.ws._socket.bytesWritten;
782 - //if (obj.wsClient != null) { inTraffc += obj.wsClient._socket.bytesRead; outTraffc += obj.wsClient._socket.bytesWritten; }
783 - //console.log('WinSSH - in', inTraffc, 'out', outTraffc);
835 + // Event the session ending
836 + if (obj.startTime) {
837 + // Collect how many raw bytes where received and sent.
838 + // We sum both the websocket and TCP client in this case.
839 + var inTraffc = obj.ws._socket.bytesRead, outTraffc = obj.ws._socket.bytesWritten;
840 + if (obj.wsClient != null) { inTraffc += obj.wsClient._socket.bytesRead; outTraffc += obj.wsClient._socket.bytesWritten; }
841 + const sessionSeconds = Math.round((Date.now() - obj.startTime) / 1000);
842 + var event = { etype: 'relay', action: 'relaylog', domain: domain.id, nodeid: obj.nodeid, userid: user._id, username: user.name, msgid: 124, msgArgs: [sessionSeconds], msg: "Left Web-SFTP session after " + sessionSeconds + " second(s).", protocol: PROTOCOL_WEBSFTP, bytesin: inTraffc, bytesout: outTraffc };
843 + parent.parent.DispatchEvent(['*', obj.nodeid, user._id, obj.meshid], obj, event);
844 + delete obj.startTime;
845 + }
846
847 if (obj.sshClient) {
848 obj.sshClient.destroy();
@@ -803,6 +865,8 @@ module.exports.CreateSshFilesRelay = function (parent, db, ws, req, domain, user
865 obj.relayActive = false;
866 delete obj.cookie;
867 delete obj.sftp;
868 + delete obj.nodeid;
869 + delete obj.meshid;
870 delete obj.ws;
871 };
872
@@ -859,6 +923,8 @@ module.exports.CreateSshFilesRelay = function (parent, db, ws, req, domain, user
923 obj.sshClient.on('ready', function () { // Authentication was successful.
924 // If requested, save the credentials
925 if (obj.keep === true) saveSshCredentials();
926 + obj.startTime = Date.now();
927 +
928 obj.sshClient.sftp(function(err, sftp) {
929 if (err) { obj.close(); return; }
930 obj.connected = true;
@@ -1165,6 +1231,7 @@ module.exports.CreateSshFilesRelay = function (parent, db, ws, req, domain, user
1231 if ((rights != 0xFFFFFFFF) && (rights & 0x00000200)) { obj.close(); return; } // MESHRIGHT_NOTERMINAL is set
1232 obj.mtype = node.mtype; // Store the device group type
1233 obj.nodeid = node._id; // Store the NodeID
1234 + obj.meshid = node.meshid; // Store the MeshID
1235
1236 // Check the SSH port
1237 obj.tcpport = 22;
meshuser.js
+24 -3
@@ -69,6 +69,18 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
69 const SITERIGHT_ALLEVENTS = 0x00000800; // 2048
70 const SITERIGHT_ADMIN = 0xFFFFFFFF;
71
72 + // Protocol Numbers
73 + const PROTOCOL_TERMINAL = 1;
74 + const PROTOCOL_DESKTOP = 2;
75 + const PROTOCOL_FILES = 5;
76 + const PROTOCOL_AMTWSMAN = 100;
77 + const PROTOCOL_AMTREDIR = 101;
78 + const PROTOCOL_MESSENGER = 200;
79 + const PROTOCOL_WEBRDP = 201;
80 + const PROTOCOL_WEBSSH = 202;
81 + const PROTOCOL_WEBSFTP = 203;
82 + const PROTOCOL_WEBVNC = 204;
83 +
84 // Events
85 /*
86 var eventsMessageId = {
@@ -5428,7 +5440,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
5440
5441 // Get the events in the time range
5442 // MySQL or MariaDB query will ignore the MsgID filter.
5431 - db.GetEventsTimeRange(ids, domain.id, [5, 10, 11, 12, 122], new Date(command.start * 1000), new Date(command.end * 1000), function (err, docs) {
5443 + db.GetEventsTimeRange(ids, domain.id, [5, 10, 11, 12, 122, 123, 124, 125, 126], new Date(command.start * 1000), new Date(command.end * 1000), function (err, docs) {
5444 if (err != null) return;
5445 var data = { groups: {} };
5446
@@ -5443,10 +5455,16 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
5455 data.columns = [{ id: 'time', title: "time", format: 'time' }, { id: 'nodeid', title: "device", format: 'node' }, { id: 'userid', title: "user", format: 'user' }, { id: 'protocol', title: "session", format: 'protocol', align: 'center' }, { id: 'length', title: "length", format: 'seconds', align: 'center', sumBy: 'protocol' } ];
5456 }
5457
5458 + // Add traffic colums
5459 + if (command.showTraffic) {
5460 + data.columns.push({ id: 'bytesin', title: "Bytes In", format: 'bytes', align: 'center', sumBy: 'protocol' });
5461 + data.columns.push({ id: 'bytesout', title: "Bytes Out", format: 'bytes', align: 'center', sumBy: 'protocol' });
5462 + }
5463 +
5464 // Rows
5465 for (var i in docs) {
5466 // If MySQL or MariaDB query, we can't filter on MsgID, so we have to do it here.
5449 - if ((docs[i].msgid != 5) && (docs[i].msgid != 10) && (docs[i].msgid != 11) && (docs[i].msgid != 12) && (docs[i].msgid != 122)) continue;
5467 + if ((docs[i].msgid != 5) && (docs[i].msgid != 10) && (docs[i].msgid != 11) && (docs[i].msgid != 12) && ((docs[i].msgid < 122) && (docs[i].msgid > 126))) continue;
5468 if ((command.devGroup != null) && (docs[i].ids != null) && (docs[i].ids.indexOf(command.devGroup) == -1)) continue;
5469
5470 var entry = { time: docs[i].time.valueOf() };
@@ -5456,9 +5474,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
5474 if (command.groupBy != 2) { entry.nodeid = docs[i].nodeid; }
5475 entry.protocol = docs[i].protocol;
5476
5477 + // Add traffic data
5478 + if (command.showTraffic) { entry.bytesin = docs[i].bytesin; entry.bytesout = docs[i].bytesout; }
5479 +
5480 // Session length
5481 if (((docs[i].msgid >= 10) && (docs[i].msgid <= 12)) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[3] == 'number')) { entry.length = docs[i].msgArgs[3]; }
5461 - else if ((docs[i].msgid == 122) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[0] == 'number')) { entry.length = docs[i].msgArgs[0]; }
5482 + else if ((docs[i].msgid >= 122) && (docs[i].msgid <= 126) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[0] == 'number')) { entry.length = docs[i].msgArgs[0]; }
5483
5484 if (command.groupBy == 1) { // Add entry to per user group
5485 if (data.groups[docs[i].userid] == null) { data.groups[docs[i].userid] = { entries: [] }; }
views/default.handlebars
+20 -3
@@ -12892,6 +12892,10 @@
12892 120: "Started local relay session \"{0}\", protocol {1} to {2}",
12893 121: "Ended local relay session \"{0}\", protocol {1} to {2}, {3} second(s)",
12894 122: "Left the desktop multiplex session after {0} second(s).",
12895 + 123: "Left Web-SSH session after {0} second(s).",
12896 + 124: "Left Web-SFTP session after {0} second(s).",
12897 + 125: "Left Web-RDP session after {0} second(s).",
12898 + 126: "Left Web-VNC session after {0} second(s)."
12899 };
12900
12901 // Highlights the device being hovered
@@ -14942,6 +14946,10 @@
14946 x += addHtmlValue("Time Range", '<input id=d2timeRangeSelector style=float:right;width:250px class=flatpickr type="text" placeholder="' + "Select Date & Time..." + '" data-id="altinput">');
14947 x += '</div>';
14948
14949 + x += '<div id=d2showTrafficDiv style=display:none>';
14950 + x += addHtmlValue("", '<div style=width:250px><label><input type=checkbox id=d2showTraffic ' + ((settings.showTraffic) ? 'checked' : '') + '>' + "Show Traffic" + '</label><div>');
14951 + x += '</div>';
14952 +
14953 setDialogMode(2, "Generate Report", 3, generateReportDialogEx, x);
14954 generateReportDialogValidate();
14955
@@ -14953,6 +14961,7 @@
14961
14962 function generateReportDialogValidate() {
14963 QV('d2timeRangeDiv', Q('d2timeRange').value == 0);
14964 + QV('d2showTrafficDiv', Q('d2reportType').value == 1);
14965 }
14966
14967 function generateReportDialogEx(b, tag) {
@@ -14969,8 +14978,8 @@
14978 try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (ex) {}
14979 var devGroup = decodeURIComponent(Q('d2groupId').value);
14980 if (devGroup == 0) { devGroup = null; }
14972 - putstore('_ReportSettings', JSON.stringify({ type: parseInt(Q('d2reportType').value), groupBy: parseInt(Q('d2groupBy').value), timeRange: parseInt(Q('d2timeRange').value), devGroup: devGroup }));
14973 - meshserver.send({ action: 'report', type: parseInt(Q('d2reportType').value), groupBy: parseInt(Q('d2groupBy').value), start: start, end: end, tz: tz, tf: new Date().getTimezoneOffset(), l: getLang(), devGroup: devGroup });
14981 + putstore('_ReportSettings', JSON.stringify({ type: parseInt(Q('d2reportType').value), groupBy: parseInt(Q('d2groupBy').value), timeRange: parseInt(Q('d2timeRange').value), devGroup: devGroup, showTraffic: Q('d2showTraffic').checked }));
14982 + meshserver.send({ action: 'report', type: parseInt(Q('d2reportType').value), groupBy: parseInt(Q('d2groupBy').value), start: start, end: end, tz: tz, tf: new Date().getTimezoneOffset(), l: getLang(), devGroup: devGroup, showTraffic: Q('d2showTraffic').checked });
14983 }
14984
14985 function renderReport(r) {
@@ -15071,13 +15080,21 @@
15080 }
15081
15082 function renderReportFormat(v, f) {
15083 + if (v == null) return '';
15084 if (f == 'datetime') { return printDateTime(new Date(v)).split(' ').join('&nbsp'); }
15085 if (f == 'time') { return printTime(new Date(v)).split(' ').join('&nbsp'); }
15086 if (f == 'protocol') {
15087 if (v == 1) return "Terminal";
15088 if (v == 2) return "Desktop";
15089 if (v == 5) return "Files";
15080 - return "Unknown";
15090 + if (v == 100) return "AMT-WSMAN";
15091 + if (v == 101) return "AMT-Redir";
15092 + if (v == 200) return "Messenger";
15093 + if (v == 201) return "Web-RDP";
15094 + if (v == 202) return "Web-SSH";
15095 + if (v == 203) return "Web-SFTP";
15096 + if (v == 204) return "Web-VNC";
15097 + return "Unknown" + ' (' + v + ')';
15098 }
15099 if (f == 'seconds') {
15100 var seconds = v % 60;