diff --git a/.greenlockrc b/.greenlockrc new file mode 100644 index 00000000..0e959e84 --- /dev/null +++ b/.greenlockrc @@ -0,0 +1,3 @@ +{ + "manager": "C:\\Users\\Default.DESKTOP-M9I88C9\\Desktop\\AmtWebApp\\meshcentral\\letsencrypt.js" +} \ No newline at end of file diff --git a/agents/MeshService.exe b/agents/MeshService.exe deleted file mode 100644 index 9cc1ca47..00000000 Binary files a/agents/MeshService.exe and /dev/null differ diff --git a/agents/meshcore.js b/agents/meshcore.js index 1bed88fc..ecf04708 100644 --- a/agents/meshcore.js +++ b/agents/meshcore.js @@ -1788,6 +1788,78 @@ function createMeshCore(agent) { response = 'Available commands: \r\n' + fin + '.'; break; } + case 'wallpaper': + if (process.platform != 'win32' && !(process.platform == 'linux' && require('linux-gnome-helpers').available)) + { + response = 'wallpaper command not supported on this platform' + } + else + { + if (args['_'].length != 1) + { + response = 'Proper usage: wallpaper (GET|TOGGLE)'; // Display usage + } + else + { + switch (args['_'][0].toUpperCase()) + { + default: + response = 'Proper usage: wallpaper (GET|TOGGLE)'; // Display usage + break; + case 'GET': + case 'TOGGLE': + if (process.platform == 'win32') + { + var id = require('user-sessions').getProcessOwnerName(process.pid).tsid == 0 ? 1 : 0; + var child = require('child_process').execFile(process.execPath, [process.execPath.split('\\').pop(), '-b64exec', 'dmFyIFNQSV9HRVRERVNLV0FMTFBBUEVSID0gMHgwMDczOwp2YXIgU1BJX1NFVERFU0tXQUxMUEFQRVIgPSAweDAwMTQ7CnZhciBHTSA9IHJlcXVpcmUoJ19HZW5lcmljTWFyc2hhbCcpOwp2YXIgdXNlcjMyID0gR00uQ3JlYXRlTmF0aXZlUHJveHkoJ3VzZXIzMi5kbGwnKTsKdXNlcjMyLkNyZWF0ZU1ldGhvZCgnU3lzdGVtUGFyYW1ldGVyc0luZm9BJyk7CgppZiAocHJvY2Vzcy5hcmd2Lmxlbmd0aCA9PSAzKQp7CiAgICB2YXIgdiA9IEdNLkNyZWF0ZVZhcmlhYmxlKDEwMjQpOwogICAgdXNlcjMyLlN5c3RlbVBhcmFtZXRlcnNJbmZvQShTUElfR0VUREVTS1dBTExQQVBFUiwgdi5fc2l6ZSwgdiwgMCk7CiAgICBjb25zb2xlLmxvZyh2LlN0cmluZyk7CiAgICBwcm9jZXNzLmV4aXQoKTsKfQplbHNlCnsKICAgIHZhciBuYiA9IEdNLkNyZWF0ZVZhcmlhYmxlKHByb2Nlc3MuYXJndlszXSk7CiAgICB1c2VyMzIuU3lzdGVtUGFyYW1ldGVyc0luZm9BKFNQSV9TRVRERVNLV0FMTFBBUEVSLCBuYi5fc2l6ZSwgbmIsIDApOwogICAgcHJvY2Vzcy5leGl0KCk7Cn0='], { type: id }); + child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); }); + child.stderr.on('data', function () { }); + child.waitExit(); + var current = child.stdout.str.trim(); + if (args['_'][0].toUpperCase() == 'GET') + { + response = current; + break; + } + if (current != '') + { + require('MeshAgent')._wallpaper = current; + response = 'Wallpaper cleared'; + } + else + { + response = 'Wallpaper restored'; + } + child = require('child_process').execFile(process.execPath, [process.execPath.split('\\').pop(), '-b64exec', 'dmFyIFNQSV9HRVRERVNLV0FMTFBBUEVSID0gMHgwMDczOwp2YXIgU1BJX1NFVERFU0tXQUxMUEFQRVIgPSAweDAwMTQ7CnZhciBHTSA9IHJlcXVpcmUoJ19HZW5lcmljTWFyc2hhbCcpOwp2YXIgdXNlcjMyID0gR00uQ3JlYXRlTmF0aXZlUHJveHkoJ3VzZXIzMi5kbGwnKTsKdXNlcjMyLkNyZWF0ZU1ldGhvZCgnU3lzdGVtUGFyYW1ldGVyc0luZm9BJyk7CgppZiAocHJvY2Vzcy5hcmd2Lmxlbmd0aCA9PSAzKQp7CiAgICB2YXIgdiA9IEdNLkNyZWF0ZVZhcmlhYmxlKDEwMjQpOwogICAgdXNlcjMyLlN5c3RlbVBhcmFtZXRlcnNJbmZvQShTUElfR0VUREVTS1dBTExQQVBFUiwgdi5fc2l6ZSwgdiwgMCk7CiAgICBjb25zb2xlLmxvZyh2LlN0cmluZyk7CiAgICBwcm9jZXNzLmV4aXQoKTsKfQplbHNlCnsKICAgIHZhciBuYiA9IEdNLkNyZWF0ZVZhcmlhYmxlKHByb2Nlc3MuYXJndlszXSk7CiAgICB1c2VyMzIuU3lzdGVtUGFyYW1ldGVyc0luZm9BKFNQSV9TRVRERVNLV0FMTFBBUEVSLCBuYi5fc2l6ZSwgbmIsIDApOwogICAgcHJvY2Vzcy5leGl0KCk7Cn0=', current != '' ? '""' : require('MeshAgent')._wallpaper], { type: id }); + child.stdout.str = ''; child.stdout.on('data', function (c) { this.str += c.toString(); }); + child.stderr.on('data', function () { }); + child.waitExit(); + } + else + { + var id = require('user-sessions').consoleUid(); + var current = require('linux-gnome-helpers').getDesktopWallpaper(id); + if (args['_'][0].toUpperCase() == 'GET') + { + response = current; + break; + } + if (current != '/dev/null') + { + require('MeshAgent')._wallpaper = current; + response = 'Wallpaper cleared'; + } + else + { + response = 'Wallpaper restored'; + } + require('linux-gnome-helpers').setDesktopWallpaper(id, current != '/dev/null' ? undefined : require('MeshAgent')._wallpaper); + } + break; + } + } + } + break; case 'safemode': if (process.platform != 'win32') { diff --git a/certoperations.js b/certoperations.js index bc5b19c5..8c96f436 100644 --- a/certoperations.js +++ b/certoperations.js @@ -313,7 +313,7 @@ module.exports.CertificateOperations = function (parent) { cert.setIssuer(attrs); // Create a root certificate //cert.setExtensions([{ name: "basicConstraints", cA: true }, { name: "nsCertType", sslCA: true, emailCA: true, objCA: true }, { name: "subjectKeyIdentifier" }]); - cert.setExtensions([{ name: "basicConstraints", cA: true }, { name: "subjectKeyIdentifier" }]); + cert.setExtensions([{ name: "basicConstraints", cA: true }, { name: "subjectKeyIdentifier" }, { name: "keyUsage", keyCertSign: true }]); cert.sign(keys.privateKey, obj.forge.md.sha384.create()); return { cert: cert, key: keys.privateKey }; @@ -418,6 +418,21 @@ module.exports.CertificateOperations = function (parent) { var rootPrivateKey = obj.fileLoad("root-cert-private.key", "utf8"); r.root = { cert: rootCertificate, key: rootPrivateKey }; rcount++; + + // Check if the root certificate has the "Certificate Signing (04)" Key usage. + // This option is required for newer versions of Intel AMT for CIRA/WS-EVENTS. + var xroot = obj.pki.certificateFromPem(rootCertificate); + var xext = xroot.getExtension("keyUsage"); + if ((xext == null) || (xext.keyCertSign !== true)) { + // We need to fix this certificate + console.log('Fixing root certificate to add signing key usage...'); + obj.fs.writeFileSync(parent.getConfigFilePath("root-cert-public-backup.crt"), rootCertificate); + xroot.setExtensions([{ name: "basicConstraints", cA: true }, { name: "subjectKeyIdentifier" }, { name: "keyUsage", keyCertSign: true }]); + var xrootPrivateKey = obj.pki.privateKeyFromPem(rootPrivateKey); + xroot.sign(xrootPrivateKey, obj.forge.md.sha384.create()); + r.root.cert = obj.pki.certificateToPem(xroot); + try { obj.fs.writeFileSync(parent.getConfigFilePath("root-cert-public.crt"), r.root.cert); } catch (ex) { } + } } if (args.tlsoffload) { diff --git a/db.js b/db.js index 2166ffa3..07a0e3f0 100644 --- a/db.js +++ b/db.js @@ -687,7 +687,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); }; @@ -703,6 +709,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) { + 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); }; @@ -852,6 +865,7 @@ module.exports.CreateDB = function (parent, func) { obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, func) { if (obj.databaseType == 1) { obj.eventsfile.find({ domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } }, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func); } else { obj.eventsfile.find({ domain: domain, nodeid: nodeid }, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func); } }; obj.RemoveAllEvents = function (domain) { obj.eventsfile.remove({ domain: domain }, { multi: true }); }; obj.RemoveAllNodeEvents = function (domain, nodeid) { obj.eventsfile.remove({ 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); }); } // Database actions on the power collection obj.getAllPower = function (func) { obj.powerfile.find({}, func); }; diff --git a/letsEncrypt.js b/letsEncrypt.js index e9448414..4f61782d 100644 --- a/letsEncrypt.js +++ b/letsEncrypt.js @@ -12,136 +12,292 @@ /*jshint node: true */ /*jshint strict: false */ /*jshint esversion: 6 */ -"use strict"; +'use strict'; module.exports.CreateLetsEncrypt = function (parent) { try { + // Get the GreenLock version + var greenLockVersion = null; + try { greenLockVersion = require('greenlock/package.json').version; } catch (ex) { } + if (greenLockVersion == null) { + parent.debug('cert', "Initializing Let's Encrypt support"); + } else { + parent.debug('cert', "Initializing Let's Encrypt support, using GreenLock v" + greenLockVersion); + } + + // Check the current node version and support for generateKeyPair + if (require('crypto').generateKeyPair == null) { return null; } + if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) < 10) { return null; } + // Try to delete the "./ursa-optional" or "./node_modules/ursa-optional" folder if present. // This is an optional module that GreenLock uses that causes issues. try { const fs = require('fs'); - if (fs.existsSync(obj.path.join(__dirname, 'ursa-optional'))) { fs.unlinkSync(obj.path.join(__dirname, 'ursa-optional')); } - if (fs.existsSync(obj.path.join(__dirname, 'node_modules', 'ursa-optional'))) { fs.unlinkSync(obj.path.join(__dirname, 'node_modules', 'ursa-optional')); } + if (fs.existsSync(parent.path.join(__dirname, 'ursa-optional'))) { fs.unlinkSync(obj.path.join(__dirname, 'ursa-optional')); } + if (fs.existsSync(parent.path.join(__dirname, 'node_modules', 'ursa-optional'))) { fs.unlinkSync(obj.path.join(__dirname, 'node_modules', 'ursa-optional')); } } catch (ex) { } // Get GreenLock setup and running. const greenlock = require('greenlock'); var obj = {}; obj.parent = parent; + obj.path = require('path'); obj.redirWebServerHooked = false; obj.leDomains = null; obj.leResults = null; + obj.leResultsStaging = null; + obj.performRestart = false; // Indicates we need to restart the server + obj.performMoveToProduction = false; // Indicates we just got a staging certificate and need to move to production + obj.runAsProduction = false; // This starts at false and moves to true if staging cert is ok. // Setup the certificate storage paths - obj.configPath = obj.parent.path.join(obj.parent.datapath, 'letsencrypt'); - obj.webrootPath = obj.parent.path.join(obj.parent.datapath, 'letsencrypt', 'webroot'); + obj.configPath = obj.path.join(obj.parent.datapath, 'letsencrypt3'); try { obj.parent.fs.mkdirSync(obj.configPath); } catch (e) { } - try { obj.parent.fs.mkdirSync(obj.webrootPath); } catch (e) { } + obj.configPathStaging = obj.path.join(obj.parent.datapath, 'letsencrypt3-staging'); + try { obj.parent.fs.mkdirSync(obj.configPathStaging); } catch (e) { } - // Storage Backend, store data in the "meshcentral-data/letencrypt" folder. - var leStore = require('le-store-certbot').create({ configDir: obj.configPath, webrootPath: obj.webrootPath, debug: obj.parent.args.debug > 0 }); + // Setup Let's Encrypt default configuration + obj.leDefaults = { agreeToTerms: true, store: { module: 'greenlock-store-fs', basePath: obj.configPath } }; + obj.leDefaultsStaging = { agreeToTerms: true, store: { module: 'greenlock-store-fs', basePath: obj.configPathStaging } }; - // ACME Challenge Handlers - var leHttpChallenge = require('le-challenge-fs').create({ webrootPath: obj.webrootPath, debug: obj.parent.args.debug > 0 }); + // Get package and maintainer email + const pkg = require('./package.json'); + var maintainerEmail = null; + if (typeof pkg.author == 'string') { + // Older NodeJS + maintainerEmail = pkg.author; + var i = maintainerEmail.indexOf('<'); + if (i >= 0) { maintainerEmail = maintainerEmail.substring(i + 1); } + var i = maintainerEmail.indexOf('>'); + if (i >= 0) { maintainerEmail = maintainerEmail.substring(0, i); } + } else if (typeof pkg.author == 'object') { + // Latest NodeJS + maintainerEmail = pkg.author.email; + } - // Function to agree to terms of service - function leAgree(opts, agreeCb) { agreeCb(null, opts.tosUrl); } + // Check if we need to be in debug mode + var ledebug = false; + try { ledebug = ((obj.parent.args.debug != null) || (obj.parent.args.debug.indexOf('cert'))); } catch (ex) { } - // Create the main GreenLock code module. + // Create the main GreenLock code module for production. var greenlockargs = { - version: 'draft-12', - server: (obj.parent.config.letsencrypt.production === true) ? 'https://acme-v02.api.letsencrypt.org/directory' : 'https://acme-staging-v02.api.letsencrypt.org/directory', - store: leStore, - challenges: { 'http-01': leHttpChallenge }, - challengeType: 'http-01', - agreeToTerms: leAgree, - debug: obj.parent.args.debug > 0 + parent: obj, + packageRoot: __dirname, + packageAgent: pkg.name + '/' + pkg.version, + manager: obj.path.join(__dirname, 'letsencrypt.js'), + maintainerEmail: maintainerEmail, + notify: function (ev, args) { if (typeof args == 'string') { parent.debug('cert', ev + ': ' + args); } else { parent.debug('cert', ev + ': ' + JSON.stringify(args)); } }, + staging: false, + debug: ledebug }; if (obj.parent.args.debug == null) { greenlockargs.log = function (debug) { }; } // If not in debug mode, ignore all console output from greenlock (makes things clean). obj.le = greenlock.create(greenlockargs); - // Hook up GreenLock to the redirection server - if (obj.parent.redirserver.port == 80) { obj.parent.redirserver.app.use('/', obj.le.middleware()); obj.redirWebServerHooked = true; } + // Create the main GreenLock code module for staging. + var greenlockargsstaging = { + parent: obj, + packageRoot: __dirname, + packageAgent: pkg.name + '/' + pkg.version, + manager: obj.path.join(__dirname, 'letsencrypt.js'), + maintainerEmail: maintainerEmail, + notify: function (ev, args) { if (typeof args == 'string') { parent.debug('cert', 'Notify: ' + ev + ': ' + args); } else { parent.debug('cert', 'Notify: ' + ev + ': ' + JSON.stringify(args)); } }, + staging: true, + debug: ledebug + }; + if (obj.parent.args.debug == null) { greenlockargsstaging.log = function (debug) { }; } // If not in debug mode, ignore all console output from greenlock (makes things clean). + obj.leStaging = greenlock.create(greenlockargsstaging); - obj.getCertificate = function (certs, func) { + // Hook up GreenLock to the redirection server + if (obj.parent.config.settings.rediraliasport === 80) { obj.redirWebServerHooked = true; } + else if ((obj.parent.config.settings.rediraliasport == null) && (obj.parent.redirserver.port == 80)) { obj.redirWebServerHooked = true; } + + // Respond to a challenge + obj.challenge = function (token, hostname, func) { + if (obj.runAsProduction === true) { + // Production + parent.debug('cert', "Challenge " + hostname + "/" + token); + obj.le.challenges.get({ type: 'http-01', servername: hostname, token: token }) + .then(function (results) { func(results.keyAuthorization); }) + .catch(function (e) { console.log('LE-ERROR', e); func(null); }); // unexpected error, not related to renewal + } else { + // Staging + parent.debug('cert', "Challenge " + hostname + "/" + token); + obj.leStaging.challenges.get({ type: 'http-01', servername: hostname, token: token }) + .then(function (results) { func(results.keyAuthorization); }) + .catch(function (e) { console.log('LE-ERROR', e); func(null); }); // unexpected error, not related to renewal + } + } + + obj.getCertificate = function(certs, func) { + parent.debug('cert', "Getting certs from local store"); if (certs.CommonName.indexOf('.') == -1) { console.log("ERROR: Use --cert to setup the default server name before using Let's Encrypt."); func(certs); return; } if (obj.parent.config.letsencrypt == null) { func(certs); return; } if (obj.parent.config.letsencrypt.email == null) { console.log("ERROR: Let's Encrypt email address not specified."); func(certs); return; } - if ((obj.parent.redirserver == null) || (obj.parent.redirserver.port !== 80)) { console.log("ERROR: Redirection web server must be active on port 80 for Let's Encrypt to work."); func(certs); return; } + if ((obj.parent.redirserver == null) || ((typeof obj.parent.config.settings.rediraliasport === 'number') && (obj.parent.config.settings.rediraliasport !== 80)) || ((obj.parent.config.settings.rediraliasport == null) && (obj.parent.redirserver.port !== 80))) { console.log("ERROR: Redirection web server must be active on port 80 for Let's Encrypt to work."); func(certs); return; } if (obj.redirWebServerHooked !== true) { console.log("ERROR: Redirection web server not setup for Let's Encrypt to work."); func(certs); return; } if ((obj.parent.config.letsencrypt.rsakeysize != null) && (obj.parent.config.letsencrypt.rsakeysize !== 2048) && (obj.parent.config.letsencrypt.rsakeysize !== 3072)) { console.log("ERROR: Invalid Let's Encrypt certificate key size, must be 2048 or 3072."); func(certs); return; } // Get the list of domains - obj.leDomains = [certs.CommonName]; + obj.leDomains = [ certs.CommonName ]; if (obj.parent.config.letsencrypt.names != null) { if (typeof obj.parent.config.letsencrypt.names == 'string') { obj.parent.config.letsencrypt.names = obj.parent.config.letsencrypt.names.split(','); } obj.parent.config.letsencrypt.names.map(function (s) { return s.trim(); }); // Trim each name if ((typeof obj.parent.config.letsencrypt.names != 'object') || (obj.parent.config.letsencrypt.names.length == null)) { console.log("ERROR: Let's Encrypt names must be an array in config.json."); func(certs); return; } obj.leDomains = obj.parent.config.letsencrypt.names; - obj.leDomains.sort(); // Sort the array so it's always going to be in the same order. } - obj.le.check({ domains: obj.leDomains }).then(function (results) { - if (results) { - obj.leResults = results; + if (obj.parent.config.letsencrypt.production !== true) { + // We are in staging mode, just go ahead + obj.getCertificateEx(certs, func); + } else { + // We are really in production mode + if (obj.runAsProduction === true) { + // Staging cert check must have been done already, move to production + obj.getCertificateEx(certs, func); + } else { + // Perform staging certificate check + parent.debug('cert', "Checking staging certificate " + obj.leDomains[0] + "..."); + obj.leStaging.get({ servername: obj.leDomains[0] }) + .then(function (results) { + if (results != null) { + // We have a staging certificate, move to production for real + parent.debug('cert', "Staging certificate is present, moving to production..."); + obj.runAsProduction = true; + obj.getCertificateEx(certs, func); + } else { + // No staging certificate + parent.debug('cert', "No staging certificate present"); + func(certs); + setTimeout(obj.checkRenewCertificate, 10000); // Check the certificate in 10 seconds. + } + }) + .catch(function (e) { + // No staging certificate + parent.debug('cert', "No staging certificate present"); + func(certs); + setTimeout(obj.checkRenewCertificate, 10000); // Check the certificate in 10 seconds. + }); + } + } + } - // If we already have real certificates, use them. - if (results.altnames.indexOf(certs.CommonName) >= 0) { - certs.web.cert = results.cert; - certs.web.key = results.privkey; - certs.web.ca = [results.chain]; - } - for (var i in obj.parent.config.domains) { - if ((obj.parent.config.domains[i].dns != null) && (obj.parent.certificateOperations.compareCertificateNames(results.altnames, obj.parent.config.domains[i].dns))) { - certs.dns[i].cert = results.cert; - certs.dns[i].key = results.privkey; - certs.dns[i].ca = [results.chain]; + obj.getCertificateEx = function (certs, func) { + // Get the Let's Encrypt certificate from our own storage + const xle = (obj.runAsProduction === true)? obj.le : obj.leStaging; + xle.get({ servername: obj.leDomains[0] }) + .then(function (results) { + // If we already have real certificates, use them + if (results) { + if (results.site.altnames.indexOf(certs.CommonName) >= 0) { + certs.web.cert = results.pems.cert; + certs.web.key = results.pems.privkey; + certs.web.ca = [results.pems.chain]; + } + for (var i in obj.parent.config.domains) { + if ((obj.parent.config.domains[i].dns != null) && (obj.parent.certificateOperations.compareCertificateNames(results.site.altnames, obj.parent.config.domains[i].dns))) { + certs.dns[i].cert = results.pems.cert; + certs.dns[i].key = results.pems.privkey; + certs.dns[i].ca = [results.pems.chain]; + } } } + parent.debug('cert', "Got certs from local store (" + (obj.runAsProduction ? "Production" : "Staging") + ")"); func(certs); // Check if the Let's Encrypt certificate needs to be renewed. setTimeout(obj.checkRenewCertificate, 60000); // Check in 1 minute. setInterval(obj.checkRenewCertificate, 86400000); // Check again in 24 hours and every 24 hours. return; - } else { - // Otherwise return default certificates and try to get a real one + }) + .catch(function (e) { + parent.debug('cert', "Unable to get certs from local store (" + (obj.runAsProduction ? "Production" : "Staging") + ")"); + setTimeout(obj.checkRenewCertificate, 10000); // Check the certificate in 10 seconds. func(certs); - } - console.log("Attempting to get Let's Encrypt certificate, may take a few minutes..."); - - // Figure out the RSA key size - var rsaKeySize = (obj.parent.config.letsencrypt.rsakeysize === 2048) ? 2048 : 3072; - - // TODO: Only register on one of the peers if multi-peers are active. - // Register Certificate manually - obj.le.register({ - domains: obj.leDomains, - email: obj.parent.config.letsencrypt.email, - agreeTos: true, - rsaKeySize: rsaKeySize, - challengeType: 'http-01', - renewWithin: 45 * 24 * 60 * 60 * 1000, // Certificate renewal may begin at this time (45 days) - renewBy: 60 * 24 * 60 * 60 * 1000 // Certificate renewal should happen by this time (60 days) - }).then(function (xresults) { - obj.parent.performServerCertUpdate(); // Reset the server, TODO: Reset all peers - }, function (err) { - console.error("ERROR: Let's encrypt error: ", err); }); - }); - }; + } // Check if we need to renew the certificate, call this every day. obj.checkRenewCertificate = function () { - if (obj.leResults == null) { return; } - // TODO: Only renew on one of the peers if multi-peers are active. - // Check if we need to renew the certificate - obj.le.renew({ duplicate: false, domains: obj.leDomains, email: obj.parent.config.letsencrypt.email }, obj.leResults).then(function (xresults) { - obj.parent.performServerCertUpdate(); // Reset the server, TODO: Reset all peers - }, function (err) { }); // If we can't renew, ignore. - }; + parent.debug('cert', "Checking certificate for " + obj.leDomains[0] + " (" + (obj.runAsProduction ? "Production" : "Staging") + ")"); + + // Setup renew options + obj.certCheckStart = Date.now(); + const xle = (obj.runAsProduction === true) ? obj.le : obj.leStaging; + var renewOptions = { servername: obj.leDomains[0], altnames: obj.leDomains }; + try { + xle.renew(renewOptions) + .then(function (results) { + if ((results == null) || (typeof results != 'object') || (results.length == 0) || (results[0].error != null)) { + parent.debug('cert', "Unable to get a certificate (" + (obj.runAsProduction ? "Production" : "Staging") + ", " + (Date.now() - obj.certCheckStart) + "ms): " + JSON.stringify(results)); + } else { + parent.debug('cert', "Checks completed (" + (obj.runAsProduction ? "Production" : "Staging") + ", " + (Date.now() - obj.certCheckStart) + "ms): " + JSON.stringify(results)); + if (obj.performRestart === true) { parent.debug('cert', "Certs changed, restarting..."); obj.parent.performServerCertUpdate(); } // Reset the server, TODO: Reset all peers + else if (obj.performMoveToProduction == true) { + parent.debug('cert', "Staging certificate received, moving to production..."); + obj.runAsProduction = true; + obj.performMoveToProduction = false; + obj.performRestart = true; + setTimeout(obj.checkRenewCertificate, 10000); // Check the certificate in 10 seconds. + } + } + }) + .catch(function (ex) { + parent.debug('cert', "checkCertificate exception: (" + JSON.stringify(ex) + ")"); + console.log(ex); + }); + } catch (ex) { + parent.debug('cert', "checkCertificate main exception: (" + JSON.stringify(ex) + ")"); + console.log(ex); + } + } return obj; } catch (ex) { console.log(ex); } // Unable to start Let's Encrypt return null; +}; + +// GreenLock v3 Manager +module.exports.create = function (options) { + var manager = { parent: options.parent }; + manager.find = async function (options) { + //console.log('LE-FIND', options); + return Promise.resolve([{ subject: options.servername, altnames: options.altnames }]); + }; + + manager.set = function (options) { + manager.parent.parent.debug('cert', "Certificate has been set: " + JSON.stringify(options)); + if (manager.parent.parent.config.letsencrypt.production == manager.parent.runAsProduction) { manager.parent.performRestart = true; } + else if ((manager.parent.parent.config.letsencrypt.production === true) && (manager.parent.runAsProduction === false)) { manager.parent.performMoveToProduction = true; } + return null; + }; + + manager.remove = function (options) { + manager.parent.parent.debug('cert', "Certificate has been removed: " + JSON.stringify(options)); + if (manager.parent.parent.config.letsencrypt.production == manager.parent.runAsProduction) { manager.parent.performRestart = true; } + else if ((manager.parent.parent.config.letsencrypt.production === true) && (manager.parent.runAsProduction === false)) { manager.parent.performMoveToProduction = true; } + return null; + }; + + // set the global config + manager.defaults = async function (options) { + var r; + if (manager.parent.runAsProduction === true) { + // Production + //console.log('LE-DEFAULTS-Production', options); + if (options != null) { for (var i in options) { if (manager.parent.leDefaults[i] == null) { manager.parent.leDefaults[i] = options[i]; } } } + r = manager.parent.leDefaults; + r.subscriberEmail = manager.parent.parent.config.letsencrypt.email; + r.sites = { mainsite: { subject: manager.parent.leDomains[0], altnames: manager.parent.leDomains } }; + } else { + // Staging + //console.log('LE-DEFAULTS-Staging', options); + if (options != null) { for (var i in options) { if (manager.parent.leDefaultsStaging[i] == null) { manager.parent.leDefaultsStaging[i] = options[i]; } } } + r = manager.parent.leDefaultsStaging; + r.subscriberEmail = manager.parent.parent.config.letsencrypt.email; + r.sites = { mainsite: { subject: manager.parent.leDomains[0], altnames: manager.parent.leDomains } }; + } + return r; + }; + + return manager; }; \ No newline at end of file diff --git a/meshcentral.js b/meshcentral.js index a1ceee15..3fe365bf 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -117,7 +117,7 @@ function CreateMeshCentralServer(config, args) { try { require('./pass').hash('test', function () { }, 0); } catch (e) { console.log('Old version of node, must upgrade.'); return; } // TODO: Not sure if this test works or not. // Check for invalid arguments - var validArguments = ['_', 'notls', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'shownodes', 'showmeshes', 'showevents', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport', 'dbmerge', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'logintoken', 'logintokenkey', 'logintokengen', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'vaultpushconfigfiles', 'vaultpullconfigfiles', 'vaultdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'memorytracking', 'serverid', 'recordencryptionrecode', 'vault', 'token', 'unsealkey', 'name']; + var validArguments = ['_', 'notls', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'shownodes', 'showmeshes', 'showevents', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'xinstall', 'xuninstall', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport', 'dbmerge', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'logintoken', 'logintokenkey', 'logintokengen', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'vaultpushconfigfiles', 'vaultpullconfigfiles', 'vaultdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'memorytracking', 'serverid', 'recordencryptionrecode', 'vault', 'token', 'unsealkey', 'name', 'log']; for (var arg in obj.args) { obj.args[arg.toLocaleLowerCase()] = obj.args[arg]; if (validArguments.indexOf(arg.toLocaleLowerCase()) == -1) { console.log('Invalid argument "' + arg + '", use --help.'); return; } } if (obj.args.mongodb == true) { console.log('Must specify: --mongodb [connectionstring] \r\nSee https://docs.mongodb.com/manual/reference/connection-string/ for MongoDB connection string.'); return; } for (i in obj.config.settings) { obj.args[i] = obj.config.settings[i]; } // Place all settings into arguments, arguments have already been placed into settings so arguments take precedence. @@ -144,23 +144,53 @@ function CreateMeshCentralServer(config, args) { return; } - // Check if we need to install, start, stop, remove ourself as a background service - if ((obj.service != null) && ((obj.args.install == true) || (obj.args.uninstall == true) || (obj.args.start == true) || (obj.args.stop == true) || (obj.args.restart == true))) { - var env = [], xenv = ['user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'exactport', 'debug']; - for (i in xenv) { if (obj.args[xenv[i]] != null) { env.push({ name: 'mesh' + xenv[i], value: obj.args[xenv[i]] }); } } // Set some args as service environement variables. - var svc = new obj.service({ name: 'MeshCentral', description: 'MeshCentral Remote Management Server', script: obj.path.join(__dirname, 'winservice.js'), env: env, wait: 2, grow: 0.5 }); - svc.on('install', function () { console.log('MeshCentral service installed.'); svc.start(); }); - svc.on('uninstall', function () { console.log('MeshCentral service uninstalled.'); process.exit(); }); - svc.on('start', function () { console.log('MeshCentral service started.'); process.exit(); }); - svc.on('stop', function () { console.log('MeshCentral service stopped.'); if (obj.args.stop) { process.exit(); } if (obj.args.restart) { console.log('Holding 5 seconds...'); setTimeout(function () { svc.start(); }, 5000); } }); - svc.on('alreadyinstalled', function () { console.log('MeshCentral service already installed.'); process.exit(); }); - svc.on('invalidinstallation', function () { console.log('Invalid MeshCentral service installation.'); process.exit(); }); + if (obj.service != null) { + // Check if we need to install, start, stop, remove ourself as a background service + if (((obj.args.xinstall == true) || (obj.args.xuninstall == true) || (obj.args.start == true) || (obj.args.stop == true) || (obj.args.restart == true))) { + var env = [], xenv = ['user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'exactport', 'rediraliasport', 'debug']; + for (i in xenv) { if (obj.args[xenv[i]] != null) { env.push({ name: 'mesh' + xenv[i], value: obj.args[xenv[i]] }); } } // Set some args as service environement variables. + var svc = new obj.service({ name: 'MeshCentral', description: 'MeshCentral Remote Management Server', script: obj.path.join(__dirname, 'winservice.js'), env: env, wait: 2, grow: 0.5 }); + svc.on('install', function () { console.log('MeshCentral service installed.'); svc.start(); }); + svc.on('uninstall', function () { console.log('MeshCentral service uninstalled.'); process.exit(); }); + svc.on('start', function () { console.log('MeshCentral service started.'); process.exit(); }); + svc.on('stop', function () { console.log('MeshCentral service stopped.'); if (obj.args.stop) { process.exit(); } if (obj.args.restart) { console.log('Holding 5 seconds...'); setTimeout(function () { svc.start(); }, 5000); } }); + svc.on('alreadyinstalled', function () { console.log('MeshCentral service already installed.'); process.exit(); }); + svc.on('invalidinstallation', function () { console.log('Invalid MeshCentral service installation.'); process.exit(); }); - if (obj.args.install == true) { try { svc.install(); } catch (e) { logException(e); } } - if (obj.args.stop == true || obj.args.restart == true) { try { svc.stop(); } catch (e) { logException(e); } } - if (obj.args.start == true || obj.args.restart == true) { try { svc.start(); } catch (e) { logException(e); } } - if (obj.args.uninstall == true) { try { svc.uninstall(); } catch (e) { logException(e); } } - return; + if (obj.args.xinstall == true) { try { svc.install(); } catch (e) { logException(e); } } + if (obj.args.stop == true || obj.args.restart == true) { try { svc.stop(); } catch (e) { logException(e); } } + if (obj.args.start == true || obj.args.restart == true) { try { svc.start(); } catch (e) { logException(e); } } + if (obj.args.xuninstall == true) { try { svc.uninstall(); } catch (e) { logException(e); } } + return; + } + + // Windows service install using the external winservice.js + if (obj.args.install == true) { + console.log('Installing MeshCentral as Windows Service...'); + if (obj.fs.existsSync(obj.path.join(__dirname, '../WinService')) == false) { try { obj.fs.mkdirSync(obj.path.join(__dirname, '../WinService')); } catch (ex) { console.log('ERROR: Unable to create WinService folder: ' + ex); process.exit(); return; } } + try { obj.fs.createReadStream(obj.path.join(__dirname, 'winservice.js')).pipe(obj.fs.createWriteStream(obj.path.join(__dirname, '../WinService/winservice.js'))); } catch (ex) { console.log('ERROR: Unable to copy winservice.js: ' + ex); process.exit(); return; } + require('child_process').exec('node winservice.js --install', { maxBuffer: 512000, timeout: 120000, cwd: obj.path.join(__dirname, '../WinService') }, function (error, stdout, stderr) { + if ((error != null) && (error != '')) { console.log('ERROR: Unable to install MeshCentral as a service: ' + error); process.exit(); return; } + console.log(stdout); + }); + return; + } else if (obj.args.uninstall == true) { + console.log('Uninstalling MeshCentral Windows Service...'); + if (obj.fs.existsSync(obj.path.join(__dirname, '../WinService')) == true) { + require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: obj.path.join(__dirname, '../WinService') }, function (error, stdout, stderr) { + if ((error != null) && (error != '')) { console.log('ERROR: Unable to uninstall MeshCentral service: ' + error); process.exit(); return; } + console.log(stdout); + try { obj.fs.unlinkSync(obj.path.join(__dirname, '../WinService/winservice.js')); } catch (ex) { } + try { obj.fs.rmdirSync(obj.path.join(__dirname, '../WinService')); } catch (ex) { } + }); + } else { + require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: __dirname }, function (error, stdout, stderr) { + if ((error != null) && (error != '')) { console.log('ERROR: Unable to uninstall MeshCentral service: ' + error); process.exit(); return; } + console.log(stdout); + }); + } + return; + } } // If "--launch" is in the arguments, launch now @@ -677,7 +707,7 @@ function CreateMeshCentralServer(config, args) { } // Read environment variables. For a subset of arguments, we allow them to be read from environment variables. - var xenv = ['user', 'port', 'mpsport', 'mpsaliasport', 'redirport', 'exactport', 'debug']; + var xenv = ['user', 'port', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'exactport', 'debug']; for (i in xenv) { if ((obj.args[xenv[i]] == null) && (process.env['mesh' + xenv[i]])) { obj.args[xenv[i]] = obj.common.toNumber(process.env['mesh' + xenv[i]]); } } // Validate the domains, this is used for multi-hosting @@ -735,6 +765,7 @@ function CreateMeshCentralServer(config, args) { if (obj.args.aliasport != null && (typeof obj.args.aliasport != 'number')) obj.args.aliasport = null; if (obj.args.mpsport == null || typeof obj.args.mpsport != 'number') obj.args.mpsport = 4433; if (obj.args.mpsaliasport != null && (typeof obj.args.mpsaliasport != 'number')) obj.args.mpsaliasport = null; + if (obj.args.rediraliasport != null && (typeof obj.args.rediraliasport != 'number')) obj.args.rediraliasport = null; if (obj.args.notls == null && obj.args.redirport == null) obj.args.redirport = 80; if (obj.args.minifycore === 0) obj.args.minifycore = false; if (typeof args.agentidletimeout != 'number') { args.agentidletimeout = 150000; } else { args.agentidletimeout *= 1000 } // Default agent idle timeout is 2m, 30sec. @@ -830,7 +861,9 @@ function CreateMeshCentralServer(config, args) { // Load server certificates obj.certificateOperations = require('./certoperations.js').CertificateOperations(obj); obj.certificateOperations.GetMeshServerCertificate(obj.args, obj.config, function (certs) { - if ((obj.config.letsencrypt == null) || (obj.redirserver == null)) { + // Get the current node version + const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); + if ((nodeVersion < 8) || (require('crypto').generateKeyPair == null) || (obj.config.letsencrypt == null) || (obj.redirserver == null)) { obj.StartEx3(certs); // Just use the configured certificates } else { var le = require('./letsencrypt.js'); @@ -1831,6 +1864,27 @@ function CreateMeshCentralServer(config, args) { // Send event to console if ((obj.debugSources != null) && ((obj.debugSources == '*') || (obj.debugSources.indexOf(source) >= 0))) { console.log(source.toUpperCase() + ':', ...args); } + // Send event to log file + if (obj.config.settings && obj.config.settings.log) { + if (typeof obj.args.log == 'string') { obj.args.log = obj.args.log.split(','); } + if (obj.args.log.indexOf(source) >= 0) { + const d = new Date(); + if (obj.xxLogFile == null) { + try { + obj.xxLogFile = obj.fs.openSync(obj.getConfigFilePath('log.txt'), 'a+', 666); + obj.fs.writeSync(obj.xxLogFile, '---- Log start at ' + new Date().toLocaleString() + ' ----\r\n'); + obj.xxLogDateStr = d.toLocaleDateString(); + } catch (ex) { } + } + if (obj.xxLogFile != null) { + try { + if (obj.xxLogDateStr != d.toLocaleDateString()) { obj.xxLogDateStr = d.toLocaleDateString(); obj.fs.writeSync(obj.xxLogFile, '---- ' + d.toLocaleDateString() + ' ----\r\n'); } + obj.fs.writeSync(obj.xxLogFile, new Date().toLocaleTimeString() + ' - ' + source + ': ' + Array.prototype.slice.call(...args).join('') + '\r\n'); + } catch (ex) { } + } + } + } + // Send the event to logged in administrators if ((obj.debugRemoteSources != null) && ((obj.debugRemoteSources == '*') || (obj.debugRemoteSources.indexOf(source) >= 0))) { var sendcount = 0; @@ -1932,8 +1986,17 @@ function InstallModules(modules, func) { var missingModules = []; if (modules.length > 0) { for (var i in modules) { + // Modules may contain a version tag (foobar@1.0.0), remove it so the module can be found using require + var moduleName = modules[i].split("@", 1)[0]; try { - var xxmodule = require(modules[i]); + if (moduleName == 'greenlock') { + // Check if we have GreenLock v3 + delete require.cache[require.resolve('greenlock')]; // Clear the require cache + if (typeof require('greenlock').challengeType == 'string') { missingModules.push(modules[i]); } + } else { + // For all other modules, do the check here. + require(moduleName); + } } catch (e) { if (previouslyInstalledModules[modules[i]] !== true) { missingModules.push(modules[i]); } } @@ -1943,7 +2006,6 @@ function InstallModules(modules, func) { } // Check if a module is present and install it if missing -var InstallModuleChildProcess = null; function InstallModule(modulename, func, tag1, tag2) { console.log('Installing ' + modulename + '...'); var child_process = require('child_process'); @@ -1952,9 +2014,7 @@ function InstallModule(modulename, func, tag1, tag2) { // Get the working directory if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); } - // Looks like we need to keep a global reference to the child process object for this to work correctly. - InstallModuleChildProcess = child_process.exec('npm install --no-optional --save ' + modulename, { maxBuffer: 512000, timeout: 10000, cwd: parentpath }, function (error, stdout, stderr) { - InstallModuleChildProcess = null; + child_process.exec(`npm install --no-optional ${modulename}`, { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { if ((error != null) && (error != '')) { console.log('ERROR: Unable to install required module "' + modulename + '". MeshCentral may not have access to npm, or npm may not have suffisent rights to load the new module. Try "npm install ' + modulename + '" to manualy install this module.\r\n'); process.exit(); @@ -2002,20 +2062,20 @@ function mainStart() { if (config.domains[i].auth == 'ldap') { ldap = true; } } + // Get the current node version + var nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); + // Build the list of required modules var modules = ['ws', 'cbor', 'nedb', 'https', 'yauzl', 'xmldom', 'ipcheck', 'express', 'archiver', 'multiparty', 'node-forge', 'express-ws', 'compression', 'body-parser', 'connect-redis', 'cookie-session', 'express-handlebars']; if (require('os').platform() == 'win32') { modules.push('node-windows'); if (sspi == true) { modules.push('node-sspi'); } } // Add Windows modules if (ldap == true) { modules.push('ldapauth-fork'); } - if (config.letsencrypt != null) { modules.push('greenlock'); modules.push('le-store-certbot'); modules.push('le-challenge-fs'); modules.push('le-acme-core'); } // Add Greenlock Modules + if (config.letsencrypt != null) { if ((nodeVersion < 10) || (require('crypto').generateKeyPair == null)) { if (!args.launch) { console.log("WARNING: Let's Encrypt support requires Node v10.12.0 or higher."); } } else { modules.push('greenlock'); } } // Add Greenlock Module if (config.settings.mqtt != null) { modules.push('aedes'); } // Add MQTT Modules if (config.settings.mongodb != null) { modules.push('mongodb'); } // Add MongoDB, official driver. if (config.settings.vault != null) { modules.push('node-vault'); } // Add official HashiCorp's Vault module. else if (config.settings.xmongodb != null) { modules.push('mongojs'); } // Add MongoJS, old driver. if (config.smtp != null) { modules.push('nodemailer'); } // Add SMTP support - // Get the current node version - var nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); - // If running NodeJS < 8, install "util.promisify" if (nodeVersion < 8) { modules.push('util.promisify'); } @@ -2028,7 +2088,7 @@ function mainStart() { if (yubikey == true) { modules.push('yubikeyotp'); } // Add YubiKey OTP support if (allsspi == false) { modules.push('otplib'); } // Google Authenticator support } - + // Install any missing modules and launch the server InstallModules(modules, function () { meshserver = CreateMeshCentralServer(config, args); meshserver.Start(); }); diff --git a/meshuser.js b/meshuser.js index 163ad47b..d2a00731 100644 --- a/meshuser.js +++ b/meshuser.js @@ -360,6 +360,14 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use try { ws.send(JSON.stringify({ action: 'traceinfo', traceSources: parent.parent.debugRemoteSources })); } catch (ex) { } } + // See how many times bad login attempts where made since the last login + const lastLoginTime = parent.users[user._id].pastlogin; + if (lastLoginTime != null) { + db.GetFailedLoginCount(user.name, user.domain, new Date(lastLoginTime * 1000), function (count) { + if (count > 0) { try { ws.send(JSON.stringify({ action: 'msg', type: 'notify', title: "Security Warning", tag: 'ServerNotify', value: "There has been " + count + " failed login attempts on this account since the last login." })); } catch (ex) { } delete user.pastlogin; } + }); + } + // We are all set, start receiving data ws._socket.resume(); }); @@ -681,14 +689,31 @@ 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': { + if (typeof parent.parent.config.settings.maxinvalidlogin.coolofftime == 'number') { + r = "Max is " + parent.parent.config.settings.maxinvalidlogin.count + " bad login(s) in " + parent.parent.config.settings.maxinvalidlogin.time + " minute(s), " + parent.parent.config.settings.maxinvalidlogin.coolofftime + " minute(s) cooloff.\r\n"; + } else { + 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++; + if (typeof parent.badLoginTable[i] == 'number') { + r += "Cooloff for " + Math.floor((parent.badLoginTable[i] - Date.now()) / 60000) + " minute(s)\r\n"; + } else { + 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 1018d726..cdb7be34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meshcentral", - "version": "0.4.3-s", + "version": "0.4.4-r", "keywords": [ "Remote Management", "Intel AMT", @@ -30,22 +30,19 @@ "dependencies": { "archiver": "^3.0.0", "body-parser": "^1.19.0", - "cbor": "4.1.5", + "cbor": "^4.1.5", "compression": "^1.7.4", "connect-redis": "^3.4.1", "cookie-session": "^2.0.0-beta.3", "express": "^4.17.0", "express-handlebars": "^3.1.0", "express-ws": "^4.0.0", - "html-minifier": "^4.0.0", "ipcheck": "^0.1.0", "meshcentral": "*", - "minify-js": "0.0.4", "minimist": "^1.2.0", "multiparty": "^4.2.1", "nedb": "^1.8.0", "node-forge": "^0.8.4", - "node-vault": "^0.9.11", "ws": "^6.2.1", "xmldom": "^0.1.27", "yauzl": "^2.10.0" diff --git a/public/commander.htm b/public/commander.htm index 66b7ca9d..0b789d3d 100644 --- a/public/commander.htm +++ b/public/commander.htm @@ -1,4 +1,4 @@ -
  
Disconnected
{{{title}}}
{{{title}}}
{{{title2}}}
\ No newline at end of file +{{{title}}}
{{{title}}}
{{{title2}}}
\ No newline at end of file diff --git a/views/default-mobile.handlebars b/views/default-mobile.handlebars index 7f5e7183..40484047 100644 --- a/views/default-mobile.handlebars +++ b/views/default-mobile.handlebars @@ -3221,6 +3221,7 @@ x += '
'; x += '
'; x += '
'; + x += '
'; x += ''; setDialogMode(2, "Add User to Mesh", 3, p20showAddMeshUserDialogEx, x); p20validateAddMeshUserDialog(); @@ -3229,20 +3230,24 @@ function p20validateAddMeshUserDialog() { var meshrights = currentMesh.links[userinfo._id].rights; - QE('idx_dlgOkButton', (Q('dp20username').value.length > 0)); + var nc = !Q('p20fulladmin').checked; QE('p20fulladmin', meshrights == 0xFFFFFFFF); - QE('p20editmesh', (!Q('p20fulladmin').checked) && (meshrights == 0xFFFFFFFF)); - QE('p20manageusers', !Q('p20fulladmin').checked); - QE('p20managecomputers', !Q('p20fulladmin').checked); - QE('p20remotecontrol', !Q('p20fulladmin').checked); - QE('p20meshagentconsole', !Q('p20fulladmin').checked); - QE('p20meshserverfiles', !Q('p20fulladmin').checked); - QE('p20wakedevices', !Q('p20fulladmin').checked); - QE('p20editnotes', !Q('p20fulladmin').checked); - QE('p20remoteview', !Q('p20fulladmin').checked && Q('p20remotecontrol').checked); - QE('p20noterminal', !Q('p20fulladmin').checked && Q('p20remotecontrol').checked); - QE('p20nofiles', !Q('p20fulladmin').checked && Q('p20remotecontrol').checked); - QE('p20noamt', !Q('p20fulladmin').checked && Q('p20remotecontrol').checked); + QE('p20editmesh', nc && (meshrights == 0xFFFFFFFF)); + QE('p20manageusers', nc); + QE('p20managecomputers', nc); + QE('p20remotecontrol', nc); + QE('p20meshagentconsole', nc); + QE('p20meshserverfiles', nc); + QE('p20wakedevices', nc); + QE('p20editnotes', nc); + QE('p20limitevents', nc); + QE('p20remoteview', nc && Q('p20remotecontrol').checked); + QE('p20remotelimitedinput', nc && Q('p20remotecontrol').checked && !Q('p20remoteview').checked); + QE('p20noterminal', nc && Q('p20remotecontrol').checked); + QE('p20nofiles', nc && Q('p20remotecontrol').checked); + QE('p20noamt', nc && Q('p20remotecontrol').checked); + QE('p20chatnotify', nc); + QE('p20uninstall', nc); } function p20showAddMeshUserDialogEx() { @@ -3260,8 +3265,14 @@ if (Q('p20noterminal').checked == true) meshadmin += 512; if (Q('p20nofiles').checked == true) meshadmin += 1024; if (Q('p20noamt').checked == true) meshadmin += 2048; + if (Q('p20remotelimitedinput').checked == true) meshadmin += 4096; + if (Q('p20limitevents').checked == true) meshadmin += 8192; + if (Q('p20chatnotify').checked == true) meshadmin += 16384; + if (Q('p20uninstall').checked == true) meshadmin += 32768; } - meshserver.send({ action: 'addmeshuser', meshid: currentMesh._id, meshname: currentMesh.name, username: Q('dp20username').value, meshadmin: meshadmin }); + var users = Q('dp20username').value.split(','), users2 = []; + for (var i in users) { users2.push(users[i].trim()); } + meshserver.send({ action: 'addmeshuser', meshid: currentMesh._id, meshname: currentMesh.name, usernames: users2, meshadmin: meshadmin }); } function p20viewuser(userid) { @@ -3284,6 +3295,7 @@ if (((meshrights & 8) != 0) && ((meshrights & 4096) != 0) && ((meshrights & 256) == 0)) r.push("Limited Input"); if ((meshrights & 8192) != 0) r.push("Self Events Only"); if ((meshrights & 16384) != 0) r.push("Chat & Notify"); + if ((meshrights & 32768) != 0) r.push("Uninstall"); } if (r.length == 0) { r.push("No Rights"); } var buttons = 1, x = addHtmlValue("User", EscapeHtml(decodeURIComponent(userid.split('/')[2]))); diff --git a/views/default.handlebars b/views/default.handlebars index f0181a6f..fc7e9b22 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -7555,10 +7555,10 @@ if (meshrights & 8) { Q('p20remotecontrol').checked = true; if (meshrights & 256) { Q('p20remoteview').checked = true; } - if (meshrights & 512) { Q('p20remotelimitedinput').checked = true; } - if (meshrights & 1024) { Q('p20noterminal').checked = true; } - if (meshrights & 2048) { Q('p20nofiles').checked = true; } - if (meshrights & 4096) { Q('p20noamt').checked = true; } + if (meshrights & 512) { Q('p20noterminal').checked = true; } + if (meshrights & 1024) { Q('p20nofiles').checked = true; } + if (meshrights & 2048) { Q('p20noamt').checked = true; } + if (meshrights & 4096) { Q('p20remotelimitedinput').checked = true; } } if (meshrights & 16) { Q('p20meshagentconsole').checked = true; } if (meshrights & 32) { Q('p20meshserverfiles').checked = true; } @@ -9254,6 +9254,7 @@ x += '
'; x += '
'; x += '
'; + x += '
'; x += '
' + "Web Server" + '
'; x += '
'; x += '
'; @@ -9270,8 +9271,8 @@ } function setServerTracingEx(b) { - var sources = [], allsources = ['cookie', 'dispatch', 'main', 'peer', 'web', 'webrequest', 'relay', 'webrelaydata', 'webrelay', 'mps', 'mpscmd', 'swarm', 'swarmcmd', 'agentupdate', 'agent']; - if (b == 1) { for (var i = 1; i < 16; i++) { try { if (Q('p41c' + i).checked) { sources.push(allsources[i - 1]); } } catch (ex) { } } } + var sources = [], allsources = ['cookie', 'dispatch', 'main', 'peer', 'web', 'webrequest', 'relay', 'webrelaydata', 'webrelay', 'mps', 'mpscmd', 'swarm', 'swarmcmd', 'agentupdate', 'agent', 'cert']; + if (b == 1) { for (var i = 1; i < 17; i++) { try { if (Q('p41c' + i).checked) { sources.push(allsources[i - 1]); } } catch (ex) { } } } meshserver.send({ action: 'traceinfo', traceSources: sources }); } diff --git a/views/login-min.handlebars b/views/login-min.handlebars index e10557a7..5278937a 100644 --- a/views/login-min.handlebars +++ b/views/login-min.handlebars @@ -1 +1 @@ -{{{title}}} - Login
{{{title}}}
{{{title2}}}

Welcome


\ No newline at end of file +{{{title}}} - Login
{{{title}}}
{{{title2}}}

Welcome


\ No newline at end of file diff --git a/views/login-mobile-min.handlebars b/views/login-mobile-min.handlebars index 352aaa7b..f95e95fd 100644 --- a/views/login-mobile-min.handlebars +++ b/views/login-mobile-min.handlebars @@ -1 +1 @@ -MeshCentral - Login
{{{title}}}
{{{title2}}}
\ No newline at end of file +MeshCentral - Login
{{{title}}}
{{{title2}}}
\ No newline at end of file diff --git a/views/login-mobile.handlebars b/views/login-mobile.handlebars index 40352073..67951822 100644 --- a/views/login-mobile.handlebars +++ b/views/login-mobile.handlebars @@ -46,9 +46,7 @@ \ No newline at end of file +{{{title}}}
{{{title}}}
{{{title2}}}
\ No newline at end of file diff --git a/views/translations/default-mobile_fr.handlebars b/views/translations/default-mobile_fr.handlebars index d0cbb50f..13ee6504 100644 --- a/views/translations/default-mobile_fr.handlebars +++ b/views/translations/default-mobile_fr.handlebars @@ -3219,6 +3219,7 @@ x += '
'; x += '
'; x += '
'; + x += '
'; x += ''; setDialogMode(2, "Ajouter un utilisateur au groupe", 3, p20showAddMeshUserDialogEx, x); p20validateAddMeshUserDialog(); @@ -3227,20 +3228,24 @@ function p20validateAddMeshUserDialog() { var meshrights = currentMesh.links[userinfo._id].rights; - QE('idx_dlgOkButton', (Q('dp20username').value.length > 0)); + var nc = !Q('p20fulladmin').checked; QE('p20fulladmin', meshrights == 0xFFFFFFFF); - QE('p20editmesh', (!Q('p20fulladmin').checked) && (meshrights == 0xFFFFFFFF)); - QE('p20manageusers', !Q('p20fulladmin').checked); - QE('p20managecomputers', !Q('p20fulladmin').checked); - QE('p20remotecontrol', !Q('p20fulladmin').checked); - QE('p20meshagentconsole', !Q('p20fulladmin').checked); - QE('p20meshserverfiles', !Q('p20fulladmin').checked); - QE('p20wakedevices', !Q('p20fulladmin').checked); - QE('p20editnotes', !Q('p20fulladmin').checked); - QE('p20remoteview', !Q('p20fulladmin').checked && Q('p20remotecontrol').checked); - QE('p20noterminal', !Q('p20fulladmin').checked && Q('p20remotecontrol').checked); - QE('p20nofiles', !Q('p20fulladmin').checked && Q('p20remotecontrol').checked); - QE('p20noamt', !Q('p20fulladmin').checked && Q('p20remotecontrol').checked); + QE('p20editmesh', nc && (meshrights == 0xFFFFFFFF)); + QE('p20manageusers', nc); + QE('p20managecomputers', nc); + QE('p20remotecontrol', nc); + QE('p20meshagentconsole', nc); + QE('p20meshserverfiles', nc); + QE('p20wakedevices', nc); + QE('p20editnotes', nc); + QE('p20limitevents', nc); + QE('p20remoteview', nc && Q('p20remotecontrol').checked); + QE('p20remotelimitedinput', nc && Q('p20remotecontrol').checked && !Q('p20remoteview').checked); + QE('p20noterminal', nc && Q('p20remotecontrol').checked); + QE('p20nofiles', nc && Q('p20remotecontrol').checked); + QE('p20noamt', nc && Q('p20remotecontrol').checked); + QE('p20chatnotify', nc); + QE('p20uninstall', nc); } function p20showAddMeshUserDialogEx() { @@ -3258,8 +3263,14 @@ if (Q('p20noterminal').checked == true) meshadmin += 512; if (Q('p20nofiles').checked == true) meshadmin += 1024; if (Q('p20noamt').checked == true) meshadmin += 2048; + if (Q('p20remotelimitedinput').checked == true) meshadmin += 4096; + if (Q('p20limitevents').checked == true) meshadmin += 8192; + if (Q('p20chatnotify').checked == true) meshadmin += 16384; + if (Q('p20uninstall').checked == true) meshadmin += 32768; } - meshserver.send({ action: 'addmeshuser', meshid: currentMesh._id, meshname: currentMesh.name, username: Q('dp20username').value, meshadmin: meshadmin }); + var users = Q('dp20username').value.split(','), users2 = []; + for (var i in users) { users2.push(users[i].trim()); } + meshserver.send({ action: 'addmeshuser', meshid: currentMesh._id, meshname: currentMesh.name, usernames: users2, meshadmin: meshadmin }); } function p20viewuser(userid) { @@ -3282,6 +3293,7 @@ if (((meshrights & 8) != 0) && ((meshrights & 4096) != 0) && ((meshrights & 256) == 0)) r.push("Limited Input"); if ((meshrights & 8192) != 0) r.push("Self Events Only"); if ((meshrights & 16384) != 0) r.push("Chat & Notify"); + if ((meshrights & 32768) != 0) r.push("Uninstall"); } if (r.length == 0) { r.push("No Rights"); } var buttons = 1, x = addHtmlValue("User", EscapeHtml(decodeURIComponent(userid.split('/')[2]))); diff --git a/views/translations/default_fr.handlebars b/views/translations/default_fr.handlebars index 42719933..9d39b2ec 100644 --- a/views/translations/default_fr.handlebars +++ b/views/translations/default_fr.handlebars @@ -7478,10 +7478,10 @@ if (meshrights & 8) { Q('p20remotecontrol').checked = true; if (meshrights & 256) { Q('p20remoteview').checked = true; } - if (meshrights & 512) { Q('p20remotelimitedinput').checked = true; } - if (meshrights & 1024) { Q('p20noterminal').checked = true; } - if (meshrights & 2048) { Q('p20nofiles').checked = true; } - if (meshrights & 4096) { Q('p20noamt').checked = true; } + if (meshrights & 512) { Q('p20noterminal').checked = true; } + if (meshrights & 1024) { Q('p20nofiles').checked = true; } + if (meshrights & 2048) { Q('p20noamt').checked = true; } + if (meshrights & 4096) { Q('p20remotelimitedinput').checked = true; } } if (meshrights & 16) { Q('p20meshagentconsole').checked = true; } if (meshrights & 32) { Q('p20meshserverfiles').checked = true; } @@ -9177,6 +9177,7 @@ x += '
'; x += '
'; x += '
'; + x += '
'; x += '
' + "Serveur Web" + '
'; x += '
'; x += '
'; @@ -9193,8 +9194,8 @@ } function setServerTracingEx(b) { - var sources = [], allsources = ['cookie', 'dispatch', 'main', 'peer', 'web', 'webrequest', 'relay', 'webrelaydata', 'webrelay', 'mps', 'mpscmd', 'swarm', 'swarmcmd', 'agentupdate', 'agent']; - if (b == 1) { for (var i = 1; i < 16; i++) { try { if (Q('p41c' + i).checked) { sources.push(allsources[i - 1]); } } catch (ex) { } } } + var sources = [], allsources = ['cookie', 'dispatch', 'main', 'peer', 'web', 'webrequest', 'relay', 'webrelaydata', 'webrelay', 'mps', 'mpscmd', 'swarm', 'swarmcmd', 'agentupdate', 'agent', 'cert']; + if (b == 1) { for (var i = 1; i < 17; i++) { try { if (Q('p41c' + i).checked) { sources.push(allsources[i - 1]); } } catch (ex) { } } } meshserver.send({ action: 'traceinfo', traceSources: sources }); } diff --git a/views/translations/login-min_fr.handlebars b/views/translations/login-min_fr.handlebars index 2bd7a83f..eb62b24b 100644 --- a/views/translations/login-min_fr.handlebars +++ b/views/translations/login-min_fr.handlebars @@ -1 +1 @@ -{{{title}}} - Login
{{{title}}}
{{{title2}}}

Bienvenue


\ No newline at end of file +{{{title}}} - Login
{{{title}}}
{{{title2}}}

Bienvenue


\ 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 89528bde..cd99da61 100644 --- a/views/translations/login-mobile-min_fr.handlebars +++ b/views/translations/login-mobile-min_fr.handlebars @@ -1 +1 @@ -MeshCentral - Login
{{{title}}}
{{{title2}}}
\ No newline at end of file +MeshCentral - Login
{{{title}}}
{{{title2}}}
\ No newline at end of file diff --git a/views/translations/login-mobile_fr.handlebars b/views/translations/login-mobile_fr.handlebars index f81ce8b9..35755e83 100644 --- a/views/translations/login-mobile_fr.handlebars +++ b/views/translations/login-mobile_fr.handlebars @@ -44,9 +44,7 @@