diff --git a/authenticode.js b/authenticode.js
index 213d3d4d..37d39d50 100644
--- a/authenticode.js
+++ b/authenticode.js
@@ -1235,7 +1235,7 @@ function createAuthenticodeHandler(path) {
if (args.hash == 'sha512') { hashOid = forge.pki.oids.sha512; fileHash = obj.getHash('sha512'); }
if (args.hash == 'sha224') { hashOid = forge.pki.oids.sha224; fileHash = obj.getHash('sha224'); }
if (args.hash == 'md5') { hashOid = forge.pki.oids.md5; fileHash = obj.getHash('md5'); }
- if (hashOid == null) { func(false); return; };
+ if (hashOid == null) { func('Invalid signing hash: ' + args.hash); return; };
// Create the signature block
var xp7 = forge.pkcs7.createSignedData();
@@ -1453,7 +1453,7 @@ function createAuthenticodeHandler(path) {
// Open the output file
var output = null;
try { output = fs.openSync(args.out, 'w+'); } catch (ex) { }
- if (output == null) { func(false); return; }
+ if (output == null) { func('Unable to open output file: ' + args.out); return; }
var tmp, written = 0, executableSize = obj.header.sigpos ? obj.header.sigpos : filesize;
// Compute pre-header length and copy that to the new file
diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json
index 3a1df702..14b39226 100644
--- a/meshcentral-config-schema.json
+++ b/meshcentral-config-schema.json
@@ -343,6 +343,7 @@
"ipkvm": { "type": "boolean", "default": false, "description": "Set to true to enable IP KVM device support in this domain." },
"minify": { "type": "boolean", "default": false, "description": "When enabled, the server will send reduced sided web pages." },
"newAccounts": { "type": "boolean", "default": false, "description": "When set to true, allow new user accounts to be created from the login page." },
+ "newAccountsPass": { "type": "string", "default": null, "description": "When set this password will be required in order to create a new account from the login screen." },
"newAccountsUserGroups": { "type": "array", "uniqueItems": true, "items": { "type": "string" } },
"userNameIsEmail": { "type": "boolean", "default": false, "description": "When enabled, the username of each account is also the email address of the account." },
"newAccountEmailDomains": { "type": "array", "uniqueItems": true, "items": { "type": "string" } },
diff --git a/meshcentral.js b/meshcentral.js
index 3aa2a17e..77867e70 100644
--- a/meshcentral.js
+++ b/meshcentral.js
@@ -3675,6 +3675,7 @@ function mainStart() {
var wildleek = false;
var nodemailer = false;
var sendgrid = false;
+ var captcha = false;
if (require('os').platform() == 'win32') { for (var i in config.domains) { domainCount++; if (config.domains[i].auth == 'sspi') { sspi = true; } else { allsspi = false; } } } else { allsspi = false; }
if (domainCount == 0) { allsspi = false; }
for (var i in config.domains) {
@@ -3697,6 +3698,7 @@ function mainStart() {
}
if (config.domains[i].sessionrecording != null) { sessionRecording = true; }
if ((config.domains[i].passwordrequirements != null) && (config.domains[i].passwordrequirements.bancommonpasswords == true)) { wildleek = true; }
+ if ((config.domains[i].newaccountscaptcha != null) && (config.domains[i].newaccountscaptcha !== false)) { captcha = true; }
}
// Build the list of required modules
@@ -3705,6 +3707,8 @@ function mainStart() {
if (ldap == true) { modules.push('ldapauth-fork'); }
if (ssh == true) { if (nodeVersion < 11) { addServerWarning('MeshCentral SSH support requires NodeJS 11 or higher.', 1); } else { modules.push('ssh2'); } }
if (passport != null) { modules.push(...passport); }
+ if (captcha == true) { modules.push('svg-captcha'); }
+
if (sessionRecording == true) { modules.push('image-size'); } // Need to get the remote desktop JPEG sizes to index the recodring file.
if (config.letsencrypt != null) { modules.push('acme-client'); } // Add acme-client module
if (config.settings.mqtt != null) { modules.push('aedes@0.39.0'); } // Add MQTT Modules
diff --git a/mpsserver.js b/mpsserver.js
index cacc4241..0fe4d631 100644
--- a/mpsserver.js
+++ b/mpsserver.js
@@ -58,7 +58,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
obj.server.listen(args.mpsport, args.mpsportbind, function () {
console.log("MeshCentral Intel(R) AMT server running on " + certificates.AmtMpsName + ":" + args.mpsport + ((args.mpsaliasport != null) ? (", alias port " + args.mpsaliasport) : "") + ".");
obj.parent.authLog('mps', 'Server listening on ' + ((args.mpsportbind != null) ? args.mpsportbind : '0.0.0.0') + ' port ' + args.mpsport + '.');
- }).on("error", function (err) { console.error("ERROR: MeshCentral Intel(R) AMT server port " + args.mpsport + " is not available."); if (args.exactports) { process.exit(); } });
+ }).on("error", function (err) { console.error("ERROR: MeshCentral Intel(R) AMT server port " + args.mpsport + " is not available. Check if the MeshCentral is already running."); if (args.exactports) { process.exit(); } });
obj.server.on('tlsClientError', function (err, tlssocket) { if (args.mpsdebug) { var remoteAddress = tlssocket.remoteAddress; if (tlssocket.remoteFamily == 'IPv6') { remoteAddress = '[' + remoteAddress + ']'; } console.log('MPS:Invalid TLS connection from ' + remoteAddress + ':' + tlssocket.remotePort + '.'); } });
}
diff --git a/package.json b/package.json
index c7126152..9eaa2dd4 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,8 @@
"sample-config-advanced.json"
],
"dependencies": {
+ "@crowdsec/express-bouncer": "^0.1.0",
+ "@yetzt/nedb": "^1.8.0",
"archiver": "^5.3.1",
"body-parser": "^1.19.0",
"cbor": "~5.2.0",
@@ -45,13 +47,21 @@
"express": "^4.17.0",
"express-handlebars": "^5.3.5",
"express-ws": "^4.0.0",
+ "image-size": "^1.0.1",
"ipcheck": "^0.1.0",
+ "loadavg-windows": "^1.1.1",
"minimist": "^1.2.5",
"multiparty": "^4.2.1",
- "@yetzt/nedb": "^1.8.0",
"node-forge": "^1.0.0",
+ "node-windows": "^0.1.4",
+ "otplib": "^10.2.3",
+ "pg": "^8.7.1",
+ "pgtools": "^0.3.2",
+ "ssh2": "^1.11.0",
+ "web-push": "^3.5.0",
"ws": "^5.2.3",
- "yauzl": "^2.10.0"
+ "yauzl": "^2.10.0",
+ "yubikeyotp": "^0.2.0"
},
"engines": {
"node": ">=10.0.0"
diff --git a/views/login.handlebars b/views/login.handlebars
index e364cd78..9d44fead 100644
--- a/views/login.handlebars
+++ b/views/login.handlebars
@@ -122,6 +122,14 @@
@@ -131,6 +139,7 @@
Back to login
+
@@ -311,6 +320,7 @@
var loginMode = '{{{loginmode}}}';
var newAccount = '{{{newAccount}}}';
var newAccountPass = parseInt('{{{newAccountPass}}}');
+ var newAccountCaptcha = '{{{newAccountCaptcha}}}';
var emailCheck = '{{{emailcheck}}}';
var passRequirements = '{{{passRequirements}}}';
var hardwareKeyChallenge = decodeURIComponent('{{{hkey}}}');
@@ -335,7 +345,7 @@
var i;
var messageid = parseInt('{{{messageid}}}');
var okmessages = ['', "If valid, reset mail sent.", "Email sent.", "Email verification required, check your mailbox and click the confirmation link.", "SMS 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.", "IP address blocked, try again later.", "Server under maintenance."];
+ 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.", "Server under maintenance.", "Unable to send device notification.", "Invalid security check."];
if (messageid > 0) {
var msg = '';
if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; }
@@ -433,6 +443,8 @@
QV('newAccountDiv', (newAccount === '1') || (newAccount === 'true')); // If new accounts are not allowed, don't display the new account link.
if ((passhint != null) && (passhint.length > 0)) { QV('showPassHintLink', true); }
QV('newAccountPass', (newAccountPass == 1));
+ QV('newAccountCaptcha', (newAccountCaptcha != ''));
+ QV('newAccountCaptchaImg', (newAccountCaptcha != ''));
QV('resetAccountDiv', (emailCheck == 'true'));
QV('hrAccountDiv', (emailCheck == 'true') || (newAccountPass == 1));
@@ -627,6 +639,7 @@
var pass1ok = (Q('apassword1').value.length > 0);
var pass2ok = (Q('apassword2').value.length > 0) && (Q('apassword2').value == Q('apassword1').value);
var newAccOk = (newAccountPass == 0) || (Q('anewaccountpass').value.length > 0);
+ var newCaptchaOk = (newAccountCaptcha == '') || (Q('anewaccountcaptcha').value.length > 0);
var ok = (userok && emailok && pass1ok && pass2ok && newAccOk);
// Color the fields
@@ -635,6 +648,7 @@
QS('nuPass1').color = pass1ok ? 'black' : '#7b241c';
QS('nuPass2').color = pass2ok ? 'black' : '#7b241c';
QS('nuToken').color = newAccOk ? 'black' : '#7b241c';
+ QS('nuCaptcha').color = newCaptchaOk ? 'black' : '#7b241c';
if (Q('apassword1').value == '') {
QH('passWarning', '');
@@ -663,13 +677,13 @@
}
}
if ((e != null) && (e.keyCode == 13)) {
-
if ((box == 1) && userok) { Q('aemail').focus(); }
if ((box == 2) && emailok) { Q('apassword1').focus(); }
if ((box == 3) && pass1ok) { Q('apassword2').focus(); }
if ((box == 4) && pass2ok) { if (passRequirements.hint === true) { Q('apasswordhint').focus(); } else { box = 5; } }
if (box == 5) { if (newAccountPass == 1) { Q('anewaccountpass').focus(); } else { box = 6; } }
- if (box == 6) { Q('createButton').click(); }
+ if (box == 6) { if (newAccountCaptcha != '') { Q('anewaccountcaptcha').focus(); } else { box = 7; } }
+ if (box == 7) { Q('createButton').click(); }
}
if (e != null) { haltEvent(e); }
QE('createButton', ok);
diff --git a/views/login2.handlebars b/views/login2.handlebars
index de42ce39..b267637d 100644
--- a/views/login2.handlebars
+++ b/views/login2.handlebars
@@ -144,6 +144,14 @@
| Creation Token: |
|
+
@@ -153,6 +161,7 @@
Back to login
+
@@ -368,6 +377,7 @@
var loginMode = '{{{loginmode}}}';
var newAccount = '{{{newAccount}}}';
var newAccountPass = parseInt('{{{newAccountPass}}}');
+ var newAccountCaptcha = '{{{newAccountCaptcha}}}';
var emailCheck = '{{{emailcheck}}}';
var passRequirements = '{{{passRequirements}}}';
var hardwareKeyChallenge = decodeURIComponent('{{{hkey}}}');
@@ -405,7 +415,7 @@
var i;
var messageid = parseInt('{{{messageid}}}');
var okmessages = ['', "If valid, reset mail sent.", "Email sent.", "Email verification required, check your mailbox and click the confirmation link.", "SMS sent.", "Sending notification..."];
- 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.", "Server under maintenance.", "Unable to send device notification."];
+ 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.", "Server under maintenance.", "Unable to send device notification.", "Invalid security check."];
if (messageid > 0) {
var msg = '';
if ((messageid < 100) && (messageid < okmessages.length)) { msg = okmessages[messageid]; }
@@ -487,6 +497,8 @@
QV('newAccountDiv', (newAccount === '1') || (newAccount === 'true')); // If new accounts are not allowed, don't display the new account link.
if ((passhint != null) && (passhint.length > 0)) { QV('showPassHintLink', true); }
QV('newAccountPass', (newAccountPass == 1));
+ QV('newAccountCaptcha', (newAccountCaptcha != ''));
+ QV('newAccountCaptchaImg', (newAccountCaptcha != ''));
QV('resetAccountDiv', (emailCheck == 'true'));
QV('hrAccountDiv', (emailCheck == 'true') || (newAccountPass == 1));
@@ -712,6 +724,7 @@
var pass1ok = (Q('apassword1').value.length > 0);
var pass2ok = (Q('apassword2').value.length > 0) && (Q('apassword2').value == Q('apassword1').value);
var newAccOk = (newAccountPass == 0) || (Q('anewaccountpass').value.length > 0);
+ var newCaptchaOk = (newAccountCaptcha == '') || (Q('anewaccountcaptcha').value.length > 0);
var ok = (userok && emailok && pass1ok && pass2ok && newAccOk);
// Color the fields
@@ -720,6 +733,7 @@
QS('nuPass1').color = pass1ok ? 'black' : '#7b241c';
QS('nuPass2').color = pass2ok ? 'black' : '#7b241c';
QS('nuToken').color = newAccOk ? 'black' : '#7b241c';
+ QS('nuCaptcha').color = newCaptchaOk ? 'black' : '#7b241c';
if (Q('apassword1').value == '') {
QH('passWarning', '');
@@ -748,13 +762,13 @@
}
}
if ((e != null) && (e.keyCode == 13)) {
-
if ((box == 1) && userok) { Q('aemail').focus(); }
if ((box == 2) && emailok) { Q('apassword1').focus(); }
if ((box == 3) && pass1ok) { Q('apassword2').focus(); }
if ((box == 4) && pass2ok) { if (passRequirements.hint === true) { Q('apasswordhint').focus(); } else { box = 5; } }
if (box == 5) { if (newAccountPass == 1) { Q('anewaccountpass').focus(); } else { box = 6; } }
- if (box == 6) { Q('createButton').click(); }
+ if (box == 6) { if (newAccountCaptcha != '') { Q('anewaccountcaptcha').focus(); } else { box = 7; } }
+ if (box == 7) { Q('createButton').click(); }
}
if (e != null) { haltEvent(e); }
QE('createButton', ok);
diff --git a/webserver.js b/webserver.js
index 15c6a039..fd919e50 100644
--- a/webserver.js
+++ b/webserver.js
@@ -1359,6 +1359,26 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
// If the email is the username, set this here.
if (domain.usernameisemail) { req.body.username = req.body.email; }
+ // Check if there is domain.newAccountToken, check if supplied token is valid
+ if ((domain.newaccountspass != null) && (domain.newaccountspass != '') && (req.body.anewaccountpass != domain.newaccountspass)) {
+ parent.debug('web', 'handleCreateAccountRequest: Invalid account creation token');
+ req.session.loginmode = 2;
+ req.session.messageid = 103; // Invalid account creation token.
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
+ return;
+ }
+
+ // If needed, check the new account creation CAPTCHA
+ if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
+ const c = parent.decodeCookie(req.body.captchaargs, parent.loginCookieEncryptionKey, 10); // 10 minute timeout
+ if ((c == null) || (c.type != 'newAccount') || (typeof c.captcha != 'string') || (c.captcha.length < 5) || (c.captcha != req.body.anewaccountcaptcha)) {
+ req.session.loginmode = 2;
+ req.session.messageid = 117; // Invalid security check
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
+ return;
+ }
+ }
+
// Accounts that start with ~ are not allowed
if ((typeof req.body.username != 'string') || (req.body.username.length < 1) || (req.body.username[0] == '~')) {
parent.debug('web', 'handleCreateAccountRequest: unable to create account (0)');
@@ -1423,14 +1443,6 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
req.session.messageid = 102; // Existing account with this email address.
if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
} else {
- // Check if there is domain.newAccountToken, check if supplied token is valid
- if ((domain.newaccountspass != null) && (domain.newaccountspass != '') && (req.body.anewaccountpass != domain.newaccountspass)) {
- parent.debug('web', 'handleCreateAccountRequest: Invalid account creation token');
- req.session.loginmode = 2;
- req.session.messageid = 103; // Invalid account creation token.
- if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
- return;
- }
// Check if user exists
if (obj.users['user/' + domain.id + '/' + req.body.username.toLowerCase()]) {
parent.debug('web', 'handleCreateAccountRequest: Username already exists');
@@ -3054,20 +3066,29 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
twoFactorTimeout = domain.passwordrequirements.twofactortimeout * 1000;
}
+ // Setup CAPTCHA if needed
+ var newAccountCaptcha = '', newAccountCaptchaImage = '';
+ if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
+ newAccountCaptcha = obj.parent.encodeCookie({ type: 'newAccount', captcha: require('svg-captcha').randomText(5) }, obj.parent.loginCookieEncryptionKey);
+ newAccountCaptchaImage = 'newAccountCaptcha.ashx?x=' + newAccountCaptcha;
+ }
+
// Render the login page
render(req, res,
getRenderPage((domain.sitestyle == 2) ? 'login2' : 'login', req, domain),
getRenderArgs({
loginmode: loginmode,
rootCertLink: getRootCertLink(domain),
- newAccount: newAccountsAllowed,
- newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1),
+ newAccount: newAccountsAllowed, // True if new accounts are allowed from the login page
+ newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), // 1 if new account creation requires password
+ newAccountCaptcha: newAccountCaptcha, // If new account creation requires a CAPTCHA, this string will not be empty
+ newAccountCaptchaImage: newAccountCaptchaImage, // Set to the URL of the CAPTCHA image
serverDnsName: obj.getWebServerName(domain),
serverPublicPort: httpsPort,
passlogin: (typeof domain.showpasswordlogin == 'boolean') ? domain.showpasswordlogin : true,
emailcheck: emailcheck,
features: features,
- sessiontime: (args.sessiontime) ? args.sessiontime : 60,
+ sessiontime: (args.sessiontime) ? args.sessiontime : 60, // Session time in minutes, 60 minutes is the default
passRequirements: passRequirements,
customui: customui,
footer: (domain.loginfooter == null) ? '' : domain.loginfooter,
@@ -3195,6 +3216,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
}
}
+ // Handle new account Captcha GET
+ function handleNewAccountCaptchaRequest(req, res) {
+ const domain = checkUserIpAddress(req, res);
+ if (domain == null) { return; }
+ if ((domain.newaccountscaptcha == null) || (domain.newaccountscaptcha === false) || (req.query.x == null)) { res.sendStatus(404); return; }
+ const c = obj.parent.decodeCookie(req.query.x, obj.parent.loginCookieEncryptionKey);
+ if ((c == null) || (c.type !== 'newAccount') || (typeof c.captcha != 'string')) { res.sendStatus(404); return; }
+ res.type('svg');
+ res.status(200).end(require('svg-captcha')(c.captcha, {}));
+ }
+
// Handle Captcha GET
function handleCaptchaGetRequest(req, res) {
const domain = checkUserIpAddress(req, res);
@@ -6104,6 +6136,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
obj.app.get(url + 'pluginHandler.js', obj.handlePluginJS);
}
+ // New account CAPTCHA request
+ if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
+ obj.app.get(url + 'newAccountCaptcha.ashx', handleNewAccountCaptchaRequest);
+ }
+
// Check CrowdSec Bounser if configured
if (parent.crowdSecBounser != null) {
obj.app.get(url + 'captcha.ashx', handleCaptchaGetRequest);
|