MeshCentral/meshcentral.js
Josiah Baldwin 150e2337f5
Add options for overriding TLS ciphers used (#5915)
* Add the ability to set TLS cipher suites

Added config option to set the TLS ciphers instead of relying on a hardcoded list of ciphers determined by meshcentral.

* Added option to use default node ciphers

This allows the ciphers used to be set to the recommended ciphers by nodejs, as well as allowing the user to override the ciphers using the "--tls-cipher-list" command line switch for node.

* Updated validArguments array to include "usenodedefaulttlsciphers" and "tlsciphers" as options
2024-03-09 23:45:10 -08:00

4135 lines
298 KiB
JavaScript

/**
* @description MeshCentral main module
* @author Ylian Saint-Hilaire
* @copyright Intel Corporation 2018-2022
* @license Apache-2.0
* @version v0.0.1
*/
/*xjslint node: true */
/*xjslint plusplus: true */
/*xjslint maxlen: 256 */
/*jshint node: true */
/*jshint strict: false */
/*jshint esversion: 6 */
"use strict";
const common = require('./common.js');
// If app metrics is available
if (process.argv[2] == '--launch') { try { require('appmetrics-dash').monitor({ url: '/', title: 'MeshCentral', port: 88, host: '127.0.0.1' }); } catch (ex) { } }
function CreateMeshCentralServer(config, args) {
const obj = {};
obj.db = null;
obj.webserver = null; // HTTPS main web server, typically on port 443
obj.redirserver = null; // HTTP relay web server, typically on port 80
obj.mpsserver = null; // Intel AMT CIRA server, typically on port 4433
obj.mqttbroker = null; // MQTT server, not is not often used
obj.swarmserver = null; // Swarm server, this is used only to update older MeshCentral v1 agents
obj.smsserver = null; // SMS server, used to send user SMS messages
obj.msgserver = null; // Messaging server, used to sent used messages
obj.amtEventHandler = null;
obj.pluginHandler = null;
obj.amtScanner = null;
obj.amtManager = null; // Intel AMT manager, used to oversee all Intel AMT devices, activate them and sync policies
obj.meshScanner = null;
obj.taskManager = null;
obj.letsencrypt = null; // Let's encrypt server, used to get and renew TLS certificates
obj.eventsDispatch = {};
obj.fs = require('fs');
obj.path = require('path');
obj.crypto = require('crypto');
obj.exeHandler = require('./exeHandler.js');
obj.platform = require('os').platform();
obj.args = args;
obj.common = common;
obj.configurationFiles = null;
obj.certificates = null;
obj.connectivityByNode = {}; // This object keeps a list of all connected CIRA and agents, by nodeid->value (value: 1 = Agent, 2 = CIRA, 4 = AmtDirect)
obj.peerConnectivityByNode = {}; // This object keeps a list of all connected CIRA and agents of peers, by serverid->nodeid->value (value: 1 = Agent, 2 = CIRA, 4 = AmtDirect)
obj.debugSources = [];
obj.debugRemoteSources = null;
obj.config = config; // Configuration file
obj.dbconfig = {}; // Persistance values, loaded from database
obj.certificateOperations = null;
obj.defaultMeshCmd = null;
obj.defaultMeshCores = {};
obj.defaultMeshCoresDeflate = {};
obj.defaultMeshCoresHash = {};
obj.meshToolsBinaries = {}; // Mesh Tools Binaries, ToolName --> { hash:(sha384 hash), size:(binary size), path:(binary path) }
obj.meshAgentBinaries = {}; // Mesh Agent Binaries, Architecture type --> { hash:(sha384 hash), size:(binary size), path:(binary path) }
obj.meshAgentInstallScripts = {}; // Mesh Install Scripts, Script ID -- { hash:(sha384 hash), size:(binary size), path:(binary path) }
obj.multiServer = null;
obj.ipKvmManager = null;
obj.maintenanceTimer = null;
obj.serverId = null;
obj.serverKey = Buffer.from(obj.crypto.randomBytes(48), 'binary');
obj.loginCookieEncryptionKey = null;
obj.invitationLinkEncryptionKey = null;
obj.serverSelfWriteAllowed = true;
obj.serverStatsCounter = Math.floor(Math.random() * 1000);
obj.taskLimiter = obj.common.createTaskLimiterQueue(50, 20, 60); // (maxTasks, maxTaskTime, cleaningInterval) This is a task limiter queue to smooth out server work.
obj.agentUpdateBlockSize = 65531; // MeshAgent update block size
obj.serverWarnings = []; // List of warnings that should be shown to administrators
obj.cookieUseOnceTable = {}; // List of cookies that are already expired
obj.cookieUseOnceTableCleanCounter = 0; // Clean the cookieUseOnceTable each 20 additions
obj.firstStats = true; // True until this server saves it's not stats to the database
// Server version
obj.currentVer = null;
function getCurrentVersion() { try { obj.currentVer = JSON.parse(obj.fs.readFileSync(obj.path.join(__dirname, 'package.json'), 'utf8')).version; } catch (ex) { } return obj.currentVer; } // Fetch server version
getCurrentVersion();
// Setup the default configuration and files paths
if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
obj.parentpath = obj.path.join(__dirname, '../..');
obj.datapath = obj.path.join(__dirname, '../../meshcentral-data');
obj.filespath = obj.path.join(__dirname, '../../meshcentral-files');
obj.backuppath = obj.path.join(__dirname, '../../meshcentral-backups');
obj.recordpath = obj.path.join(__dirname, '../../meshcentral-recordings');
obj.webViewsPath = obj.path.join(__dirname, 'views');
obj.webPublicPath = obj.path.join(__dirname, 'public');
obj.webEmailsPath = obj.path.join(__dirname, 'emails');
if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/views'))) { obj.webViewsOverridePath = obj.path.join(__dirname, '../../meshcentral-web/views'); }
if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/public'))) { obj.webPublicOverridePath = obj.path.join(__dirname, '../../meshcentral-web/public'); }
if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/emails'))) { obj.webEmailsOverridePath = obj.path.join(__dirname, '../../meshcentral-web/emails'); }
} else {
obj.parentpath = __dirname;
obj.datapath = obj.path.join(__dirname, '../meshcentral-data');
obj.filespath = obj.path.join(__dirname, '../meshcentral-files');
obj.backuppath = obj.path.join(__dirname, '../meshcentral-backups');
obj.recordpath = obj.path.join(__dirname, '../meshcentral-recordings');
obj.webViewsPath = obj.path.join(__dirname, 'views');
obj.webPublicPath = obj.path.join(__dirname, 'public');
obj.webEmailsPath = obj.path.join(__dirname, 'emails');
if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/views'))) { obj.webViewsOverridePath = obj.path.join(__dirname, '../meshcentral-web/views'); }
if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/public'))) { obj.webPublicOverridePath = obj.path.join(__dirname, '../meshcentral-web/public'); }
if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/emails'))) { obj.webEmailsOverridePath = obj.path.join(__dirname, '../meshcentral-web/emails'); }
}
// Clean up any temporary files
const removeTime = new Date(Date.now()).getTime() - (30 * 60 * 1000); // 30 minutes
const dir = obj.fs.readdir(obj.path.join(obj.filespath, 'tmp'), function (err, files) {
if (err != null) return;
for (var i in files) { try { const filepath = obj.path.join(obj.filespath, 'tmp', files[i]); if (obj.fs.statSync(filepath).mtime.getTime() < removeTime) { obj.fs.unlink(filepath, function () { }); } } catch (ex) { } }
});
// Look to see if data and/or file path is specified
if (obj.config.settings && (typeof obj.config.settings.datapath == 'string')) { obj.datapath = obj.config.settings.datapath; }
if (obj.config.settings && (typeof obj.config.settings.filespath == 'string')) { obj.filespath = obj.config.settings.filespath; }
// Create data and files folders if needed
try { obj.fs.mkdirSync(obj.datapath); } catch (ex) { }
try { obj.fs.mkdirSync(obj.filespath); } catch (ex) { }
// Windows Specific Code, setup service and event log
obj.service = null;
obj.servicelog = null;
if (obj.platform == 'win32') {
const nodewindows = require('node-windows');
obj.service = nodewindows.Service;
const eventlogger = nodewindows.EventLogger;
obj.servicelog = new eventlogger('MeshCentral');
}
// Start the Meshcentral server
obj.Start = function () {
var i;
try { require('./pass').hash('test', function () { }, 0); } catch (ex) { console.log('Old version of node, must upgrade.'); return; } // TODO: Not sure if this test works or not.
// Check for invalid arguments
const validArguments = ['_', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'showitem', 'listuserids', 'showusergroups', 'shownodes', 'showallmeshes', 'showmeshes', 'showevents', 'showsmbios', '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', 'dbfix', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'usenodedefaulttlsciphers', 'tlsciphers', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'logintoken', 'logintokenkey', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'vaultpushconfigfiles', 'vaultpullconfigfiles', 'vaultdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'serverid', 'recordencryptionrecode', 'vault', 'token', 'unsealkey', 'name', 'log', 'dbstats', 'translate', 'createaccount', 'setuptelegram', 'resetaccount', 'pass', 'removesubdomain', 'adminaccount', 'domain', 'email', 'configfile', 'maintenancemode', 'nedbtodb', 'removetestagents', 'agentupdatetest', 'hashpassword', 'hashpass', 'indexmcrec', 'mpsdebug', 'dumpcores', 'dev'];
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.
if ((obj.args.help == true) || (obj.args['?'] == true)) {
console.log('MeshCentral v' + getCurrentVersion() + ', remote computer management web portal.');
console.log('This software is open source under Apache 2.0 license.');
console.log('Details at: https://www.meshcentral.com\r\n');
if ((obj.platform == 'win32') || (obj.platform == 'linux')) {
console.log('Run as a background service');
console.log(' --install/uninstall Install MeshCentral as a background service.');
console.log(' --start/stop/restart Control MeshCentral background service.');
console.log('');
console.log('Run standalone, console application');
}
console.log(' --user [username] Always login as [username] if account exists.');
console.log(' --port [number] Web server port number.');
console.log(' --redirport [number] Creates an additional HTTP server to redirect users to the HTTPS server.');
console.log(' --exactports Server must run with correct ports or exit.');
console.log(' --noagentupdate Server will not update mesh agent native binaries.');
console.log(' --nedbtodb Transfer all NeDB records into current database.');
console.log(' --listuserids Show a list of a user identifiers in the database.');
console.log(' --cert [name], (country), (org) Create a web server certificate with [name] server name.');
console.log(' country and organization can optionally be set.');
console.log('');
console.log('Server recovery commands, use only when MeshCentral is offline.');
console.log(' --createaccount [userid] Create a new user account.');
console.log(' --resetaccount [userid] Unlock an account, disable 2FA and set a new account password.');
console.log(' --adminaccount [userid] Promote account to site administrator.');
return;
}
// Fix a NeDB database
if (obj.args.dbfix) {
var lines = null, badJsonCount = 0, fieldNames = [], fixedDb = [];
try { lines = obj.fs.readFileSync(obj.getConfigFilePath(obj.args.dbfix), { encoding: 'utf8' }).split('\n'); } catch (ex) { console.log('Invalid file: ' + obj.args.dbfix + ': ' + ex); process.exit(); }
for (var i = 0; i < lines.length; i++) {
var x = null;
try { x = JSON.parse(lines[i]); } catch (ex) { badJsonCount++; }
if (x != null) { fixedDb.push(lines[i]); for (var j in x) { if (fieldNames.indexOf(j) == -1) { fieldNames.push(j); } } }
}
console.log('Lines: ' + lines.length + ', badJSON: ' + badJsonCount + ', Feilds: ' + fieldNames);
obj.fs.writeFileSync(obj.getConfigFilePath(obj.args.dbfix) + '-fixed', fixedDb.join('\n'), { encoding: 'utf8' });
return;
}
// Check for invalid cert name
if ((obj.args.cert != null) && ((typeof obj.args.cert != "string") || (obj.args.cert.indexOf('@') >= 0) || (obj.args.cert.indexOf('/') >= 0) || (obj.args.cert.indexOf(':') >= 0))) { console.log("Invalid certificate name"); process.exit(); return; }
// Perform a password hash
if (obj.args.hashpassword) { require('./pass').hash(obj.args.hashpassword, function (err, salt, hash, tag) { console.log(salt + ',' + hash); process.exit(); }); return; }
// Dump to mesh cores
if (obj.args.dumpcores) { obj.updateMeshCore(function () { console.log('Done.'); }, true); return; }
// Setup Telegram
if (obj.args.setuptelegram) { require('./meshmessaging.js').SetupTelegram(obj); return; }
// Perform web site translations into different languages
if (obj.args.translate) {
// Check NodeJS version
const NodeJSVer = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
if (NodeJSVer < 8) { console.log("Translation feature requires Node v8 or above, current version is " + process.version + "."); process.exit(); return; }
// Check if translate.json is in the "meshcentral-data" folder, if so use that and translate default pages.
var translationFile = null, customTranslation = false;
if (require('fs').existsSync(obj.path.join(obj.datapath, 'translate.json'))) { translationFile = obj.path.join(obj.datapath, 'translate.json'); console.log("Using translate.json in meshcentral-data."); customTranslation = true; }
if (translationFile == null) { if (require('fs').existsSync(obj.path.join(__dirname, 'translate', 'translate.json'))) { translationFile = obj.path.join(__dirname, 'translate', 'translate.json'); console.log("Using default translate.json."); } }
if (translationFile == null) { console.log("Unable to find translate.json."); process.exit(); return; }
// Perform translation operations
var didSomething = false;
process.chdir(obj.path.join(__dirname, 'translate'));
const translateEngine = require('./translate/translate.js')
if (customTranslation == true) {
// Translate all of the default files using custom translation file
translateEngine.startEx(['', '', 'minifyall']);
translateEngine.startEx(['', '', 'translateall', translationFile]);
translateEngine.startEx(['', '', 'extractall', translationFile]);
didSomething = true;
}
// Check if "meshcentral-web" exists, if so, translate all pages in that folder.
if (obj.webViewsOverridePath != null) {
didSomething = true;
var files = obj.fs.readdirSync(obj.webViewsOverridePath);
for (var i in files) {
var file = obj.path.join(obj.webViewsOverridePath, files[i]);
if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
translateEngine.startEx(['', '', 'minify', file]);
}
}
files = obj.fs.readdirSync(obj.webViewsOverridePath);
for (var i in files) {
var file = obj.path.join(obj.webViewsOverridePath, files[i]);
if (file.endsWith('.handlebars') || file.endsWith('-min.handlebars')) {
translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']);
}
}
}
/*
if (obj.webPublicOverridePath != null) {
didSomething = true;
var files = obj.fs.readdirSync(obj.webPublicOverridePath);
for (var i in files) {
var file = obj.path.join(obj.webPublicOverridePath, files[i]);
if (file.endsWith('.htm') && !file.endsWith('-min.htm')) {
translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']);
}
}
}
*/
if (didSomething == false) { console.log("Nothing to do."); }
process.exit();
return;
}
// Setup the Node+NPM path if possible, this makes it possible to update the server even if NodeJS and NPM are not in default paths.
if (obj.args.npmpath == null) {
try {
var nodepath = process.argv[0];
var npmpath = obj.path.join(obj.path.dirname(process.argv[0]), 'npm');
if (obj.fs.existsSync(nodepath) && obj.fs.existsSync(npmpath)) {
if (nodepath.indexOf(' ') >= 0) { nodepath = '"' + nodepath + '"'; }
if (npmpath.indexOf(' ') >= 0) { npmpath = '"' + npmpath + '"'; }
if (obj.platform == 'win32') { obj.args.npmpath = npmpath; } else { obj.args.npmpath = (nodepath + ' ' + npmpath); }
}
} catch (ex) { }
}
// Linux background service systemd handling
if (obj.platform == 'linux') {
if (obj.args.install == true) {
// Install MeshCentral in Systemd
console.log('Installing MeshCentral as background Service...');
var systemdConf = null;
const userinfo = require('os').userInfo();
if (require('fs').existsSync('/etc/systemd/system')) { systemdConf = '/etc/systemd/system/meshcentral.service'; }
else if (require('fs').existsSync('/lib/systemd/system')) { systemdConf = '/lib/systemd/system/meshcentral.service'; }
else if (require('fs').existsSync('/usr/lib/systemd/system')) { systemdConf = '/usr/lib/systemd/system/meshcentral.service'; }
else { console.log('Unable to find systemd configuration folder.'); process.exit(); return; }
console.log('Writing config file...');
require('child_process').exec('which node', {}, function (error, stdout, stderr) {
if ((error != null) || (stdout.indexOf('\n') == -1)) { console.log('ERROR: Unable to get node location: ' + error); process.exit(); return; }
const nodePath = stdout.substring(0, stdout.indexOf('\n'));
const config = '[Unit]\nDescription=MeshCentral Server\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=simple\nLimitNOFILE=1000000\nExecStart=' + nodePath + ' ' + __dirname + '/meshcentral\nWorkingDirectory=' + userinfo.homedir + '\nEnvironment=NODE_ENV=production\nUser=' + userinfo.username + '\nGroup=' + userinfo.username + '\nRestart=always\n# Restart service after 10 seconds if node service crashes\nRestartSec=10\n# Set port permissions capability\nAmbientCapabilities=cap_net_bind_service\n\n[Install]\nWantedBy=multi-user.target\n';
require('child_process').exec('echo \"' + config + '\" | sudo tee ' + systemdConf, {}, function (error, stdout, stderr) {
if ((error != null) && (error != '')) { console.log('ERROR: Unable to write config file: ' + error); process.exit(); return; }
console.log('Enabling service...');
require('child_process').exec('sudo systemctl enable meshcentral.service', {}, function (error, stdout, stderr) {
if ((error != null) && (error != '')) { console.log('ERROR: Unable to enable MeshCentral as a service: ' + error); process.exit(); return; }
if (stdout.length > 0) { console.log(stdout); }
console.log('Starting service...');
require('child_process').exec('sudo systemctl start meshcentral.service', {}, function (error, stdout, stderr) {
if ((error != null) && (error != '')) { console.log('ERROR: Unable to start MeshCentral as a service: ' + error); process.exit(); return; }
if (stdout.length > 0) { console.log(stdout); }
console.log('Done.');
});
});
});
});
return;
} else if (obj.args.uninstall == true) {
// Uninstall MeshCentral in Systemd
console.log('Uninstalling MeshCentral background service...');
var systemdConf = null;
if (require('fs').existsSync('/etc/systemd/system')) { systemdConf = '/etc/systemd/system/meshcentral.service'; }
else if (require('fs').existsSync('/lib/systemd/system')) { systemdConf = '/lib/systemd/system/meshcentral.service'; }
else if (require('fs').existsSync('/usr/lib/systemd/system')) { systemdConf = '/usr/lib/systemd/system/meshcentral.service'; }
else { console.log('Unable to find systemd configuration folder.'); process.exit(); return; }
console.log('Stopping service...');
require('child_process').exec('sudo systemctl stop meshcentral.service', {}, function (err, stdout, stderr) {
if ((err != null) && (err != '')) { console.log('ERROR: Unable to stop MeshCentral as a service: ' + err); }
if (stdout.length > 0) { console.log(stdout); }
console.log('Disabling service...');
require('child_process').exec('sudo systemctl disable meshcentral.service', {}, function (err, stdout, stderr) {
if ((err != null) && (err != '')) { console.log('ERROR: Unable to disable MeshCentral as a service: ' + err); }
if (stdout.length > 0) { console.log(stdout); }
console.log('Removing config file...');
require('child_process').exec('sudo rm ' + systemdConf, {}, function (err, stdout, stderr) {
if ((err != null) && (err != '')) { console.log('ERROR: Unable to delete MeshCentral config file: ' + err); }
console.log('Done.');
});
});
});
return;
} else if (obj.args.start == true) {
// Start MeshCentral in Systemd
require('child_process').exec('sudo systemctl start meshcentral.service', {}, function (err, stdout, stderr) {
if ((err != null) && (err != '')) { console.log('ERROR: Unable to start MeshCentral: ' + err); process.exit(); return; }
console.log('Done.');
});
return;
} else if (obj.args.stop == true) {
// Stop MeshCentral in Systemd
require('child_process').exec('sudo systemctl stop meshcentral.service', {}, function (err, stdout, stderr) {
if ((err != null) && (err != '')) { console.log('ERROR: Unable to stop MeshCentral: ' + err); process.exit(); return; }
console.log('Done.');
});
return;
} else if (obj.args.restart == true) {
// Restart MeshCentral in Systemd
require('child_process').exec('sudo systemctl restart meshcentral.service', {}, function (err, stdout, stderr) {
if ((err != null) && (err != '')) { console.log('ERROR: Unable to restart MeshCentral: ' + err); process.exit(); return; }
console.log('Done.');
});
return;
}
}
// Index a recorded file
if (obj.args.indexmcrec != null) {
if (typeof obj.args.indexmcrec != 'string') {
console.log('Usage: --indexmrec [filename.mcrec]');
} else if (obj.fs.existsSync(obj.args.indexmcrec)) {
console.log('Indexing file: ' + obj.args.indexmcrec);
require(require('path').join(__dirname, 'mcrec.js')).indexFile(obj.args.indexmcrec);
} else {
console.log('Unable to find file: ' + obj.args.indexmcrec);
}
return;
}
// Windows background service handling
if ((obj.platform == 'win32') && (obj.service != null)) {
// Build MeshCentral parent path and Windows Service path
var mcpath = __dirname;
if (mcpath.endsWith('\\node_modules\\meshcentral') || mcpath.endsWith('/node_modules/meshcentral')) { mcpath = require('path').join(mcpath, '..', '..'); }
const servicepath = obj.path.join(mcpath, 'WinService');
// 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 environment variables.
var serviceFilePath = null;
if (obj.fs.existsSync(obj.path.join(servicepath, 'winservice.js'))) { serviceFilePath = obj.path.join(servicepath, 'winservice.js'); }
else if (obj.fs.existsSync(obj.path.join(__dirname, '../WinService/winservice.js'))) { serviceFilePath = obj.path.join(__dirname, '../WinService/winservice.js'); }
else if (obj.fs.existsSync(obj.path.join(__dirname, 'winservice.js'))) { serviceFilePath = obj.path.join(__dirname, 'winservice.js'); }
if (serviceFilePath == null) { console.log('Unable to find winservice.js'); return; }
const svc = new obj.service({ name: 'MeshCentral', description: 'MeshCentral Remote Management Server', script: servicepath, 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.xinstall == true) { try { svc.install(); } catch (ex) { logException(ex); } }
if (obj.args.stop == true || obj.args.restart == true) { try { svc.stop(); } catch (ex) { logException(ex); } }
if (obj.args.start == true) { try { svc.start(); } catch (ex) { logException(ex); } }
if (obj.args.xuninstall == true) { try { svc.uninstall(); } catch (ex) { logException(ex); } }
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(servicepath) == false) { try { obj.fs.mkdirSync(servicepath); } 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(servicepath, '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: servicepath }, 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(servicepath) == true) {
require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: servicepath }, 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(servicepath, 'winservice.js')); } catch (ex) { }
try { obj.fs.rmdirSync(servicepath); } catch (ex) { }
});
} else 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
if (obj.args.launch) {
if (obj.args.vault) { obj.StartVault(); } else { obj.StartEx(); }
} else {
// if "--launch" is not specified, launch the server as a child process.
const startArgs = [];
for (i in process.argv) {
if (i > 0) {
const arg = process.argv[i];
if ((arg.length > 0) && ((arg.indexOf(' ') >= 0) || (arg.indexOf('&') >= 0))) { startArgs.push(arg); } else { startArgs.push(arg); }
}
}
startArgs.push('--launch', process.pid);
obj.launchChildServer(startArgs);
}
};
// Launch MeshCentral as a child server and monitor it.
obj.launchChildServer = function (startArgs) {
const child_process = require('child_process');
try { if (process.traceDeprecation === true) { startArgs.unshift('--trace-deprecation'); } } catch (ex) { }
try { if (process.traceProcessWarnings === true) { startArgs.unshift('--trace-warnings'); } } catch (ex) { }
childProcess = child_process.execFile(process.argv[0], startArgs, { maxBuffer: Infinity, cwd: obj.parentpath }, function (error, stdout, stderr) {
if (childProcess.xrestart == 1) {
setTimeout(function () { obj.launchChildServer(startArgs); }, 500); // This is an expected restart.
} else if (childProcess.xrestart == 2) {
console.log('Expected exit...');
process.exit(); // User CTRL-C exit.
} else if (childProcess.xrestart == 3) {
// Server self-update exit
var version = '';
if (typeof obj.args.selfupdate == 'string') { version = '@' + obj.args.selfupdate; }
else if (typeof obj.args.specificupdate == 'string') { version = '@' + obj.args.specificupdate; delete obj.args.specificupdate; }
const child_process = require('child_process');
const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
const env = Object.assign({}, process.env); // Shallow clone
if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
// always use --save-exact - https://stackoverflow.com/a/64507176/1210734
const xxprocess = child_process.exec(npmpath + ' install --save-exact --no-audit meshcentral' + version + npmproxy, { maxBuffer: Infinity, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) {
if ((error != null) && (error != '')) { console.log('Update failed: ' + error); }
});
xxprocess.data = '';
xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
xxprocess.stderr.on('data', function (data) { xxprocess.data += data; });
xxprocess.on('close', function (code) {
if (code == 0) { console.log('Update completed...'); }
// Run the server updated script if present
if (typeof obj.config.settings.runonserverupdated == 'string') {
const child_process = require('child_process');
var parentpath = __dirname;
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, '../..'); }
child_process.exec(obj.config.settings.runonserverupdated + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
}
if (obj.args.cleannpmcacheonupdate === true) {
// Perform NPM cache clean
console.log('Cleaning NPM cache...');
const xxxprocess = child_process.exec(npmpath + ' cache clean --force', { maxBuffer: Infinity, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
xxxprocess.on('close', function (code) { setTimeout(function () { obj.launchChildServer(startArgs); }, 1000); });
} else {
// Run the updated server
setTimeout(function () { obj.launchChildServer(startArgs); }, 1000);
}
});
} else {
if (error != null) {
// This is an un-expected restart
console.log(error);
console.log('ERROR: MeshCentral failed with critical error, check mesherrors.txt. Restarting in 5 seconds...');
setTimeout(function () { obj.launchChildServer(startArgs); }, 5000);
// Run the server error script if present
if (typeof obj.config.settings.runonservererror == 'string') {
const child_process = require('child_process');
var parentpath = __dirname;
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, '../..'); }
child_process.exec(obj.config.settings.runonservererror + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
}
}
}
});
childProcess.stdout.on('data', function (data) {
if (data[data.length - 1] == '\n') { data = data.substring(0, data.length - 1); }
if (data.indexOf('Updating settings folder...') >= 0) { childProcess.xrestart = 1; }
else if (data.indexOf('Updating server certificates...') >= 0) { childProcess.xrestart = 1; }
else if (data.indexOf('Server Ctrl-C exit...') >= 0) { childProcess.xrestart = 2; }
else if (data.indexOf('Starting self upgrade...') >= 0) { childProcess.xrestart = 3; }
else if (data.indexOf('Server restart...') >= 0) { childProcess.xrestart = 1; }
else if (data.indexOf('Starting self upgrade to: ') >= 0) { obj.args.specificupdate = data.substring(26).split('\r')[0].split('\n')[0]; childProcess.xrestart = 3; }
var datastr = data;
while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
console.log(datastr);
});
childProcess.stderr.on('data', function (data) {
var datastr = data;
while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
console.log('ERR: ' + datastr);
if (data.startsWith('le.challenges[tls-sni-01].loopback')) { return; } // Ignore this error output from GreenLock
if (data[data.length - 1] == '\n') { data = data.substring(0, data.length - 1); }
obj.logError(data);
});
childProcess.on('close', function (code) { if ((code != 0) && (code != 123)) { /* console.log("Exited with code " + code); */ } });
};
obj.logError = function (err) {
try {
var errlogpath = null;
if (typeof obj.args.mesherrorlogpath == 'string') { errlogpath = obj.path.join(obj.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = obj.getConfigFilePath('mesherrors.txt'); }
obj.fs.appendFileSync(errlogpath, '-------- ' + new Date().toLocaleString() + ' ---- ' + getCurrentVersion() + ' --------\r\n\r\n' + err + '\r\n\r\n\r\n');
} catch (ex) { console.log('ERROR: Unable to write to mesherrors.txt.'); }
};
// Get current and latest MeshCentral server versions using NPM
obj.getLatestServerVersion = function (callback) {
if (callback == null) return;
try {
if (typeof obj.args.selfupdate == 'string') { callback(getCurrentVersion(), obj.args.selfupdate); return; } // If we are targetting a specific version, return that one as current.
const child_process = require('child_process');
const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
const env = Object.assign({}, process.env); // Shallow clone
if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
const xxprocess = child_process.exec(npmpath + npmproxy + ' view meshcentral dist-tags.latest', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
xxprocess.data = '';
xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
xxprocess.stderr.on('data', function (data) { });
xxprocess.on('close', function (code) {
var latestVer = null;
if (code == 0) { try { latestVer = xxprocess.data.split(' ').join('').split('\r').join('').split('\n').join(''); } catch (ex) { } }
callback(getCurrentVersion(), latestVer);
});
} catch (ex) { callback(getCurrentVersion(), null, ex); } // If the system is running out of memory, an exception here can easily happen.
};
// Get current version and all MeshCentral server tags using NPM
obj.getServerTags = function (callback) {
if (callback == null) return;
try {
if (typeof obj.args.selfupdate == 'string') { callback({ current: getCurrentVersion(), latest: obj.args.selfupdate }); return; } // If we are targetting a specific version, return that one as current.
const child_process = require('child_process');
const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
const env = Object.assign({}, process.env); // Shallow clone
if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
const xxprocess = child_process.exec(npmpath + npmproxy + ' dist-tag ls meshcentral', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
xxprocess.data = '';
xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
xxprocess.stderr.on('data', function (data) { });
xxprocess.on('close', function (code) {
var tags = { current: getCurrentVersion() };
if (code == 0) {
try {
var lines = xxprocess.data.split('\r\n').join('\n').split('\n');
for (var i in lines) { var s = lines[i].split(': '); if ((s.length == 2) && (obj.args.npmtag == null) || (obj.args.npmtag == s[0])) { tags[s[0]] = s[1]; } }
} catch (ex) { }
}
callback(tags);
});
} catch (ex) { callback({ current: getCurrentVersion() }, ex); } // If the system is running out of memory, an exception here can easily happen.
};
// Use NPM to get list of versions
obj.getServerVersions = function (callback) {
try {
const child_process = require('child_process');
const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
const env = Object.assign({}, process.env); // Shallow clone
if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
const xxprocess = child_process.exec(npmpath + npmproxy + ' view meshcentral versions --json', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
xxprocess.data = '';
xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
xxprocess.stderr.on('data', function (data) { });
xxprocess.on('close', function (code) {
(code == 0) ? callback(xxprocess.data) : callback('{}');
});
} catch (ex) { callback('{}'); }
};
// Initiate server self-update
obj.performServerUpdate = function (version) {
if (obj.serverSelfWriteAllowed != true) return false;
if ((version == null) || (version == '') || (typeof version != 'string')) { console.log('Starting self upgrade...'); } else { console.log('Starting self upgrade to: ' + version); }
process.exit(200);
return true;
};
// Initiate server self-update
obj.performServerCertUpdate = function () { console.log('Updating server certificates...'); process.exit(200); };
// Start by loading configuration from Vault
obj.StartVault = function () {
// Check that the configuration can only be loaded from one place
if ((obj.args.vault != null) && (obj.args.loadconfigfromdb != null)) { console.log("Can't load configuration from both database and Vault."); process.exit(); return; }
// Fix arguments if needed
if (typeof obj.args.vault == 'string') {
obj.args.vault = { endpoint: obj.args.vault };
if (typeof obj.args.token == 'string') { obj.args.vault.token = obj.args.token; }
if (typeof obj.args.unsealkey == 'string') { obj.args.vault.unsealkey = obj.args.unsealkey; }
if (typeof obj.args.name == 'string') { obj.args.vault.name = obj.args.name; }
}
// Load configuration for HashiCorp's Vault if needed
if (obj.args.vault) {
if (obj.args.vault.endpoint == null) { console.log('Missing Vault endpoint.'); process.exit(); return; }
if (obj.args.vault.token == null) { console.log('Missing Vault token.'); process.exit(); return; }
if (obj.args.vault.unsealkey == null) { console.log('Missing Vault unsealkey.'); process.exit(); return; }
if (obj.args.vault.name == null) { obj.args.vault.name = 'meshcentral'; }
// Get new instance of the client
const vault = require("node-vault")({ endpoint: obj.args.vault.endpoint, token: obj.args.vault.token });
vault.unseal({ key: obj.args.vault.unsealkey })
.then(function() {
if (obj.args.vaultdeleteconfigfiles) {
vault.delete('secret/data/' + obj.args.vault.name)
.then(function (r) { console.log('Done.'); process.exit(); })
.catch(function (x) { console.log(x); process.exit(); });
} else if (obj.args.vaultpushconfigfiles) {
// Push configuration files into Vault
if ((obj.args.vaultpushconfigfiles == '*') || (obj.args.vaultpushconfigfiles === true)) { obj.args.vaultpushconfigfiles = obj.datapath; }
obj.fs.readdir(obj.args.vaultpushconfigfiles, function (err, files) {
if (err != null) { console.log('ERROR: Unable to read from folder ' + obj.args.vaultpushconfigfiles); process.exit(); return; }
var configFound = false;
for (var i in files) { if (files[i] == 'config.json') { configFound = true; } }
if (configFound == false) { console.log('ERROR: No config.json in folder ' + obj.args.vaultpushconfigfiles); process.exit(); return; }
var configFiles = {};
for (var i in files) {
const file = files[i];
if ((file == 'config.json') || file.endsWith('.key') || file.endsWith('.crt') || (file == 'terms.txt') || file.endsWith('.jpg') || file.endsWith('.png')) {
const path = obj.path.join(obj.args.vaultpushconfigfiles, files[i]), binary = Buffer.from(obj.fs.readFileSync(path, { encoding: 'binary' }), 'binary');
console.log('Pushing ' + file + ', ' + binary.length + ' bytes.');
if (file.endsWith('.json') || file.endsWith('.key') || file.endsWith('.crt')) { configFiles[file] = binary.toString(); } else { configFiles[file] = binary.toString('base64'); }
}
}
vault.write('secret/data/' + obj.args.vault.name, { "data": configFiles })
.then(function (r) { console.log('Done.'); process.exit(); })
.catch(function (x) { console.log(x); process.exit(); });
});
} else {
// Read configuration files from Vault
vault.read('secret/data/' + obj.args.vault.name)
.then(function (r) {
if ((r == null) || (r.data == null) || (r.data.data == null)) { console.log('Unable to read configuration from Vault.'); process.exit(); return; }
var configFiles = obj.configurationFiles = r.data.data;
// Decode Base64 when needed
for (var file in configFiles) { if (!file.endsWith('.json') && !file.endsWith('.key') && !file.endsWith('.crt')) { configFiles[file] = Buffer.from(configFiles[file], 'base64'); } }
// Save all of the files
if (obj.args.vaultpullconfigfiles) {
for (var i in configFiles) {
var fullFileName = obj.path.join(obj.args.vaultpullconfigfiles, i);
try { obj.fs.writeFileSync(fullFileName, configFiles[i]); } catch (ex) { console.log('Unable to write to ' + fullFileName); process.exit(); return; }
console.log('Pulling ' + i + ', ' + configFiles[i].length + ' bytes.');
}
console.log('Done.');
process.exit();
}
// Parse the new configuration file
var config2 = null;
try { config2 = JSON.parse(configFiles['config.json']); } catch (ex) { console.log('Error, unable to parse config.json from Vault.'); process.exit(); return; }
// Set the command line arguments to the config file if they are not present
if (!config2.settings) { config2.settings = {}; }
for (var i in args) { config2.settings[i] = args[i]; }
obj.args = args = config2.settings;
// Lower case all keys in the config file
obj.common.objKeysToLower(config2, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
// Grad some of the values from the original config.json file if present.
if ((config.settings.vault != null) && (config2.settings != null)) { config2.settings.vault = config.settings.vault; }
// We got a new config.json from the database, let's use it.
config = obj.config = config2;
obj.StartEx();
})
.catch(function (x) { console.log(x); process.exit(); });
}
}).catch(function (x) { console.log(x); process.exit(); });
return;
}
}
// Look for easy command line instructions and do them here.
obj.StartEx = function () {
var i;
//var wincmd = require('node-windows');
//wincmd.list(function (svc) { console.log(svc); }, true);
// Setup syslog support. Not supported on Windows.
if ((require('os').platform() != 'win32') && ((config.settings.syslog != null) || (config.settings.syslogjson != null) || (config.settings.syslogauth != null))) {
if (config.settings.syslog === true) { config.settings.syslog = 'meshcentral'; }
if (config.settings.syslogjson === true) { config.settings.syslogjson = 'meshcentral-json'; }
if (config.settings.syslogauth === true) { config.settings.syslogauth = 'meshcentral-auth'; }
if (typeof config.settings.syslog == 'string') {
obj.syslog = require('modern-syslog');
console.log('Starting ' + config.settings.syslog + ' syslog.');
obj.syslog.init(config.settings.syslog, obj.syslog.LOG_PID | obj.syslog.LOG_ODELAY, obj.syslog.LOG_LOCAL0);
obj.syslog.log(obj.syslog.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
}
if (typeof config.settings.syslogjson == 'string') {
obj.syslogjson = require('modern-syslog');
console.log('Starting ' + config.settings.syslogjson + ' JSON syslog.');
obj.syslogjson.init(config.settings.syslogjson, obj.syslogjson.LOG_PID | obj.syslogjson.LOG_ODELAY, obj.syslogjson.LOG_LOCAL0);
obj.syslogjson.log(obj.syslogjson.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
}
if (typeof config.settings.syslogauth == 'string') {
obj.syslogauth = require('modern-syslog');
console.log('Starting ' + config.settings.syslogauth + ' auth syslog.');
obj.syslogauth.init(config.settings.syslogauth, obj.syslogauth.LOG_PID | obj.syslogauth.LOG_ODELAY, obj.syslogauth.LOG_LOCAL0);
obj.syslogauth.log(obj.syslogauth.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
}
}
// Setup TCP syslog support, this works on all OS's.
if (config.settings.syslogtcp != null) {
const syslog = require('syslog');
if (config.settings.syslogtcp === true) {
obj.syslogtcp = syslog.createClient(514, 'localhost');
} else {
const sp = config.settings.syslogtcp.split(':');
obj.syslogtcp = syslog.createClient(parseInt(sp[1]), sp[0]);
}
obj.syslogtcp.log("MeshCentral v" + getCurrentVersion() + " Server Start", obj.syslogtcp.LOG_INFO);
}
// Check top level configuration for any unrecognized values
if (config) { for (var i in config) { if ((typeof i == 'string') && (i.length > 0) && (i[0] != '_') && (['settings', 'domaindefaults', 'domains', 'configfiles', 'smtp', 'letsencrypt', 'peers', 'sms', 'messaging', 'sendgrid', 'sendmail', 'firebase', 'firebaserelay', '$schema'].indexOf(i) == -1)) { addServerWarning('Unrecognized configuration option \"' + i + '\".', 3, [ i ]); } } }
// Read IP lists from files if applicable
config.settings.userallowedip = obj.args.userallowedip = readIpListFromFile(obj.args.userallowedip);
config.settings.userblockedip = obj.args.userblockedip = readIpListFromFile(obj.args.userblockedip);
config.settings.agentallowedip = obj.args.agentallowedip = readIpListFromFile(obj.args.agentallowedip);
config.settings.agentblockedip = obj.args.agentblockedip = readIpListFromFile(obj.args.agentblockedip);
config.settings.swarmallowedip = obj.args.swarmallowedip = readIpListFromFile(obj.args.swarmallowedip);
// Check IP lists and ranges
if (typeof obj.args.userallowedip == 'string') { if (obj.args.userallowedip == '') { config.settings.userallowedip = obj.args.userallowedip = null; } else { config.settings.userallowedip = obj.args.userallowedip = obj.args.userallowedip.split(' ').join('').split(','); } }
if (typeof obj.args.userblockedip == 'string') { if (obj.args.userblockedip == '') { config.settings.userblockedip = obj.args.userblockedip = null; } else { config.settings.userblockedip = obj.args.userblockedip = obj.args.userblockedip.split(' ').join('').split(','); } }
if (typeof obj.args.agentallowedip == 'string') { if (obj.args.agentallowedip == '') { config.settings.agentallowedip = obj.args.agentallowedip = null; } else { config.settings.agentallowedip = obj.args.agentallowedip = obj.args.agentallowedip.split(' ').join('').split(','); } }
if (typeof obj.args.agentblockedip == 'string') { if (obj.args.agentblockedip == '') { config.settings.agentblockedip = obj.args.agentblockedip = null; } else { config.settings.agentblockedip = obj.args.agentblockedip = obj.args.agentblockedip.split(' ').join('').split(','); } }
if (typeof obj.args.swarmallowedip == 'string') { if (obj.args.swarmallowedip == '') { obj.args.swarmallowedip = null; } else { obj.args.swarmallowedip = obj.args.swarmallowedip.split(' ').join('').split(','); } }
if ((typeof obj.args.agentupdateblocksize == 'number') && (obj.args.agentupdateblocksize >= 1024) && (obj.args.agentupdateblocksize <= 65531)) { obj.agentUpdateBlockSize = obj.args.agentupdateblocksize; }
if (typeof obj.args.trustedproxy == 'string') { obj.args.trustedproxy = obj.args.trustedproxy.split(' ').join('').split(','); }
if (typeof obj.args.tlsoffload == 'string') { obj.args.tlsoffload = obj.args.tlsoffload.split(' ').join('').split(','); }
// Check the "cookieIpCheck" value
if ((obj.args.cookieipcheck === false) || (obj.args.cookieipcheck == 'none')) { obj.args.cookieipcheck = 'none'; }
else if ((typeof obj.args.cookieipcheck != 'string') || (obj.args.cookieipcheck.toLowerCase() != 'strict')) { obj.args.cookieipcheck = 'lax'; }
else { obj.args.cookieipcheck = 'strict'; }
// Check the "cookieSameSite" value
if (typeof obj.args.cookiesamesite != 'string') { delete obj.args.cookiesamesite; }
else if (['none', 'lax', 'strict'].indexOf(obj.args.cookiesamesite.toLowerCase()) == -1) { delete obj.args.cookiesamesite; } else { obj.args.cookiesamesite = obj.args.cookiesamesite.toLowerCase(); }
// Check if WebSocket compression is supported. It's known to be broken in NodeJS v11.11 to v12.15, and v13.2
const verSplit = process.version.substring(1).split('.');
const ver = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
if (((ver >= 11.11) && (ver <= 12.15)) || (ver == 13.2)) {
if ((obj.args.wscompression === true) || (obj.args.agentwscompression === true)) { addServerWarning('WebSocket compression is disabled, this feature is broken in NodeJS v11.11 to v12.15 and v13.2', 4); }
obj.args.wscompression = obj.args.agentwscompression = false;
obj.config.settings.wscompression = obj.config.settings.agentwscompression = false;
}
// Local console tracing
if (typeof obj.args.debug == 'string') { obj.debugSources = obj.args.debug.toLowerCase().split(','); }
else if (typeof obj.args.debug == 'object') { obj.debugSources = obj.args.debug; }
else if (obj.args.debug === true) { obj.debugSources = '*'; }
require('./db.js').CreateDB(obj,
function (db) {
obj.db = db;
obj.db.SetupDatabase(function (dbversion) {
// See if any database operations needs to be completed
if (obj.args.deletedomain) { obj.db.DeleteDomain(obj.args.deletedomain, function () { console.log('Deleted domain ' + obj.args.deletedomain + '.'); process.exit(); }); return; }
if (obj.args.deletedefaultdomain) { obj.db.DeleteDomain('', function () { console.log('Deleted default domain.'); process.exit(); }); return; }
if (obj.args.showall) { obj.db.GetAll(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
if (obj.args.showusers) { obj.db.GetAllType('user', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
if (obj.args.showitem) { obj.db.Get(obj.args.showitem, function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
if (obj.args.listuserids) { obj.db.GetAllType('user', function (err, docs) { for (var i in docs) { console.log(docs[i]._id); } process.exit(); }); return; }
if (obj.args.showusergroups) { obj.db.GetAllType('ugrp', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
if (obj.args.shownodes) { obj.db.GetAllType('node', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
if (obj.args.showallmeshes) { obj.db.GetAllType('mesh', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
if (obj.args.showmeshes) { obj.db.GetAllType('mesh', function (err, docs) { var x = []; for (var i in docs) { if (docs[i].deleted == null) { x.push(docs[i]); } } console.log(JSON.stringify(x, null, 2)); process.exit(); }); return; }
if (obj.args.showevents) { obj.db.GetAllEvents(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
if (obj.args.showsmbios) { obj.db.GetAllSMBIOS(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
if (obj.args.showpower) { obj.db.getAllPower(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
if (obj.args.clearpower) { obj.db.removeAllPowerEvents(function () { process.exit(); }); return; }
if (obj.args.showiplocations) { obj.db.GetAllType('iploc', function (err, docs) { console.log(docs); process.exit(); }); return; }
if (obj.args.logintoken) { obj.getLoginToken(obj.args.logintoken, function (r) { console.log(r); process.exit(); }); return; }
if (obj.args.logintokenkey) { obj.showLoginTokenKey(function (r) { console.log(r); process.exit(); }); return; }
if (obj.args.recordencryptionrecode) { obj.db.performRecordEncryptionRecode(function (count) { console.log('Re-encoded ' + count + ' record(s).'); process.exit(); }); return; }
if (obj.args.dbstats) { obj.db.getDbStats(function (stats) { console.log(stats); process.exit(); }); return; }
if (obj.args.createaccount) { // Create a new user account
if ((typeof obj.args.createaccount != 'string') || ((obj.args.pass == null) && (obj.args.hashpass == null)) || (obj.args.pass == '') || (obj.args.hashpass == '') || (obj.args.createaccount.indexOf(' ') >= 0)) { console.log("Usage: --createaccount [userid] --pass [password] --domain (domain) --email (email) --name (name)."); process.exit(); return; }
var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.createaccount.toLowerCase(), domainid = obj.args.domain ? obj.args.domain : '';
if (obj.args.createaccount.startsWith('user/')) { userid = obj.args.createaccount; domainid = obj.args.createaccount.split('/')[1]; }
if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
obj.db.Get(userid, function (err, docs) {
if (err != null) { console.log("Database error: " + err); process.exit(); return; }
if ((docs != null) && (docs.length != 0)) { console.log('User already exists.'); process.exit(); return; }
if ((domainid != '') && ((config.domains == null) || (config.domains[domainid] == null))) { console.log("Invalid domain."); process.exit(); return; }
const user = { _id: userid, type: 'user', name: (typeof obj.args.name == 'string') ? obj.args.name : (userid.split('/')[2]), domain: domainid, creation: Math.floor(Date.now() / 1000), links: {} };
if (typeof obj.args.email == 'string') { user.email = obj.args.email; user.emailVerified = true; }
if (obj.args.hashpass) {
// Create an account using a pre-hashed password. Use --hashpassword to pre-hash a password.
var hashpasssplit = obj.args.hashpass.split(',');
if (hashpasssplit.length != 2) { console.log("Invalid hashed password."); process.exit(); return; }
user.salt = hashpasssplit[0];
user.hash = hashpasssplit[1];
obj.db.Set(user, function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
} else {
// Hash the password and create the account.
require('./pass').hash(obj.args.pass, function (err, salt, hash, tag) { if (err) { console.log("Unable create account password: " + err); process.exit(); return; } user.salt = salt; user.hash = hash; obj.db.Set(user, function () { console.log("Done."); process.exit(); return; }); }, 0);
}
});
return;
}
if (obj.args.resetaccount) { // Unlock a user account, set a new password and remove 2FA
if ((typeof obj.args.resetaccount != 'string') || (obj.args.resetaccount.indexOf(' ') >= 0)) { console.log("Usage: --resetaccount [userid] --domain (domain) --pass [password]."); process.exit(); return; }
var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.resetaccount.toLowerCase();
if (obj.args.resetaccount.startsWith('user/')) { userid = obj.args.resetaccount; }
if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
obj.db.Get(userid, function (err, docs) {
if (err != null) { console.log("Database error: " + err); process.exit(); return; }
if ((docs == null) || (docs.length == 0)) { console.log("Unknown userid, usage: --resetaccount [userid] --domain (domain) --pass [password]."); process.exit(); return; }
const user = docs[0]; if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { user.siteadmin -= 32; } // Unlock the account.
delete user.phone; delete user.otpekey; delete user.otpsecret; delete user.otpkeys; delete user.otphkeys; delete user.otpdev; delete user.otpsms; delete user.otpmsg; // Disable 2FA
delete user.msghandle; // Disable users 2fa messaging too
var config = getConfig(false);
if(config.domains[user.domain].auth || config.domains[user.domain].authstrategies){
console.log('This users domain has external authentication methods enabled so the password will not be changed if you set one')
obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
}else{
if (obj.args.hashpass && (typeof obj.args.hashpass == 'string')) {
// Reset an account using a pre-hashed password. Use --hashpassword to pre-hash a password.
var hashpasssplit = obj.args.hashpass.split(',');
if (hashpasssplit.length != 2) { console.log("Invalid hashed password."); process.exit(); return; }
user.salt = hashpasssplit[0];
user.hash = hashpasssplit[1];
obj.db.Set(user, function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
} else if(obj.args.pass && (typeof obj.args.pass == 'string')) {
// Hash the password and reset the account.
require('./pass').hash(String(obj.args.pass), user.salt, function (err, hash, tag) {
if (err) { console.log("Unable to reset password: " + err); process.exit(); return; }
user.hash = hash;
obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
}, 0);
}else{
console.log('Not setting a users password');
obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
}
}
});
return;
}
if (obj.args.adminaccount) { // Set a user account to server administrator
if ((typeof obj.args.adminaccount != 'string') || (obj.args.adminaccount.indexOf(' ') >= 0)) { console.log("Invalid userid, usage: --adminaccount [username] --domain (domain)"); process.exit(); return; }
var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.adminaccount.toLowerCase();
if (obj.args.adminaccount.startsWith('user/')) { userid = obj.args.adminaccount; }
if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
obj.db.Get(userid, function (err, docs) {
if (err != null) { console.log("Database error: " + err); process.exit(); return; }
if ((docs == null) || (docs.length == 0)) { console.log("Unknown userid, usage: --adminaccount [userid] --domain (domain)."); process.exit(); return; }
docs[0].siteadmin = 0xFFFFFFFF; // Set user as site administrator
obj.db.Set(docs[0], function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
});
return;
}
if (obj.args.removesubdomain) { // Remove all references to a sub domain from the database
if ((typeof obj.args.removesubdomain != 'string') || (obj.args.removesubdomain.indexOf(' ') >= 0)) { console.log("Invalid sub domain, usage: --removesubdomain [domain]"); process.exit(); return; }
obj.db.removeDomain(obj.args.removesubdomain, function () { console.log("Done."); process.exit(); return; });
return;
}
if (obj.args.removetestagents) { // Remove all test agents from the database
db.GetAllType('node', function (err, docs) {
if ((err != null) || (docs.length == 0)) {
console.log('Unable to get any nodes from the database');
process.exit(0);
} else {
// Load all users
const allusers = {}, removeCount = 0;
obj.db.GetAllType('user', function (err, docs) {
obj.common.unEscapeAllLinksFieldName(docs);
for (i in docs) { allusers[docs[i]._id] = docs[i]; }
});
// Look at all devices
for (var i in docs) {
if ((docs[i] != null) && (docs[i].agent != null) && (docs[i].agent.id == 23)) {
// Remove this test node
const node = docs[i];
// Delete this node including network interface information, events and timeline
removeCount++;
db.Remove(node._id); // Remove node with that id
db.Remove('if' + node._id); // Remove interface information
db.Remove('nt' + node._id); // Remove notes
db.Remove('lc' + node._id); // Remove last connect time
db.Remove('si' + node._id); // Remove system information
if (db.RemoveSMBIOS) { db.RemoveSMBIOS(node._id); } // Remove SMBios data
db.RemoveAllNodeEvents(node._id); // Remove all events for this node
db.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
if (typeof node.pmt == 'string') { db.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
db.Get('ra' + node._id, function (err, nodes) {
if ((nodes != null) && (nodes.length == 1)) { db.Remove('da' + nodes[0].daid); } // Remove diagnostic agent to real agent link
db.Remove('ra' + node._id); // Remove real agent to diagnostic agent link
});
// Remove any user node links
if (node.links != null) {
for (var i in node.links) {
if (i.startsWith('user/')) {
var cuser = allusers[i];
if ((cuser != null) && (cuser.links != null) && (cuser.links[node._id] != null)) {
// Remove the user link & save the user
delete cuser.links[node._id];
if (Object.keys(cuser.links).length == 0) { delete cuser.links; }
db.SetUser(cuser);
}
}
}
}
}
}
if (removeCount == 0) {
console.log("Done, no devices removed.");
process.exit(0);
} else {
console.log("Removed " + removeCount + " device(s), holding 10 seconds...");
setTimeout(function () { console.log("Done."); process.exit(0); }, 10000)
}
}
});
return;
}
// Import NeDB data into database
if (obj.args.nedbtodb) {
if (db.databaseType == 1) { console.log("NeDB is current database, can't perform transfer."); process.exit(); return; }
console.log("Transfering NeDB data into database...");
db.nedbtodb(function (msg) { console.log(msg); process.exit(); })
return;
}
// Show a list of all configuration files in the database
if (obj.args.dblistconfigfiles) {
obj.db.GetAllType('cfile', function (err, docs) { if (err == null) { if (docs.length == 0) { console.log("No files found."); } else { for (var i in docs) { console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' bytes.'); } } } else { console.log('Unable to read from database.'); } process.exit(); }); return;
}
// Display the content of a configuration file in the database
if (obj.args.dbshowconfigfile) {
if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
obj.db.getConfigFile(obj.args.dbshowconfigfile, function (err, docs) {
if (err == null) {
if (docs.length == 0) { console.log("File not found."); } else {
const data = obj.db.decryptData(obj.args.configkey, docs[0].data);
if (data == null) { console.log("Invalid config key."); } else { console.log(data); }
}
} else { console.log("Unable to read from database."); }
process.exit();
}); return;
}
// Delete all configuration files from database
if (obj.args.dbdeleteconfigfiles) {
console.log("Deleting all configuration files from the database..."); obj.db.RemoveAllOfType('cfile', function () { console.log('Done.'); process.exit(); });
}
// Push all relevent files from meshcentral-data into the database
if (obj.args.dbpushconfigfiles) {
if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
if ((obj.args.dbpushconfigfiles !== true) && (typeof obj.args.dbpushconfigfiles != 'string')) {
console.log("Usage: --dbpulldatafiles (path) This will import files from folder into the database");
console.log(" --dbpulldatafiles This will import files from meshcentral-data into the db.");
process.exit();
} else {
if ((obj.args.dbpushconfigfiles == '*') || (obj.args.dbpushconfigfiles === true)) { obj.args.dbpushconfigfiles = obj.datapath; }
obj.fs.readdir(obj.args.dbpushconfigfiles, function (err, files) {
if (err != null) { console.log('ERROR: Unable to read from folder ' + obj.args.dbpushconfigfiles); process.exit(); return; }
var configFound = false;
for (var i in files) { if (files[i] == 'config.json') { configFound = true; } }
if (configFound == false) { console.log('ERROR: No config.json in folder ' + obj.args.dbpushconfigfiles); process.exit(); return; }
obj.db.RemoveAllOfType('cfile', function () {
obj.fs.readdir(obj.args.dbpushconfigfiles, function (err, files) {
var lockCount = 1
for (var i in files) {
const file = files[i];
if ((file == 'config.json') || file.endsWith('.key') || file.endsWith('.crt') || (file == 'terms.txt') || file.endsWith('.jpg') || file.endsWith('.png')) {
const path = obj.path.join(obj.args.dbpushconfigfiles, files[i]), binary = Buffer.from(obj.fs.readFileSync(path, { encoding: 'binary' }), 'binary');
console.log('Pushing ' + file + ', ' + binary.length + ' bytes.');
lockCount++;
obj.db.setConfigFile(file, obj.db.encryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } });
}
}
if (--lockCount == 0) { process.exit(); }
});
});
});
}
return;
}
// Pull all database files into meshcentral-data
if (obj.args.dbpullconfigfiles) {
if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
if (typeof obj.args.dbpullconfigfiles != 'string') {
console.log("Usage: --dbpulldatafiles (path)");
process.exit();
} else {
obj.db.GetAllType('cfile', function (err, docs) {
if (err == null) {
if (docs.length == 0) {
console.log("File not found.");
} else {
for (var i in docs) {
const file = docs[i]._id.split('/')[1], binary = obj.db.decryptData(obj.args.configkey, docs[i].data);
if (binary == null) {
console.log("Invalid config key.");
} else {
const fullFileName = obj.path.join(obj.args.dbpullconfigfiles, file);
try { obj.fs.writeFileSync(fullFileName, binary); } catch (ex) { console.log('Unable to write to ' + fullFileName); process.exit(); return; }
console.log('Pulling ' + file + ', ' + binary.length + ' bytes.');
}
}
}
} else {
console.log("Unable to read from database.");
}
process.exit();
});
}
return;
}
if (obj.args.dbexport) {
// Export the entire database to a JSON file
if (obj.args.dbexport == true) { obj.args.dbexport = obj.getConfigFilePath('meshcentral.db.json'); }
obj.db.GetAll(function (err, docs) {
obj.fs.writeFileSync(obj.args.dbexport, JSON.stringify(docs));
console.log('Exported ' + docs.length + ' objects(s) to ' + obj.args.dbexport + '.'); process.exit();
});
return;
}
if (obj.args.dbexportmin) {
// Export a minimal database to a JSON file. Export only users, meshes and nodes.
// This is a useful command to look at the database.
if (obj.args.dbexportmin == true) { obj.args.dbexportmin = obj.getConfigFilePath('meshcentral.db.json'); }
obj.db.GetAllType({ $in: ['user', 'node', 'mesh'] }, function (err, docs) {
obj.fs.writeFileSync(obj.args.dbexportmin, JSON.stringify(docs));
console.log('Exported ' + docs.length + ' objects(s) to ' + obj.args.dbexportmin + '.'); process.exit();
});
return;
}
if (obj.args.dbimport) {
// Import the entire database from a JSON file
if (obj.args.dbimport == true) { obj.args.dbimport = obj.getConfigFilePath('meshcentral.db.json'); }
var json = null, json2 = '', badCharCount = 0;
try { json = obj.fs.readFileSync(obj.args.dbimport, { encoding: 'utf8' }); } catch (ex) { console.log('Invalid JSON file: ' + obj.args.dbimport + ': ' + ex); process.exit(); }
for (i = 0; i < json.length; i++) { if (json.charCodeAt(i) >= 32) { json2 += json[i]; } else { var tt = json.charCodeAt(i); if (tt != 10 && tt != 13) { badCharCount++; } } } // Remove all bad chars
if (badCharCount > 0) { console.log(badCharCount + ' invalid character(s) where removed.'); }
try { json = JSON.parse(json2); } catch (ex) { console.log('Invalid JSON format: ' + obj.args.dbimport + ': ' + e); process.exit(); }
if ((json == null) || (typeof json.length != 'number') || (json.length < 1)) { console.log('Invalid JSON format: ' + obj.args.dbimport + '.'); }
// Escape MongoDB invalid field chars
for (i in json) {
const doc = json[i];
for (var j in doc) { if (j.indexOf('.') >= 0) { console.log("Invalid field name (" + j + ") in document: " + json[i]); return; } }
//if ((json[i].type == 'ifinfo') && (json[i].netif2 != null)) { for (var j in json[i].netif2) { var esc = obj.common.escapeFieldName(j); if (esc !== j) { json[i].netif2[esc] = json[i].netif2[j]; delete json[i].netif2[j]; } } }
//if ((json[i].type == 'mesh') && (json[i].links != null)) { for (var j in json[i].links) { var esc = obj.common.escapeFieldName(j); if (esc !== j) { json[i].links[esc] = json[i].links[j]; delete json[i].links[j]; } } }
}
//for (i in json) { if ((json[i].type == "node") && (json[i].host != null)) { json[i].rname = json[i].host; delete json[i].host; } } // DEBUG: Change host to rname
setTimeout(function () { // If the Mongo database is being created for the first time, there is a race condition here. This will get around it.
obj.db.RemoveAll(function () {
obj.db.InsertMany(json, function (err) {
if (err != null) { console.log(err); } else { console.log('Imported ' + json.length + ' objects(s) from ' + obj.args.dbimport + '.'); } process.exit();
});
});
}, 100);
return;
}
/*
if (obj.args.dbimport) {
// Import the entire database from a very large JSON file
obj.db.RemoveAll(function () {
if (obj.args.dbimport == true) { obj.args.dbimport = obj.getConfigFilePath('meshcentral.db.json'); }
var json = null, json2 = "", badCharCount = 0;
const StreamArray = require('stream-json/streamers/StreamArray');
const jsonStream = StreamArray.withParser();
jsonStream.on('data', function (data) { obj.db.Set(data.value); });
jsonStream.on('end', () => { console.log('Done.'); process.exit(); });
obj.fs.createReadStream(obj.args.dbimport).pipe(jsonStream.input);
});
return;
}
*/
if (obj.args.dbmerge) {
// Import the entire database from a JSON file
if (obj.args.dbmerge == true) { obj.args.dbmerge = obj.getConfigFilePath('meshcentral.db.json'); }
var json = null, json2 = "", badCharCount = 0;
try { json = obj.fs.readFileSync(obj.args.dbmerge, { encoding: 'utf8' }); } catch (ex) { console.log('Invalid JSON file: ' + obj.args.dbmerge + ': ' + ex); process.exit(); }
for (i = 0; i < json.length; i++) { if (json.charCodeAt(i) >= 32) { json2 += json[i]; } else { var tt = json.charCodeAt(i); if (tt != 10 && tt != 13) { badCharCount++; } } } // Remove all bad chars
if (badCharCount > 0) { console.log(badCharCount + ' invalid character(s) where removed.'); }
try { json = JSON.parse(json2); } catch (ex) { console.log('Invalid JSON format: ' + obj.args.dbmerge + ': ' + ex); process.exit(); }
if ((json == null) || (typeof json.length != 'number') || (json.length < 1)) { console.log('Invalid JSON format: ' + obj.args.dbimport + '.'); }
// Get all users from current database
obj.db.GetAllType('user', function (err, docs) {
const users = {}, usersCount = 0;
for (var i in docs) { users[docs[i]._id] = docs[i]; usersCount++; }
// Fetch all meshes from the database
obj.db.GetAllType('mesh', function (err, docs) {
obj.common.unEscapeAllLinksFieldName(docs);
const meshes = {}, meshesCount = 0;
for (var i in docs) { meshes[docs[i]._id] = docs[i]; meshesCount++; }
console.log('Loaded ' + usersCount + ' users and ' + meshesCount + ' meshes.');
// Look at each object in the import file
const objectToAdd = [];
for (var i in json) {
const newobj = json[i];
if (newobj.type == 'user') {
// Check if the user already exists
var existingUser = users[newobj._id];
if (existingUser) {
// Merge the links
if (typeof newobj.links == 'object') {
for (var j in newobj.links) {
if ((existingUser.links == null) || (existingUser.links[j] == null)) {
if (existingUser.links == null) { existingUser.links = {}; }
existingUser.links[j] = newobj.links[j];
}
}
}
if (existingUser.name == 'admin') { existingUser.links = {}; }
objectToAdd.push(existingUser); // Add this user
} else {
objectToAdd.push(newobj); // Add this user
}
} else if (newobj.type == 'mesh') {
// Add this object
objectToAdd.push(newobj);
} // Don't add nodes.
}
console.log('Importing ' + objectToAdd.length + ' object(s)...');
var pendingCalls = 1;
for (var i in objectToAdd) {
pendingCalls++;
obj.db.Set(objectToAdd[i], function (err) { if (err != null) { console.log(err); } else { if (--pendingCalls == 0) { process.exit(); } } });
}
if (--pendingCalls == 0) { process.exit(); }
});
});
return;
}
// Check if the database is capable of performing a backup
obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
// Load configuration for database if needed
if (obj.args.loadconfigfromdb) {
var key = null;
if (typeof obj.args.configkey == 'string') { key = obj.args.configkey; }
else if (typeof obj.args.loadconfigfromdb == 'string') { key = obj.args.loadconfigfromdb; }
if (key == null) { console.log("Error, --configkey is required."); process.exit(); return; }
obj.db.getAllConfigFiles(key, function (configFiles) {
if (configFiles == null) { console.log("Error, no configuration files found or invalid configkey."); process.exit(); return; }
if (!configFiles['config.json']) { console.log("Error, could not file config.json from database."); process.exit(); return; }
if (typeof configFiles['config.json'] == 'object') { configFiles['config.json'] = configFiles['config.json'].toString(); }
if (configFiles['config.json'].charCodeAt(0) == 65279) { configFiles['config.json'] = configFiles['config.json'].substring(1); }
obj.configurationFiles = configFiles;
// Parse the new configuration file
var config2 = null;
try { config2 = JSON.parse(configFiles['config.json']); } catch (ex) { console.log('Error, unable to parse config.json from database.', ex); process.exit(); return; }
// Set the command line arguments to the config file if they are not present
if (!config2.settings) { config2.settings = {}; }
for (i in args) { config2.settings[i] = args[i]; }
// Lower case all keys in the config file
common.objKeysToLower(config2, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
// Grab some of the values from the original config.json file if present.
config2['mysql'] = config['mysql'];
config2['mariadb'] = config['mariadb'];
config2['mongodb'] = config['mongodb'];
config2['mongodbcol'] = config['mongodbcol'];
config2['dbencryptkey'] = config['dbencryptkey'];
config2['acebase'] = config['acebase'];
config2['sqlite3'] = config['sqlite3'];
// We got a new config.json from the database, let's use it.
config = obj.config = config2;
obj.StartEx1b();
});
} else {
config = obj.config = getConfig(obj.args.vault == null);
obj.StartEx1b();
}
});
}
);
};
// Time to start the server of real.
obj.StartEx1b = async function () {
var i;
// Add NodeJS version warning if needed
if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) < 16) { addServerWarning("MeshCentral will require Node v16 or above in the future, your current version is " + process.version + "."); }
// Setup certificate operations
obj.certificateOperations = require('./certoperations.js').CertificateOperations(obj);
// Linux format /var/log/auth.log
if (obj.config.settings.authlog != null) {
obj.fs.open(obj.config.settings.authlog, 'a', function (err, fd) {
if (err == null) { obj.authlogfile = fd; } else { console.log('ERROR: Unable to open: ' + obj.config.settings.authlog); }
})
}
// Start CrowdSec bouncer if needed: https://www.crowdsec.net/
if (typeof obj.args.crowdsec == 'object') { obj.crowdSecBounser = require('./crowdsec.js').CreateCrowdSecBouncer(obj, obj.args.crowdsec); }
// Check if self update is allowed. If running as a Windows service, self-update is not possible.
if (obj.fs.existsSync(obj.path.join(__dirname, 'daemon'))) { obj.serverSelfWriteAllowed = false; }
// If we are targetting a specific version, update now.
if ((obj.serverSelfWriteAllowed == true) && (typeof obj.args.selfupdate == 'string')) {
obj.args.selfupdate = obj.args.selfupdate.toLowerCase();
if (getCurrentVersion() !== obj.args.selfupdate) { obj.performServerUpdate(); return; } // We are targetting a specific version, run self update now.
}
// Write the server state
obj.updateServerState('state', 'starting');
if (process.pid) { obj.updateServerState('server-pid', process.pid); }
if (process.ppid) { obj.updateServerState('server-parent-pid', process.ppid); }
// Read environment variables. For a subset of arguments, we allow them to be read from environment variables.
const 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
if (obj.config.domains == null) { obj.config.domains = {}; }
if (obj.config.domains[''] == null) { obj.config.domains[''] = {}; }
if (obj.config.domains[''].dns != null) { console.log("ERROR: Default domain can't have a DNS name."); return; }
var xdomains = {}; for (i in obj.config.domains) { xdomains[i.toLowerCase()] = obj.config.domains[i]; } obj.config.domains = xdomains;
var bannedDomains = ['public', 'private', 'images', 'scripts', 'styles', 'views']; // List of banned domains
for (i in obj.config.domains) { for (var j in bannedDomains) { if (i == bannedDomains[j]) { console.log("ERROR: Domain '" + i + "' is not allowed domain name in config.json."); delete obj.config.domains[i]; } } }
for (i in obj.config.domains) { if ((i.length > 64) || (Buffer.from(i).length > 64)) { console.log("ERROR: Domain '" + i + "' is longer that 64 bytes, this is not allowed."); delete obj.config.domains[i]; } }
for (i in obj.config.domains) {
// Remove any domains that start with underscore
if (i.startsWith('_')) { delete obj.config.domains[i]; continue; }
// Apply default domain settings if present
if (typeof obj.config.domaindefaults == 'object') { for (var j in obj.config.domaindefaults) { if (obj.config.domains[i][j] == null) { obj.config.domains[i][j] = obj.config.domaindefaults[j]; } } }
// Perform domain setup
if (typeof obj.config.domains[i] != 'object') { console.log("ERROR: Invalid domain configuration in config.json."); process.exit(); return; }
if ((i.length > 0) && (i[0] == '_')) { delete obj.config.domains[i]; continue; } // Remove any domains with names that start with _
if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); }
if (obj.config.domains[i].limits == null) { obj.config.domains[i].limits = {}; }
if (obj.config.domains[i].dns == null) { obj.config.domains[i].url = (i == '') ? '/' : ('/' + i + '/'); } else { obj.config.domains[i].url = '/'; }
obj.config.domains[i].id = i;
if ((typeof obj.config.domains[i].maxdeviceview != 'number') || (obj.config.domains[i].maxdeviceview < 1)) { delete obj.config.domains[i].maxdeviceview; }
if (typeof obj.config.domains[i].loginkey == 'string') { obj.config.domains[i].loginkey = [obj.config.domains[i].loginkey]; }
if ((obj.config.domains[i].loginkey != null) && (obj.common.validateAlphaNumericArray(obj.config.domains[i].loginkey, 1, 128) == false)) { console.log("ERROR: Invalid login key, must be alpha-numeric string with no spaces."); process.exit(); return; }
if (typeof obj.config.domains[i].agentkey == 'string') { obj.config.domains[i].agentkey = [obj.config.domains[i].agentkey]; }
if ((obj.config.domains[i].agentkey != null) && (obj.common.validateAlphaNumericArray(obj.config.domains[i].agentkey, 1, 128) == false)) { console.log("ERROR: Invalid agent key, must be alpha-numeric string with no spaces."); process.exit(); return; }
if (typeof obj.config.domains[i].userallowedip == 'string') { if (obj.config.domains[i].userallowedip == '') { delete obj.config.domains[i].userallowedip; } else { obj.config.domains[i].userallowedip = obj.config.domains[i].userallowedip.split(' ').join('').split(','); } }
if (typeof obj.config.domains[i].userblockedip == 'string') { if (obj.config.domains[i].userblockedip == '') { delete obj.config.domains[i].userblockedip; } else { obj.config.domains[i].userblockedip = obj.config.domains[i].userblockedip.split(' ').join('').split(','); } }
if (typeof obj.config.domains[i].agentallowedip == 'string') { if (obj.config.domains[i].agentallowedip == '') { delete obj.config.domains[i].agentallowedip; } else { obj.config.domains[i].agentallowedip = obj.config.domains[i].agentallowedip.split(' ').join('').split(','); } }
if (typeof obj.config.domains[i].agentblockedip == 'string') { if (obj.config.domains[i].agentblockedip == '') { delete obj.config.domains[i].agentblockedip; } else { obj.config.domains[i].agentblockedip = obj.config.domains[i].agentblockedip.split(' ').join('').split(','); } }
if (typeof obj.config.domains[i].ignoreagenthashcheck == 'string') { if (obj.config.domains[i].ignoreagenthashcheck == '') { delete obj.config.domains[i].ignoreagenthashcheck; } else { obj.config.domains[i].ignoreagenthashcheck = obj.config.domains[i].ignoreagenthashcheck.split(','); } }
if (typeof obj.config.domains[i].allowedorigin == 'string') { if (obj.config.domains[i].allowedorigin == '') { delete obj.config.domains[i].allowedorigin; } else { obj.config.domains[i].allowedorigin = obj.config.domains[i].allowedorigin.split(','); } }
if ((obj.config.domains[i].passwordrequirements != null) && (typeof obj.config.domains[i].passwordrequirements == 'object')) {
if (typeof obj.config.domains[i].passwordrequirements.skip2factor == 'string') {
obj.config.domains[i].passwordrequirements.skip2factor = obj.config.domains[i].passwordrequirements.skip2factor.split(',');
} else {
delete obj.config.domains[i].passwordrequirements.skip2factor;
}
// Fix the list of users to add "user/domain/" if needed
if (Array.isArray(obj.config.domains[i].passwordrequirements.logintokens)) {
var newValues = [];
for (var j in obj.config.domains[i].passwordrequirements.logintokens) {
var splitVal = obj.config.domains[i].passwordrequirements.logintokens[j].split('/');;
if (splitVal.length == 1) { newValues.push('user/' + i + '/' + splitVal[0]); }
if (splitVal.length == 2) { newValues.push('user/' + splitVal[0] + '/' + splitVal[1]); }
if (splitVal.length == 3) { newValues.push(splitVal[0] + '/' + splitVal[1] + '/' + splitVal[2]); }
}
obj.config.domains[i].passwordrequirements.logintokens = newValues;
}
}
if ((obj.config.domains[i].auth == 'ldap') && (typeof obj.config.domains[i].ldapoptions != 'object')) {
if (i == '') { console.log("ERROR: Default domain is LDAP, but is missing LDAPOptions."); } else { console.log("ERROR: Domain '" + i + "' is LDAP, but is missing LDAPOptions."); }
process.exit();
return;
}
if ((obj.config.domains[i].auth == 'ldap') || (obj.config.domains[i].auth == 'sspi')) { obj.config.domains[i].newaccounts = 0; } // No new accounts allowed in SSPI/LDAP authentication modes.
if (obj.config.domains[i].sitestyle == null) { obj.config.domains[i].sitestyle = 2; } // Default to site style #2
// Convert newAccountsRights from a array of strings to flags number.
obj.config.domains[i].newaccountsrights = obj.common.meshServerRightsArrayToNumber(obj.config.domains[i].newaccountsrights);
if (typeof (obj.config.domains[i].newaccountsrights) != 'number') { delete obj.config.domains[i].newaccountsrights; }
// Check if there is a web views path and/or web public path for this domain
if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
if ((obj.config.domains[i].webviewspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web-' + i + '/views')))) { obj.config.domains[i].webviewspath = obj.path.join(__dirname, '../../meshcentral-web-' + i + '/views'); }
if ((obj.config.domains[i].webpublicpath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web-' + i + '/public')))) { obj.config.domains[i].webpublicpath = obj.path.join(__dirname, '../../meshcentral-web-' + i + '/public'); }
if ((obj.config.domains[i].webemailspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web-' + i + '/emails')))) { obj.config.domains[i].webemailspath = obj.path.join(__dirname, '../../meshcentral-web-' + i + '/emails'); }
} else {
if ((obj.config.domains[i].webviewspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web-' + i + '/views')))) { obj.config.domains[i].webviewspath = obj.path.join(__dirname, '../meshcentral-web-' + i + '/views'); }
if ((obj.config.domains[i].webpublicpath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web-' + i + '/public')))) { obj.config.domains[i].webpublicpath = obj.path.join(__dirname, '../meshcentral-web-' + i + '/public'); }
if ((obj.config.domains[i].webemailspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web-' + i + '/emails')))) { obj.config.domains[i].webemailspath = obj.path.join(__dirname, '../meshcentral-web-' + i + '/emails'); }
}
// Check agent customization if any
if (typeof obj.config.domains[i].agentcustomization == 'object') {
if (typeof obj.config.domains[i].agentcustomization.displayname != 'string') { delete obj.config.domains[i].agentcustomization.displayname; } else { obj.config.domains[i].agentcustomization.displayname = obj.config.domains[i].agentcustomization.displayname.split('\r').join('').split('\n').join(''); }
if (typeof obj.config.domains[i].agentcustomization.description != 'string') { delete obj.config.domains[i].agentcustomization.description; } else { obj.config.domains[i].agentcustomization.description = obj.config.domains[i].agentcustomization.description.split('\r').join('').split('\n').join(''); }
if (typeof obj.config.domains[i].agentcustomization.companyname != 'string') { delete obj.config.domains[i].agentcustomization.companyname; } else { obj.config.domains[i].agentcustomization.companyname = obj.config.domains[i].agentcustomization.companyname.split('\r').join('').split('\n').join(''); }
if (typeof obj.config.domains[i].agentcustomization.servicename != 'string') { delete obj.config.domains[i].agentcustomization.servicename; } else { obj.config.domains[i].agentcustomization.servicename = obj.config.domains[i].agentcustomization.servicename.split('\r').join('').split('\n').join('').split(' ').join('').split('"').join('').split('\'').join('').split('>').join('').split('<').join('').split('/').join('').split('\\').join(''); }
if (typeof obj.config.domains[i].agentcustomization.image != 'string') { delete obj.config.domains[i].agentcustomization.image; } else { try { obj.config.domains[i].agentcustomization.image = 'data:image/png;base64,' + Buffer.from(obj.fs.readFileSync(obj.getConfigFilePath(obj.config.domains[i].agentcustomization.image)), 'binary').toString('base64'); } catch (ex) { console.log(ex); delete obj.config.domains[i].agentcustomization.image; } }
} else {
delete obj.config.domains[i].agentcustomization;
}
// Convert user consent flags
if (typeof obj.config.domains[i].userconsentflags == 'object') {
var flags = 0;
if (obj.config.domains[i].userconsentflags.desktopnotify == true) { flags |= 1; }
if (obj.config.domains[i].userconsentflags.terminalnotify == true) { flags |= 2; }
if (obj.config.domains[i].userconsentflags.filenotify == true) { flags |= 4; }
if (obj.config.domains[i].userconsentflags.desktopprompt == true) { flags |= 8; }
if (obj.config.domains[i].userconsentflags.terminalprompt == true) { flags |= 16; }
if (obj.config.domains[i].userconsentflags.fileprompt == true) { flags |= 32; }
if (obj.config.domains[i].userconsentflags.desktopprivacybar == true) { flags |= 64; }
obj.config.domains[i].userconsentflags = flags;
}
// If we have Intel AMT manager settings, take a look at them here.
if (typeof obj.config.domains[i].amtmanager == 'object') {
if (typeof obj.config.domains[i].amtmanager.tlsrootcert == 'object') {
obj.config.domains[i].amtmanager.tlsrootcert2 = obj.certificateOperations.loadGenericCertAndKey(obj.config.domains[i].amtmanager.tlsrootcert);
if (obj.config.domains[i].amtmanager.tlsrootcert2 == null) { // Show an error message if needed
if (i == '') {
addServerWarning("Unable to load Intel AMT TLS root certificate for default domain.", 5);
} else {
addServerWarning("Unable to load Intel AMT TLS root certificate for domain " + i + ".", 6, [ i ]);
}
}
}
}
// Check agentfileinfo
if (typeof obj.config.domains[i].agentfileinfo == 'object') {
if ((obj.config.domains[i].agentfileinfo.fileversionnumber != null) && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.fileversionnumber) == null)) { delete obj.config.domains[i].agentfileinfo.fileversionnumber; }
if ((obj.config.domains[i].agentfileinfo.productversionnumber != null) && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.productversionnumber) == null)) { delete obj.config.domains[i].agentfileinfo.productversionnumber; }
if ((obj.config.domains[i].agentfileinfo.fileversionnumber == null) && (typeof obj.config.domains[i].agentfileinfo.fileversion == 'string') && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.fileversion) != null)) { obj.config.domains[i].agentfileinfo.fileversionnumber = obj.config.domains[i].agentfileinfo.fileversion; }
if (typeof obj.config.domains[i].agentfileinfo.icon == 'string') {
// Load the agent .ico file
var icon = null;
try { icon = require('./authenticode.js').loadIcon(obj.path.join(obj.datapath, obj.config.domains[i].agentfileinfo.icon)); } catch (ex) { }
if (icon != null) {
// The icon file was correctly loaded
obj.config.domains[i].agentfileinfo.icon = icon;
} else {
// Failed to load the icon file, display a server warning
addServerWarning("Unable to load agent icon file: " + obj.config.domains[i].agentfileinfo.icon + ".", 23, [obj.config.domains[i].agentfileinfo.icon]);
delete obj.config.domains[i].agentfileinfo.icon;
}
} else {
// Invalid icon file path
delete obj.config.domains[i].agentfileinfo.icon;
}
if (typeof obj.config.domains[i].agentfileinfo.logo == 'string') {
// Load the agent .bmp file
var logo = null;
try { logo = require('./authenticode.js').loadBitmap(obj.path.join(obj.datapath, obj.config.domains[i].agentfileinfo.logo)); } catch (ex) { }
if (logo != null) {
// The logo file was correctly loaded
obj.config.domains[i].agentfileinfo.logo = logo;
} else {
// Failed to load the icon file, display a server warning
addServerWarning("Unable to load agent logo file: " + obj.config.domains[i].agentfileinfo.logo + ".", 24, [obj.config.domains[i].agentfileinfo.logo]);
delete obj.config.domains[i].agentfileinfo.logo;
}
} else {
// Invalid icon file path
delete obj.config.domains[i].agentfileinfo.logo;
}
}
}
// Log passed arguments into Windows Service Log
//if (obj.servicelog != null) { var s = ''; for (i in obj.args) { if (i != '_') { if (s.length > 0) { s += ', '; } s += i + "=" + obj.args[i]; } } logInfoEvent('MeshServer started with arguments: ' + s); }
// Look at passed in arguments
if ((obj.args.user != null) && (typeof obj.args.user != 'string')) { delete obj.args.user; }
if ((obj.args.ciralocalfqdn != null) && ((obj.args.lanonly == true) || (obj.args.wanonly == true))) { addServerWarning("CIRA local FQDN's ignored when server in LAN-only or WAN-only mode.", 7); }
if ((obj.args.ciralocalfqdn != null) && (obj.args.ciralocalfqdn.split(',').length > 4)) { addServerWarning("Can't have more than 4 CIRA local FQDN's. Ignoring value.", 8); obj.args.ciralocalfqdn = null; }
if (obj.args.ignoreagenthashcheck === true) { addServerWarning("Agent hash checking is being skipped, this is unsafe.", 9); }
if (obj.args.port == null || typeof obj.args.port != 'number') { obj.args.port = 443; }
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.redirport == null) obj.args.redirport = 80;
if (obj.args.minifycore == null) obj.args.minifycore = false;
if (typeof args.agentidletimeout != 'number') { args.agentidletimeout = 150000; } else { args.agentidletimeout *= 1000 } // Default agent idle timeout is 2m, 30sec.
if ((obj.args.lanonly != true) && (obj.args.webrtconfig == null)) { obj.args.webrtconfig = { iceservers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.services.mozilla.com' }] }; } // Setup default WebRTC STUN servers
if (typeof obj.args.ignoreagenthashcheck == 'string') { if (obj.args.ignoreagenthashcheck == '') { delete obj.args.ignoreagenthashcheck; } else { obj.args.ignoreagenthashcheck = obj.args.ignoreagenthashcheck.split(','); } }
// Setup a site administrator
if ((obj.args.admin) && (typeof obj.args.admin == 'string')) {
var adminname = obj.args.admin.split('/');
if (adminname.length == 1) { adminname = 'user//' + adminname[0]; }
else if (adminname.length == 2) { adminname = 'user/' + adminname[0] + '/' + adminname[1]; }
else { console.log("Invalid administrator name."); process.exit(); return; }
obj.db.Get(adminname, function (err, user) {
if (user.length != 1) { console.log("Invalid user name."); process.exit(); return; }
user[0].siteadmin = 4294967295; // 0xFFFFFFFF
obj.db.Set(user[0], function () {
if (user[0].domain == '') { console.log('User ' + user[0].name + ' set to site administrator.'); } else { console.log("User " + user[0].name + " of domain " + user[0].domain + " set to site administrator."); }
process.exit();
return;
});
});
return;
}
// Remove a site administrator
if ((obj.args.unadmin) && (typeof obj.args.unadmin == 'string')) {
var adminname = obj.args.unadmin.split('/');
if (adminname.length == 1) { adminname = 'user//' + adminname[0]; }
else if (adminname.length == 2) { adminname = 'user/' + adminname[0] + '/' + adminname[1]; }
else { console.log("Invalid administrator name."); process.exit(); return; }
obj.db.Get(adminname, function (err, user) {
if (user.length != 1) { console.log("Invalid user name."); process.exit(); return; }
if (user[0].siteadmin) { delete user[0].siteadmin; }
obj.db.Set(user[0], function () {
if (user[0].domain == '') { console.log("User " + user[0].name + " is not a site administrator."); } else { console.log("User " + user[0].name + " of domain " + user[0].domain + " is not a site administrator."); }
process.exit();
return;
});
});
return;
}
// Setup agent error log
if ((obj.config) && (obj.config.settings) && (obj.config.settings.agentlogdump != null)) {
obj.fs.open(obj.path.join(obj.datapath, 'agenterrorlogs.txt'), 'a', function (err, fd) { obj.agentErrorLog = fd; })
}
// Perform other database cleanup
obj.db.cleanup();
// Set all nodes to power state of unknown (0)
obj.db.storePowerEvent({ time: new Date(), nodeid: '*', power: 0, s: 1 }, obj.multiServer); // s:1 indicates that the server is starting up.
// Read or setup database configuration values
obj.db.Get('dbconfig', function (err, dbconfig) {
if ((dbconfig != null) && (dbconfig.length == 1)) { obj.dbconfig = dbconfig[0]; } else { obj.dbconfig = { _id: 'dbconfig', version: 1 }; }
if (obj.dbconfig.amtWsEventSecret == null) { obj.crypto.randomBytes(32, function (err, buf) { obj.dbconfig.amtWsEventSecret = buf.toString('hex'); obj.db.Set(obj.dbconfig); }); }
// This is used by the user to create a username/password for a Intel AMT WSMAN event subscription
if (obj.args.getwspass) {
if (obj.args.getwspass.length == 64) {
obj.crypto.randomBytes(6, function (err, buf) {
while (obj.dbconfig.amtWsEventSecret == null) { process.nextTick(); }
const username = buf.toString('hex');
const nodeid = obj.args.getwspass;
const pass = obj.crypto.createHash('sha384').update(username.toLowerCase() + ':' + nodeid + ':' + obj.dbconfig.amtWsEventSecret).digest('base64').substring(0, 12).split('/').join('x').split('\\').join('x');
console.log("--- Intel(r) AMT WSMAN eventing credentials ---");
console.log("Username: " + username);
console.log("Password: " + pass);
console.log("Argument: " + nodeid);
process.exit();
});
} else {
console.log("Invalid NodeID.");
process.exit();
}
return;
}
// Setup the task manager
if ((obj.config) && (obj.config.settings) && (obj.config.settings.taskmanager == true)) {
obj.taskManager = require('./taskmanager').createTaskManager(obj);
}
// Start plugin manager if configuration allows this.
if ((obj.config) && (obj.config.settings) && (obj.config.settings.plugins != null) && (obj.config.settings.plugins != false) && ((typeof obj.config.settings.plugins != 'object') || (obj.config.settings.plugins.enabled != false))) {
obj.pluginHandler = require('./pluginHandler.js').pluginHandler(obj);
}
// Load the default meshcore and meshcmd
obj.updateMeshCore();
obj.updateMeshCmd();
// Setup and start the redirection server if needed. We must start the redirection server before Let's Encrypt.
if ((obj.args.redirport != null) && (typeof obj.args.redirport == 'number') && (obj.args.redirport != 0)) {
obj.redirserver = require('./redirserver.js').CreateRedirServer(obj, obj.db, obj.args, obj.StartEx2);
} else {
obj.StartEx2(); // If not needed, move on.
}
});
}
// Done starting the redirection server, go on to load the server certificates
obj.StartEx2 = function () {
// Load server certificates
obj.certificateOperations.GetMeshServerCertificate(obj.args, obj.config, function (certs) {
// Get the current node version
if ((obj.config.letsencrypt == null) || (obj.redirserver == null)) {
obj.StartEx3(certs); // Just use the configured certificates
} else if ((obj.config.letsencrypt != null) && (obj.config.letsencrypt.nochecks == true)) {
// Use Let's Encrypt with no checking
obj.letsencrypt = require('./letsencrypt.js').CreateLetsEncrypt(obj);
obj.letsencrypt.getCertificate(certs, obj.StartEx3); // Use Let's Encrypt with no checking, use at your own risk.
} else {
// Check Let's Encrypt settings
var leok = true;
if ((typeof obj.config.letsencrypt.names != 'string') && (typeof obj.config.settings.cert == 'string')) { obj.config.letsencrypt.names = obj.config.settings.cert; }
if (typeof obj.config.letsencrypt.email != 'string') { leok = false; addServerWarning("Missing Let's Encrypt email address.", 10); }
else if (typeof obj.config.letsencrypt.names != 'string') { leok = false; addServerWarning("Invalid Let's Encrypt host names.", 11); }
else if (obj.config.letsencrypt.names.indexOf('*') >= 0) { leok = false; addServerWarning("Invalid Let's Encrypt names, can't contain a *.", 12); }
else if (obj.config.letsencrypt.email.split('@').length != 2) { leok = false; addServerWarning("Invalid Let's Encrypt email address.", 10); }
else if (obj.config.letsencrypt.email.trim() !== obj.config.letsencrypt.email) { leok = false; addServerWarning("Invalid Let's Encrypt email address.", 10); }
else {
const le = require('./letsencrypt.js');
try { obj.letsencrypt = le.CreateLetsEncrypt(obj); } catch (ex) { console.log(ex); }
if (obj.letsencrypt == null) { addServerWarning("Unable to setup Let's Encrypt module.", 13); leok = false; }
}
if (leok == true) {
// Check that the email address domain MX resolves.
require('dns').resolveMx(obj.config.letsencrypt.email.split('@')[1], function (err, addresses) {
if (err == null) {
// Check that all names resolve
checkResolveAll(obj.config.letsencrypt.names.split(','), function (err) {
if (err == null) {
obj.letsencrypt.getCertificate(certs, obj.StartEx3); // Use Let's Encrypt
} else {
for (var i in err) { addServerWarning("Invalid Let's Encrypt names, unable to resolve: " + err[i], 14, [err[i]]); }
obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
}
});
} else {
addServerWarning("Invalid Let's Encrypt email address, unable to resolve: " + obj.config.letsencrypt.email.split('@')[1], 15, [obj.config.letsencrypt.email.split('@')[1]]);
obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
}
});
} else {
obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
}
}
});
};
// Start the server with the given certificates, but check if we have web certificates to load
obj.StartEx3 = function (certs) {
obj.certificates = certs;
obj.certificateOperations.acceleratorStart(certs); // Set the state of the accelerators
// Load any domain web certificates
for (var i in obj.config.domains) {
// Load any Intel AMT ACM activation certificates
if (obj.config.domains[i].amtacmactivation == null) { obj.config.domains[i].amtacmactivation = {}; }
obj.certificateOperations.loadIntelAmtAcmCerts(obj.config.domains[i].amtacmactivation);
if (obj.config.domains[i].amtacmactivation.acmCertErrors != null) { for (var j in obj.config.domains[i].amtacmactivation.acmCertErrors) { obj.addServerWarning(obj.config.domains[i].amtacmactivation.acmCertErrors[j]); } }
if (typeof obj.config.domains[i].certurl == 'string') {
obj.supportsProxyCertificatesRequest = true; // If a certurl is set, enable proxy cert requests
// Then, fix the URL and add 'https://' if needed
if (obj.config.domains[i].certurl.indexOf('://') < 0) { obj.config.domains[i].certurl = 'https://' + obj.config.domains[i].certurl; }
}
}
// Load CloudFlare trusted proxies list if needed
if ((obj.config.settings.trustedproxy != null) && (typeof obj.config.settings.trustedproxy == 'string') && (obj.config.settings.trustedproxy.toLowerCase() == 'cloudflare')) {
obj.config.settings.extrascriptsrc = 'ajax.cloudflare.com'; // Add CloudFlare as a trusted script source. This allows for CloudFlare's RocketLoader feature.
delete obj.args.trustedproxy;
delete obj.config.settings.trustedproxy;
obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v4', null, function (url, data, tag) {
if (data != null) {
if (Array.isArray(obj.args.trustedproxy) == false) { obj.args.trustedproxy = []; }
const ipranges = data.split('\n');
for (var i in ipranges) { if (ipranges[i] != '') { obj.args.trustedproxy.push(ipranges[i]); } }
obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v6', null, function (url, data, tag) {
if (data != null) {
var ipranges = data.split('\n');
for (var i in ipranges) { if (ipranges[i] != '') { obj.args.trustedproxy.push(ipranges[i]); } }
obj.config.settings.trustedproxy = obj.args.trustedproxy;
} else {
addServerWarning("Unable to load CloudFlare trusted proxy IPv6 address list.", 16);
}
obj.StartEx4(); // Keep going
});
} else {
addServerWarning("Unable to load CloudFlare trusted proxy IPv4 address list.", 16);
obj.StartEx4(); // Keep going
}
});
} else {
obj.StartEx4(); // Keep going
}
}
// Start the server with the given certificates
obj.StartEx4 = function () {
var i;
// If the certificate is un-configured, force LAN-only mode
if (obj.certificates.CommonName.indexOf('.') == -1) { /*console.log('Server name not configured, running in LAN-only mode.');*/ obj.args.lanonly = true; }
// Write server version and run mode
const productionMode = (process.env.NODE_ENV && (process.env.NODE_ENV == 'production'));
const runmode = (obj.args.lanonly ? 2 : (obj.args.wanonly ? 1 : 0));
console.log("MeshCentral v" + getCurrentVersion() + ', ' + (["Hybrid (LAN + WAN) mode", "WAN mode", "LAN mode"][runmode]) + (productionMode ? ", Production mode." : '.'));
// Check that no sub-domains have the same DNS as the parent
for (i in obj.config.domains) {
if ((obj.config.domains[i].dns != null) && (obj.certificates.CommonName.toLowerCase() === obj.config.domains[i].dns.toLowerCase())) {
console.log("ERROR: Server sub-domain can't have same DNS name as the parent."); process.exit(0); return;
}
}
// Load the list of MeshCentral tools
obj.updateMeshTools();
// Load MeshAgent translation strings
try {
var translationpath = obj.path.join(__dirname, 'agents', 'agent-translations.json');
const translationpath2 = obj.path.join(obj.datapath, 'agents', 'agent-translations.json');
if (obj.fs.existsSync(translationpath2)) { translationpath = translationpath2; } // If the agent is present in "meshcentral-data/agents", use that one instead.
var translations = JSON.parse(obj.fs.readFileSync(translationpath).toString());
if (translations['zh-chs']) { translations['zh-hans'] = translations['zh-chs']; delete translations['zh-chs']; }
if (translations['zh-cht']) { translations['zh-hant'] = translations['zh-cht']; delete translations['zh-cht']; }
// If there is domain customizations to the agent strings, do this here.
for (var i in obj.config.domains) {
var domainTranslations = translations;
if ((typeof obj.config.domains[i].agentcustomization == 'object') && (typeof obj.config.domains[i].agentcustomization.installtext == 'string')) {
domainTranslations = Object.assign({}, domainTranslations); // Shallow clone
for (var j in domainTranslations) { delete domainTranslations[j].description; }
domainTranslations.en.description = obj.config.domains[i].agentcustomization.installtext;
}
obj.config.domains[i].agentTranslations = JSON.stringify(domainTranslations);
}
} catch (ex) { }
// Load any domain specific agents
for (var i in obj.config.domains) { if ((i != '') && (obj.config.domains[i].share == null)) { obj.updateMeshAgentsTable(obj.config.domains[i], function () { }); } }
// Load the list of mesh agents and install scripts
if ((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true)) { for (i in obj.meshAgentsArchitectureNumbers) { obj.meshAgentsArchitectureNumbers[i].update = false; } }
obj.signMeshAgents(obj.config.domains[''], function () {
obj.updateMeshAgentsTable(obj.config.domains[''], function () {
obj.updateMeshAgentInstallScripts();
// Setup and start the web server
obj.crypto.randomBytes(48, function (err, buf) {
// Setup Mesh Multi-Server if needed
obj.multiServer = require('./multiserver.js').CreateMultiServer(obj, obj.args);
if (obj.multiServer != null) {
if ((obj.db.databaseType != 3) || (obj.db.changeStream != true)) { console.log("ERROR: Multi-server support requires use of MongoDB with ReplicaSet and ChangeStream enabled."); process.exit(0); return; }
if (typeof obj.args.sessionkey != 'string') { console.log("ERROR: Multi-server support requires \"SessionKey\" be set in the settings section of config.json, same key for all servers."); process.exit(0); return; }
obj.serverId = obj.multiServer.serverid;
for (var serverid in obj.config.peers.servers) { obj.peerConnectivityByNode[serverid] = {}; }
}
// If the server is set to "nousers", allow only loopback unless IP filter is set
if ((obj.args.nousers == true) && (obj.args.userallowedip == null)) { obj.args.userallowedip = "::1,127.0.0.1"; }
// Set the session length to 60 minutes if not set and set a random key if needed
if ((obj.args.sessiontime != null) && ((typeof obj.args.sessiontime != 'number') || (obj.args.sessiontime < 1))) { delete obj.args.sessiontime; }
if (typeof obj.args.sessionkey != 'string') { obj.args.sessionkey = buf.toString('hex').toUpperCase(); }
// Create MQTT Broker to hook into webserver and mpsserver
if ((typeof obj.config.settings.mqtt == 'object') && (typeof obj.config.settings.mqtt.auth == 'object') && (typeof obj.config.settings.mqtt.auth.keyid == 'string') && (typeof obj.config.settings.mqtt.auth.key == 'string')) { obj.mqttbroker = require("./mqttbroker.js").CreateMQTTBroker(obj, obj.db, obj.args); }
// Start the web server and if needed, the redirection web server.
obj.webserver = require('./webserver.js').CreateWebServer(obj, obj.db, obj.args, obj.certificates, obj.StartEx5);
if (obj.redirserver != null) { obj.redirserver.hookMainWebServer(obj.certificates); }
// Change RelayDNS to a array of strings
if (typeof obj.args.relaydns == 'string') { obj.args.relaydns = [obj.args.relaydns]; }
if (obj.common.validateStrArray(obj.args.relaydns, 1) == false) { delete obj.args.relaydns; }
// Start the HTTP relay web server if needed
if ((obj.args.relaydns == null) && (typeof obj.args.relayport == 'number') && (obj.args.relayport != 0)) {
obj.webrelayserver = require('./webrelayserver.js').CreateWebRelayServer(obj, obj.db, obj.args, obj.certificates, function () { });
}
// Update proxy certificates
if (obj.supportsProxyCertificatesRequest == true) { obj.updateProxyCertificates(true); }
// Setup the Intel AMT event handler
obj.amtEventHandler = require('./amtevents.js').CreateAmtEventsHandler(obj);
// Setup the Intel AMT local network scanner
if (obj.args.wanonly != true) {
if (obj.args.amtscanner != false) { obj.amtScanner = require('./amtscanner.js').CreateAmtScanner(obj).start(); }
if (obj.args.meshscanner != false) { obj.meshScanner = require('./meshscanner.js').CreateMeshScanner(obj).start(); }
}
// Setup and start the MPS server
obj.mpsserver = require('./mpsserver.js').CreateMpsServer(obj, obj.db, obj.args, obj.certificates);
// Setup the Intel AMT manager
if (obj.args.amtmanager !== false) {
obj.amtManager = require('./amtmanager.js').CreateAmtManager(obj);
}
// Setup and start the legacy swarm server
if ((obj.certificates.swarmserver != null) && (obj.args.swarmport != null) && (obj.args.swarmport !== 0)) {
obj.swarmserver = require('./swarmserver.js').CreateSwarmServer(obj, obj.db, obj.args, obj.certificates);
}
// Setup the main email server
if (obj.config.sendgrid != null) {
// Sendgrid server
obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
obj.mailserver.verify();
if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode.", 17); }
} else if (obj.config.smtp != null) {
// SMTP server
obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
obj.mailserver.verify();
if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
} else if (obj.config.sendmail != null) {
// Sendmail server
obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
obj.mailserver.verify();
if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
}
// Setup the email server for each domain
for (i in obj.config.domains) {
if (obj.config.domains[i].sendgrid != null) {
// Sendgrid server
obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
obj.config.domains[i].mailserver.verify();
if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode.", 17); }
} else if ((obj.config.domains[i].smtp != null) && (obj.config.domains[i].smtp.host != null) && (obj.config.domains[i].smtp.from != null)) {
// SMTP server
obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
obj.config.domains[i].mailserver.verify();
if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
} else if (obj.config.domains[i].sendmail != null) {
// Sendmail server
obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
obj.config.domains[i].mailserver.verify();
if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
} else {
// Setup the parent mail server for this domain
if (obj.mailserver != null) { obj.config.domains[i].mailserver = obj.mailserver; }
}
}
// Setup SMS gateway
if (config.sms != null) {
obj.smsserver = require('./meshsms.js').CreateMeshSMS(obj);
if ((obj.smsserver != null) && (obj.args.lanonly == true)) { addServerWarning("SMS gateway has limited use in LAN mode.", 19); }
}
// Setup user messaging
if (config.messaging != null) {
obj.msgserver = require('./meshmessaging.js').CreateServer(obj);
}
// Setup web based push notifications
if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) {
obj.webpush = require('web-push');
var vapidKeys = null;
try { vapidKeys = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, 'vapid.json')).toString()); } catch (ex) { }
if ((vapidKeys == null) || (typeof vapidKeys.publicKey != 'string') || (typeof vapidKeys.privateKey != 'string')) {
console.log("Generating web push VAPID keys...");
vapidKeys = obj.webpush.generateVAPIDKeys();
obj.common.moveOldFiles([obj.path.join(obj.datapath, 'vapid.json')]);
obj.fs.writeFileSync(obj.path.join(obj.datapath, 'vapid.json'), JSON.stringify(vapidKeys));
}
obj.webpush.vapidPublicKey = vapidKeys.publicKey;
obj.webpush.setVapidDetails('mailto:' + config.settings.webpush.email, vapidKeys.publicKey, vapidKeys.privateKey);
if (typeof config.settings.webpush.gcmapi == 'string') { webpush.setGCMAPIKey(config.settings.webpush.gcmapi); }
}
// Setup Firebase
if ((config.firebase != null) && (typeof config.firebase.senderid == 'string') && (typeof config.firebase.serverkey == 'string')) {
obj.firebase = require('./firebase').CreateFirebase(obj, config.firebase.senderid, config.firebase.serverkey);
} else if ((typeof config.firebaserelay == 'object') && (typeof config.firebaserelay.url == 'string')) {
// Setup the push messaging relay
obj.firebase = require('./firebase').CreateFirebaseRelay(obj, config.firebaserelay.url, config.firebaserelay.key);
} else if (obj.config.settings.publicpushnotifications === true) {
// Setup the Firebase push messaging relay using https://alt.meshcentral.com, this is the public push notification server.
obj.firebase = require('./firebase').CreateFirebaseRelay(obj, 'https://alt.meshcentral.com/firebaserelay.aspx');
}
// Start periodic maintenance
obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 60 * 60); // Run this every hour
// Dispatch an event that the server is now running
obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'started', msg: 'Server started' });
// Plugin hook. Need to run something at server startup? This is the place.
if (obj.pluginHandler) { obj.pluginHandler.callHook('server_startup'); }
// Setup the login cookie encryption key
if ((obj.config) && (obj.config.settings) && (typeof obj.config.settings.logincookieencryptionkey == 'string')) {
// We have a string, hash it and use that as a key
try { obj.loginCookieEncryptionKey = Buffer.from(obj.config.settings.logincookieencryptionkey, 'hex'); } catch (ex) { }
if ((obj.loginCookieEncryptionKey == null) || (obj.loginCookieEncryptionKey.length != 80)) { addServerWarning("Invalid \"LoginCookieEncryptionKey\" in config.json.", 20); obj.loginCookieEncryptionKey = null; }
}
// Login cookie encryption key not set, use one from the database
if (obj.loginCookieEncryptionKey == null) {
obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
} else {
obj.loginCookieEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() });
}
});
}
// Load the invitation link encryption key from the database
obj.db.Get('InvitationLinkEncryptionKey', function (err, docs) {
if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (docs[0].key.length >= 160)) {
obj.invitationLinkEncryptionKey = Buffer.from(docs[0].key, 'hex');
} else {
obj.invitationLinkEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'InvitationLinkEncryptionKey', key: obj.invitationLinkEncryptionKey.toString('hex'), time: Date.now() });
}
});
// Setup Intel AMT hello server
if ((typeof config.settings.amtprovisioningserver == 'object') && (typeof config.settings.amtprovisioningserver.devicegroup == 'string') && (typeof config.settings.amtprovisioningserver.newmebxpassword == 'string') && (typeof config.settings.amtprovisioningserver.trustedfqdn == 'string') && (typeof config.settings.amtprovisioningserver.ip == 'string')) {
obj.amtProvisioningServer = require('./amtprovisioningserver').CreateAmtProvisioningServer(obj, config.settings.amtprovisioningserver);
}
// Start collecting server stats every 5 minutes
obj.trafficStats = obj.webserver.getTrafficStats();
setInterval(function () {
obj.serverStatsCounter++;
var hours = 720; // Start with all events lasting 30 days.
if (((obj.serverStatsCounter) % 2) == 1) { hours = 3; } // Half of the event get removed after 3 hours.
else if ((Math.floor(obj.serverStatsCounter / 2) % 2) == 1) { hours = 8; } // Another half of the event get removed after 8 hours.
else if ((Math.floor(obj.serverStatsCounter / 4) % 2) == 1) { hours = 24; } // Another half of the event get removed after 24 hours.
else if ((Math.floor(obj.serverStatsCounter / 8) % 2) == 1) { hours = 48; } // Another half of the event get removed after 48 hours.
else if ((Math.floor(obj.serverStatsCounter / 16) % 2) == 1) { hours = 72; } // Another half of the event get removed after 72 hours.
const expire = new Date();
expire.setTime(expire.getTime() + (60 * 60 * 1000 * hours));
// Get traffic data
var trafficStats = obj.webserver.getTrafficDelta(obj.trafficStats);
obj.trafficStats = trafficStats.current;
var data = {
time: new Date(),
expire: expire,
mem: process.memoryUsage(),
conn: {
ca: Object.keys(obj.webserver.wsagents).length,
cu: Object.keys(obj.webserver.wssessions).length,
us: Object.keys(obj.webserver.wssessions2).length,
rs: obj.webserver.relaySessionCount
},
traffic: trafficStats.delta
};
try { data.cpu = require('os').loadavg(); } catch (ex) { }
if (obj.mpsserver != null) {
data.conn.am = 0;
for (var i in obj.mpsserver.ciraConnections) { data.conn.am += obj.mpsserver.ciraConnections[i].length; }
}
if (obj.firstStats === true) { delete obj.firstStats; data.first = true; }
if (obj.multiServer != null) { data.s = obj.multiServer.serverid; }
obj.db.SetServerStats(data); // Save the stats to the database
obj.DispatchEvent(['*'], obj, { action: 'servertimelinestats', data: data }); // Event the server stats
}, 300000);
obj.debug('main', "Server started");
if (obj.args.nousers == true) { obj.updateServerState('nousers', '1'); }
obj.updateServerState('state', "running");
// Setup auto-backup defaults
if (obj.config.settings.autobackup == null) { obj.config.settings.autobackup = { backupintervalhours: 24, keeplastdaysbackup: 10 }; }
else if (obj.config.settings.autobackup === false) { delete obj.config.settings.autobackup; }
// Check that autobackup path is not within the "meshcentral-data" folder.
if ((typeof obj.config.settings.autobackup == 'object') && (typeof obj.config.settings.autobackup.backuppath == 'string') && (obj.path.normalize(obj.config.settings.autobackup.backuppath).startsWith(obj.path.normalize(obj.datapath)))) {
addServerWarning("Backup path can't be set within meshcentral-data folder, backup settings ignored.", 21);
delete obj.config.settings.autobackup;
}
// Load Intel AMT passwords from the "amtactivation.log" file
obj.loadAmtActivationLogPasswords(function (amtPasswords) {
obj.amtPasswords = amtPasswords;
});
// Setup users that can see all device groups
if (typeof obj.config.settings.managealldevicegroups == 'string') { obj.config.settings.managealldevicegroups = obj.config.settings.managealldevicegroups.split(','); }
else if (Array.isArray(obj.config.settings.managealldevicegroups) == false) { obj.config.settings.managealldevicegroups = []; }
for (i in obj.config.domains) {
if (Array.isArray(obj.config.domains[i].managealldevicegroups)) {
for (var j in obj.config.domains[i].managealldevicegroups) {
if (typeof obj.config.domains[i].managealldevicegroups[j] == 'string') {
const u = 'user/' + i + '/' + obj.config.domains[i].managealldevicegroups[j];
if (obj.config.settings.managealldevicegroups.indexOf(u) == -1) { obj.config.settings.managealldevicegroups.push(u); }
}
}
}
}
obj.config.settings.managealldevicegroups.sort();
// Start watchdog timer if needed
// This is used to monitor if NodeJS is servicing IO correctly or getting held up a lot. Add this line to the settings section of config.json
// "watchDog": { "interval": 100, "timeout": 150 }
// This will check every 100ms, if the timer is more than 150ms late, it will warn.
if ((typeof config.settings.watchdog == 'object') && (typeof config.settings.watchdog.interval == 'number') && (typeof config.settings.watchdog.timeout == 'number') && (config.settings.watchdog.interval >= 50) && (config.settings.watchdog.timeout >= 50)) {
obj.watchdogtime = Date.now();
obj.watchdogmax = 0;
obj.watchdogmaxtime = null;
obj.watchdogtable = [];
obj.watchdog = setInterval(function () {
const now = Date.now(), delta = now - obj.watchdogtime - config.settings.watchdog.interval;
if (delta > obj.watchdogmax) { obj.watchdogmax = delta; obj.watchdogmaxtime = new Date().toLocaleString(); }
if (delta > config.settings.watchdog.timeout) {
const msg = obj.common.format("Watchdog timer timeout, {0}ms.", delta);
obj.watchdogtable.push(new Date().toLocaleString() + ', ' + delta + 'ms');
while (obj.watchdogtable.length > 10) { obj.watchdogtable.shift(); }
obj.debug('main', msg);
try {
var errlogpath = null;
if (typeof obj.args.mesherrorlogpath == 'string') { errlogpath = obj.path.join(obj.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = obj.getConfigFilePath('mesherrors.txt'); }
obj.fs.appendFileSync(errlogpath, new Date().toLocaleString() + ': ' + msg + '\r\n');
} catch (ex) { console.log('ERROR: Unable to write to mesherrors.txt.'); }
}
obj.watchdogtime = now;
}, config.settings.watchdog.interval);
obj.debug('main', "Started watchdog timer.");
}
});
});
});
};
// Called when the web server finished loading
obj.StartEx5 = function () {
// Setup the email server for each domain
var ipKvmSupport = false;
for (var i in obj.config.domains) { if (obj.config.domains[i].ipkvm == true) { ipKvmSupport = true; } }
if (ipKvmSupport) { obj.ipKvmManager = require('./meshipkvm').CreateIPKVMManager(obj); }
// Run the server start script if present
if (typeof obj.config.settings.runonserverstarted == 'string') {
const child_process = require('child_process');
var parentpath = __dirname;
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, '../..'); }
child_process.exec(obj.config.settings.runonserverstarted + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
}
}
// Refresh any certificate hashs from the reverse proxy
obj.pendingProxyCertificatesRequests = 0;
obj.lastProxyCertificatesRequest = null;
obj.supportsProxyCertificatesRequest = false;
obj.updateProxyCertificates = function (force) {
if (force !== true) {
if ((obj.pendingProxyCertificatesRequests > 0) || (obj.supportsProxyCertificatesRequest == false)) return;
if ((obj.lastProxyCertificatesRequest != null) && ((Date.now() - obj.lastProxyCertificatesRequest) < 120000)) return; // Don't allow this call more than every 2 minutes.
obj.lastProxyCertificatesRequest = Date.now();
}
// Load any domain web certificates
for (var i in obj.config.domains) {
if (obj.config.domains[i].certurl != null) {
// Load web certs
obj.pendingProxyCertificatesRequests++;
var dnsname = obj.config.domains[i].dns;
if ((dnsname == null) && (obj.config.settings.cert != null)) { dnsname = obj.config.settings.cert; }
obj.certificateOperations.loadCertificate(obj.config.domains[i].certurl, dnsname, obj.config.domains[i], function (url, cert, xhostname, xdomain) {
obj.pendingProxyCertificatesRequests--;
if (cert != null) {
// Hash the entire cert
const hash = obj.crypto.createHash('sha384').update(Buffer.from(cert, 'binary')).digest('hex');
if (xdomain.certhash != hash) { // The certificate has changed.
xdomain.certkeyhash = hash;
xdomain.certhash = hash;
try {
// Decode a RSA certificate and hash the public key, if this is not RSA, skip this.
const forgeCert = obj.certificateOperations.forge.pki.certificateFromAsn1(obj.certificateOperations.forge.asn1.fromDer(cert));
xdomain.certkeyhash = obj.certificateOperations.forge.pki.getPublicKeyFingerprint(forgeCert.publicKey, { md: obj.certificateOperations.forge.md.sha384.create(), encoding: 'hex' });
obj.webserver.webCertificateExpire[xdomain.id] = Date.parse(forgeCert.validity.notAfter); // Update certificate expire time
//console.log('V1: ' + xdomain.certkeyhash);
} catch (ex) {
delete obj.webserver.webCertificateExpire[xdomain.id]; // Remove certificate expire time
delete xdomain.certkeyhash;
}
if (obj.webserver) {
obj.webserver.webCertificateHashs[xdomain.id] = obj.webserver.webCertificateFullHashs[xdomain.id] = Buffer.from(hash, 'hex').toString('binary');
if (xdomain.certkeyhash != null) { obj.webserver.webCertificateHashs[xdomain.id] = Buffer.from(xdomain.certkeyhash, 'hex').toString('binary'); }
// Disconnect all agents with bad web certificates
for (var i in obj.webserver.wsagentsWithBadWebCerts) { obj.webserver.wsagentsWithBadWebCerts[i].close(1); }
}
console.log(obj.common.format("Loaded web certificate from \"{0}\", host: \"{1}\"", url, xhostname));
console.log(obj.common.format(" SHA384 cert hash: {0}", xdomain.certhash));
if ((xdomain.certkeyhash != null) && (xdomain.certhash != xdomain.certkeyhash)) { console.log(obj.common.format(" SHA384 key hash: {0}", xdomain.certkeyhash)); }
}
} else {
console.log(obj.common.format("Failed to load web certificate at: \"{0}\", host: \"{1}\"", url, xhostname));
}
});
}
}
}
// Perform maintenance operations (called every hour)
obj.maintenanceActions = function () {
// Perform database maintenance
obj.db.maintenance();
// Clean up any temporary files
const removeTime = new Date(Date.now()).getTime() - (30 * 60 * 1000); // 30 minutes
const dir = obj.fs.readdir(obj.path.join(obj.filespath, 'tmp'), function (err, files) {
if (err != null) return;
for (var i in files) { try { const filepath = obj.path.join(obj.filespath, 'tmp', files[i]); if (obj.fs.statSync(filepath).mtime.getTime() < removeTime) { obj.fs.unlink(filepath, function () { }); } } catch (ex) { } }
});
// Check for self-update that targets a specific version
if ((typeof obj.args.selfupdate == 'string') && (getCurrentVersion() === obj.args.selfupdate)) { obj.args.selfupdate = false; }
// Check if we need to perform server self-update
if ((obj.args.selfupdate) && (obj.serverSelfWriteAllowed == true)) {
obj.db.getValueOfTheDay('performSelfUpdate', 1, function (performSelfUpdate) {
if (performSelfUpdate.value > 0) {
performSelfUpdate.value--;
obj.db.Set(performSelfUpdate);
obj.getLatestServerVersion(function (currentVer, latestVer) { if (currentVer != latestVer) { obj.performServerUpdate(); return; } });
} else {
checkAutobackup();
}
});
} else {
checkAutobackup();
}
};
// Check if we need to perform an automatic backup
function checkAutobackup() {
if (obj.config.settings.autobackup && (typeof obj.config.settings.autobackup.backupintervalhours == 'number')) {
obj.db.Get('LastAutoBackupTime', function (err, docs) {
if (err != null) return;
var lastBackup = 0;
const now = new Date().getTime();
if (docs.length == 1) { lastBackup = docs[0].value; }
const delta = now - lastBackup;
if (delta > (obj.config.settings.autobackup.backupintervalhours * 60 * 60 * 1000)) {
// A new auto-backup is required.
obj.db.Set({ _id: 'LastAutoBackupTime', value: now }); // Save the current time in the database
obj.db.performBackup(); // Perform the backup
}
});
}
}
// Stop the Meshcentral server
obj.Stop = function (restoreFile) {
// If the database is not setup, exit now.
if (!obj.db) return;
// Dispatch an event saying the server is now stopping
obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'stopped', msg: "Server stopped" });
// Set all nodes to power state of unknown (0)
obj.db.storePowerEvent({ time: new Date(), nodeid: '*', power: 0, s: 2 }, obj.multiServer, function () { // s:2 indicates that the server is shutting down.
if (restoreFile) {
obj.debug('main', obj.common.format("Server stopped, updating settings: {0}", restoreFile));
console.log("Updating settings folder...");
const yauzl = require('yauzl');
yauzl.open(restoreFile, { lazyEntries: true }, function (err, zipfile) {
if (err) throw err;
zipfile.readEntry();
zipfile.on('entry', function (entry) {
if (/\/$/.test(entry.fileName)) {
// Directory file names end with '/'.
// Note that entires for directories themselves are optional.
// An entry's fileName implicitly requires its parent directories to exist.
zipfile.readEntry();
} else {
// File entry
zipfile.openReadStream(entry, function (err, readStream) {
if (err) throw err;
readStream.on('end', function () { zipfile.readEntry(); });
var directory = obj.path.dirname(entry.fileName);
if (directory != '.') {
directory = obj.getConfigFilePath(directory)
if (obj.fs.existsSync(directory) == false) { obj.fs.mkdirSync(directory); }
}
//console.log('Extracting:', obj.getConfigFilePath(entry.fileName));
readStream.pipe(obj.fs.createWriteStream(obj.getConfigFilePath(entry.fileName)));
});
}
});
zipfile.on('end', function () { setTimeout(function () { obj.fs.unlinkSync(restoreFile); process.exit(123); }); });
});
} else {
obj.debug('main', "Server stopped");
process.exit(0);
}
});
// Update the server state
obj.updateServerState('state', "stopped");
};
// Event Dispatch
obj.AddEventDispatch = function (ids, target) {
obj.debug('dispatch', 'AddEventDispatch', ids);
for (var i in ids) { var id = ids[i]; if (!obj.eventsDispatch[id]) { obj.eventsDispatch[id] = [target]; } else { obj.eventsDispatch[id].push(target); } }
};
obj.RemoveEventDispatch = function (ids, target) {
obj.debug('dispatch', 'RemoveEventDispatch', ids);
for (var i in ids) {
const id = ids[i];
if (obj.eventsDispatch[id]) {
var j = obj.eventsDispatch[id].indexOf(target);
if (j >= 0) {
if (obj.eventsDispatch[id].length == 1) {
delete obj.eventsDispatch[id];
} else {
const newList = []; // We create a new list so not to modify the original list. Allows this function to be called during an event dispatch.
for (var k in obj.eventsDispatch[i]) { if (obj.eventsDispatch[i][k] != target) { newList.push(obj.eventsDispatch[i][k]); } }
obj.eventsDispatch[i] = newList;
}
}
}
}
};
obj.RemoveEventDispatchId = function (id) {
obj.debug('dispatch', 'RemoveEventDispatchId', id);
if (obj.eventsDispatch[id] != null) { delete obj.eventsDispatch[id]; }
};
obj.RemoveAllEventDispatch = function (target) {
obj.debug('dispatch', 'RemoveAllEventDispatch');
for (var i in obj.eventsDispatch) {
const j = obj.eventsDispatch[i].indexOf(target);
if (j >= 0) {
if (obj.eventsDispatch[i].length == 1) {
delete obj.eventsDispatch[i];
} else {
const newList = []; // We create a new list so not to modify the original list. Allows this function to be called during an event dispatch.
for (var k in obj.eventsDispatch[i]) { if (obj.eventsDispatch[i][k] != target) { newList.push(obj.eventsDispatch[i][k]); } }
obj.eventsDispatch[i] = newList;
}
}
}
};
obj.DispatchEvent = function (ids, source, event, fromPeerServer) {
// If the database is not setup, exit now.
if (!obj.db) return;
// Send event to syslog if needed
if (obj.syslog && event.msg) { obj.syslog.log(obj.syslog.LOG_INFO, event.msg); }
if (obj.syslogjson) { obj.syslogjson.log(obj.syslogjson.LOG_INFO, JSON.stringify(event)); }
if (obj.syslogtcp && event.msg) { obj.syslogtcp.log(event.msg, obj.syslogtcp.LOG_INFO); }
obj.debug('dispatch', 'DispatchEvent', ids);
if ((typeof event == 'object') && (!event.nolog)) {
event.time = new Date();
// The event we store is going to skip some of the fields so we don't store too much stuff in the database.
const storeEvent = Object.assign({}, event);
if (storeEvent.node) { delete storeEvent.node; } // Skip the "node" field. May skip more in the future.
if (storeEvent.links) {
// Escape "links" names that may have "." and/or "$"
storeEvent.links = Object.assign({}, storeEvent.links);
for (var i in storeEvent.links) { var ue = obj.common.escapeFieldName(i); if (ue !== i) { storeEvent.links[ue] = storeEvent.links[i]; delete storeEvent.links[i]; } }
}
storeEvent.ids = ids;
obj.db.StoreEvent(storeEvent);
}
const targets = []; // List of targets we dispatched the event to, we don't want to dispatch to the same target twice.
for (var j in ids) {
const id = ids[j];
const eventsDispatch = obj.eventsDispatch[id];
if (eventsDispatch) {
for (var i in eventsDispatch) {
if (targets.indexOf(eventsDispatch[i]) == -1) { // Check if we already displatched to this target
targets.push(eventsDispatch[i]);
try { eventsDispatch[i].HandleEvent(source, event, ids, id); } catch (ex) { console.log(ex, eventsDispatch[i]); }
}
}
}
}
if ((fromPeerServer == null) && (obj.multiServer != null) && ((typeof event != 'object') || (event.nopeers != 1))) { obj.multiServer.DispatchEvent(ids, source, event); }
};
// Get the connection state of a node
obj.GetConnectivityState = function (nodeid) { return obj.connectivityByNode[nodeid]; };
// Get the routing server id for a given node and connection type, can never be self.
obj.GetRoutingServerIdNotSelf = function (nodeid, connectType) {
if (obj.multiServer == null) return null;
for (var serverid in obj.peerConnectivityByNode) {
if (serverid == obj.serverId) continue;
var state = obj.peerConnectivityByNode[serverid][nodeid];
if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: serverid, meshid: state.meshid }; }
}
return null;
};
// Get the routing server id for a given node and connection type, self first
obj.GetRoutingServerId = function (nodeid, connectType) {
if (obj.multiServer == null) return null;
// Look at our own server first
var connections = obj.peerConnectivityByNode[obj.serverId];
if (connections != null) {
var state = connections[nodeid];
if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: obj.serverId, meshid: state.meshid }; }
}
// Look at other servers
for (var serverid in obj.peerConnectivityByNode) {
if (serverid == obj.serverId) continue;
var state = obj.peerConnectivityByNode[serverid][nodeid];
if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: serverid, meshid: state.meshid }; }
}
return null;
};
// Update the connection state of a node when in multi-server mode
// Update obj.connectivityByNode using obj.peerConnectivityByNode for the list of nodes in argument
obj.UpdateConnectivityState = function (nodeids) {
for (var nodeid in nodeids) {
var meshid = null, state = null, oldConnectivity = 0, oldPowerState = 0, newConnectivity = 0, newPowerState = 0;
var oldState = obj.connectivityByNode[nodeid];
if (oldState != null) { meshid = oldState.meshid; oldConnectivity = oldState.connectivity; oldPowerState = oldState.powerState; }
for (var serverid in obj.peerConnectivityByNode) {
var peerState = obj.peerConnectivityByNode[serverid][nodeid];
if (peerState != null) {
if (state == null) {
// Copy the state
state = {};
newConnectivity = state.connectivity = peerState.connectivity;
newPowerState = state.powerState = peerState.powerState;
meshid = state.meshid = peerState.meshid;
//if (peerState.agentPower) { state.agentPower = peerState.agentPower; }
//if (peerState.ciraPower) { state.ciraPower = peerState.ciraPower; }
//if (peerState.amtPower) { state.amtPower = peerState.amtPower; }
} else {
// Merge the state
state.connectivity |= peerState.connectivity;
newConnectivity = state.connectivity;
if ((peerState.powerState != 0) && ((state.powerState == 0) || (peerState.powerState < state.powerState))) { newPowerState = state.powerState = peerState.powerState; }
meshid = state.meshid = peerState.meshid;
//if (peerState.agentPower) { state.agentPower = peerState.agentPower; }
//if (peerState.ciraPower) { state.ciraPower = peerState.ciraPower; }
//if (peerState.amtPower) { state.amtPower = peerState.amtPower; }
}
}
}
obj.connectivityByNode[nodeid] = state;
//console.log('xx', nodeid, meshid, newConnectivity, oldPowerState, newPowerState, oldPowerState);
// Event any changes on this server only
if ((newConnectivity != oldPowerState) || (newPowerState != oldPowerState)) {
obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: newConnectivity, pwr: newPowerState, nolog: 1, nopeers: 1 });
}
}
};
// See if we need to notifiy any user of device state change
obj.NotifyUserOfDeviceStateChange = function (meshid, nodeid, connectTime, connectType, powerState, serverid, stateSet, extraInfo) {
// Check if there is a email server for this domain
const meshSplit = meshid.split('/');
if (meshSplit.length != 3) return;
const domainId = meshSplit[1];
if (obj.config.domains[domainId] == null) return;
const mailserver = obj.config.domains[domainId].mailserver;
if ((mailserver == null) && (obj.msgserver == null)) return;
// Get the device group for this device
const mesh = obj.webserver.meshes[meshid];
if ((mesh == null) || (mesh.links == null)) return;
// Get the list of users that have visibility to this device
// This includes users that are part of user groups
const users = [];
for (var i in mesh.links) {
if (i.startsWith('user/') && (users.indexOf(i) < 0)) { users.push(i); }
if (i.startsWith('ugrp/')) {
var usergrp = obj.webserver.userGroups[i];
if (usergrp.links != null) { for (var j in usergrp.links) { if (j.startsWith('user/') && (users.indexOf(j) < 0)) { users.push(j); } } }
}
}
// Check if any user needs email notification
for (var i in users) {
const user = obj.webserver.users[users[i]];
if (user != null) {
var notify = 0;
// Device group notifications
const meshLinks = user.links[meshid];
if ((meshLinks != null) && (meshLinks.notify != null)) { notify |= meshLinks.notify; }
// User notifications
if (user.notify != null) {
if (user.notify[meshid] != null) { notify |= user.notify[meshid]; }
if (user.notify[nodeid] != null) { notify |= user.notify[nodeid]; }
}
// Email notifications
if ((user.email != null) && (user.emailVerified == true) && (mailserver != null) && ((notify & 48) != 0)) {
if (stateSet == true) {
if ((notify & 16) != 0) {
mailserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
} else {
mailserver.cancelNotifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
}
}
else if (stateSet == false) {
if ((notify & 32) != 0) {
mailserver.notifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
} else {
mailserver.cancelNotifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
}
}
}
// Messaging notifications
if ((obj.msgserver != null) && ((notify & 384) != 0)) {
if (stateSet == true) {
if ((notify & 128) != 0) {
obj.msgserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
} else {
obj.msgserver.cancelNotifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
}
}
else if (stateSet == false) {
if ((notify & 256) != 0) {
obj.msgserver.notifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
} else {
obj.msgserver.cancelNotifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
}
}
}
}
}
}
// See if we need to notifiy any user of device requested help
//if (typeof device.name == 'string') { parent.parent.NotifyUserOfDeviceHelpRequest(domain, device._id, device.meshid, device.name, command.msgArgs[0], command.msgArgs[1]); }
obj.NotifyUserOfDeviceHelpRequest = function (domain, meshid, nodeid, devicename, helpusername, helprequest) {
// Check if there is a email server for this domain
const meshSplit = meshid.split('/');
if (meshSplit.length != 3) return;
const domainId = meshSplit[1];
if (obj.config.domains[domainId] == null) return;
const mailserver = obj.config.domains[domainId].mailserver;
if ((mailserver == null) && (obj.msgserver == null)) return;
// Get the device group for this device
const mesh = obj.webserver.meshes[meshid];
if ((mesh == null) || (mesh.links == null)) return;
// Get the list of users that have visibility to this device
// This includes users that are part of user groups
const users = [];
for (var i in mesh.links) {
if (i.startsWith('user/') && (users.indexOf(i) < 0)) { users.push(i); }
if (i.startsWith('ugrp/')) {
var usergrp = obj.webserver.userGroups[i];
if (usergrp.links != null) { for (var j in usergrp.links) { if (j.startsWith('user/') && (users.indexOf(j) < 0)) { users.push(j); } } }
}
}
// Check if any user needs email notification
for (var i in users) {
const user = obj.webserver.users[users[i]];
if (user != null) {
var notify = 0;
// Device group notifications
const meshLinks = user.links[meshid];
if ((meshLinks != null) && (meshLinks.notify != null)) { notify |= meshLinks.notify; }
// User notifications
if (user.notify != null) {
if (user.notify[meshid] != null) { notify |= user.notify[meshid]; }
if (user.notify[nodeid] != null) { notify |= user.notify[nodeid]; }
}
// Mail help request
if ((user.email != null) && (user.emailVerified == true) && ((notify & 64) != 0)) { mailserver.sendDeviceHelpMail(domain, user.name, user.email, devicename, nodeid, helpusername, helprequest, user.llang); }
// Message help request
if ((user.msghandle != null) && ((notify & 512) != 0)) { obj.msgserver.sendDeviceHelpRequest(domain, user.name, user.msghandle, devicename, nodeid, helpusername, helprequest, user.llang); }
}
}
}
// Set the connectivity state of a node and setup the server so that messages can be routed correctly.
// meshId: mesh identifier of format mesh/domain/meshidhex
// nodeId: node identifier of format node/domain/nodeidhex
// connectTime: time of connection, milliseconds elapsed since the UNIX epoch.
// connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 4 = Intel AMT local, 8 = Intel AMT Relay, 16 = MQTT
// powerState: Value, 0 = Unknown, 1 = S0 power on, 2 = S1 Sleep, 3 = S2 Sleep, 4 = S3 Sleep, 5 = S4 Hibernate, 6 = S5 Soft-Off, 7 = Present, 8 = Off
//var connectTypeStrings = ['', 'MeshAgent', 'Intel AMT CIRA', '', 'Intel AMT local', '', '', '', 'Intel AMT Relay', '', '', '', '', '', '', '', 'MQTT'];
//var powerStateStrings = ['Unknown', 'Powered', 'Sleep', 'Sleep', 'Deep Sleep', 'Hibernating', 'Soft-Off', 'Present', 'Off'];
obj.SetConnectivityState = function (meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
//console.log('SetConnectivity for ' + nodeid.substring(0, 16) + ', Type: ' + connectTypeStrings[connectType] + ', Power: ' + powerStateStrings[powerState] + (serverid == null ? ('') : (', ServerId: ' + serverid)));
if ((serverid == null) && (obj.multiServer != null)) { obj.multiServer.DispatchMessage({ action: 'SetConnectivityState', meshid: meshid, nodeid: nodeid, connectTime: connectTime, connectType: connectType, powerState: powerState, extraInfo: extraInfo }); }
if (obj.multiServer == null) {
// Single server mode
// Change the node connection state
var eventConnectChange = 0;
var state = obj.connectivityByNode[nodeid];
if (state) {
// Change the connection in the node and mesh state lists
if ((state.connectivity & connectType) == 0) { state.connectivity |= connectType; eventConnectChange = 1; }
state.meshid = meshid;
} else {
// Add the connection to the node and mesh state list
obj.connectivityByNode[nodeid] = state = { connectivity: connectType, meshid: meshid };
eventConnectChange = 1;
}
// Set node power state
if (connectType == 1) { state.agentPower = powerState; } else if (connectType == 2) { state.ciraPower = powerState; } else if (connectType == 4) { state.amtPower = powerState; }
var powerState = 0, oldPowerState = state.powerState;
if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
if ((state.powerState == null)|| (state.powerState == undefined) || (state.powerState != powerState)) {
state.powerState = powerState;
eventConnectChange = 1;
// Set new power state in database
const record = { time: new Date(connectTime), nodeid: nodeid, power: powerState };
if (oldPowerState != null) { record.oldPower = oldPowerState; }
obj.db.storePowerEvent(record, obj.multiServer);
}
// Event the node connection change
if (eventConnectChange == 1) {
obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: state.connectivity, pwr: state.powerState, ct: connectTime, nolog: 1, nopeers: 1 });
// Save indication of node connection change
const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 1, connectType: connectType };
if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
obj.db.Set(lc);
// Notify any users of device connection
obj.NotifyUserOfDeviceStateChange(meshid, nodeid, connectTime, connectType, powerState, serverid, true, extraInfo);
}
} else {
// Multi server mode
// Change the node connection state
if (serverid == null) { serverid = obj.serverId; }
if (obj.peerConnectivityByNode[serverid] == null) return; // Guard against unknown serverid's
var eventConnectChange = 0;
var state = obj.peerConnectivityByNode[serverid][nodeid];
if (state) {
// Change the connection in the node and mesh state lists
if ((state.connectivity & connectType) == 0) { state.connectivity |= connectType; eventConnectChange = 1; }
state.meshid = meshid;
} else {
// Add the connection to the node and mesh state list
obj.peerConnectivityByNode[serverid][nodeid] = state = { connectivity: connectType, meshid: meshid };
eventConnectChange = 1;
}
// Set node power state
if (connectType == 1) { state.agentPower = powerState; } else if (connectType == 2) { state.ciraPower = powerState; } else if (connectType == 4) { state.amtPower = powerState; }
var powerState = 0, oldPowerState = state.powerState;
if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
if ((state.powerState == null)|| (state.powerState == undefined) || (state.powerState != powerState)) {
state.powerState = powerState;
eventConnectChange = 1;
// Set new power state in database
var record = { time: new Date(connectTime), nodeid: nodeid, power: powerState, server: obj.multiServer.serverid };
if (oldPowerState != null) { record.oldPower = oldPowerState; }
obj.db.storePowerEvent(record, obj.multiServer);
}
if (eventConnectChange == 1) {
// Update the combined node state
var x = {}; x[nodeid] = 1;
obj.UpdateConnectivityState(x);
// Save indication of node connection change
if (serverid == obj.serverId) {
const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 1, connectType: connectType, serverid: obj.serverId };
if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
obj.db.Set(lc);
}
// Notify any users of device connection
obj.NotifyUserOfDeviceStateChange(meshid, nodeid, connectTime, connectType, powerState, serverid, true, extraInfo);
}
}
};
// Clear the connectivity state of a node and setup the server so that messages can be routed correctly.
// meshId: mesh identifier of format mesh/domain/meshidhex
// nodeId: node identifier of format node/domain/nodeidhex
// connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 3 = Intel AMT local.
obj.ClearConnectivityState = function (meshid, nodeid, connectType, serverid, extraInfo) {
//console.log('ClearConnectivity for ' + nodeid.substring(0, 16) + ', Type: ' + connectTypeStrings[connectType] + (serverid == null?(''):(', ServerId: ' + serverid)));
if ((serverid == null) && (obj.multiServer != null)) { obj.multiServer.DispatchMessage({ action: 'ClearConnectivityState', meshid: meshid, nodeid: nodeid, connectType: connectType, extraInfo: extraInfo }); }
if (obj.multiServer == null) {
// Single server mode
var eventConnectChange = 0;
// Remove the agent connection from the nodes connection list
const state = obj.connectivityByNode[nodeid];
if (state == null) return;
if ((state.connectivity & connectType) != 0) {
state.connectivity -= connectType;
// Save indication of node connection change
const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 0, connectType: connectType };
if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
obj.db.Set(lc);
// If the node is completely disconnected, clean it up completely
if (state.connectivity == 0) { delete obj.connectivityByNode[nodeid]; }
eventConnectChange = 1;
}
// Clear node power state
var powerState = 0;
const oldPowerState = state.powerState;
if (connectType == 1) { state.agentPower = 0; } else if (connectType == 2) { state.ciraPower = 0; } else if (connectType == 4) { state.amtPower = 0; }
if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
if ((state.powerState == null) || (state.powerState != powerState)) {
state.powerState = powerState;
eventConnectChange = 1;
// Set new power state in database
obj.db.storePowerEvent({ time: new Date(), nodeid: nodeid, power: powerState, oldPower: oldPowerState }, obj.multiServer);
}
// Event the node connection change
if (eventConnectChange == 1) {
obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: state.connectivity, pwr: state.powerState, nolog: 1, nopeers: 1 });
// Notify any users of device disconnection
obj.NotifyUserOfDeviceStateChange(meshid, nodeid, Date.now(), connectType, -1, serverid, false, extraInfo);
}
} else {
// Multi server mode
// Remove the agent connection from the nodes connection list
if (serverid == null) { serverid = obj.serverId; }
if (obj.peerConnectivityByNode[serverid] == null) return; // Guard against unknown serverid's
var state = obj.peerConnectivityByNode[serverid][nodeid];
if (state == null) return;
// If existing state exist, remove this connection
if ((state.connectivity & connectType) != 0) {
state.connectivity -= connectType; // Remove one connectivity mode
// Save indication of node connection change
if (serverid == obj.serverId) {
const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 0, connectType: connectType, serverid: obj.serverId };
if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
obj.db.Set(lc);
}
// If the node is completely disconnected, clean it up completely
if (state.connectivity == 0) { delete obj.peerConnectivityByNode[serverid][nodeid]; state.powerState = 0; }
// Notify any users of device disconnection
obj.NotifyUserOfDeviceStateChange(meshid, nodeid, Date.now(), connectType, -1, serverid, false, extraInfo);
}
// Clear node power state
if (connectType == 1) { state.agentPower = 0; } else if (connectType == 2) { state.ciraPower = 0; } else if (connectType == 4) { state.amtPower = 0; }
var powerState = 0;
if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
if ((state.powerState == null) || (state.powerState != powerState)) { state.powerState = powerState; }
// Update the combined node state
var x = {}; x[nodeid] = 1;
obj.UpdateConnectivityState(x);
}
};
// Escape a code string
obj.escapeCodeString = function (str, keepUtf8) {
const escapeCodeStringTable = { '\'': '\\\'', '\"': '\\"', '\\': '\\\\', '\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t' };
var r = '', c, cr, table;
for (var i = 0; i < str.length; i++) {
c = str[i];
table = escapeCodeStringTable[c];
if (table != null) {
r += table;
} else if (keepUtf8 === true) {
r += c;
} else {
cr = c.charCodeAt(0);
if ((cr >= 32) && (cr <= 127)) { r += c; }
}
}
return r;
}
// Update the default mesh core
obj.updateMeshCore = function (func, dumpToFile) {
// Figure out where meshcore.js is
var meshcorePath = obj.datapath;
if (obj.fs.existsSync(obj.path.join(meshcorePath, 'meshcore.js')) == false) {
meshcorePath = obj.path.join(__dirname, 'agents');
if (obj.fs.existsSync(obj.path.join(meshcorePath, 'meshcore.js')) == false) {
obj.defaultMeshCores = obj.defaultMeshCoresHash = { }; if (func != null) { func(false); } // meshcore.js not found
}
}
// Read meshcore.js and all .js files in the modules folder.
var meshCore = null, modulesDir = null;
const modulesAdd = {
'windows-amt': ['var addedModules = [];\r\n'],
'linux-amt': ['var addedModules = [];\r\n'],
'linux-noamt': ['var addedModules = [];\r\n']
};
// Read the recovery core if present
var meshRecoveryCore = null;
if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'recoverycore.js')) == true) {
try { meshRecoveryCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'recoverycore.js')).toString(); } catch (ex) { }
if (meshRecoveryCore != null) {
modulesAdd['windows-recovery'] = ['var addedModules = [];\r\n'];
modulesAdd['linux-recovery'] = ['var addedModules = [];\r\n'];
}
}
// Read the agent recovery core if present
var meshAgentRecoveryCore = null;
if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'meshcore_diagnostic.js')) == true) {
try { meshAgentRecoveryCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'meshcore_diagnostic.js')).toString(); } catch (ex) { }
if (meshAgentRecoveryCore != null) {
modulesAdd['windows-agentrecovery'] = ['var addedModules = [];\r\n'];
modulesAdd['linux-agentrecovery'] = ['var addedModules = [];\r\n'];
}
}
// Read the tiny core if present
var meshTinyCore = null;
if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'tinycore.js')) == true) {
try { meshTinyCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'tinycore.js')).toString(); } catch (ex) { }
if (meshTinyCore != null) {
modulesAdd['windows-tiny'] = ['var addedModules = [];\r\n'];
modulesAdd['linux-tiny'] = ['var addedModules = [];\r\n'];
}
}
if (obj.args.minifycore !== false) { try { meshCore = obj.fs.readFileSync(obj.path.join(meshcorePath, 'meshcore.min.js')).toString(); } catch (ex) { } } // Favor minified meshcore if present.
if (meshCore == null) { try { meshCore = obj.fs.readFileSync(obj.path.join(meshcorePath, 'meshcore.js')).toString(); } catch (ex) { } } // Use non-minified meshcore.
if (meshCore != null) {
var moduleDirPath = null;
if (obj.args.minifycore !== false) { try { moduleDirPath = obj.path.join(meshcorePath, 'modules_meshcore_min'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Favor minified modules if present.
if (modulesDir == null) { try { moduleDirPath = obj.path.join(meshcorePath, 'modules_meshcore'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
if (modulesDir != null) {
for (var i in modulesDir) {
if (modulesDir[i].toLowerCase().endsWith('.json')) {
// We are adding a JSON file to the meshcores
var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 5);
if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 6); } // Remove the ".min" for ".min.json" files.
const jsonData = obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('utf8'), true);
const moduleData = ['var ', moduleName, ' = JSON.parse(\'', jsonData, '\');\r\n'];
// Add to all major cores
modulesAdd['windows-amt'].push(...moduleData);
modulesAdd['linux-amt'].push(...moduleData);
modulesAdd['linux-noamt'].push(...moduleData);
}
if (modulesDir[i].toLowerCase().endsWith('.js')) {
// We are adding a JS file to the meshcores
var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 3);
if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 4); } // Remove the ".min" for ".min.js" files.
const moduleData = [ 'try { addModule("', moduleName, '", "', obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('binary')), '"); addedModules.push("', moduleName, '"); } catch (ex) { }\r\n' ];
// Merge this module
// NOTE: "smbios" module makes some non-AI Linux segfault, only include for IA platforms.
if (moduleName.startsWith('amt-') || (moduleName == 'smbios')) {
// Add to IA / Intel AMT cores only
modulesAdd['windows-amt'].push(...moduleData);
modulesAdd['linux-amt'].push(...moduleData);
} else if (moduleName.startsWith('win-')) {
// Add to Windows cores only
modulesAdd['windows-amt'].push(...moduleData);
} else if (moduleName.startsWith('linux-')) {
// Add to Linux cores only
modulesAdd['linux-amt'].push(...moduleData);
modulesAdd['linux-noamt'].push(...moduleData);
} else {
// Add to all cores
modulesAdd['windows-amt'].push(...moduleData);
modulesAdd['linux-amt'].push(...moduleData);
modulesAdd['linux-noamt'].push(...moduleData);
}
// Merge this module to recovery modules if needed
if (modulesAdd['windows-recovery'] != null) {
if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal') || (moduleName == 'win-virtual-terminal')) {
modulesAdd['windows-recovery'].push(...moduleData);
}
}
// Merge this module to agent recovery modules if needed
if (modulesAdd['windows-agentrecovery'] != null) {
if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal') || (moduleName == 'win-virtual-terminal')) {
modulesAdd['windows-agentrecovery'].push(...moduleData);
}
}
}
}
}
// Add plugins to cores
if (obj.pluginHandler) { obj.pluginHandler.addMeshCoreModules(modulesAdd); }
// If we need to dump modules to file, create a meshcores folder
if (dumpToFile) { try { obj.fs.mkdirSync('meshcores'); } catch (ex) { } }
// Merge the cores and compute the hashes
for (var i in modulesAdd) {
if ((i == 'windows-recovery') || (i == 'linux-recovery')) {
obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshRecoveryCore].join('');
} else if ((i == 'windows-agentrecovery') || (i == 'linux-agentrecovery')) {
obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshAgentRecoveryCore].join('');
} else if ((i == 'windows-tiny') || (i == 'linux-tiny')) {
obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshTinyCore].join('');
} else {
obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshCore].join('');
}
obj.defaultMeshCores[i] = Buffer.from(obj.defaultMeshCores[i], 'utf8');
obj.defaultMeshCoresHash[i] = obj.crypto.createHash('sha384').update(obj.defaultMeshCores[i]).digest('binary');
obj.debug('main', 'Core module ' + i + ' is ' + obj.defaultMeshCores[i].length + ' bytes.');
// Write all modules to files. Great for debugging.
if (dumpToFile) {
console.log('Core module ' + i + ' is ' + obj.defaultMeshCores[i].length + ' bytes, saving to meshcores/' + i + '.js.'); // Print the core size and filename
obj.fs.writeFile('meshcores/' + i + '.js', obj.defaultMeshCores[i].slice(4), function () { }); // Write the core to file
}
// Compress the mesh cores with DEFLATE
const callback = function MeshCoreDeflateCb(err, buffer) { if (err == null) { obj.defaultMeshCoresDeflate[MeshCoreDeflateCb.i] = buffer; } }
callback.i = i;
require('zlib').deflate(obj.defaultMeshCores[i], { level: require('zlib').Z_BEST_COMPRESSION }, callback);
}
}
// We are done creating all the mesh cores.
if (func != null) { func(true); }
};
// Update the default meshcmd
obj.updateMeshCmdTimer = 'notset';
obj.updateMeshCmd = function (func) {
// Figure out where meshcmd.js is and read it.
var meshCmd = null, meshcmdPath, moduleAdditions = ['var addedModules = [];\r\n'], moduleDirPath, modulesDir = null;
if ((obj.args.minifycore !== false) && (obj.fs.existsSync(obj.path.join(obj.datapath, 'meshcmd.min.js')))) { meshcmdPath = obj.path.join(obj.datapath, 'meshcmd.min.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
else if (obj.fs.existsSync(obj.path.join(obj.datapath, 'meshcmd.js'))) { meshcmdPath = obj.path.join(obj.datapath, 'meshcmd.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
else if ((obj.args.minifycore !== false) && (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'meshcmd.min.js')))) { meshcmdPath = obj.path.join(__dirname, 'agents', 'meshcmd.min.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
else if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'meshcmd.js'))) { meshcmdPath = obj.path.join(__dirname, 'agents', 'meshcmd.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
else { obj.defaultMeshCmd = null; if (func != null) { func(false); } return; } // meshcmd.js not found
meshCmd = meshCmd.replace("'***Mesh*Cmd*Version***'", '\'' + getCurrentVersion() + '\'');
// Figure out where the modules_meshcmd folder is.
if (obj.args.minifycore !== false) { try { moduleDirPath = obj.path.join(meshcmdPath, 'modules_meshcmd_min'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Favor minified modules if present.
if (modulesDir == null) { try { moduleDirPath = obj.path.join(meshcmdPath, 'modules_meshcmd'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
if (obj.args.minifycore !== false) { if (modulesDir == null) { try { moduleDirPath = obj.path.join(__dirname, 'agents', 'modules_meshcmd_min'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } } // Favor minified modules if present.
if (modulesDir == null) { try { moduleDirPath = obj.path.join(__dirname, 'agents', 'modules_meshcmd'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
// Read all .js files in the meshcmd modules folder.
if (modulesDir != null) {
for (var i in modulesDir) {
if (modulesDir[i].toLowerCase().endsWith('.js')) {
// Merge this module
var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 3);
if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 4); } // Remove the ".min" for ".min.js" files.
moduleAdditions.push('try { addModule("', moduleName, '", "', obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('binary')), '"); addedModules.push("', moduleName, '"); } catch (ex) { }\r\n');
}
}
}
// Set the new default meshcmd.js
moduleAdditions.push(meshCmd);
obj.defaultMeshCmd = moduleAdditions.join('');
//console.log('MeshCmd is ' + obj.defaultMeshCmd.length + ' bytes.'); // DEBUG, Print the merged meshcmd.js size
//obj.fs.writeFile("C:\\temp\\meshcmd.js", obj.defaultMeshCmd.substring(4)); // DEBUG, Write merged meshcmd.js to file
if (func != null) { func(true); }
// Monitor for changes in meshcmd.js
if (obj.updateMeshCmdTimer === 'notset') {
obj.updateMeshCmdTimer = null;
obj.fs.watch(meshcmdPath, function (eventType, filename) {
if (obj.updateMeshCmdTimer != null) { clearTimeout(obj.updateMeshCmdTimer); obj.updateMeshCmdTimer = null; }
obj.updateMeshCmdTimer = setTimeout(function () { obj.updateMeshCmd(); }, 5000);
});
}
};
// List of possible mesh agent install scripts
const meshToolsList = {
'MeshCentralRouter': { localname: 'MeshCentralRouter.exe', dlname: 'winrouter' },
'MeshCentralAssistant': { localname: 'MeshCentralAssistant.exe', dlname: 'winassistant', winhash: true }
//'MeshCentralRouterMacOS': { localname: 'MeshCentralRouter.dmg', dlname: 'MeshCentralRouter.dmg' }
};
// Update the list of available mesh agents
obj.updateMeshTools = function () {
for (var toolname in meshToolsList) {
if (meshToolsList[toolname].winhash === true) {
var toolpath = obj.path.join(__dirname, 'agents', meshToolsList[toolname].localname);
const toolpath2 = obj.path.join(obj.datapath, 'agents', meshToolsList[toolname].localname);
if (obj.fs.existsSync(toolpath2)) { toolpath = toolpath2; } // If the tool is present in "meshcentral-data/agents", use that one instead.
var hashStream = obj.crypto.createHash('sha384');
hashStream.toolname = toolname;
hashStream.toolpath = toolpath;
hashStream.dlname = meshToolsList[toolname].dlname;
hashStream.hashx = 0;
hashStream.on('data', function (data) {
obj.meshToolsBinaries[this.toolname] = { hash: data.toString('hex'), hashx: this.hashx, path: this.toolpath, dlname: this.dlname, url: this.url };
obj.meshToolsBinaries[this.toolname].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?meshaction=' + this.dlname;
var stats = null;
try { stats = obj.fs.statSync(this.toolpath); } catch (ex) { }
if (stats != null) { obj.meshToolsBinaries[this.toolname].size = stats.size; }
});
const options = { sourcePath: toolpath, targetStream: hashStream };
obj.exeHandler.hashExecutableFile(options);
} else {
var toolpath = obj.path.join(__dirname, 'agents', meshToolsList[toolname].localname);
const toolpath2 = obj.path.join(obj.datapath, 'agents', meshToolsList[toolname].localname);
if (obj.fs.existsSync(toolpath2)) { toolpath = toolpath2; } // If the tool is present in "meshcentral-data/agents", use that one instead.
var stream = null;
try {
stream = obj.fs.createReadStream(toolpath);
stream.on('data', function (data) { this.hash.update(data, 'binary'); this.hashx += data.length; });
stream.on('error', function (data) {
// If there is an error reading this file, make sure this agent is not in the agent table
if (obj.meshToolsBinaries[this.toolname] != null) { delete obj.meshToolsBinaries[this.toolname]; }
});
stream.on('end', function () {
// Add the agent to the agent table with all information and the hash
obj.meshToolsBinaries[this.toolname] = {};
obj.meshToolsBinaries[this.toolname].hash = this.hash.digest('hex');
obj.meshToolsBinaries[this.toolname].hashx = this.hashx;
obj.meshToolsBinaries[this.toolname].path = this.agentpath;
obj.meshToolsBinaries[this.toolname].dlname = this.dlname;
obj.meshToolsBinaries[this.toolname].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?meshaction=' + this.dlname;
var stats = null;
try { stats = obj.fs.statSync(this.agentpath); } catch (ex) { }
if (stats != null) { obj.meshToolsBinaries[this.toolname].size = stats.size; }
});
stream.toolname = toolname;
stream.agentpath = toolpath;
stream.dlname = meshToolsList[toolname].dlname;
stream.hash = obj.crypto.createHash('sha384', stream);
stream.hashx = 0;
} catch (ex) { }
}
}
};
// List of possible mesh agent install scripts
const meshAgentsInstallScriptList = {
1: { id: 1, localname: 'meshinstall-linux.sh', rname: 'meshinstall.sh', linux: true },
2: { id: 2, localname: 'meshinstall-initd.sh', rname: 'meshagent', linux: true },
5: { id: 5, localname: 'meshinstall-bsd-rcd.sh', rname: 'meshagent', linux: true },
6: { id: 6, localname: 'meshinstall-linux.js', rname: 'meshinstall.js', linux: true }
};
// Update the list of available mesh agents
obj.updateMeshAgentInstallScripts = function () {
for (var scriptid in meshAgentsInstallScriptList) {
var scriptpath = obj.path.join(__dirname, 'agents', meshAgentsInstallScriptList[scriptid].localname);
var stream = null;
try {
stream = obj.fs.createReadStream(scriptpath);
stream.xdata = '';
stream.on('data', function (data) { this.hash.update(data, 'binary'); this.xdata += data; });
stream.on('error', function (data) {
// If there is an error reading this file, make sure this agent is not in the agent table
if (obj.meshAgentInstallScripts[this.info.id] != null) { delete obj.meshAgentInstallScripts[this.info.id]; }
});
stream.on('end', function () {
// Add the agent to the agent table with all information and the hash
obj.meshAgentInstallScripts[this.info.id] = Object.assign({}, this.info);
obj.meshAgentInstallScripts[this.info.id].hash = this.hash.digest('hex');
obj.meshAgentInstallScripts[this.info.id].path = this.agentpath;
obj.meshAgentInstallScripts[this.info.id].data = this.xdata;
obj.meshAgentInstallScripts[this.info.id].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?script=' + this.info.id;
var stats = null;
try { stats = obj.fs.statSync(this.agentpath); } catch (ex) { }
if (stats != null) { obj.meshAgentInstallScripts[this.info.id].size = stats.size; }
// Place Unit line breaks on Linux scripts if not already present.
if (obj.meshAgentInstallScripts[this.info.id].linux === true) { obj.meshAgentInstallScripts[this.info.id].data = obj.meshAgentInstallScripts[this.info.id].data.split('\r\n').join('\n') }
});
stream.info = meshAgentsInstallScriptList[scriptid];
stream.agentpath = scriptpath;
stream.hash = obj.crypto.createHash('sha384', stream);
} catch (ex) { }
}
};
// List of possible mesh agents
obj.meshAgentsArchitectureNumbers = {
0: { id: 0, localname: 'Unknown', rname: 'meshconsole.exe', desc: 'Unknown agent', update: false, amt: true, platform: 'unknown', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
1: { id: 1, localname: 'MeshConsole.exe', rname: 'meshconsole32.exe', desc: 'Windows x86-32 console', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
2: { id: 2, localname: 'MeshConsole64.exe', rname: 'meshconsole64.exe', desc: 'Windows x86-64 console', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
3: { id: 3, localname: 'MeshService.exe', rname: 'meshagent32.exe', desc: 'Windows x86-32 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', codesign: true },
4: { id: 4, localname: 'MeshService64.exe', rname: 'meshagent64.exe', desc: 'Windows x86-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', codesign: true },
5: { id: 5, localname: 'meshagent_x86', rname: 'meshagent', desc: 'Linux x86-32', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
6: { id: 6, localname: 'meshagent_x86-64', rname: 'meshagent', desc: 'Linux x86-64', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
7: { id: 7, localname: 'meshagent_mips', rname: 'meshagent', desc: 'Linux MIPS', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
8: { id: 8, localname: 'MeshAgent-Linux-XEN-x86-32', rname: 'meshagent', desc: 'XEN x86-64', update: true, amt: false, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
9: { id: 9, localname: 'meshagent_arm', rname: 'meshagent', desc: 'Linux ARM5', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
10: { id: 10, localname: 'MeshAgent-Linux-ARM-PlugPC', rname: 'meshagent', desc: 'Linux ARM PlugPC', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
11: { id: 11, localname: 'meshagent_osx-x86-32', rname: 'meshosx', desc: 'Apple macOS x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple x86-32 binary, no longer supported.
12: { id: 12, localname: 'MeshAgent-Android-x86', rname: 'meshandroid', desc: 'Android x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
13: { id: 13, localname: 'meshagent_pogo', rname: 'meshagent', desc: 'Linux ARM PogoPlug', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
14: { id: 14, localname: 'meshagent_android.apk', rname: 'meshandroid.apk', desc: 'Android', update: false, amt: false, platform: 'android', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Get this one from Google Play
15: { id: 15, localname: 'meshagent_poky', rname: 'meshagent', desc: 'Linux Poky x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
16: { id: 16, localname: 'meshagent_osx-x86-64', rname: 'meshagent', desc: 'Apple macOS x86-64', update: true, amt: false, platform: 'osx', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple x86-64 binary
17: { id: 17, localname: 'MeshAgent-ChromeOS', rname: 'meshagent', desc: 'Google ChromeOS', update: false, amt: false, platform: 'chromeos', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Get this one from Chrome store
18: { id: 18, localname: 'meshagent_poky64', rname: 'meshagent', desc: 'Linux Poky x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
19: { id: 19, localname: 'meshagent_x86_nokvm', rname: 'meshagent', desc: 'Linux x86-32 NoKVM', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
20: { id: 20, localname: 'meshagent_x86-64_nokvm', rname: 'meshagent', desc: 'Linux x86-64 NoKVM', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
21: { id: 21, localname: 'MeshAgent-WinMinCore-Console-x86-32.exe', rname: 'meshagent.exe', desc: 'Windows MinCore Console x86-32', update: true, amt: false, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
22: { id: 22, localname: 'MeshAgent-WinMinCore-Service-x86-64.exe', rname: 'meshagent.exe', desc: 'Windows MinCore Service x86-32', update: true, amt: false, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
23: { id: 23, localname: 'MeshAgent-NodeJS', rname: 'meshagent', desc: 'NodeJS', update: false, amt: false, platform: 'node', core: 'nodejs', rcore: 'nodejs', arcore: 'nodejs', tcore: 'nodejs' }, // NodeJS based agent
24: { id: 24, localname: 'meshagent_arm-linaro', rname: 'meshagent', desc: 'Linux ARM Linaro', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
25: { id: 25, localname: 'meshagent_armhf', rname: 'meshagent', desc: 'Linux ARM - HardFloat', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // "armv6l" and "armv7l"
26: { id: 26, localname: 'meshagent_aarch64', rname: 'meshagent', desc: 'Linux ARM 64 bit (glibc/2.24)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // This is replaced by ARCHID 32
27: { id: 27, localname: 'meshagent_armhf2', rname: 'meshagent', desc: 'Linux ARM - HardFloat', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Raspbian 7 2015-02-02 for old Raspberry Pi.
28: { id: 28, localname: 'meshagent_mips24kc', rname: 'meshagent', desc: 'Linux MIPS24KC/MUSL (OpenWRT)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // MIPS Router with OpenWRT
29: { id: 29, localname: 'meshagent_osx-arm-64', rname: 'meshagent', desc: 'Apple macOS ARM-64', update: true, amt: false, platform: 'osx', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple Silicon ARM 64bit
30: { id: 30, localname: 'meshagent_freebsd_x86-64', rname: 'meshagent', desc: 'FreeBSD x86-64', update: true, amt: false, platform: 'freebsd', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // FreeBSD x64
32: { id: 32, localname: 'meshagent_aarch64', rname: 'meshagent', desc: 'Linux ARM 64 bit (glibc/2.24)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
33: { id: 33, localname: 'meshagent_openwrt_x86_64', rname: 'meshagent', desc: 'OpenWRT x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // This is replaced with ARCHID 36.
34: { id: 34, localname: 'assistant_windows', rname: 'meshassistant', desc: 'MeshCentral Assistant (Windows)', update: false, amt: false, platform: 'assistant', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // MeshCentral Assistant for Windows
35: { id: 35, localname: 'meshagent_linux-armada370-hf', rname: 'meshagent', desc: 'Armada370 - ARM32/HF (libc/2.26)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Armada370
36: { id: 36, localname: 'meshagent_openwrt_x86_64', rname: 'meshagent', desc: 'OpenWRT x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // OpenWRT x86-64
37: { id: 37, localname: 'meshagent_openbsd_x86-64', rname: 'meshagent', desc: 'OpenBSD x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // OpenBSD x86-64
40: { id: 40, localname: 'meshagent_mipsel24kc', rname: 'meshagent', desc: 'Linux MIPSEL24KC (OpenWRT)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // MIPS Router with OpenWRT
41: { id: 41, localname: 'meshagent_aarch64-cortex-a53', rname: 'meshagent', desc: 'ARMADA/CORTEX-A53/MUSL (OpenWRT)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // OpenWRT Routers
42: { id: 42, localname: 'MeshConsoleARM64.exe', rname: 'meshconsolearm64.exe', desc: 'Windows ARM-64 console', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
43: { id: 43, localname: 'MeshServiceARM64.exe', rname: 'meshagentarm64.exe', desc: 'Windows ARM-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', codesign: true },
10003: { id: 10003, localname: 'MeshService.exe', rname: 'meshagent32.exe', desc: 'Windows x86-32 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', unsigned: true },
10004: { id: 10004, localname: 'MeshService64.exe', rname: 'meshagent64.exe', desc: 'Windows x86-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', unsigned: true },
10005: { id: 10005, localname: 'meshagent_osx-universal-64', rname: 'meshagent', desc: 'Apple macOS Universal Binary', update: true, amt: false, platform: 'osx', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple Silicon + x86 universal binary
10006: { id: 10006, localname: 'MeshCentralAssistant.exe', rname: 'MeshCentralAssistant.exe', desc: 'MeshCentral Assistant for Windows', update: false, amt: false, platform: 'win32' }, // MeshCentral Assistant
11000: { id: 11000, localname: 'MeshCmd.exe', rname: 'MeshCmd.exe', desc: 'Windows x86-32 meshcmd', update: false, amt: true, platform: 'win32', codesign: true }, // MeshCMD for Windows x86 32-bit
11001: { id: 11001, localname: 'MeshCmd64.exe', rname: 'MeshCmd64.exe', desc: 'Windows x86-64 meshcmd', update: false, amt: true, platform: 'win32', codesign: true }, // MeshCMD for Windows x86 64-bit
11002: { id: 11002, localname: 'MeshCmdARM64.exe', rname: 'MeshCmdARM64.exe', desc: 'Windows ARM-64 meshcmd', update: false, amt: true, platform: 'win32', codesign: true } // MeshCMD for Windows ARM 64-bit
};
// Sign windows agents
obj.signMeshAgents = function (domain, func) {
// Setup the domain is specified
var objx = domain, suffix = '';
if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
// Check if a custom agent signing certificate is available
var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
// If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
agentSignCertInfo = {
cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
}
}
if (agentSignCertInfo == null) { func(); return; } // No code signing certificate, nothing to do.
// Setup the domain is specified
var objx = domain, suffix = '';
if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
// Generate the agent signature description and URL
const serverSignedAgentsPath = obj.path.join(obj.datapath, 'signedagents' + suffix);
const signDesc = (domain.title ? domain.title : agentSignCertInfo.cert.subject.hash);
const httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
var httpsHost = ((domain.dns != null) ? domain.dns : obj.certificates.CommonName);
if (obj.args.agentaliasdns != null) { httpsHost = obj.args.agentaliasdns; }
var signUrl = 'https://' + httpsHost;
if (httpsPort != 443) { signUrl += ':' + httpsPort; }
var xdomain = (domain.dns == null) ? domain.id : '';
if (xdomain != '') xdomain += '/';
signUrl += '/' + xdomain;
// If requested, lock the agent to this server
if (obj.config.settings.agentsignlock) { signUrl += '?ServerID=' + obj.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert).toUpperCase(); }
// Setup the time server
var timeStampUrl = 'http://timestamp.comodoca.com/authenticode';
if (args.agenttimestampserver === false) { timeStampUrl = null; }
else if (typeof args.agenttimestampserver == 'string') { timeStampUrl = args.agenttimestampserver; }
// Setup the time server proxy
var timeStampProxy = null;
if (typeof args.agenttimestampproxy == 'string') { timeStampProxy = args.agenttimestampproxy; }
else if ((args.agenttimestampproxy !== false) && (typeof args.npmproxy == 'string')) { timeStampProxy = args.npmproxy; }
// Setup the pending operations counter
var pendingOperations = 1;
for (var archid in obj.meshAgentsArchitectureNumbers) {
if (obj.meshAgentsArchitectureNumbers[archid].codesign !== true) continue;
var agentpath;
if (domain.id == '') {
// Load all agents when processing the default domain
agentpath = obj.path.join(__dirname, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
var agentpath2 = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
if (obj.fs.existsSync(agentpath2)) { agentpath = agentpath2; delete obj.meshAgentsArchitectureNumbers[archid].codesign; } // If the agent is present in "meshcentral-data/agents", use that one instead.
} else {
// When processing an extra domain, only load agents that are specific to that domain
agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
}
// Open the original agent with authenticode
const signeedagentpath = obj.path.join(serverSignedAgentsPath, obj.meshAgentsArchitectureNumbers[archid].localname);
const originalAgent = require('./authenticode.js').createAuthenticodeHandler(agentpath);
if (originalAgent != null) {
// Check if the agent is already signed correctly
const destinationAgent = require('./authenticode.js').createAuthenticodeHandler(signeedagentpath);
var destinationAgentOk = (
(destinationAgent != null) &&
(destinationAgent.fileHashSigned != null) &&
(Buffer.compare(destinationAgent.fileHashSigned, destinationAgent.fileHashActual) == 0) &&
(destinationAgent.signingAttribs.indexOf(signUrl) >= 0) &&
(destinationAgent.signingAttribs.indexOf(signDesc) >= 0)
);
if (destinationAgent != null) {
// If the agent is signed correctly, look to see if the resources in the destination agent are correct
var orgVersionStrings = originalAgent.getVersionInfo();
if (destinationAgentOk == true) {
const versionStrings = destinationAgent.getVersionInfo();
const versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
for (var i in versionProperties) {
const prop = versionProperties[i], propl = prop.toLowerCase();
if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo[propl] == 'string')) {
if (domain.agentfileinfo[propl] != versionStrings[prop]) { destinationAgentOk = false; break; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
} else {
if (orgVersionStrings[prop] != versionStrings[prop]) { destinationAgentOk = false; break; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
}
}
// Check file version number
if (destinationAgentOk == true) {
if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['fileversionnumber'] == 'string')) {
if (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion']) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
} else {
if (orgVersionStrings['~FileVersion'] != versionStrings['~FileVersion']) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
}
}
// Check product version number
if (destinationAgentOk == true) {
if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['productversionnumber'] == 'string')) {
if (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion']) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
} else {
if (orgVersionStrings['~ProductVersion'] != versionStrings['~ProductVersion']) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
}
}
// Check the agent icon
if (destinationAgentOk == true) {
if ((domain.agentfileinfo != null) && (domain.agentfileinfo.icon != null)) {
// Check if the destination agent matches the icon we want
const agentIconGroups = destinationAgent.getIconInfo();
if (agentIconGroups != null) {
const agentIconGroupNames = Object.keys(agentIconGroups);
if (agentIconGroupNames.length > 0) {
const agentMainIconGroup = agentIconGroups[agentIconGroupNames[0]];
if (agentMainIconGroup.resCount != domain.agentfileinfo.icon.resCount) {
destinationAgentOk = false; // The icon image count is different, don't bother hashing to see if the icons are different.
} else {
const agentMainIconGroupHash = require('./authenticode.js').hashObject(agentMainIconGroup);
const iconHash = require('./authenticode.js').hashObject(domain.agentfileinfo.icon);
if (agentMainIconGroupHash != iconHash) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
}
}
}
} else {
// Check if the destination agent has the default icon
const agentIconGroups1 = destinationAgent.getIconInfo();
const agentIconGroups2 = originalAgent.getIconInfo();
if (agentIconGroups1.resCount != agentIconGroups2.resCount) {
destinationAgentOk = false; // The icon image count is different, don't bother hashing to see if the icons are different.
} else {
const iconHash1 = require('./authenticode.js').hashObject(agentIconGroups1);
const iconHash2 = require('./authenticode.js').hashObject(agentIconGroups2);
if (iconHash1 != iconHash2) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
}
}
}
// Check the agent logo
if (destinationAgentOk == true) {
if ((domain.agentfileinfo != null) && (domain.agentfileinfo.logo != null)) {
// Check if the destination agent matches the logo we want
const agentBitmaps = destinationAgent.getBitmapInfo();
if (agentBitmaps != null) {
const agentBitmapNames = Object.keys(agentBitmaps);
if (agentBitmapNames.length > 0) {
const agentMainBitmap = agentBitmaps[agentBitmapNames[0]];
const agentMainBitmapHash = require('./authenticode.js').hashObject(agentMainBitmap);
const bitmapHash = require('./authenticode.js').hashObject(domain.agentfileinfo.logo);
if (agentMainBitmapHash != bitmapHash) { destinationAgentOk = false; } // If the existing agent logo does not match the desired logo, we need to re-sign the agent.
}
}
} else {
// Check if the destination agent has the default icon
const agentBitmaps1 = destinationAgent.getBitmapInfo();
const agentBitmaps2 = originalAgent.getBitmapInfo();
const agentBitmapNames = Object.keys(agentBitmaps1);
if (agentBitmapNames.length == 0) {
destinationAgentOk = false;
} else {
const iconHash1 = require('./authenticode.js').hashObject(agentBitmaps1[agentBitmapNames[0]]);
const iconHash2 = require('./authenticode.js').hashObject(agentBitmaps2[agentBitmapNames[0]]);
if (iconHash1 != iconHash2) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
}
}
}
}
// If everything looks ok, runs a hash of the original and destination agent .text, .data and .rdata sections. If different, sign the agent again.
if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.text').compare(destinationAgent.getHashOfSection('sha384', '.text')) != 0)) { destinationAgentOk = false; }
if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.data').compare(destinationAgent.getHashOfSection('sha384', '.data')) != 0)) { destinationAgentOk = false; }
if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.rdata').compare(destinationAgent.getHashOfSection('sha384', '.rdata')) != 0)) { destinationAgentOk = false; }
// We are done comparing the destination agent, close it.
destinationAgent.close();
}
if (destinationAgentOk == false) {
// If not signed correctly, sign it. First, create the server signed agent folder if needed
try { obj.fs.mkdirSync(serverSignedAgentsPath); } catch (ex) { }
const xagentSignedFunc = function agentSignedFunc(err, size) {
if (err == null) {
// Agent was signed succesfuly
console.log(obj.common.format('Code signed {0}.', agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname));
} else {
// Failed to sign agent
addServerWarning('Failed to sign \"' + agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname + '\": ' + err, 22, [ agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname, err ]);
}
if (--pendingOperations === 0) { agentSignedFunc.func(); }
}
pendingOperations++;
xagentSignedFunc.func = func;
xagentSignedFunc.objx = objx;
xagentSignedFunc.archid = archid;
xagentSignedFunc.signeedagentpath = signeedagentpath;
// Parse the resources in the executable and make any required changes
var resChanges = false, versionStrings = null;
if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object')) {
versionStrings = originalAgent.getVersionInfo();
var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
// Change the agent string properties
for (var i in versionProperties) {
const prop = versionProperties[i], propl = prop.toLowerCase();
if (domain.agentfileinfo[propl] && (domain.agentfileinfo[propl] != versionStrings[prop])) { versionStrings[prop] = domain.agentfileinfo[propl]; resChanges = true; }
}
// Change the agent file version
if (domain.agentfileinfo['fileversionnumber'] && (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion'])) {
versionStrings['~FileVersion'] = domain.agentfileinfo['fileversionnumber']; resChanges = true;
}
// Change the agent product version
if (domain.agentfileinfo['productversionnumber'] && (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion'])) {
versionStrings['~ProductVersion'] = domain.agentfileinfo['productversionnumber']; resChanges = true;
}
if (resChanges == true) { originalAgent.setVersionInfo(versionStrings); }
// Change the agent icon
if (domain.agentfileinfo.icon != null) {
const agentIconGroups = originalAgent.getIconInfo();
if (agentIconGroups != null) {
const agentIconGroupNames = Object.keys(agentIconGroups);
if (agentIconGroupNames.length > 0) {
const agentMainIconGroupName = agentIconGroupNames[0];
agentIconGroups[agentIconGroupNames[0]] = domain.agentfileinfo.icon;
originalAgent.setIconInfo(agentIconGroups);
}
}
}
// Change the agent logo
if (domain.agentfileinfo.logo != null) {
const agentBitmaps = originalAgent.getBitmapInfo();
if (agentBitmaps != null) {
const agentBitmapNames = Object.keys(agentBitmaps);
if (agentBitmapNames.length > 0) {
agentBitmaps[agentBitmapNames[0]] = domain.agentfileinfo.logo;
originalAgent.setBitmapInfo(agentBitmaps);
}
}
}
}
const signingArguments = { out: signeedagentpath, desc: signDesc, url: signUrl, time: timeStampUrl, proxy: timeStampProxy }; // Shallow clone
obj.debug('main', "Code signing with arguments: " + JSON.stringify(signingArguments));
if (resChanges == false) {
// Sign the agent the simple way, without changing any resources.
originalAgent.sign(agentSignCertInfo, signingArguments, xagentSignedFunc);
} else {
// Change the agent resources and sign the agent, this is a much more involved process.
// NOTE: This is experimental and could corupt the agent.
originalAgent.writeExecutable(signingArguments, agentSignCertInfo, xagentSignedFunc);
}
} else {
// Signed agent is already ok, use it.
originalAgent.close();
}
}
}
if (--pendingOperations === 0) { func(); }
}
// Update the list of available mesh agents
obj.updateMeshAgentsTable = function (domain, func) {
// Check if a custom agent signing certificate is available
var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
// If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
agentSignCertInfo = {
cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
}
}
// Setup the domain is specified
var objx = domain, suffix = '';
if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
// Load agent information file. This includes the data & time of the agent.
const agentInfo = [];
try { agentInfo = JSON.parse(obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'hashagents.json'), 'utf8')); } catch (ex) { }
var archcount = 0;
for (var archid in obj.meshAgentsArchitectureNumbers) {
var agentpath;
if (domain.id == '') {
// Load all agents when processing the default domain
agentpath = obj.path.join(__dirname, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
if (obj.meshAgentsArchitectureNumbers[archid].unsigned !== true) {
const agentpath2 = obj.path.join(obj.datapath, 'signedagents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
if (obj.fs.existsSync(agentpath2)) { agentpath = agentpath2; } // If the agent is present in "meshcentral-data/signedagents", use that one instead.
const agentpath3 = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
if (obj.fs.existsSync(agentpath3)) { agentpath = agentpath3; } // If the agent is present in "meshcentral-data/agents", use that one instead.
}
} else {
// When processing an extra domain, only load agents that are specific to that domain
agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
}
// Fetch agent binary information
var stats = null;
try { stats = obj.fs.statSync(agentpath); } catch (ex) { }
if ((stats == null)) continue; // If this agent does not exist, skip it.
// Setup agent information
archcount++;
objx.meshAgentBinaries[archid] = Object.assign({}, obj.meshAgentsArchitectureNumbers[archid]);
objx.meshAgentBinaries[archid].path = agentpath;
objx.meshAgentBinaries[archid].url = 'http://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?id=' + archid;
objx.meshAgentBinaries[archid].size = stats.size;
if ((agentInfo[archid] != null) && (agentInfo[archid].mtime != null)) { objx.meshAgentBinaries[archid].mtime = new Date(agentInfo[archid].mtime); } // Set agent time if available
// If this is a windows binary, pull binary information
if (obj.meshAgentsArchitectureNumbers[archid].platform == 'win32') {
try { objx.meshAgentBinaries[archid].pe = obj.exeHandler.parseWindowsExecutable(agentpath); } catch (ex) { }
}
// If agents must be stored in RAM or if this is a Windows 32/64 agent, load the agent in RAM.
if ((obj.args.agentsinram === true) || (((archid == 3) || (archid == 4)) && (obj.args.agentsinram !== false))) {
if ((archid == 3) || (archid == 4)) {
// Load the agent with a random msh added to it.
const outStream = new require('stream').Duplex();
outStream.meshAgentBinary = objx.meshAgentBinaries[archid];
if (agentSignCertInfo) { outStream.meshAgentBinary.randomMsh = agentSignCertInfo.cert.subject.hash; } else { outStream.meshAgentBinary.randomMsh = obj.crypto.randomBytes(16).toString('hex'); }
outStream.bufferList = [];
outStream._write = function (chunk, encoding, callback) { this.bufferList.push(chunk); if (callback) callback(); }; // Append the chuck.
outStream._read = function (size) { }; // Do nothing, this is not going to be called.
outStream.on('finish', function () {
// Merge all chunks
this.meshAgentBinary.data = Buffer.concat(this.bufferList);
this.meshAgentBinary.size = this.meshAgentBinary.data.length;
delete this.bufferList;
// Hash the uncompressed binary
const hash = obj.crypto.createHash('sha384').update(this.meshAgentBinary.data);
this.meshAgentBinary.fileHash = hash.digest('binary');
this.meshAgentBinary.fileHashHex = Buffer.from(this.meshAgentBinary.fileHash, 'binary').toString('hex');
// Compress the agent using ZIP
const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method.
const onZipData = function onZipData(buffer) { onZipData.x.zacc.push(buffer); }
const onZipEnd = function onZipEnd() {
// Concat all the buffer for create compressed zip agent
const concatData = Buffer.concat(onZipData.x.zacc);
delete onZipData.x.zacc;
// Hash the compressed binary
const hash = obj.crypto.createHash('sha384').update(concatData);
onZipData.x.zhash = hash.digest('binary');
onZipData.x.zhashhex = Buffer.from(onZipData.x.zhash, 'binary').toString('hex');
// Set the agent
onZipData.x.zdata = concatData;
onZipData.x.zsize = concatData.length;
}
const onZipError = function onZipError() { delete onZipData.x.zacc; }
this.meshAgentBinary.zacc = [];
onZipData.x = this.meshAgentBinary;
onZipEnd.x = this.meshAgentBinary;
onZipError.x = this.meshAgentBinary;
archive.on('data', onZipData);
archive.on('end', onZipEnd);
archive.on('error', onZipError);
// Starting with NodeJS v16, passing in a buffer at archive.append() will result a compressed file with zero byte length. To fix this, we pass in the buffer as a stream.
// archive.append(this.meshAgentBinary.data, { name: 'meshagent' }); // This is the version that does not work on NodeJS v16.
const ReadableStream = require('stream').Readable;
const zipInputStream = new ReadableStream();
zipInputStream.push(this.meshAgentBinary.data);
zipInputStream.push(null);
archive.append(zipInputStream, { name: 'meshagent' });
archive.finalize();
})
obj.exeHandler.streamExeWithMeshPolicy(
{
platform: 'win32',
sourceFileName: agentpath,
destinationStream: outStream,
randomPolicy: true, // Indicates that the msh policy is random data.
msh: outStream.meshAgentBinary.randomMsh,
peinfo: objx.meshAgentBinaries[archid].pe
});
} else {
// Load the agent as-is
objx.meshAgentBinaries[archid].data = obj.fs.readFileSync(agentpath);
// Compress the agent using ZIP
const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method.
const onZipData = function onZipData(buffer) { onZipData.x.zacc.push(buffer); }
const onZipEnd = function onZipEnd() {
// Concat all the buffer for create compressed zip agent
const concatData = Buffer.concat(onZipData.x.zacc);
delete onZipData.x.zacc;
// Hash the compressed binary
const hash = obj.crypto.createHash('sha384').update(concatData);
onZipData.x.zhash = hash.digest('binary');
onZipData.x.zhashhex = Buffer.from(onZipData.x.zhash, 'binary').toString('hex');
// Set the agent
onZipData.x.zdata = concatData;
onZipData.x.zsize = concatData.length;
//console.log('Packed', onZipData.x.size, onZipData.x.zsize);
}
const onZipError = function onZipError() { delete onZipData.x.zacc; }
objx.meshAgentBinaries[archid].zacc = [];
onZipData.x = objx.meshAgentBinaries[archid];
onZipEnd.x = objx.meshAgentBinaries[archid];
onZipError.x = objx.meshAgentBinaries[archid];
archive.on('data', onZipData);
archive.on('end', onZipEnd);
archive.on('error', onZipError);
archive.append(objx.meshAgentBinaries[archid].data, { name: 'meshagent' });
archive.finalize();
}
}
// Hash the binary
const hashStream = obj.crypto.createHash('sha384');
hashStream.archid = archid;
hashStream.on('data', function (data) {
objx.meshAgentBinaries[this.archid].hash = data.toString('binary');
objx.meshAgentBinaries[this.archid].hashhex = data.toString('hex');
if ((--archcount == 0) && (func != null)) { func(); }
});
const options = { sourcePath: agentpath, targetStream: hashStream, platform: obj.meshAgentsArchitectureNumbers[archid].platform };
if (objx.meshAgentBinaries[archid].pe != null) { options.peinfo = objx.meshAgentBinaries[archid].pe; }
obj.exeHandler.hashExecutableFile(options);
// If we are not loading Windows binaries to RAM, compute the RAW file hash of the signed binaries here.
if ((obj.args.agentsinram === false) && ((archid == 3) || (archid == 4))) {
const hash = obj.crypto.createHash('sha384').update(obj.fs.readFileSync(agentpath));
objx.meshAgentBinaries[archid].fileHash = hash.digest('binary');
objx.meshAgentBinaries[archid].fileHashHex = Buffer.from(objx.meshAgentBinaries[archid].fileHash, 'binary').toString('hex');
}
}
};
// Generate a time limited user login token
obj.getLoginToken = function (userid, func) {
if ((userid == null) || (typeof userid != 'string')) { func('Invalid userid.'); return; }
const x = userid.split('/');
if (x == null || x.length != 3 || x[0] != 'user') { func('Invalid userid.'); return; }
obj.db.Get(userid, function (err, docs) {
if (err != null || docs == null || docs.length == 0) {
func('User ' + userid + ' not found.'); return;
} else {
// Load the login cookie encryption key from the database
obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
// Key is present, use it.
obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
func(obj.encodeCookie({ u: userid, a: 3 }, obj.loginCookieEncryptionKey));
} else {
// Key is not present, generate one.
obj.loginCookieEncryptionKey = obj.generateCookieKey();
obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() }, function () { func(obj.encodeCookie({ u: userid, a: 3 }, obj.loginCookieEncryptionKey)); });
}
});
}
});
};
// Show the user login token generation key
obj.showLoginTokenKey = function (func) {
// Load the login cookie encryption key from the database
obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
// Key is present, use it.
func(docs[0].key);
} else {
// Key is not present, generate one.
obj.loginCookieEncryptionKey = obj.generateCookieKey();
obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() }, function () { func(obj.loginCookieEncryptionKey.toString('hex')); });
}
});
};
// Load the list of Intel AMT UUID and passwords from "amtactivation.log"
obj.loadAmtActivationLogPasswords = function (func) {
const amtlogfilename = obj.path.join(obj.datapath, 'amtactivation.log');
obj.fs.readFile(amtlogfilename, 'utf8', function (err, data) {
const amtPasswords = {}; // UUID --> [Passwords]
if ((err == null) && (data != null)) {
const lines = data.split('\n');
for (var i in lines) {
const line = lines[i];
if (line.startsWith('{')) {
var j = null;
try { j = JSON.parse(line); } catch (ex) { }
if ((j != null) && (typeof j == 'object')) {
if ((typeof j.amtUuid == 'string') && (typeof j.password == 'string')) {
if (amtPasswords[j.amtUuid] == null) {
amtPasswords[j.amtUuid] = [j.password]; // Add password to array
} else {
amtPasswords[j.amtUuid].unshift(j.password); // Add password at the start of the array
}
}
}
}
}
// Remove all duplicates and only keep the 3 last passwords for any given device
for (var i in amtPasswords) {
amtPasswords[i] = [...new Set(amtPasswords[i])];
while (amtPasswords[i].length > 3) { amtPasswords[i].pop(); }
}
}
func(obj.common.sortObj(amtPasswords)); // Sort by UUID
});
}
// Encrypt session data
obj.encryptSessionData = function (data, key) {
if (data == null) return null;
if (key == null) { key = obj.loginCookieEncryptionKey; }
try {
const iv = Buffer.from(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
const crypted = Buffer.concat([cipher.update(JSON.stringify(data), 'utf8'), cipher.final()]);
return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
} catch (ex) { return null; }
}
// Decrypt the session data
obj.decryptSessionData = function (data, key) {
if ((typeof data != 'string') || (data.length < 13)) return {};
if (key == null) { key = obj.loginCookieEncryptionKey; }
try {
const buf = Buffer.from(data, 'base64');
const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), buf.slice(0, 12));
decipher.setAuthTag(buf.slice(12, 28));
return JSON.parse(decipher.update(buf.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
} catch (ex) { return {}; }
}
// Generate a cryptographic key used to encode and decode cookies
obj.generateCookieKey = function () {
return Buffer.from(obj.crypto.randomBytes(80), 'binary');
//return Buffer.alloc(80, 0); // Sets the key to zeros, debug only.
};
// Encode an object as a cookie using a key using AES-GCM. (key must be 32 bytes or more)
obj.encodeCookie = function (o, key) {
try {
if (key == null) { key = obj.serverKey; }
o.time = Math.floor(Date.now() / 1000); // Add the cookie creation time
const iv = Buffer.from(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
const crypted = Buffer.concat([cipher.update(JSON.stringify(o), 'utf8'), cipher.final()]);
const r = Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64').replace(/\+/g, '@').replace(/\//g, '$');
obj.debug('cookie', 'Encoded AESGCM cookie: ' + JSON.stringify(o));
return r;
} catch (ex) { obj.debug('cookie', 'ERR: Failed to encode AESGCM cookie due to exception: ' + ex); return null; }
};
// Decode a cookie back into an object using a key using AES256-GCM or AES128-CBC/HMAC-SHA384. Return null if it's not a valid cookie. (key must be 32 bytes or more)
obj.decodeCookie = function (cookie, key, timeout) {
if (cookie == null) return null;
var r = obj.decodeCookieAESGCM(cookie, key, timeout);
if (r === -1) { r = obj.decodeCookieAESSHA(cookie, key, timeout); } // If decodeCookieAESGCM() failed to decode, try decodeCookieAESSHA()
if ((r == null) && (obj.args.cookieencoding == null) && (cookie.length != 64) && ((cookie == cookie.toLowerCase()) || (cookie == cookie.toUpperCase()))) {
obj.debug('cookie', 'Upper/Lowercase cookie, try "CookieEncoding":"hex" in settings section of config.json.');
console.log('Upper/Lowercase cookie, try "CookieEncoding":"hex" in settings section of config.json.');
}
if ((r != null) && (typeof r.once == 'string') && (r.once.length > 0)) {
// This cookie must only be used once.
if (timeout == null) { timeout = 2; }
if (obj.cookieUseOnceTable[r.once] == null) {
const ctimeout = (((r.expire) == null || (typeof r.expire != 'number')) ? (r.time + ((timeout + 3) * 60000)) : (r.time + ((r.expire + 3) * 60000)));
// Store the used cookie in RAM
obj.cookieUseOnceTable[r.once] = ctimeout;
// Store the used cookie in the database
// TODO
// Send the used cookie to peer servers
// TODO
// Clean up the used table
if (++obj.cookieUseOnceTableCleanCounter > 20) {
const now = Date.now();
for (var i in obj.cookieUseOnceTable) { if (obj.cookieUseOnceTable[i] < now) { delete obj.cookieUseOnceTable[i]; } }
obj.cookieUseOnceTableCleanCounter = 0;
}
} else { return null; }
}
return r;
}
// Decode a cookie back into an object using a key using AES256-GCM. Return null if it's not a valid cookie. (key must be 32 bytes or more)
obj.decodeCookieAESGCM = function (cookie, key, timeout) {
try {
if (key == null) { key = obj.serverKey; }
cookie = Buffer.from(cookie.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), cookie.slice(0, 12));
decipher.setAuthTag(cookie.slice(12, 28));
const o = JSON.parse(decipher.update(cookie.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { obj.debug('cookie', 'ERR: Bad cookie due to invalid time'); return null; }
o.time = o.time * 1000; // Decode the cookie creation time
o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds)
if ((o.expire) == null || (typeof o.expire != 'number')) {
// Use a fixed cookie expire time
if (timeout == null) { timeout = 2; }
if ((o.dtime > (timeout * 60000)) || (o.dtime < -30000)) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
} else {
// An expire time is included in the cookie (in minutes), use this.
if ((o.expire !== 0) && ((o.dtime > (o.expire * 60000)) || (o.dtime < -30000))) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
}
obj.debug('cookie', 'Decoded AESGCM cookie: ' + JSON.stringify(o));
return o;
} catch (ex) { obj.debug('cookie', 'ERR: Bad AESGCM cookie due to exception: ' + ex); return -1; }
};
// Decode a cookie back into an object using a key using AES256 / HMAC-SHA384. Return null if it's not a valid cookie. (key must be 80 bytes or more)
// We do this because poor .NET does not support AES256-GCM.
obj.decodeCookieAESSHA = function (cookie, key, timeout) {
try {
if (key == null) { key = obj.serverKey; }
if (key.length < 80) { return null; }
cookie = Buffer.from(cookie.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
const decipher = obj.crypto.createDecipheriv('aes-256-cbc', key.slice(48, 80), cookie.slice(0, 16));
const rawmsg = decipher.update(cookie.slice(16), 'binary', 'binary') + decipher.final('binary');
const hmac = obj.crypto.createHmac('sha384', key.slice(0, 48));
hmac.update(rawmsg.slice(48));
if (Buffer.compare(hmac.digest(), Buffer.from(rawmsg.slice(0, 48))) == false) { return null; }
const o = JSON.parse(rawmsg.slice(48).toString('utf8'));
if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { obj.debug('cookie', 'ERR: Bad cookie due to invalid time'); return null; }
o.time = o.time * 1000; // Decode the cookie creation time
o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds)
if ((o.expire) == null || (typeof o.expire != 'number')) {
// Use a fixed cookie expire time
if (timeout == null) { timeout = 2; }
if ((o.dtime > (timeout * 60000)) || (o.dtime < -30000)) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
} else {
// An expire time is included in the cookie (in minutes), use this.
if ((o.expire !== 0) && ((o.dtime > (o.expire * 60000)) || (o.dtime < -30000))) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
}
obj.debug('cookie', 'Decoded AESSHA cookie: ' + JSON.stringify(o));
return o;
} catch (ex) { obj.debug('cookie', 'ERR: Bad AESSHA cookie due to exception: ' + ex); return null; }
};
// Debug
obj.debug = function (source, ...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+', 0o666);
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 + ': ' + 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;
for (var sessionid in obj.webserver.wssessions2) {
const ws = obj.webserver.wssessions2[sessionid];
if ((ws != null) && (ws.userid != null)) {
const user = obj.webserver.users[ws.userid];
if ((user != null) && (user.siteadmin == 4294967295)) {
try { ws.send(JSON.stringify({ action: 'trace', source: source, args: args, time: Date.now() })); sendcount++; } catch (ex) { }
}
}
}
if (sendcount == 0) { obj.debugRemoteSources = null; } // If there are no listeners, remove debug sources.
}
};
// Update server state. Writes a server state file.
const meshServerState = {};
obj.updateServerState = function (name, val) {
//console.log('updateServerState', name, val);
try {
if ((name != null) && (val != null)) {
var changed = false;
if ((name != null) && (meshServerState[name] != val)) { if ((val == null) && (meshServerState[name] != null)) { delete meshServerState[name]; changed = true; } else { if (meshServerState[name] != val) { meshServerState[name] = val; changed = true; } } }
if (changed == false) return;
}
var r = 'time=' + Date.now() + '\r\n';
for (var i in meshServerState) { r += (i + '=' + meshServerState[i] + '\r\n'); }
try {
obj.fs.writeFileSync(obj.getConfigFilePath('serverstate.txt'), r); // Try to write the server state, this may fail if we don't have permission.
} catch (ex) { obj.serverSelfWriteAllowed = false; }
} catch (ex) { } // Do nothing since this is not a critical feature.
};
// Read a list of IP addresses from a file
function readIpListFromFile(arg) {
if ((typeof arg != 'string') || (!arg.startsWith('file:'))) return arg;
var lines = null;
try { lines = obj.fs.readFileSync(obj.path.join(obj.datapath, arg.substring(5))).toString().split('\r\n').join('\r').split('\r'); } catch (ex) { }
if (lines == null) return null;
const validLines = [];
for (var i in lines) { if ((lines[i].length > 0) && (((lines[i].charAt(0) > '0') && (lines[i].charAt(0) < '9')) || (lines[i].charAt(0) == ':'))) validLines.push(lines[i]); }
return validLines;
}
// Logging funtions
function logException(e) { e += ''; logErrorEvent(e); }
function logInfoEvent(msg) { if (obj.servicelog != null) { obj.servicelog.info(msg); } console.log(msg); }
function logWarnEvent(msg) { if (obj.servicelog != null) { obj.servicelog.warn(msg); } console.log(msg); }
function logErrorEvent(msg) { if (obj.servicelog != null) { obj.servicelog.error(msg); } console.error(msg); }
obj.getServerWarnings = function () { return serverWarnings; }
obj.addServerWarning = function (msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
// auth.log functions
obj.authLog = function (server, msg, args) {
if (typeof msg != 'string') return;
var str = msg;
if (args != null) {
if (typeof args.sessionid == 'string') { str += ', SessionID: ' + args.sessionid; }
if (typeof args.useragent == 'string') { const userAgentInfo = obj.webserver.getUserAgentInfo(args.useragent); str += ', Browser: ' + userAgentInfo.browserStr + ', OS: ' + userAgentInfo.osStr; }
}
obj.debug('authlog', str);
if (obj.syslogauth != null) { try { obj.syslogauth.log(obj.syslogauth.LOG_INFO, str); } catch (ex) { } }
if (obj.authlogfile != null) { // Write authlog to file
try {
const d = new Date(), month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()];
str = month + ' ' + d.getDate() + ' ' + obj.common.zeroPad(d.getHours(), 2) + ':' + obj.common.zeroPad(d.getMinutes(), 2) + ':' + d.getSeconds() + ' meshcentral ' + server + '[' + process.pid + ']: ' + msg + ((obj.platform == 'win32') ? '\r\n' : '\n');
obj.fs.write(obj.authlogfile, str, function (err, written, string) { if (err) {console.error(err); } });
} catch (ex) { console.error(ex); }
}
}
// Return the path of a file into the meshcentral-data path
obj.getConfigFilePath = function (filename) {
if ((obj.config != null) && (obj.config.configfiles != null) && (obj.config.configfiles[filename] != null) && (typeof obj.config.configfiles[filename] == 'string')) {
//console.log('getConfigFilePath(\"' + filename + '\") = ' + obj.config.configfiles[filename]);
return obj.config.configfiles[filename];
}
//console.log('getConfigFilePath(\"' + filename + '\") = ' + obj.path.join(obj.datapath, filename));
return obj.path.join(obj.datapath, filename);
};
return obj;
}
// Resolve a list of names, call back with list of failed resolves.
function checkResolveAll(names, func) {
const dns = require('dns'), state = { func: func, count: names.length, err: null };
for (var i in names) {
dns.resolve(names[i], function (err, records) {
if (err != null) { if (this.state.err == null) { this.state.err = [this.name]; } else { this.state.err.push(this.name); } }
if (--this.state.count == 0) { this.state.func(this.state.err); }
}.bind({ name: names[i], state: state }))
}
}
// Return the server configuration
function getConfig(createSampleConfig) {
// Figure out the datapath location
var i, datapath = null;
const fs = require('fs'), path = require('path'), args = require('minimist')(process.argv.slice(2));
if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
datapath = path.join(__dirname, '../../meshcentral-data');
} else {
datapath = path.join(__dirname, '../meshcentral-data');
}
if (args.datapath) { datapath = args.datapath; }
try { fs.mkdirSync(datapath); } catch (ex) { }
// Read configuration file if present and change arguments.
var config = {}, configFilePath = path.join(datapath, 'config.json');
if (args.configfile) { configFilePath = common.joinPath(datapath, args.configfile); }
if (fs.existsSync(configFilePath)) {
// Load and validate the configuration file
try { config = require(configFilePath); } catch (ex) { console.log('ERROR: Unable to parse ' + configFilePath + '.'); return null; }
if (config.domains == null) { config.domains = {}; }
for (i in config.domains) { if ((i.split('/').length > 1) || (i.split(' ').length > 1)) { console.log("ERROR: Error in config.json, domain names can't have spaces or /."); return null; } }
} else {
if (createSampleConfig === true) {
// Copy the "sample-config.json" to give users a starting point
const sampleConfigPath = path.join(__dirname, 'sample-config.json');
if (fs.existsSync(sampleConfigPath)) { fs.createReadStream(sampleConfigPath).pipe(fs.createWriteStream(configFilePath)); }
}
}
// Set the command line arguments to the config file if they are not present
if (!config.settings) { config.settings = {}; }
for (i in args) { config.settings[i] = args[i]; }
// Lower case all keys in the config file
try {
require('./common.js').objKeysToLower(config, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
} catch (ex) {
console.log('CRITICAL ERROR: Unable to access the file \"./common.js\".\r\nCheck folder & file permissions.');
process.exit();
}
return config;
}
// Check if a list of modules are present and install any missing ones
function InstallModules(modules, args, func) {
var missingModules = [];
if (modules.length > 0) {
const dependencies = require('./package.json').dependencies;
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
const moduleNameAndVersion = modules[i];
const moduleInfo = moduleNameAndVersion.split('@', 2);
var moduleName = moduleInfo[0];
var moduleVersion = moduleInfo[1];
if (moduleName == '') { moduleName = moduleNameAndVersion; moduleVersion = null; } // If the module name starts with @, don't use @ as a version seperator.
try {
// Does the module need a specific version?
if (moduleVersion) {
if (require(`${moduleName}/package.json`).version != moduleVersion) { throw new Error(); }
} else {
// For all other modules, do the check here.
// Is the module in package.json? Install exact version.
if (typeof dependencies[moduleName] != null) { moduleVersion = dependencies[moduleName]; }
require(moduleName);
}
} catch (ex) {
missingModules.push(moduleNameAndVersion);
}
}
if (missingModules.length > 0) { if (args.debug) { console.log('Missing Modules: ' + missingModules.join(', ')); } InstallModuleEx(missingModules, args, func); } else { func(); }
}
}
// Install all missing modules at once. We will be running "npm install" once, with a full list of all modules we need, no matter if they area already installed or not,
// this is to make sure NPM gives us exactly what we need. Also, we install the meshcentral with current version, so that NPM does not update it - which it will do if obmitted.
function InstallModuleEx(modulenames, args, func) {
var names = modulenames.join(' ');
console.log('Installing modules', modulenames);
const child_process = require('child_process');
var parentpath = __dirname;
function getCurrentVersion() { try { return JSON.parse(require('fs').readFileSync(require('path').join(__dirname, 'package.json'), 'utf8')).version; } catch (ex) { } return null; } // Fetch server version
//const meshCentralVersion = getCurrentVersion();
//if ((meshCentralVersion != null) && (args.dev == null)) { names = 'meshcentral@' + getCurrentVersion() + ' ' + names; }
// 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, '../..'); }
if (args.debug) { console.log('NPM Command Line: ' + npmpath + ` install --save-exact --no-audit --omit=optional --no-fund ${names}`); }
// always use --save-exact - https://stackoverflow.com/a/64507176/1210734
child_process.exec(npmpath + ` install --save-exact --no-audit --no-optional --omit=optional ${names}`, { maxBuffer: 512000, timeout: 300000, cwd: parentpath }, function (error, stdout, stderr) {
if ((error != null) && (error != '')) {
var mcpath = __dirname;
if (mcpath.endsWith('\\node_modules\\meshcentral') || mcpath.endsWith('/node_modules/meshcentral')) { mcpath = require('path').join(mcpath, '..', '..'); }
console.log('ERROR: Unable to install required modules. MeshCentral may not have access to npm, or npm may not have suffisent rights to load the new module. To manualy install this module try:\r\n\r\n cd "' + mcpath + '"\r\n npm install --no-audit --no-optional --omit=optional ' + names + '\r\n node node_modules' + ((require('os').platform() == 'win32') ? '\\' : '/') + 'meshcentral');
process.exit();
return;
}
func();
return;
});
}
// Detect CTRL-C on Linux and stop nicely
process.on('SIGINT', function () { if (meshserver != null) { meshserver.Stop(); meshserver = null; } console.log('Server Ctrl-C exit...'); process.exit(); });
// Add a server warning, warnings will be shown to the administrator on the web application
const serverWarnings = [];
function addServerWarning(msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
/*
var ServerWarnings = {
1: "",
2: "Missing WebDAV parameters.",
3: "Unrecognized configuration option \"{0}\".",
4: "WebSocket compression is disabled, this feature is broken in NodeJS v11.11 to v12.15 and v13.2",
5: "Unable to load Intel AMT TLS root certificate for default domain.",
6: "Unable to load Intel AMT TLS root certificate for domain {0}.",
7: "CIRA local FQDN's ignored when server in LAN-only or WAN-only mode.",
8: "Can't have more than 4 CIRA local FQDN's. Ignoring value.",
9: "Agent hash checking is being skipped, this is unsafe.",
10: "Missing Let's Encrypt email address.",
11: "Invalid Let's Encrypt host names.",
12: "Invalid Let's Encrypt names, can't contain a *.",
13: "Unable to setup Let's Encrypt module.",
14: "Invalid Let's Encrypt names, unable to resolve: {0}",
15: "Invalid Let's Encrypt email address, unable to resolve: {0}",
16: "Unable to load CloudFlare trusted proxy IPv6 address list.",
17: "SendGrid server has limited use in LAN mode.",
18: "SMTP server has limited use in LAN mode.",
19: "SMS gateway has limited use in LAN mode.",
20: "Invalid \"LoginCookieEncryptionKey\" in config.json.",
21: "Backup path can't be set within meshcentral-data folder, backup settings ignored.",
22: "Failed to sign agent {0}: {1}",
23: "Unable to load agent icon file: {0}.",
24: "Unable to load agent logo file: {0}.",
25: "This NodeJS version does not support OpenID.",
26: "This NodeJS version does not support Discord.js."
};
*/
// Load the really basic modules
var npmpath = 'npm';
var meshserver = null;
var childProcess = null;
var previouslyInstalledModules = {};
function mainStart() {
// Check the NodeJS is version 10 or better.
if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) < 11) { console.log("MeshCentral requires Node v11 or above, current version is " + process.version + "."); return; }
// If running within the node_modules folder, move working directory to the parent of the node_modules folder.
if (__dirname.endsWith('\\node_modules\\meshcentral') || __dirname.endsWith('/node_modules/meshcentral')) { process.chdir(require('path').join(__dirname, '..', '..')); }
// Check for any missing modules.
InstallModules(['minimist'], {}, function () {
// Parse inbound arguments
const args = require('minimist')(process.argv.slice(2));
// Setup the NPM path
if (args.npmpath == null) {
try {
var xnodepath = process.argv[0];
var xnpmpath = require('path').join(require('path').dirname(process.argv[0]), 'npm');
if (require('fs').existsSync(xnodepath) && require('fs').existsSync(xnpmpath)) {
if (xnodepath.indexOf(' ') >= 0) { xnodepath = '"' + xnodepath + '"'; }
if (xnpmpath.indexOf(' ') >= 0) { xnpmpath = '"' + xnpmpath + '"'; }
if (require('os').platform() == 'win32') { npmpath = xnpmpath; } else { npmpath = (xnodepath + ' ' + xnpmpath); }
}
} catch (ex) { console.log(ex); }
} else {
npmpath = args.npmpath;
}
// Get the server configuration
var config = getConfig(false);
if (config == null) { process.exit(); }
// Lowercase the auth value if present
for (var i in config.domains) { if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); } }
// Get the current node version
const verSplit = process.version.substring(1).split('.');
var nodeVersion = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
// Check if RDP support if present
var mstsc = true;
try { require('./rdp') } catch (ex) { mstsc = false; }
// Check if Windows SSPI, LDAP, Passport and YubiKey OTP will be used
var sspi = false;
var ldap = false;
var passport = null;
var allsspi = true;
var yubikey = false;
var ssh = false;
var sessionRecording = false;
var domainCount = 0;
var wildleek = false;
var nodemailer = false;
var sendgrid = false;
var captcha = false;
if (require('os').platform() == 'win32') { for (var i in config.domains) { domainCount++; if (config.domains[i].auth == 'sspi') { sspi = true; } else { allsspi = false; } } } else { allsspi = false; }
if (domainCount == 0) { allsspi = false; }
for (var i in config.domains) {
if (i.startsWith('_')) continue;
if (((config.domains[i].smtp != null) && (config.domains[i].smtp.name != 'console')) || (config.domains[i].sendmail != null)) { nodemailer = true; }
if (config.domains[i].sendgrid != null) { sendgrid = true; }
if (config.domains[i].yubikey != null) { yubikey = true; }
if (config.domains[i].auth == 'ldap') { ldap = true; }
if (mstsc == false) { config.domains[i].mstsc = false; }
if (config.domains[i].ssh == true) { ssh = true; }
if ((typeof config.domains[i].authstrategies == 'object')) {
if (passport == null) { passport = ['passport']; } // Passport v0.6.0 requires a patch, see https://github.com/jaredhanson/passport/issues/904
if ((typeof config.domains[i].authstrategies.twitter == 'object') && (typeof config.domains[i].authstrategies.twitter.clientid == 'string') && (typeof config.domains[i].authstrategies.twitter.clientsecret == 'string') && (passport.indexOf('passport-twitter') == -1)) { passport.push('passport-twitter'); }
if ((typeof config.domains[i].authstrategies.google == 'object') && (typeof config.domains[i].authstrategies.google.clientid == 'string') && (typeof config.domains[i].authstrategies.google.clientsecret == 'string') && (passport.indexOf('passport-google-oauth20') == -1)) { passport.push('passport-google-oauth20'); }
if ((typeof config.domains[i].authstrategies.github == 'object') && (typeof config.domains[i].authstrategies.github.clientid == 'string') && (typeof config.domains[i].authstrategies.github.clientsecret == 'string') && (passport.indexOf('passport-github2') == -1)) { passport.push('passport-github2'); }
if ((typeof config.domains[i].authstrategies.azure == 'object') && (typeof config.domains[i].authstrategies.azure.clientid == 'string') && (typeof config.domains[i].authstrategies.azure.clientsecret == 'string') && (typeof config.domains[i].authstrategies.azure.tenantid == 'string') && (passport.indexOf('passport-azure-oauth2') == -1)) { passport.push('passport-azure-oauth2'); passport.push('jwt-simple'); }
if ((typeof config.domains[i].authstrategies.oidc == 'object') && (passport.indexOf('openid-client') == -1)) {
if ((nodeVersion >= 17)
|| ((Math.floor(nodeVersion) == 16) && (nodeVersion >= 16.13))
|| ((Math.floor(nodeVersion) == 14) && (nodeVersion >= 14.15))
|| ((Math.floor(nodeVersion) == 12) && (nodeVersion >= 12.19))) {
passport.push('openid-client');
} else {
addServerWarning('This NodeJS version does not support OpenID Connect on MeshCentral.', 25);
delete config.domains[i].authstrategies.oidc;
}
}
if ((typeof config.domains[i].authstrategies.saml == 'object') || (typeof config.domains[i].authstrategies.jumpcloud == 'object')) { passport.push('passport-saml'); }
}
if (config.domains[i].sessionrecording != null) { sessionRecording = true; }
if ((config.domains[i].passwordrequirements != null) && (config.domains[i].passwordrequirements.bancommonpasswords == true)) { wildleek = true; }
if ((config.domains[i].newaccountscaptcha != null) && (config.domains[i].newaccountscaptcha !== false)) { captcha = true; }
}
// Build the list of required modules
// NOTE: ALL MODULES MUST HAVE A VERSION NUMBER AND THE VERSION MUST MATCH THAT USED IN Dockerfile
var modules = ['archiver@7.0.0','body-parser@1.20.2','cbor@5.2.0','compression@1.7.4','cookie-session@2.0.0','express@4.18.2','express-handlebars@5.3.5','express-ws@4.0.0','ipcheck@0.1.0','minimist@1.2.8','multiparty@4.2.3','@yetzt/nedb','node-forge@1.3.1','ua-parser-js@1.0.37','ws@8.14.2','yauzl@2.10.0'];
if (require('os').platform() == 'win32') { modules.push('node-windows@0.1.14'); modules.push('loadavg-windows@1.1.1'); if (sspi == true) { modules.push('node-sspi@0.2.10'); } } // Add Windows modules
if (ldap == true) { modules.push('ldapauth-fork@5.0.5'); }
if (ssh == true) { modules.push('ssh2@1.15.0'); }
if (passport != null) { modules.push(...passport); }
if (captcha == true) { modules.push('svg-captcha@1.4.0'); }
if (sessionRecording == true) { modules.push('image-size@1.0.2'); } // Need to get the remote desktop JPEG sizes to index the recodring file.
if (config.letsencrypt != null) { modules.push('acme-client@4.2.5'); } // Add acme-client module. We need to force v4.2.4 or higher since olver versions using SHA-1 which is no longer supported by Let's Encrypt.
if (config.settings.mqtt != null) { modules.push('aedes@0.39.0'); } // Add MQTT Modules
if (config.settings.mysql != null) { modules.push('mysql2@3.6.2'); } // Add MySQL.
//if (config.settings.mysql != null) { modules.push('@mysql/xdevapi@8.0.33'); } // Add MySQL, official driver (https://dev.mysql.com/doc/dev/connector-nodejs/8.0/)
if (config.settings.mongodb != null) { modules.push('mongodb@4.13.0'); modules.push('saslprep@1.0.3'); } // Add MongoDB, official driver.
if (config.settings.postgres != null) { modules.push('pg@8.7.1'); modules.push('pgtools@0.3.2'); } // Add Postgres, Postgres driver.
if (config.settings.mariadb != null) { modules.push('mariadb@3.2.2'); } // Add MariaDB, official driver.
if (config.settings.acebase != null) { modules.push('acebase@1.29.5'); } // Add AceBase, official driver.
if (config.settings.sqlite3 != null) { modules.push('sqlite3@5.1.6'); } // Add sqlite3, official driver.
if (config.settings.vault != null) { modules.push('node-vault@0.10.2'); } // Add official HashiCorp's Vault module.
if (config.settings.plugins != null) { modules.push('semver@7.5.4'); } // Required for version compat testing and update checks
if ((config.settings.plugins != null) && (config.settings.plugins.proxy != null)) { modules.push('https-proxy-agent@7.0.2'); } // Required for HTTP/HTTPS proxy support
else if (config.settings.xmongodb != null) { modules.push('mongojs@3.1.0'); } // Add MongoJS, old driver.
if (nodemailer || ((config.smtp != null) && (config.smtp.name != 'console')) || (config.sendmail != null)) { modules.push('nodemailer@6.9.8'); } // Add SMTP support
if (sendgrid || (config.sendgrid != null)) { modules.push('@sendgrid/mail'); } // Add SendGrid support
if ((args.translate || args.dev) && (Number(process.version.match(/^v(\d+\.\d+)/)[1]) >= 16)) { modules.push('jsdom@22.1.0'); modules.push('esprima@4.0.1'); modules.push('minify-js@0.0.4'); modules.push('html-minifier@4.0.0'); } // Translation support
if (typeof config.settings.crowdsec == 'object') { modules.push('@crowdsec/express-bouncer@0.1.0'); } // Add CrowdSec bounser module (https://www.npmjs.com/package/@crowdsec/express-bouncer)
if (typeof config.settings.autobackup == 'object') {
// Setup encrypted zip support if needed
if (config.settings.autobackup.zippassword) { modules.push('archiver-zip-encrypted@1.0.11'); }
// Enable Google Drive Support
if (typeof config.settings.autobackup.googledrive == 'object') { modules.push('googleapis@128.0.0'); }
// Enable WebDAV Support
if (typeof config.settings.autobackup.webdav == 'object') {
if ((typeof config.settings.autobackup.webdav.url != 'string') || (typeof config.settings.autobackup.webdav.username != 'string') || (typeof config.settings.autobackup.webdav.password != 'string')) { addServerWarning("Missing WebDAV parameters.", 2, null, !args.launch); } else { modules.push('webdav@4.11.3'); }
}
}
// Setup common password blocking
if (wildleek == true) { modules.push('wildleek@2.0.0'); }
// Setup 2nd factor authentication
if (config.settings.no2factorauth !== true) {
// Setup YubiKey OTP if configured
if (yubikey == true) { modules.push('yubikeyotp@0.2.0'); } // Add YubiKey OTP support
if (allsspi == false) { modules.push('otplib@10.2.3'); } // Google Authenticator support (v10 supports older NodeJS versions).
}
// Desktop multiplexor support
if (config.settings.desktopmultiplex === true) { modules.push('image-size@1.0.2'); }
// SMS support
if (config.sms != null) {
if (config.sms.provider == 'twilio') { modules.push('twilio@4.19.0'); }
if (config.sms.provider == 'plivo') { modules.push('plivo@4.58.0'); }
if (config.sms.provider == 'telnyx') { modules.push('telnyx@1.25.5'); }
}
// Messaging support
if (config.messaging != null) {
if (config.messaging.telegram != null) { modules.push('telegram@2.19.8'); modules.push('input@1.0.1'); }
if (config.messaging.discord != null) { if (nodeVersion >= 17) { modules.push('discord.js@14.6.0'); } else { delete config.messaging.discord; addServerWarning('This NodeJS version does not support Discord.js.', 26); } }
if (config.messaging.xmpp != null) { modules.push('@xmpp/client@0.13.1'); }
if (config.messaging.pushover != null) { modules.push('node-pushover@1.0.0'); }
if (config.messaging.zulip != null) { modules.push('zulip@0.1.0'); }
}
// Setup web based push notifications
if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) { modules.push('web-push@3.6.6'); }
// Firebase Support
// Avoid 0.1.8 due to bugs: https://github.com/guness/node-xcs/issues/43
if (config.firebase != null) { modules.push('node-xcs@0.1.7'); }
// Syslog support
if ((require('os').platform() != 'win32') && (config.settings.syslog || config.settings.syslogjson)) { modules.push('modern-syslog@1.2.0'); }
if (config.settings.syslogtcp) { modules.push('syslog@0.1.1-1'); }
// Setup heapdump support if needed, useful for memory leak debugging
// https://www.arbazsiddiqui.me/a-practical-guide-to-memory-leaks-in-nodejs/
if (config.settings.heapdump === true) { modules.push('heapdump@0.3.15'); }
// Install any missing modules and launch the server
InstallModules(modules, args, function () {
if (require('os').platform() == 'win32') { try { require('node-windows'); } catch (ex) { console.log("Module node-windows can't be loaded. Restart MeshCentral."); process.exit(); return; } }
meshserver = CreateMeshCentralServer(config, args);
meshserver.Start();
});
// On exit, also terminate the child process if applicable
process.on('exit', function () { if (childProcess) { childProcess.kill(); childProcess = null; } });
// If our parent exits, we also exit
if (args.launch) {
process.stderr.on('end', function () { process.exit(); });
process.stdout.on('end', function () { process.exit(); });
process.stdin.on('end', function () { process.exit(); });
process.stdin.on('data', function (data) { });
}
});
}
if (require.main === module) {
mainStart(); // Called directly, launch normally.
} else {
module.exports.mainStart = mainStart; // Required as a module, useful for winservice.js
}