diff --git a/db.js b/db.js
index 4220c1bb..547efc1b 100644
--- a/db.js
+++ b/db.js
@@ -677,7 +677,13 @@ module.exports.CreateDB = function (parent, func) {
// TODO: Starting in MongoDB 4.0.3, you should use countDocuments() instead of count() that is deprecated. We should detect MongoDB version and switch.
// https://docs.mongodb.com/manual/reference/method/db.collection.countDocuments/
//obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max)); }); } }
- obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); } }
+ obj.isMaxType = function (max, type, domainid, func) {
+ if (obj.eventsfile.countDocuments) {
+ if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
+ } else {
+ if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
+ }
+ }
// Database actions on the events collection
obj.GetAllEvents = function (func) { obj.eventsfile.find({}).toArray(func); };
@@ -693,7 +699,13 @@ module.exports.CreateDB = function (parent, func) {
obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, func) { obj.eventsfile.find({ domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } }).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func); };
obj.RemoveAllEvents = function (domain) { obj.eventsfile.deleteMany({ domain: domain }, { multi: true }); };
obj.RemoveAllNodeEvents = function (domain, nodeid) { obj.eventsfile.deleteMany({ domain: domain, nodeid: nodeid }, { multi: true }); };
- obj.GetFailedLoginCount = function (username, domainid, lastlogin, func) { obj.eventsfile.count({ action: 'authfail', username: username, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null)?count:0); }); }
+ obj.GetFailedLoginCount = function (username, domainid, lastlogin, func) {
+ if (obj.eventsfile.countDocuments) {
+ obj.eventsfile.countDocuments({ action: 'authfail', username: username, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
+ } else {
+ obj.eventsfile.count({ action: 'authfail', username: username, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
+ }
+ }
// Database actions on the power collection
obj.getAllPower = function (func) { obj.powerfile.find({}).toArray(func); };
diff --git a/letsEncrypt.js b/letsEncrypt.js
index 2bad30bd..9825b3a0 100644
--- a/letsEncrypt.js
+++ b/letsEncrypt.js
@@ -223,7 +223,6 @@ module.exports.CreateLetsEncrypt = function (parent) {
var somethingIsinFolder = false;
try {
var filesinFolder = require('fs').readdirSync(obj.runAsProduction ? obj.configPath : obj.configPathStaging);
- console.log('filesinFolder', filesinFolder);
somethingIsinFolder = (filesinFolder.indexOf(obj.runAsProduction ? 'live' : 'staging') != -1);
} catch (ex) { console.log(ex); }
diff --git a/meshuser.js b/meshuser.js
index c1189ccb..14d690ad 100644
--- a/meshuser.js
+++ b/meshuser.js
@@ -689,14 +689,20 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
case 'help': {
r = 'Available commands: help, info, versions, args, resetserver, showconfig, usersessions, tasklimiter, setmaxtasks, cores,\r\n'
r += 'migrationagents, agentstats, webstats, mpsstats, swarmstats, acceleratorsstats, updatecheck, serverupdate, nodeconfig,\r\n';
- r += 'heapdump, relays, autobackup, backupconfig, dupagents, dispatchtable.';
+ r += 'heapdump, relays, autobackup, backupconfig, dupagents, dispatchtable, badlogins.';
+ break;
+ }
+ case 'badlogins': {
+ r = "Max is " + parent.parent.config.settings.maxinvalidlogin.count + " bad login(s) in " + parent.parent.config.settings.maxinvalidlogin.time + " minute(s).\r\n";
+ var badLoginCount = 0;
+ parent.cleanBadLoginTable();
+ for (var i in parent.badLoginTable) { badLoginCount++; r += (i + ' - ' + parent.badLoginTable[i].length + " entries\r\n"); }
+ if (badLoginCount == 0) { r += 'No bad logins.'; }
break;
}
case 'dispatchtable': {
r = '';
- for (var i in parent.parent.eventsDispatch) {
- r += (i + ', ' + parent.parent.eventsDispatch[i].length + '\r\n');
- }
+ for (var i in parent.parent.eventsDispatch) { r += (i + ', ' + parent.parent.eventsDispatch[i].length + '\r\n'); }
break;
}
case 'dupagents': {
diff --git a/package.json b/package.json
index 1d01b352..37cff4fd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "meshcentral",
- "version": "0.4.4-l",
+ "version": "0.4.4-m",
"keywords": [
"Remote Management",
"Intel AMT",
diff --git a/sample-config.json b/sample-config.json
index 0d7b675a..03d9b526 100644
--- a/sample-config.json
+++ b/sample-config.json
@@ -58,7 +58,9 @@
},
"_Redirects": {
"meshcommander": "https://www.meshcommander.com/"
- }
+ },
+ "__MaxInvalidLogin": "Time in minutes, max amount of bad logins from a source IP in the time before logins are rejected.",
+ "MaxInvalidLogin": { "time": 10, "count": 10 }
},
"_domains": {
"": {
diff --git a/views/login-min.handlebars b/views/login-min.handlebars
index 053a4f07..5278937a 100644
--- a/views/login-min.handlebars
+++ b/views/login-min.handlebars
@@ -1 +1 @@
-
{{{title}}} - Login
{{{title}}}
{{{title2}}}
♦
Welcome
Connect to your home or office devices from anywhere in the world using MeshCentral, the real time, open source remote monitoring and management web site. You will need to download and install a management agent on your computers. Once installed, computers will show up in the "My Devices" section of this web site and you will be able to monitor them and take control of them.
X
\ No newline at end of file
+{{{title}}} - Login
{{{title}}}
{{{title2}}}
♦
Welcome
Connect to your home or office devices from anywhere in the world using MeshCentral, the real time, open source remote monitoring and management web site. You will need to download and install a management agent on your computers. Once installed, computers will show up in the "My Devices" section of this web site and you will be able to monitor them and take control of them.
X
\ No newline at end of file
diff --git a/views/login-mobile-min.handlebars b/views/login-mobile-min.handlebars
index 1203ecdf..f95e95fd 100644
--- a/views/login-mobile-min.handlebars
+++ b/views/login-mobile-min.handlebars
@@ -1 +1 @@
-MeshCentral - Login
{{{title}}}
{{{title2}}}
X
\ No newline at end of file
+MeshCentral - Login
{{{title}}}
{{{title2}}}
X
\ No newline at end of file
diff --git a/views/login-mobile.handlebars b/views/login-mobile.handlebars
index 146d5086..67951822 100644
--- a/views/login-mobile.handlebars
+++ b/views/login-mobile.handlebars
@@ -270,7 +270,7 @@
// Display the right server message
var messageid = parseInt('{{{messageid}}}');
var okmessages = ['', "Hold on, reset mail sent."];
- var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested."];
+ var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested.", "IP address blocked, try again later."];
if (messageid > 0) {
var msg = '';
if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; }
diff --git a/views/login.handlebars b/views/login.handlebars
index 84f8b232..54adc728 100644
--- a/views/login.handlebars
+++ b/views/login.handlebars
@@ -267,7 +267,7 @@
// Display the right server message
var messageid = parseInt('{{{messageid}}}');
var okmessages = ['', "Hold on, reset mail sent."];
- var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested."];
+ var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested.", "IP address blocked, try again later."];
if (messageid > 0) {
var msg = '';
if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; }
diff --git a/views/translations/login-min_fr.handlebars b/views/translations/login-min_fr.handlebars
index d3a9fbb8..eb62b24b 100644
--- a/views/translations/login-min_fr.handlebars
+++ b/views/translations/login-min_fr.handlebars
@@ -1 +1 @@
-{{{title}}} - Login
{{{title}}}
{{{title2}}}
♦
Bienvenue
Connect to your home or office devices from anywhere in the world using MeshCentral, the real time, open source remote monitoring and management web site. You will need to download and install a management agent on your computers. Once installed, computers will show up in the "My Devices" section of this web site and you will be able to monitor them and take control of them.
X
\ No newline at end of file
+{{{title}}} - Login
{{{title}}}
{{{title2}}}
♦
Bienvenue
Connect to your home or office devices from anywhere in the world using MeshCentral, the real time, open source remote monitoring and management web site. You will need to download and install a management agent on your computers. Once installed, computers will show up in the "My Devices" section of this web site and you will be able to monitor them and take control of them.
X
\ No newline at end of file
diff --git a/views/translations/login-mobile-min_fr.handlebars b/views/translations/login-mobile-min_fr.handlebars
index 16b62764..cd99da61 100644
--- a/views/translations/login-mobile-min_fr.handlebars
+++ b/views/translations/login-mobile-min_fr.handlebars
@@ -1 +1 @@
-MeshCentral - Login
{{{title}}}
{{{title2}}}
X
\ No newline at end of file
+MeshCentral - Login
{{{title}}}
{{{title2}}}
X
\ No newline at end of file
diff --git a/views/translations/login-mobile_fr.handlebars b/views/translations/login-mobile_fr.handlebars
index e377dd77..35755e83 100644
--- a/views/translations/login-mobile_fr.handlebars
+++ b/views/translations/login-mobile_fr.handlebars
@@ -268,7 +268,7 @@
// Display the right server message
var messageid = parseInt('{{{messageid}}}');
var okmessages = ['', "Hold on, reset mail sent."];
- var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested."];
+ var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested.", "IP address blocked, try again later."];
if (messageid > 0) {
var msg = '';
if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; }
diff --git a/views/translations/login_fr.handlebars b/views/translations/login_fr.handlebars
index 5b093bfc..16d9f8ed 100644
--- a/views/translations/login_fr.handlebars
+++ b/views/translations/login_fr.handlebars
@@ -265,7 +265,7 @@
// Display the right server message
var messageid = parseInt('{{{messageid}}}');
var okmessages = ['', "Hold on, reset mail sent."];
- var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested."];
+ var failmessages = ["Unable to create account.", "Account limit reached.", "Existing account with this email address.", "Invalid account creation token.", "Username already exists.", "Password rejected, use a different one.", "Invalid email.", "Account not found.", "Invalid token, try again.", "Unable to sent email.", "Account locked.", "Access denied.", "Login failed, check username and password.", "Password change requested.", "IP address blocked, try again later."];
if (messageid > 0) {
var msg = '';
if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; }
diff --git a/webserver.js b/webserver.js
index 37ff7f4a..fad9c6fc 100644
--- a/webserver.js
+++ b/webserver.js
@@ -639,6 +639,16 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { parent.debug('web', 'handleLoginRequest: invalid domain'); res.sendStatus(404); return; }
+ // Check if this is a banned ip address
+ if (obj.checkAllowLogin(req) == false) {
+ // Wait and redirect the user
+ setTimeout(function () {
+ req.session.messageid = 114; // IP address blocked, try again later.
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
+ }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
+ return;
+ }
+
// Normally, use the body username/password. If this is a token, use the username/password in the session.
var xusername = req.body.username, xpassword = req.body.password;
if ((xusername == null) && (xpassword == null) && (req.body.token != null)) { xusername = req.session.tokenusername; xpassword = req.session.tokenpassword; }
@@ -660,6 +670,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
req.session.messageid = 108; // Invalid token, try again.
parent.debug('web', 'handleLoginRequest: invalid 2FA token');
obj.parent.DispatchEvent(['*', 'server-users', 'user/' + domain.id + '/' + user.name], obj, { action: 'authfail', username: user.name, userid: 'user/' + domain.id + '/' + user.name, domain: domain.id, msg: 'User login attempt with incorrect 2nd factor from ' + cleanRemoteAddr(req.ip) });
+ obj.setbadLogin(req);
} else {
parent.debug('web', 'handleLoginRequest: 2FA token required');
}
@@ -693,10 +704,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
parent.debug('web', 'handleLoginRequest: login failed, locked account');
req.session.messageid = 110; // Account locked.
obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'User login attempt on locked account from ' + cleanRemoteAddr(req.ip) });
+ obj.setbadLogin(req);
} else {
parent.debug('web', 'handleLoginRequest: login failed, bad username and password');
req.session.messageid = 112; // Login failed, check username and password.
obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'Invalid user login attempt from ' + cleanRemoteAddr(req.ip) });
+ obj.setbadLogin(req);
}
}
@@ -1015,6 +1028,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if ((req.body.token != null) || (req.body.hwtoken != null)) {
req.session.messageid = 108; // Invalid token, try again.
obj.parent.DispatchEvent(['*', 'server-users', 'user/' + domain.id + '/' + user.name], obj, { action: 'authfail', username: user.name, userid: 'user/' + domain.id + '/' + user.name, domain: domain.id, msg: 'User login attempt with incorrect 2nd factor from ' + cleanRemoteAddr(req.ip) });
+ obj.setbadLogin(req);
}
req.session.loginmode = '5';
req.session.tokenemail = email;
@@ -3434,6 +3448,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
// Authenticates a session and forwards
function PerformWSSessionAuth(ws, req, noAuthOk, func) {
+ // Check if this is a banned ip address
+ if (obj.checkAllowLogin(req) == false) { try { ws.send(JSON.stringify({ action: 'close', cause: 'banned', msg: 'banned-1' })); ws.close(); } catch (e) { } return; }
try {
// Hold this websocket until we are ready.
ws._socket.pause();
@@ -3476,6 +3492,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
// If not authenticated, close the websocket connection
parent.debug('web', 'ERR: Websocket bad user/pass auth');
//obj.parent.DispatchEvent(['*', 'server-users', 'user/' + domain.id + '/' + obj.args.user.toLowerCase()], obj, { action: 'authfail', userid: 'user/' + domain.id + '/' + obj.args.user.toLowerCase(), username: obj.args.user, domain: domain.id, msg: 'Invalid user login attempt from ' + cleanRemoteAddr(req.ip) });
+ //obj.setbadLogin(req);
try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2' })); ws.close(); } catch (e) { }
}
}
@@ -4014,5 +4031,36 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
} catch (ex) { console.log(ex); func(fd, tag); }
}
+ // This is the invalid login throttling code
+ obj.badLoginTable = {};
+ obj.badLoginTableLastClean = 0;
+ if (parent.config.settings == null) { parent.config.settings = {}; }
+ if (parent.config.settings.maxinvalidlogin == null) { parent.config.settings.maxinvalidlogin = { time: 10, count: 10 }; }
+ if (typeof parent.config.settings.maxinvalidlogin.time != 'number') { parent.config.settings.maxinvalidlogin.time = 10; }
+ if (typeof parent.config.settings.maxinvalidlogin.count != 'number') { parent.config.settings.maxinvalidlogin.count = 10; }
+ obj.setbadLogin = function (ip) { // Set an IP address that just did a bad login request
+ if (typeof ip == 'object') { ip = cleanRemoteAddr(ip.ip); }
+ if (++obj.badLoginTableLastClean > 100) { obj.cleanBadLoginTable(); }
+ if (obj.badLoginTable[ip] == null) { obj.badLoginTable[ip] = [Date.now()]; } else { obj.badLoginTable[ip].push(Date.now()); }
+ }
+ obj.checkAllowLogin = function (ip) { // Check if an IP address is allowed to login
+ if (typeof ip == 'object') { ip = cleanRemoteAddr(ip.ip); }
+ var cutoffTime = Date.now() - (parent.config.settings.maxinvalidlogin.time * 60000); // Time in minutes
+ var ipTable = obj.badLoginTable[ip];
+ if (ipTable == null) return true;
+ while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
+ if (ipTable.length == 0) { delete obj.badLoginTable[ip]; return true; }
+ return (ipTable.length < parent.config.settings.maxinvalidlogin.count); // No more than x bad logins in x minutes
+ }
+ obj.cleanBadLoginTable = function () { // Clean up the IP address login blockage table, we do this occasionaly.
+ var cutoffTime = Date.now() - (parent.config.settings.maxinvalidlogin.time * 60000); // Time in minutes
+ for (var i in ipTable) {
+ var ipTable = obj.badLoginTable[ip];
+ while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
+ if (ipTable.length == 0) { delete obj.badLoginTable[ip]; }
+ }
+ obj.badLoginTableLastClean = 0;
+ }
+
return obj;
};
\ No newline at end of file