From ea9edefad161485fcb8a897a616b6753318b0a4e Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Thu, 30 Apr 2020 02:02:23 -0700 Subject: [PATCH] Added recording space/count quota. --- meshcentral.js | 6 +++++- meshdesktopmultiplex.js | 29 +++++++++++++++++++++++++++++ meshrelay.js | 29 +++++++++++++++++++++++++++++ meshuser.js | 15 ++++++++++++++- package.json | 2 +- sample-config.json | 9 +++++++++ views/default.handlebars | 24 +++++++++++++++--------- 7 files changed, 102 insertions(+), 12 deletions(-) diff --git a/meshcentral.js b/meshcentral.js index 90ed5f30..b1088fa2 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -633,7 +633,7 @@ function CreateMeshCentralServer(config, args) { } // Check top level configuration for any unreconized values - 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 + '\".'); } } } + 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 + '\".'); } } } 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(','); } } 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) { var bannedDomains = ['public', 'private', 'images', 'scripts', 'styles', 'views']; // List of banned domains 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; } } } for (i in obj.config.domains) { + // Apply default domain settings if present + 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]; } } } + + // Perform domain setup if (typeof obj.config.domains[i] != 'object') { console.log("ERROR: Invalid domain configuration in config.json."); process.exit(); return; } if ((i.length > 0) && (i[0] == '_')) { delete obj.config.domains[i]; continue; } // Remove any domains with names that start with _ if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); } diff --git a/meshdesktopmultiplex.js b/meshdesktopmultiplex.js index 356b6e39..8a0fc317 100644 --- a/meshdesktopmultiplex.js +++ b/meshdesktopmultiplex.js @@ -216,6 +216,7 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, func) { parent.parent.fs.close(fd); // Now that the recording file is closed, check if we need to index this file. if (domain.sessionrecording.index !== false) { parent.parent.certificateOperations.acceleratorPerformOperation('indexMcRec', filename); } + cleanUpRecordings(); }, rf.filename); } @@ -663,6 +664,34 @@ function CreateDesktopMultiplexor(parent, domain, nodeid, func) { } catch (ex) { console.log(ex); func(fd, tag); } } + // If there is a recording quota, remove any old recordings if needed + function cleanUpRecordings() { + if (domain.sessionrecording && ((typeof domain.sessionrecording.maxrecordings == 'number') || (typeof domain.sessionrecording.maxrecordingsizemegabytes == 'number'))) { + var recPath = null, fs = require('fs'); + if (domain.sessionrecording.filepath) { recPath = domain.sessionrecording.filepath; } else { recPath = parent.parent.recordpath; } + fs.readdir(recPath, function (err, files) { + if ((err != null) || (files == null)) return; + var recfiles = []; + for (var i in files) { + if (files[i].endsWith('.mcrec')) { + var j = files[i].indexOf('-'); + 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 }); } + } + } + recfiles.sort(function (a, b) { if (a.r < b.r) return 1; if (a.r > b.r) return -1; return 0; }); + var totalFiles = 0, totalSize = 0; + for (var i in recfiles) { + var overQuota = false; + if ((typeof domain.sessionrecording.maxrecordings == 'number') && (totalFiles >= domain.sessionrecording.maxrecordings)) { overQuota = true; } + else if ((typeof domain.sessionrecording.maxrecordingsizemegabytes == 'number') && (totalSize >= (domain.sessionrecording.maxrecordingsizemegabytes * 1048576))) { overQuota = true; } + if (overQuota) { fs.unlinkSync(parent.parent.path.join(recPath, recfiles[i].n)); } + totalFiles++; + totalSize += recfiles[i].s; + } + }); + } + } + recordingSetup(domain, function () { func(obj); }); return obj; } diff --git a/meshrelay.js b/meshrelay.js index 21e18e15..15d76d9a 100644 --- a/meshrelay.js +++ b/meshrelay.js @@ -408,6 +408,7 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie parent.parent.fs.close(fd); // Now that the recording file is closed, check if we need to index this file. if (domain.sessionrecording.index !== false) { parent.parent.certificateOperations.acceleratorPerformOperation('indexMcRec', tag.logfile.filename); } + cleanUpRecordings(); }, { ws: ws, pws: peer.ws, logfile: logfile }); } @@ -500,6 +501,34 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie } } + // If there is a recording quota, remove any old recordings if needed + function cleanUpRecordings() { + if (domain.sessionrecording && ((typeof domain.sessionrecording.maxrecordings == 'number') || (typeof domain.sessionrecording.maxrecordingsizemegabytes == 'number'))) { + var recPath = null, fs = require('fs'); + if (domain.sessionrecording.filepath) { recPath = domain.sessionrecording.filepath; } else { recPath = parent.parent.recordpath; } + fs.readdir(recPath, function (err, files) { + if ((err != null) || (files == null)) return; + var recfiles = []; + for (var i in files) { + if (files[i].endsWith('.mcrec')) { + var j = files[i].indexOf('-'); + 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 }); } + } + } + recfiles.sort(function (a, b) { if (a.r < b.r) return 1; if (a.r > b.r) return -1; return 0; }); + var totalFiles = 0, totalSize = 0; + for (var i in recfiles) { + var overQuota = false; + if ((typeof domain.sessionrecording.maxrecordings == 'number') && (totalFiles >= domain.sessionrecording.maxrecordings)) { overQuota = true; } + else if ((typeof domain.sessionrecording.maxrecordingsizemegabytes == 'number') && (totalSize >= (domain.sessionrecording.maxrecordingsizemegabytes * 1048576))) { overQuota = true; } + if (overQuota) { fs.unlinkSync(parent.parent.path.join(recPath, recfiles[i].n)); } + totalFiles++; + totalSize += recfiles[i].s; + } + }); + } + } + // If this is not an authenticated session, or the session does not have routing instructions, just go ahead an connect to existing session. performRelay(); return obj; diff --git a/meshuser.js b/meshuser.js index 924cf5b8..3d8e043d 100644 --- a/meshuser.js +++ b/meshuser.js @@ -3569,7 +3569,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (actionTaken) { parent.db.SetUser(user); } // Return one time passwords for this user - if (user.otpsecret || ((user.otphkeys != null) && (user.otphkeys.length > 0))) { + if (count2factoraAuths() > 0) { ws.send(JSON.stringify({ action: 'otpauth-getpasswords', passwords: user.otpkeys ? user.otpkeys.keys : null })); } @@ -4260,5 +4260,18 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } } + // Return the number of 2nd factor for this account + function count2factoraAuths() { + var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.parent.mailserver != null)); + var sms2fa = ((parent.parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false))); + var authFactorCount = 0; + if (user.otpsecret == 1) { authFactorCount++; } // Authenticator time factor + if (email2fa && (user.otpekey != null)) { authFactorCount++; } // EMail factor + if (sms2fa && (user.phone != null)) { authFactorCount++; } // SMS factor + if (user.otphkeys != null) { authFactorCount += user.otphkeys.length; } // FIDO hardware factor + if ((authFactorCount > 0) && (user.otpkeys != null)) { authFactorCount++; } // Backup keys + return authFactorCount; + } + return obj; }; \ No newline at end of file diff --git a/package.json b/package.json index 89a8a0e8..76f745e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meshcentral", - "version": "0.5.17", + "version": "0.5.18", "keywords": [ "Remote Management", "Intel AMT", diff --git a/sample-config.json b/sample-config.json index 83da1d40..b69991af 100644 --- a/sample-config.json +++ b/sample-config.json @@ -39,6 +39,7 @@ "_NpmPath": "c:\\npm.exe", "_NpmProxy": "http://1.2.3.4:80", "_AllowHighQualityDesktop": true, + "_DesktopMultiplex": true, "_UserAllowedIP": "127.0.0.1,192.168.1.0/24", "_UserBlockedIP": "127.0.0.1,::1,192.168.0.100", "_AgentAllowedIP": "192.168.0.100/24", @@ -78,6 +79,12 @@ "_MaxInvalidLogin": { "time": 10, "count": 10, "coolofftime": 10 }, "_Plugins": { "enabled": true } }, + "_domaindefaults": { + "__comment__": "Any settings in this section is used as default setting for all domains", + "Title": "MyDefaultTitle", + "Footer": "Default page footer", + "NewAccounts": false + }, "_domains": { "": { "Title": "MyServer", @@ -137,6 +144,8 @@ "_SessionRecording": { "_filepath": "C:\\temp", "_index": true, + "_maxRecordings": 10, + "_maxRecordingSizeMegabytes": 3, "__protocols__": "Is an array: 1 = Terminal, 2 = Desktop, 5 = Files, 100 = Intel AMT WSMAN, 101 = Intel AMT Redirection", "protocols": [ 1, 2, 101 ] } diff --git a/views/default.handlebars b/views/default.handlebars index dca62fef..a34a0c8d 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -1694,11 +1694,23 @@ } } + // Return the number of 2nd factor for this account + function count2factoraAuths() { + var authFactorCount = 0; + if (userinfo.otpsecret == 1) { authFactorCount++; } // Authenticator time factor + if ((features & 0x00800000) && (userinfo.otpekey == 1)) { authFactorCount++; } // EMail factor + if ((features & 0x04000000) && (userinfo.phone != null)) { authFactorCount++; } // SMS factor + if (userinfo.otphkeys != null) { authFactorCount += userinfo.otphkeys; } // FIDO hardware factor + if ((authFactorCount > 0) && (userinfo.otpkeys == 1)) { authFactorCount++; } // Backup keys + return authFactorCount; + } + var backupCodesWarningDone = false; function updateSelf() { + var authFactorCount = count2factoraAuths(); // Get the number of 2nd factors QV('verifyEmailId', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true)); QV('verifyEmailId2', (userinfo.emailVerified !== true) && (userinfo.email != null) && (serverinfo.emailcheck == true)); - QV('manageOtp', (userinfo.otpsecret == 1) || (userinfo.otphkeys > 0)); + QV('manageOtp', authFactorCount > 0); QV('authPhoneNumberCheck', (userinfo.phone != null)); QV('authEmailSetupCheck', (userinfo.otpekey == 1) && (userinfo.email != null) && (userinfo.emailVerified == true)); QV('authAppSetupCheck', userinfo.otpsecret == 1); @@ -1707,12 +1719,6 @@ masterUpdate(4 + 128 + 4096); // Check if none or at least 2 factors are enabled. - var authFactorCount = 0; - if ((features & 0x00800000) && (userinfo.otpekey == 1)) { authFactorCount += 1; } - if ((features & 0x02000000) && (features & 0x04000000) && (userinfo.phone != null)) { authFactorCount += 1; } - if (userinfo.otpkeys == 1) { authFactorCount += 1; } - if (userinfo.otpsecret == 1) { authFactorCount += 1; } - if (userinfo.otphkeys != null) { authFactorCount += userinfo.otphkeys; } if ((backupCodesWarningDone == false) && (authFactorCount == 1)) { 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" }; addNotification(n); @@ -4838,7 +4844,7 @@ 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; } // Remind the user to add two factor authentication - 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; } + 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; } if (event && (event.shiftKey == true)) { // Open the device in a different tab @@ -7987,7 +7993,7 @@ function account_manageOtp(action) { if ((xxdialogMode == 2) && (xxdialogTag == 'otpauth-manage')) { dialogclose(0); } if (xxdialogMode || ((features & 4096) == 0)) return false; - if ((userinfo.otpsecret == 1) || (userinfo.otphkeys > 0)) { meshserver.send({ action: 'otpauth-getpasswords', subaction: action }); } + if (count2factoraAuths() > 0) { meshserver.send({ action: 'otpauth-getpasswords', subaction: action }); } return false; }