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