Added recording space/count quota.

Ylian Saint-Hilaire committed Apr 30, 2020 at 02:02 UTC ea9edefad161485fcb8a897a616b6753318b0a4e
7 files changed +102 -12
meshcentral.js
+5 -1
@@ -633,7 +633,7 @@ function CreateMeshCentralServer(config, args) {
633 }
634
635 // Check top level configuration for any unreconized values
636 - if (config) { for (var i in config) { if ((typeof i == 'string') && (i.length > 0) && (i[0] != '_') && (['settings', 'domains', 'configfiles', 'smtp', 'letsencrypt', 'peers', 'sms'].indexOf(i) == -1)) { addServerWarning('Unrecognized configuration option \"' + i + '\".'); } } }
636 + if (config) { for (var i in config) { if ((typeof i == 'string') && (i.length > 0) && (i[0] != '_') && (['settings', 'domaindefaults', 'domains', 'configfiles', 'smtp', 'letsencrypt', 'peers', 'sms'].indexOf(i) == -1)) { addServerWarning('Unrecognized configuration option \"' + i + '\".'); } } }
637
638 if (typeof obj.args.userallowedip == 'string') { if (obj.args.userallowedip == '') { config.settings.userallowedip = obj.args.userallowedip = null; } else { config.settings.userallowedip = obj.args.userallowedip = obj.args.userallowedip.split(','); } }
639 if (typeof obj.args.userblockedip == 'string') { if (obj.args.userblockedip == '') { config.settings.userblockedip = obj.args.userblockedip = null; } else { config.settings.userblockedip = obj.args.userblockedip = obj.args.userblockedip.split(','); } }
@@ -1009,6 +1009,10 @@ function CreateMeshCentralServer(config, args) {
1009 var bannedDomains = ['public', 'private', 'images', 'scripts', 'styles', 'views']; // List of banned domains
1010 for (i in obj.config.domains) { for (var j in bannedDomains) { if (i == bannedDomains[j]) { console.log("ERROR: Domain '" + i + "' is not allowed domain name in config.json."); return; } } }
1011 for (i in obj.config.domains) {
1012 + // Apply default domain settings if present
1013 + if (typeof obj.config.domaindefaults == 'object') { for (var j in obj.config.domaindefaults) { if (obj.config.domains[i][j] == null) { obj.config.domains[i][j] = obj.config.domaindefaults[j]; } } }
1014 +
1015 + // Perform domain setup
1016 if (typeof obj.config.domains[i] != 'object') { console.log("ERROR: Invalid domain configuration in config.json."); process.exit(); return; }
1017 if ((i.length > 0) && (i[0] == '_')) { delete obj.config.domains[i]; continue; } // Remove any domains with names that start with _
1018 if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); }
meshdesktopmultiplex.js
+29
@@ -216,6 +216,7 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, func) {
216 parent.parent.fs.close(fd);
217 // Now that the recording file is closed, check if we need to index this file.
218 if (domain.sessionrecording.index !== false) { parent.parent.certificateOperations.acceleratorPerformOperation('indexMcRec', filename); }
219 + cleanUpRecordings();
220 }, rf.filename);
221 }
222
@@ -663,6 +664,34 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, func) {
664 } catch (ex) { console.log(ex); func(fd, tag); }
665 }
666
667 + // If there is a recording quota, remove any old recordings if needed
668 + function cleanUpRecordings() {
669 + if (domain.sessionrecording && ((typeof domain.sessionrecording.maxrecordings == 'number') || (typeof domain.sessionrecording.maxrecordingsizemegabytes == 'number'))) {
670 + var recPath = null, fs = require('fs');
671 + if (domain.sessionrecording.filepath) { recPath = domain.sessionrecording.filepath; } else { recPath = parent.parent.recordpath; }
672 + fs.readdir(recPath, function (err, files) {
673 + if ((err != null) || (files == null)) return;
674 + var recfiles = [];
675 + for (var i in files) {
676 + if (files[i].endsWith('.mcrec')) {
677 + var j = files[i].indexOf('-');
678 + if (j > 0) { recfiles.push({ n: files[i], r: files[i].substring(j + 1), s: fs.statSync(parent.parent.path.join(recPath, files[i])).size }); }
679 + }
680 + }
681 + recfiles.sort(function (a, b) { if (a.r < b.r) return 1; if (a.r > b.r) return -1; return 0; });
682 + var totalFiles = 0, totalSize = 0;
683 + for (var i in recfiles) {
684 + var overQuota = false;
685 + if ((typeof domain.sessionrecording.maxrecordings == 'number') && (totalFiles >= domain.sessionrecording.maxrecordings)) { overQuota = true; }
686 + else if ((typeof domain.sessionrecording.maxrecordingsizemegabytes == 'number') && (totalSize >= (domain.sessionrecording.maxrecordingsizemegabytes * 1048576))) { overQuota = true; }
687 + if (overQuota) { fs.unlinkSync(parent.parent.path.join(recPath, recfiles[i].n)); }
688 + totalFiles++;
689 + totalSize += recfiles[i].s;
690 + }
691 + });
692 + }
693 + }
694 +
695 recordingSetup(domain, function () { func(obj); });
696 return obj;
697 }
meshrelay.js
+29
@@ -408,6 +408,7 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
408 parent.parent.fs.close(fd);
409 // Now that the recording file is closed, check if we need to index this file.
410 if (domain.sessionrecording.index !== false) { parent.parent.certificateOperations.acceleratorPerformOperation('indexMcRec', tag.logfile.filename); }
411 + cleanUpRecordings();
412 }, { ws: ws, pws: peer.ws, logfile: logfile });
413 }
414
@@ -500,6 +501,34 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie
501 }
502 }
503
504 + // If there is a recording quota, remove any old recordings if needed
505 + function cleanUpRecordings() {
506 + if (domain.sessionrecording && ((typeof domain.sessionrecording.maxrecordings == 'number') || (typeof domain.sessionrecording.maxrecordingsizemegabytes == 'number'))) {
507 + var recPath = null, fs = require('fs');
508 + if (domain.sessionrecording.filepath) { recPath = domain.sessionrecording.filepath; } else { recPath = parent.parent.recordpath; }
509 + fs.readdir(recPath, function (err, files) {
510 + if ((err != null) || (files == null)) return;
511 + var recfiles = [];
512 + for (var i in files) {
513 + if (files[i].endsWith('.mcrec')) {
514 + var j = files[i].indexOf('-');
515 + if (j > 0) { recfiles.push({ n: files[i], r: files[i].substring(j + 1), s: fs.statSync(parent.parent.path.join(recPath, files[i])).size }); }
516 + }
517 + }
518 + recfiles.sort(function (a, b) { if (a.r < b.r) return 1; if (a.r > b.r) return -1; return 0; });
519 + var totalFiles = 0, totalSize = 0;
520 + for (var i in recfiles) {
521 + var overQuota = false;
522 + if ((typeof domain.sessionrecording.maxrecordings == 'number') && (totalFiles >= domain.sessionrecording.maxrecordings)) { overQuota = true; }
523 + else if ((typeof domain.sessionrecording.maxrecordingsizemegabytes == 'number') && (totalSize >= (domain.sessionrecording.maxrecordingsizemegabytes * 1048576))) { overQuota = true; }
524 + if (overQuota) { fs.unlinkSync(parent.parent.path.join(recPath, recfiles[i].n)); }
525 + totalFiles++;
526 + totalSize += recfiles[i].s;
527 + }
528 + });
529 + }
530 + }
531 +
532 // If this is not an authenticated session, or the session does not have routing instructions, just go ahead an connect to existing session.
533 performRelay();
534 return obj;
meshuser.js
+14 -1
@@ -3569,7 +3569,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
3569 if (actionTaken) { parent.db.SetUser(user); }
3570
3571 // Return one time passwords for this user
3572 - if (user.otpsecret || ((user.otphkeys != null) && (user.otphkeys.length > 0))) {
3572 + if (count2factoraAuths() > 0) {
3573 ws.send(JSON.stringify({ action: 'otpauth-getpasswords', passwords: user.otpkeys ? user.otpkeys.keys : null }));
3574 }
3575
@@ -4260,5 +4260,18 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
4260 }
4261 }
4262
4263 + // Return the number of 2nd factor for this account
4264 + function count2factoraAuths() {
4265 + var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.parent.mailserver != null));
4266 + var sms2fa = ((parent.parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)));
4267 + var authFactorCount = 0;
4268 + if (user.otpsecret == 1) { authFactorCount++; } // Authenticator time factor
4269 + if (email2fa && (user.otpekey != null)) { authFactorCount++; } // EMail factor
4270 + if (sms2fa && (user.phone != null)) { authFactorCount++; } // SMS factor
4271 + if (user.otphkeys != null) { authFactorCount += user.otphkeys.length; } // FIDO hardware factor
4272 + if ((authFactorCount > 0) && (user.otpkeys != null)) { authFactorCount++; } // Backup keys
4273 + return authFactorCount;
4274 + }
4275 +
4276 return obj;
4277 };
\ No newline at end of file
package.json
+1 -1
@@ -1,6 +1,6 @@
1 {
2 "name": "meshcentral",
3 - "version": "0.5.17",
3 + "version": "0.5.18",
4 "keywords": [
5 "Remote Management",
6 "Intel AMT",
sample-config.json
+9
@@ -39,6 +39,7 @@
39 "_NpmPath": "c:\\npm.exe",
40 "_NpmProxy": "http://1.2.3.4:80",
41 "_AllowHighQualityDesktop": true,
42 + "_DesktopMultiplex": true,
43 "_UserAllowedIP": "127.0.0.1,192.168.1.0/24",
44 "_UserBlockedIP": "127.0.0.1,::1,192.168.0.100",
45 "_AgentAllowedIP": "192.168.0.100/24",
@@ -78,6 +79,12 @@
79 "_MaxInvalidLogin": { "time": 10, "count": 10, "coolofftime": 10 },
80 "_Plugins": { "enabled": true }
81 },
82 + "_domaindefaults": {
83 + "__comment__": "Any settings in this section is used as default setting for all domains",
84 + "Title": "MyDefaultTitle",
85 + "Footer": "Default page footer",
86 + "NewAccounts": false
87 + },
88 "_domains": {
89 "": {
90 "Title": "MyServer",
@@ -137,6 +144,8 @@
144 "_SessionRecording": {
145 "_filepath": "C:\\temp",
146 "_index": true,
147 + "_maxRecordings": 10,
148 + "_maxRecordingSizeMegabytes": 3,
149 "__protocols__": "Is an array: 1 = Terminal, 2 = Desktop, 5 = Files, 100 = Intel AMT WSMAN, 101 = Intel AMT Redirection",
150 "protocols": [ 1, 2, 101 ]
151 }
views/default.handlebars
+15 -9
@@ -1694,11 +1694,23 @@
1694 }
1695 }
1696
1697 + // Return the number of 2nd factor for this account
1698 + function count2factoraAuths() {
1699 + var authFactorCount = 0;
1700 + if (userinfo.otpsecret == 1) { authFactorCount++; } // Authenticator time factor
1701 + if ((features & 0x00800000) && (userinfo.otpekey == 1)) { authFactorCount++; } // EMail factor
1702 + if ((features & 0x04000000) && (userinfo.phone != null)) { authFactorCount++; } // SMS factor
1703 + if (userinfo.otphkeys != null) { authFactorCount += userinfo.otphkeys; } // FIDO hardware factor
1704 + if ((authFactorCount > 0) && (userinfo.otpkeys == 1)) { authFactorCount++; } // Backup keys
1705 + return authFactorCount;
1706 + }
1707 +
1708 var backupCodesWarningDone = false;
1709 function updateSelf() {
1710 + var authFactorCount = count2factoraAuths(); // Get the number of 2nd factors
1711 QV('verifyEmailId', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true));
1712 QV('verifyEmailId2', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true));
1701 - QV('manageOtp', (userinfo.otpsecret == 1) || (userinfo.otphkeys > 0));
1713 + QV('manageOtp', authFactorCount > 0);
1714 QV('authPhoneNumberCheck', (userinfo.phone != null));
1715 QV('authEmailSetupCheck', (userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true));
1716 QV('authAppSetupCheck', userinfo.otpsecret == 1);
@@ -1707,12 +1719,6 @@
1719 masterUpdate(4 + 128 + 4096);
1720
1721 // Check if none or at least 2 factors are enabled.
1710 - var authFactorCount = 0;
1711 - if ((features & 0x00800000) && (userinfo.otpekey == 1)) { authFactorCount += 1; }
1712 - if ((features & 0x02000000) && (features & 0x04000000) && (userinfo.phone != null)) { authFactorCount += 1; }
1713 - if (userinfo.otpkeys == 1) { authFactorCount += 1; }
1714 - if (userinfo.otpsecret == 1) { authFactorCount += 1; }
1715 - if (userinfo.otphkeys != null) { authFactorCount += userinfo.otphkeys; }
1722 if ((backupCodesWarningDone == false) && (authFactorCount == 1)) {
1723 var n = { text: "Please add two-factor backup codes. If the current factor is lost, there is not way to recover this account.", title: "Two factor authentication" };
1724 addNotification(n);
@@ -4838,7 +4844,7 @@
4844 if ((userinfo.emailVerified !== true) && (serverinfo.emailcheck == true) && (userinfo.siteadmin != 0xFFFFFFFF)) { setDialogMode(2, "Account Security", 1, null, "Unable to access a device until a email address is verified. This is required for password recovery. Go to the \"My Account\" tab to change and verify an email address."); return; }
4845
4846 // Remind the user to add two factor authentication
4841 - if ((features & 0x00040000) && !((userinfo.otpsecret == 1) || (userinfo.otphkeys > 0) || (userinfo.otpkeys > 0) || ((features & 0x00800000) && (userinfo.otpekey == 1)))) { setDialogMode(2, "Account Security", 1, null, "Unable to access a device until two-factor authentication is enabled. This is required for extra security. Go to the \"My Account\" tab and look at the \"Account Security\" section."); return; }
4847 + if ((features & 0x00040000) && (count2factoraAuths() == 0)) { setDialogMode(2, "Account Security", 1, null, "Unable to access a device until two-factor authentication is enabled. This is required for extra security. Go to the \"My Account\" tab and look at the \"Account Security\" section."); return; }
4848
4849 if (event && (event.shiftKey == true)) {
4850 // Open the device in a different tab
@@ -7987,7 +7993,7 @@
7993 function account_manageOtp(action) {
7994 if ((xxdialogMode == 2) && (xxdialogTag == 'otpauth-manage')) { dialogclose(0); }
7995 if (xxdialogMode || ((features & 4096) == 0)) return false;
7990 - if ((userinfo.otpsecret == 1) || (userinfo.otphkeys > 0)) { meshserver.send({ action: 'otpauth-getpasswords', subaction: action }); }
7996 + if (count2factoraAuths() > 0) { meshserver.send({ action: 'otpauth-getpasswords', subaction: action }); }
7997 return false;
7998 }
7999