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
Loading...
System Status
Hardware Information
Event Log
Network Settings
User Accounts Serial-over-LAN Terminal
Intel® AMT Redirection port or Serial-over-LAN feature is disabled, click here to enable it.
Remote computer is not powered on, click here to issue a power command.
Remote Desktop
Intel® AMT Redirection port or KVM feature is disabled, click here to enable it.
Remote computer is not powered on, click here to issue a power command.
Audit Log
Security Settings
Internet Settings
System Defense
Agent Presence Script Editor Script
Compiled Script Variables
Console
Storage
Event Subscriptions
Wake Alarms Local Network Any Permission
Granted Permissions
*Minimum 8 characters with upper, lowercase, 0-9, and one of !@#$%^&*()+-
Warning: Some power actions may result in data loss and may disconnect the desktop, terminal or disk redirection sessions.
Primary display Secondary display Third display Consent Display
RLE8, Color Fast RLE16, Color RLE4G, Gray Fastest RLE8G, Gray Fast RAW8, Color Slow RAW16, Color Very Slow Image Encoding
Software KVM 50% 40% 30% 20% 10% 5% 1% Quality
100% 87.5% 75% 62.5% 50% 37.5% 25% 12.5% Scaling
Not Required Required for KVM only Always Required
WPA2 PSK WPA PSK Authentication
CCMP-AES TKIP-RC4 WEP None Encryption
This will save the entire state of Intel® AMT for this machine into file. Passwords will not be saved, but some sensitive data may be included.
Disabled ICMP response RMCP response ICMP & RMCP response
Disabled Disabled, DHCP update Enabled Dynamic DNS client
Defaut Interval is 1440 minutes, Default TTL is 900 seconds.
Power up Power cycle Power down Reset Set boot options Remote Command
None Force CD/DVD Boot Force PXE Boot Force Hard Disk Boot Force Diagnostic Boot Boot Source
None Index 1 Index 2 Index 3 Index 4 Boot Media Index
Boot to floppy Boot to CDROM IDER Boot Device
System Default Quiet Verbose Blank Screen Verbocity
Keep alarm Delete on completion
After wake
{{{title}}}
Server disconnected ,click to reconnect .
◀
Account Security
Account Actions
Device Groups ( New )
◀
My Files
◀
\ No newline at end of file
+{{{title}}}
Server disconnected ,click to reconnect .
◀
Account Security
Account Actions
Device Groups ( New )
◀
My Files
◀
\ 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 += ' ' + "Edit Device Notes" + ' ';
x += ' ' + "Show Only Own Events" + ' ';
x += ' ' + "Chat & Notify" + ' ';
+ x += ' ' + "Uninstall Agent" + ' ';
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 += ' = 0) ? 'checked' : '') + '>' + "MeshCentral Server Peering" + '
';
x += ' = 0) ? 'checked' : '') + '>' + "MeshAgent traffic" + '
';
x += ' = 0) ? 'checked' : '') + '>' + "MeshAgent update" + '
';
+ x += ' = 0) ? 'checked' : '') + '>' + "Server Certificate" + '
';
x += '' + "Web Server" + '
';
x += ' = 0) ? 'checked' : '') + '>' + "Web Server" + '
';
x += ' = 0) ? 'checked' : '') + '>' + "Web Server Requests" + '
';
@@ -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
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.
\ No newline at end of file
+{{{title}}} - Login
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.
\ 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
\ No newline at end of file
+MeshCentral - Login
\ 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 @@