Ctrl K
ComunidadGuías…

meshcentral.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
* @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');
const { zipExtract, moveExtractedFolders } = require('./backup.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();

    // Load Windows load average module if on Windows
    if (process.platform === 'win32') try { require('loadavg-windows'); } catch (ex) { }

    // 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');
        obj.webOverrideBasePath = obj.path.join(__dirname, '../../meshcentral-web'); // set even if absent, a restore may create it
        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');
        obj.webOverrideBasePath = obj.path.join(__dirname, '../meshcentral-web'); // set even if absent, a restore may create it
        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', 'portbind', 'relayport', 'relayaliasport', 'agentport',
            'agentportbind', 'agentaliasport', 'aliasport', 'mpsport', 'mpsaliasport',
            'redirport', 'redirportbind', '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', 'agentallowedipnewagents', 'agentblockedip', 'fastcert', 'swarmport',
            'logintoken', 'logintokenkey', 'logintokengen', 'mailtokengen', 'admin',
            'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore',
            'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'oldencrypt',
            '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', 'mysql', 'mariadb', 'trustedproxy', 'migratevolumeinfo'
        ];
        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; } }
        const ENVVAR_PREFIX = "meshcentral_"
        let envArgs = []
        for (let [envvar, envval] of Object.entries(process.env)) {
            if (envvar.toLocaleLowerCase().startsWith(ENVVAR_PREFIX)) {
                let argname = envvar.slice(ENVVAR_PREFIX.length).toLocaleLowerCase()
                if (!!argname && !(validArguments.indexOf(argname) == -1)) {
                    envArgs = envArgs.concat([`--${argname}`, envval])
                }
            }
        }
        envArgs = require('minimist')(envArgs)
        obj.args = Object.assign(envArgs, obj.args)
        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; }
        if (obj.args.mysql == true) { console.log('Must specify: --mysql [connectionstring] \r\nExample mysql://user:password@127.0.0.1:3306/database'); return; }
        if (obj.args.mariadb == true) { console.log('Must specify: --mariadb [connectionstring] \r\nExample mariadb://user:password@127.0.0.1:3306/database'); 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\t\t\t\t\tInstall MeshCentral as a background service.');
                console.log('   --start/stop/restart\t\t\t\t\tControl MeshCentral background service.');
                console.log('');
                console.log('Run standalone, console application');
            }
            console.log('   --user [username]\t\t\t\t\tAlways login as [username] if account exists.');
            console.log('   --port [number]\t\t\t\t\tWeb server port number.');
            console.log('   --redirport [number]\t\t\t\t\tCreates an additional HTTP server to redirect users to the HTTPS server.');
            console.log('   --exactports\t\t\t\t\t\tServer must run with correct ports or exit.');
            console.log('   --noagentupdate\t\t\t\t\tServer will not update mesh agent native binaries.');
            console.log('   --nedbtodb\t\t\t\t\t\tTransfer all NeDB records into current database.');
            console.log('   --listuserids\t\t\t\t\tShow a list of a user identifiers in the database.');
            console.log('   --cert [name], (country), (org)\t\t\tCreate a web server certificate with [name] server name.');
            console.log('\t\t\t\t\t\t\tcountry and organization can optionally be set.');
            console.log('\r\nRun stateless, config files (config.json and certificates) are stored encrypted with [password] in the database.');
            console.log('   --dblistconfigfile (password)\t\t\tShow the files in the db and optionally check the files against the password.');
            console.log('   --dbpushconfigfiles (path) --configkey [password]\tThis will import the current configfiles or from (path) into the database.');
            console.log('   --dbshowconfigfile [file] --configkey [password]\tShow contents of config [file].');
            console.log('   --dbpullconfigfiles [path] --configkey [password]\tPull configfiles out of the database into [path].');
            console.log('   --dbdeleteconfigfiles\t\t\t\tDelete config files from the database.');
            console.log('   --loadconfigfromdb [password]\t\t\tRun meshcentral with configfiles found in the database.')
            console.log('\r\nServer recovery commands, use only when MeshCentral is offline.');
            console.log('   --createaccount [userid]\t\t\t\tCreate a new user account.');
            console.log('   --resetaccount [userid]\t\t\t\tUnlock an account, disable 2FA and set a new account password.');
            console.log('   --adminaccount [userid]\t\t\t\tPromote 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;
            } else {
                // Translate all of the default files
                translateEngine.startEx(['', '', 'minifyall']);
                translateEngine.startEx(['', '', 'translateall']);
                translateEngine.startEx(['', '', 'extractall']);
                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']);
                    }
                }
            }

            // Check domains and see if "meshcentral-web-DOMAIN" exists, if so, translate all pages in that folder
            for (i in obj.config.domains) {
                if (i == "") continue;
                var path = obj.path.join(obj.datapath, '..', 'meshcentral-web-' + i, 'views');
                if (require('fs').existsSync(path)) {
                    didSomething = true;
                    var files = obj.fs.readdirSync(path);
                    for (var a in files) {
                        var file = obj.path.join(path, files[a]);
                        if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
                            translateEngine.startEx(['', '', 'minify', file]);
                        }
                    }
                    files = obj.fs.readdirSync(path);
                    for (var a in files) {
                        var file = obj.path.join(path, files[a]);
                        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."); }
            console.log('Finished Translating.')
            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;
            }
        }
        
        // FreeBSD background service handling, MUST USE SPAWN FOR SERVICE COMMANDS!
        if (obj.platform == 'freebsd') {
            if (obj.args.install == true) {
                // Install MeshCentral in rc.d
                console.log('Installing MeshCentral as background Service...');
                var systemdConf = "/usr/local/etc/rc.d/meshcentral";
                const userinfo = require('os').userInfo();
                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 = '#!/bin/sh\n# MeshCentral FreeBSD Service Script\n# PROVIDE: meshcentral\n# REQUIRE: NETWORKING\n# KEYWORD: shutdown\n. /etc/rc.subr\nname=meshcentral\nuser=' + userinfo.username + '\nrcvar=meshcentral_enable\n: \\${meshcentral_enable:=\\"NO\\"}\n: \\${meshcentral_args:=\\"\\"}\npidfile=/var/run/meshcentral/meshcentral.pid\ncommand=\\"/usr/sbin/daemon\\"\nmeshcentral_chdir=\\"' + obj.parentpath + '\\"\ncommand_args=\\"-r -u \\${user} -P \\${pidfile} -S -T meshcentral -m 3 ' + nodePath + ' ' + __dirname + ' \\${meshcentral_args}\\"\nload_rc_config \\$name\nrun_rc_command \\"\\$1\\"\n';
                    require('child_process').exec('echo \"' + config + '\" | tee ' + systemdConf + ' && chmod +x ' + 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('sysrc meshcentral_enable="YES"', {}, 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...');
                            const service = require('child_process').spawn('service', ['meshcentral', 'start']);
                            service.stdout.on('data', function (data) { console.log(data.toString()); });
                            service.stderr.on('data', function (data) { console.log(data.toString()); });
                            service.on('exit', function (code) {
                                console.log((code === 0) ? 'Done.' : 'ERROR: Unable to start MeshCentral as a service');
                                process.exit(); // Must exit otherwise we just hang
                            });
                        });
                    });
                });
                return;
            } else if (obj.args.uninstall == true) {
                // Uninstall MeshCentral in rc.d
                console.log('Uninstalling MeshCentral background service...');
                var systemdConf = "/usr/local/etc/rc.d/meshcentral";
                console.log('Stopping service...');
                const service = require('child_process').spawn('service', ['meshcentral', 'stop']);
                service.stdout.on('data', function (data) { console.log(data.toString()); });
                service.stderr.on('data', function (data) { console.log(data.toString()); });
                service.on('exit', function (code) {
                    if (code !== 0) { console.log('ERROR: Unable to stop MeshCentral as a service'); }
                    console.log('Disabling service...');
                    require('child_process').exec('sysrc -x meshcentral_enable', {}, 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('rm ' + systemdConf, {}, function (err, stdout, stderr) {
                            if ((err != null) && (err != '')) { console.log('ERROR: Unable to delete MeshCentral config file: ' + err); }
                            console.log('Done.');
                            process.exit(); // Must exit otherwise we just hang
                        });
                    });
                });
                return;
            } else if (obj.args.start == true) {
                // Start MeshCentral in rc.d
                const service = require('child_process').spawn('service', ['meshcentral', 'start']);
                service.stdout.on('data', function (data) { console.log(data.toString()); });
                service.stderr.on('data', function (data) { console.log(data.toString()); });
                service.on('exit', function (code) {
                    console.log((code === 0) ? 'Done.' : 'ERROR: Unable to start MeshCentral as a service: ' + error);
                    process.exit(); // Must exit otherwise we just hang
                });
                return;
            } else if (obj.args.stop == true) {
                // Stop MeshCentral in rc.d
                const service = require('child_process').spawn('service', ['meshcentral', 'stop']);
                service.stdout.on('data', function (data) { console.log(data.toString()); });
                service.stderr.on('data', function (data) { console.log(data.toString()); });
                service.on('exit', function (code) {
                    console.log((code === 0) ? 'Done.' : 'ERROR: Unable to stop MeshCentral as a service: ' + error);
                    process.exit(); // Must exit otherwise we just hang
                });
                return;
            } else if (obj.args.restart == true) {
                // Restart MeshCentral in rc.d
                const service = require('child_process').spawn('service', ['meshcentral', 'restart']);
                service.stdout.on('data', function (data) { console.log(data.toString()); });
                service.stderr.on('data', function (data) { console.log(data.toString()); });
                service.on('exit', function (code) {
                    console.log((code === 0) ? 'Done.' : 'ERROR: Unable to restart MeshCentral as a service: ' + error);
                    process.exit(); // Must exit otherwise we just hang
                });
                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 (const flag of process.execArgv) { startArgs.push(flag); }
            startArgs.push(process.argv[1]);
            for (let i = 2; i < process.argv.length; i++) { startArgs.push(process.argv[i]); }
            startArgs.push('--launch', process.pid.toString());
            obj.launchChildServer(startArgs);
        }
    };

    // Launch MeshCentral as a child server and monitor it.
    obj.launchChildServer = function (startArgs) {
        const child_process = require('child_process');
        const isInspectorAttached = (()=> { try { return require('node:inspector').url() !== undefined; } catch (_) { return false; } }).call();
        const logFromChildProcess = isInspectorAttached ? () => {} : console.log.bind(console);
        if (!startArgs.includes('--disable-proto=delete')) { startArgs.unshift('--disable-proto=delete'); }
        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);
                    const restartOnServerError = (obj.config.settings.restartonservererror !== false);
                    if (restartOnServerError) {
                        console.log('ERROR: MeshCentral failed with critical error, check mesherrors.txt. Restarting in 5 seconds...');
                        setTimeout(function () { obj.launchChildServer(startArgs); }, 5000);
                    } else {
                        console.log('ERROR: MeshCentral failed with critical error, check mesherrors.txt. Automatic restart is disabled, exiting.');
                    }

                    // 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) { });
                    }
                    if (restartOnServerError == false) { process.exit(1); }
                }
            }
        });
        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) {
                const specificupdate = data.substring(data.indexOf('Starting self upgrade to: ') + 26).split('\r')[0].split('\n')[0];
                if (/^[0-9\.\-]+$/i.test(specificupdate)) {
                    var isUpgrade = false;
                    try {
                        var currentVersion = getCurrentVersion();
                        if (currentVersion) {
                            var sVer = specificupdate.split('-')[0].split('.');
                            var cVer = currentVersion.split('-')[0].split('.');
                            for (var i = 0; i <= 2; i++) {
                                var sVal = parseInt(sVer[i] || 0);
                                var cVal = parseInt(cVer[i] || 0);
                                if (sVal > cVal) { isUpgrade = true; break; }
                                else if (sVal < cVal) { break; }
                            }
                        }
                    } catch (ex) { }
                    if (isUpgrade) {
                        obj.args.specificupdate = specificupdate;
                        childProcess.xrestart = 3;
                    } else {
                        data += '\nERROR: Downgrade from ' + currentVersion + ' to ' + specificupdate + ' is not allowed.';
                        childProcess.xrestart = 1;
                    }
                }
            }
            var datastr = data;
            while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
            logFromChildProcess(datastr);
        });
        childProcess.stderr.on('data', function (data) {
            var datastr = data;
            while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
            logFromChildProcess('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 = async 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
        obj.agentBlockedIpFile = ((typeof obj.args.agentblockedip == 'string') && obj.args.agentblockedip.startsWith('file:')) ? obj.path.join(obj.datapath, obj.args.agentblockedip.substring(5)) : null;
        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.agentallowedipnewagents = obj.args.agentallowedipnewagents = readIpListFromFile(obj.args.agentallowedipnewagents);
        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.agentallowedipnewagents == 'string') { if (obj.args.agentallowedipnewagents == '') { config.settings.agentallowedipnewagents = obj.args.agentallowedipnewagents = null; } else { config.settings.agentallowedipnewagents = obj.args.agentallowedipnewagents = obj.args.agentallowedipnewagents.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 IP lists and ranges and if DNS return IP addresses
        config.settings.userallowedip = await resolveDomainsToIps(config.settings.userallowedip);
        config.settings.userblockedip = await resolveDomainsToIps(config.settings.userblockedip);
        config.settings.agentallowedip = await resolveDomainsToIps(config.settings.agentallowedip);
        config.settings.agentallowedipnewagents = await resolveDomainsToIps(config.settings.agentallowedipnewagents);
        config.settings.agentblockedip = await resolveDomainsToIps(config.settings.agentblockedip);
        config.settings.swarmallowedip = await resolveDomainsToIps(config.settings.swarmallowedip);

        // 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, err) {
                            if (err) { console.error('Record encryption recode failed after ' + count + ' record(s): ' + err); process.exit(1); return; }
                            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.migratevolumeinfo) { require('./migrate-volume-info.js').migrateVolumeInfo(obj.db, function (err, r) { if (err != null) { console.log('Volume info migration error: ' + err); } else { console.log('Volume info migration complete. Scanned ' + r.scanned + ' sysinfo document(s), migrated ' + r.migrated + ', moved ' + r.keysMoved + ' key(s).'); } 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; delete user.otpduo; // 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 = {};
                                let 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.domain, 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) {
                                        if (typeof obj.args.dblistconfigfiles == 'string') {
                                            const data = obj.db.decryptData(obj.args.dblistconfigfiles, docs[i].data);
                                            if (data == null) {
                                                console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' encrypted bytes - Unable to decrypt.');
                                            } else {
                                                console.log(docs[i]._id.split('/')[1] + ', ' + data.length + ' bytes, decoded correctly.');
                                            }
                                        } else {
                                            console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' encrypted 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.toString()); }
                                }
                            } 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++;
                                                if (obj.args.oldencrypt) {
                                                    obj.db.setConfigFile(file, obj.db.oldEncryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } });
                                                } else {
                                                    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 = {};
                            let 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 = {};
                                let 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
                    // Moved behind autobackup config init in startex4: 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;

        // 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; }
            obj.config.domains[i].userallowedip = obj.config.domains[i].userallowedip = readIpListFromFile(obj.config.domains[i].userallowedip);
            obj.config.domains[i].userblockedip = obj.config.domains[i].userblockedip = readIpListFromFile(obj.config.domains[i].userblockedip);
            obj.config.domains[i].agentallowedip = obj.config.domains[i].agentallowedip = readIpListFromFile(obj.config.domains[i].agentallowedip);
            obj.config.domains[i].agentallowedipnewagents = readIpListFromFile(obj.config.domains[i].agentallowedipnewagents);
            obj.config.domains[i].agentblockedip = obj.config.domains[i].agentblockedip = readIpListFromFile(obj.config.domains[i].agentblockedip);
            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].agentallowedipnewagents == 'string') { if (obj.config.domains[i].agentallowedipnewagents == '') { delete obj.config.domains[i].agentallowedipnewagents; } else { obj.config.domains[i].agentallowedipnewagents = obj.config.domains[i].agentallowedipnewagents.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(','); } }
            // Check IP lists and ranges and if DNS return IP addresses
            obj.config.domains[i].userallowedip = await resolveDomainsToIps(obj.config.domains[i].userallowedip);
            obj.config.domains[i].userblockedip = await resolveDomainsToIps(obj.config.domains[i].userblockedip);
            obj.config.domains[i].agentallowedip = await resolveDomainsToIps(obj.config.domains[i].agentallowedip);
            obj.config.domains[i].agentallowedipnewagents = await resolveDomainsToIps(obj.config.domains[i].agentallowedipnewagents);
            obj.config.domains[i].agentblockedip = await resolveDomainsToIps(obj.config.domains[i].agentblockedip);
            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 obj.args.agentidletimeout != 'number') { obj.args.agentidletimeout = 150000; } else { obj.args.agentidletimeout *= 1000 } // Default agent idle timeout is 2m, 30sec.
        if ((obj.args.lanonly != true) && (typeof obj.args.webrtconfig == 'object')) { // fix incase you are using an old mis-spelt webrtconfig 
            obj.args.webrtcconfig = obj.args.webrtconfig;
            delete obj.args.webrtconfig;
        }
        if ((obj.args.lanonly != true) && (obj.args.webrtcconfig == null)) { obj.args.webrtcconfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.cloudflare.com:3478' }] }; } // Setup default WebRTC STUN servers
        else if ((obj.args.lanonly != true) && (typeof obj.args.webrtcconfig == 'object')) {
            if (obj.args.webrtcconfig.iceservers) { // webrtc is case-sensitive, so must rename iceservers to iceServers!
                obj.args.webrtcconfig.iceServers = obj.args.webrtcconfig.iceservers;
                delete obj.args.webrtcconfig.iceservers;
            }
        }
        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)) {
            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
        const trustedproxyIsCloudflareString = (obj.args.trustedproxy != null) && (typeof obj.args.trustedproxy == 'string') && (obj.args.trustedproxy.toLowerCase() == 'cloudflare');
        const trustedproxyIsCloudflareArray = Array.isArray(obj.args.trustedproxy) && obj.args.trustedproxy.some(function (x) { return (typeof x == 'string') && (x.toLowerCase() == 'cloudflare'); });
        if (trustedproxyIsCloudflareString || trustedproxyIsCloudflareArray) {
            obj.config.settings.extrascriptsrc = 'ajax.cloudflare.com'; // Add CloudFlare as a trusted script source. This allows for CloudFlare's RocketLoader feature.
            // Preserve any non-'cloudflare' entries already in the array
            const existingProxies = trustedproxyIsCloudflareArray ? obj.args.trustedproxy.filter(function (x) { return !(typeof x == 'string' && x.toLowerCase() == 'cloudflare'); }) : [];
            obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v4', null, function (url, data, tag) {
                if (data != null) {
                    const newProxies = existingProxies.slice();
                    const ipranges = data.split('\n');
                    for (var i in ipranges) { if (ipranges[i] != '') { newProxies.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] != '') { newProxies.push(ipranges[i]); } }
                        } else {
                            addServerWarning("Unable to load CloudFlare trusted proxy IPv6 address list.", 16);
                        }
                        obj.args.trustedproxy = newProxies;
                        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 = async 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;
            }
        }

        // Release files that land after the wait below are switched in by activateAgentDefaults, which
        // holds until this first signing pass and table load have finished.
        obj.agentTablesReady = new Promise(function (resolve) { obj.agentTablesLoaded = resolve; });
        obj.agentDefaults = require('./agentbuilds').CreateAgentDefaults(obj);
        // Never block server startup on default agent downloads. An offline or slow-network server must come up
        // promptly and run from cached, bundled or manually uploaded builds; downloads finish in the background.
        const agentDefaultsReady = obj.agentDefaults.prepare();
        agentDefaultsReady.then(function () { for (const error of obj.agentDefaults.status().errors) { addServerWarning(error); } }, function () { });
        try { await Promise.race([agentDefaultsReady, new Promise(function (r) { setTimeout(r, 6000); })]); } catch (ex) { }
        if (obj.agentDefaults.status().busy) { console.log('Default agent downloads are still running. Starting without them; they load when they finish.'); }
        obj.agentDefaults.start();

        // 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'];
                translations['zh-tw'] = translations['zh-cht'];
                translations['zh-hk'] = 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)) { await new Promise(function (resolve) { obj.updateMeshAgentsTable(obj.config.domains[i], resolve); }); } }

        // 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();
                obj.agentTablesLoaded();

                // 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); }
                    }

                    // Get the current node version
                    const verSplit = process.version.substring(1).split('.');
                    var nodeVersion = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);

                    // Setup Firebase
                    if ((config.firebase != null) && (typeof config.firebase.senderid == 'string') && (typeof config.firebase.serverkey == 'string')) {
                        addServerWarning('Firebase now requires a service account JSON file, Firebase disabled.', 27);
                    } else if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) {
                        var serviceAccount;
                        try { serviceAccount = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, config.firebase.serviceaccountfile)).toString()); } catch (ex) { console.log(ex); }
                        if (serviceAccount != null) { obj.firebase = require('./firebase').CreateFirebase(obj, serviceAccount); }
                    } 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');
                    }

                    // Setup monitoring
                    if (obj.config.settings.prometheus != null) { obj.monitoring = require('./monitoring.js').CreateMonitoring(obj, obj.args); }

                    // Start periodic maintenance
                    obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 60 * 60); // Run this every hour
                    //obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 10 * 1); // DEBUG: Run this more often

                    // 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,
                                am: 0
                            },
                            traffic: trafficStats.delta
                        };
                        try { data.cpu = require('os').loadavg(); } catch (ex) { }
                        if (obj.mpsserver != null) {
                            data.conn.amc = 0;
                            for (var i in obj.mpsserver.ciraConnections) { data.conn.amc += obj.mpsserver.ciraConnections[i].length; }
                        }
                        for (var i in obj.connectivityByNode) {
                            const node = obj.connectivityByNode[i];
                            if (node && typeof node.connectivity !== 'undefined' && node.connectivity === 4) { data.conn.am++; }
                        }
                        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. Unless autobackup is set to false try to make a backup.
                    if (obj.config.settings.autobackup == false || obj.config.settings.autobackup == 'false') { obj.config.settings.autobackup = {backupintervalhours: -1}; } //block all autobackup functions
                    else {
                        if (typeof obj.config.settings.autobackup != 'object') { obj.config.settings.autobackup = {}; };
                        if (Object.hasOwn(obj.config.settings.autobackup, "zippassword")) {
                            if ((obj.config.settings.autobackup.zippassword).length == 0) {
                                delete obj.config.settings.autobackup.zippassword;
                                obj.addServerWarning('Empty zip password in config.json', true);
                            } else {
                                // convert to string regardless of type
                                obj.config.settings.autobackup.zippassword = String(obj.config.settings.autobackup.zippassword);
                            }
                        }
                        if (typeof obj.config.settings.autobackup.backupintervalhours != 'number') { obj.config.settings.autobackup.backupintervalhours = 24; };
                        if (typeof obj.config.settings.autobackup.keeplastdaysbackup != 'number') { obj.config.settings.autobackup.keeplastdaysbackup = 10; };
                        if ((typeof obj.config.settings.autobackup.minfreespacemb != 'number') || (obj.config.settings.autobackup.minfreespacemb < 0)) { obj.config.settings.autobackup.minfreespacemb = 200; };
                        if (obj.config.settings.autobackup.backuphour != null ) { obj.config.settings.autobackup.backupintervalhours = 24; if ((typeof obj.config.settings.autobackup.backuphour != 'number') || (obj.config.settings.autobackup.backuphour > 23 || obj.config.settings.autobackup.backuphour < 0 )) { obj.config.settings.autobackup.backuphour = 0; }}
                        else {obj.config.settings.autobackup.backuphour = -1 };
                        //arrayfi in case of string and remove possible ', ' space. !! If a string instead of an array is passed, it will be split by ',' so *{.txt,.log} won't work in that case !!
                        if (!obj.config.settings.autobackup.backupignorefilesglob) {obj.config.settings.autobackup.backupignorefilesglob = []}
                        else if (typeof obj.config.settings.autobackup.backupignorefilesglob == 'string') { obj.config.settings.autobackup.backupignorefilesglob = obj.config.settings.autobackup.backupignorefilesglob.replaceAll(', ', ',').split(','); };
                        if (!obj.config.settings.autobackup.backupskipfoldersglob) {obj.config.settings.autobackup.backupskipfoldersglob = []}
                        else if (typeof obj.config.settings.autobackup.backupskipfoldersglob == 'string') { obj.config.settings.autobackup.backupskipfoldersglob = obj.config.settings.autobackup.backupskipfoldersglob.replaceAll(', ', ',').split(','); };
                        if (typeof obj.config.settings.autobackup.backuppath == 'string') { obj.backuppath = (obj.config.settings.autobackup.backuppath = (obj.path.resolve(obj.config.settings.autobackup.backuppath))) } else { obj.config.settings.autobackup.backuppath = obj.backuppath };
                        if (typeof obj.config.settings.autobackup.backupname != 'string') { obj.config.settings.autobackup.backupname = 'meshcentral-autobackup-'};
                        if (typeof obj.config.settings.autobackup.webdav == 'object') {
                            //make webdav compliant: http://www.webdav.org/specs/rfc4918.html#rfc.section.5.2, http://www.webdav.org/specs/rfc2518.html#METHOD_MKCOL
                            // So with leading and trailing slash in the foldername, and no double and backslashes
                            if (typeof obj.config.settings.autobackup.webdav.foldername != 'string') {obj.config.settings.autobackup.webdav.foldername = '/MeshCentral-Backups/'}
                            else {obj.config.settings.autobackup.webdav.foldername = ('/' + obj.config.settings.autobackup.webdav.foldername + '/').replaceAll("\\", "/").replaceAll("//", "/").replaceAll("//", "/")};
                        }
                    }

                    // Check if the database is capable of performing a backup
                    obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });

                    // 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.backupintervalhours >= 1 ) {
            obj.db.Get('LastAutoBackupTime', function (err, docs) {
                if (err != null) { console.error("checkAutobackup: Error getting LastBackupTime from DB"); return}
                var lastBackup = 0;
                const currentdate = new Date();
                let currentHour = currentdate.getHours();
                let now = currentdate.getTime();
                if (docs.length == 1) { lastBackup = docs[0].value; }
                const delta = now - lastBackup;
                //const delta = 9999999999; // DEBUG: backup always
                obj.debug ('backup', 'Entering checkAutobackup, lastAutoBackupTime: ' + new Date(lastBackup).toLocaleString('default', { dateStyle: 'medium', timeStyle: 'short' }) + ', delta: ' + (delta/(1000*60*60)).toFixed(2) + ' hours');
                //start autobackup if interval has passed or at configured hour, whichever comes first. When an hour schedule is missed, it will make a backup immediately.
                if ((delta > (obj.config.settings.autobackup.backupintervalhours * 60 * 60 * 1000)) || ((currentHour == obj.config.settings.autobackup.backuphour) && (delta >= 2 * 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" });
        const restorePassword = obj.config.settings.autobackup.zippasswordrequest;
        delete obj.config.settings.autobackup.zippasswordrequest;
        // 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...");     // do not alter. This specific log message, with the process.exit(123) further on, triggers a process restart. See obj.launchChildServer>childProcess.stdout.on function
                const webBase = obj.webOverrideBasePath;
                const destinations = {
                    'meshcentral-views': obj.webViewsOverridePath || obj.path.join(webBase, 'views'),
                    'meshcentral-public': obj.webPublicOverridePath || obj.path.join(webBase, 'public'),
                    'meshcentral-emails': obj.webEmailsOverridePath || obj.path.join(webBase, 'emails'),
                    'meshcentral-files': obj.filespath,
                    'meshcentral-recordings': obj.recordpath
                };
                zipExtract(restoreFile, obj.datapath, Object.keys(destinations), restorePassword)
                    .then(async (res) => {
                        if (res.res) {
                            console.log(res.mes);
                            const move = await moveExtractedFolders(obj.datapath, destinations);
                            move.moved.forEach(function (folder) { console.log('Restored ' + folder); });
                            if (!move.res) { console.error(move.mes); }
                        } else { console.error(res.mes); }
                        process.exit(123);      // this triggers the childserver process restart
                    });
            } 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[id]) { if (obj.eventsDispatch[id][k] != target) { newList.push(obj.eventsDispatch[id][k]); } }
                        obj.eventsDispatch[id] = 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]; } }
            }
            if (storeEvent.mesh) {
                // Escape "mesh" names that may have "." and/or "$"
                storeEvent.mesh = obj.common.escapeLinksFieldNameEx(storeEvent.mesh);
            }
            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, id: Math.random() });
            }
        }
    };

    // 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, id: Math.random() });

                // 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, 4 = 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, id: Math.random() });

                // 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 modulePath = obj.path.join(moduleDirPath, modulesDir[i]);
                        var moduleDate = '';
                        try { moduleDate = ', "' + obj.fs.statSync(modulePath).mtime.toISOString() + '"'; } catch (ex) { }
                        const moduleData = ['try { addModule("', moduleName, '", "', obj.escapeCodeString(obj.fs.readFileSync(modulePath).toString('binary')), '"', moduleDate, '); 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');
                // The previous compressed core no longer matches the new hash, agents would reject it and drop their core. Until compressed, this core is sent uncompressed.
                delete obj.defaultMeshCoresDeflate[i];
                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, dropping the result if the core was rebuilt meanwhile
                const callback = function MeshCoreDeflateCb(err, buffer) { if ((err == null) && (obj.defaultMeshCores[MeshCoreDeflateCb.i] === MeshCoreDeflateCb.core)) { obj.defaultMeshCoresDeflate[MeshCoreDeflateCb.i] = buffer; } }
                callback.i = i;
                callback.core = obj.defaultMeshCores[i];
                // agents expect a zlib.deflateRaw instead of zlib.deflate, fixed in meshagent.js, maybe change to zlib.deflateRaw here? Compressed cores also served at <server>/meshagents?cores
                require('zlib').deflate(obj.defaultMeshCores[i], { level: require('zlib').constants.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);
            });
        }
    };

    obj.getAgentBinaryPath = function (name) {
        return obj.agentDefaults ? obj.agentDefaults.sourcePath(name) : obj.path.join(__dirname, 'agents', name);
    };

    // 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.getAgentBinaryPath(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.
                if (!toolpath || !obj.fs.existsSync(toolpath)) { delete obj.meshToolsBinaries[toolname]; continue; }

                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.getAgentBinaryPath(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.
                if (!toolpath || !obj.fs.existsSync(toolpath)) { delete obj.meshToolsBinaries[toolname]; continue; }

                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) { }
        }
    };

    obj.meshAgentsArchitectureNumbers = require('./agenttypes')();

    // 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 = {}; }
        // Published when the pass ends: a table built during a later pass must not see a half-filled map.
        const signedSources = {};
        const done = function () { objx.signedAgentSources = signedSources; 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)]
            }
        }
        if (agentSignCertInfo == null) { done(); 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 (obj.args.agenttimestampserver === false) { timeStampUrl = null; }
        else if (typeof obj.args.agenttimestampserver == 'string') { timeStampUrl = obj.args.agenttimestampserver; }

        // Setup the time server proxy
        var timeStampProxy = null;
        if (typeof obj.args.agenttimestampproxy == 'string') { timeStampProxy = obj.args.agenttimestampproxy; }
        else if ((obj.args.agenttimestampproxy !== false) && (typeof obj.args.npmproxy == 'string')) { timeStampProxy = obj.args.npmproxy; }

        // Collect architectures that need signing
        var archIds = [];
        for (var archid in obj.meshAgentsArchitectureNumbers) {
            if (obj.meshAgentsArchitectureNumbers[archid].codesign !== true) continue;
            archIds.push(archid);
        }

        if (archIds.length === 0) { done(); return; }

        var currentArchIndex = 0;

        function signNextAgent() {
            if (currentArchIndex >= archIds.length) { done(); return; }

            var archid = archIds[currentArchIndex];
            currentArchIndex++;

            var agentpath, merged = null;
            if (domain.id == '') {
                // Load all agents when processing the default domain
                agentpath = obj.getAgentBinaryPath(obj.meshAgentsArchitectureNumbers[archid].base || 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 if (obj.meshAgentsArchitectureNumbers[archid].base != null) { merged = obj.path.join(serverSignedAgentsPath, obj.meshAgentsArchitectureNumbers[archid].localname + '.tmp'); }
            } 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 { signNextAgent(); return; } // If the agent is not present in "meshcentral-data/agents" skip.
            }

            if (!agentpath) { signNextAgent(); return; }

            // Open the original agent with authenticode
            const signeedagentpath = obj.path.join(serverSignedAgentsPath, obj.meshAgentsArchitectureNumbers[archid].localname);
            if (merged != null) {
                // MeshCmd is the service executable with the merged meshcmd.js appended. Build it next to the signed
                // copy so the checks below compare that copy with what would be signed now, script included.
                if (obj.defaultMeshCmd == null) { signNextAgent(); return; }
                try { obj.fs.mkdirSync(serverSignedAgentsPath); } catch (ex) { }
                try {
                    obj.exeHandler.writeExeWithJavaScript({ sourceFileName: agentpath, destinationFileName: merged, js: Buffer.from(obj.defaultMeshCmd, 'utf8') });
                } catch (ex) {
                    addServerWarning('Failed to sign \"' + obj.meshAgentsArchitectureNumbers[archid].localname + '\": ' + ex, 22, [obj.meshAgentsArchitectureNumbers[archid].localname, ex]);
                    signNextAgent(); return;
                }
            }
            const discard = function () { if (merged != null) { try { obj.fs.unlinkSync(merged); } catch (ex) { } } };
            const originalAgent = require('./authenticode.js').createAuthenticodeHandler(merged || 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; }
                    // MeshCmd's script is appended after the PE sections.
                    if ((destinationAgentOk == true) && (originalAgent.getHashOfOverlay('sha384').compare(destinationAgent.getHashOfOverlay('sha384')) != 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) { err = obj.callExternalSignJob(agentSignedFunc.signingArguments); }
                        if (err == null) {
                            signedSources[archid] = agentpath;
                            // Agent was signed succesfuly
                            console.log(obj.common.format('Code signed {0}.', agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname));
                        } else {
                            // Failed to sign agent
                            try { obj.fs.unlinkSync(agentSignedFunc.signeedagentpath); } catch (ex) { }
                            addServerWarning('Failed to sign \"' + agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname + '\": ' + err, 22, [agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname, err]);
                        }
                        if (merged != null) { originalAgent.close(); discard(); }
                        // Wait 2 seconds between each codesign to avoid rate limiting from Sectigo's timestamp server
                        // https://www.sectigo.com/resource-library/time-stamping-server
                        setTimeout(signNextAgent, 2000);
                    }
                    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
                    signingArguments.resChanges = resChanges;

                    obj.debug('main', "Code signing with arguments: " + JSON.stringify(signingArguments));
                    xagentSignedFunc.signingArguments = signingArguments; // Attach the signing arguments to the callback function
                    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.
                    signedSources[archid] = agentpath;
                    originalAgent.close();
                    discard();
                    signNextAgent();
                }

            } else {
                discard();
                signNextAgent();
            }
        }

        signNextAgent();
    }

    obj.callExternalSignJob = function (signingArguments) {
        if (!obj.config.settings || !obj.config.settings.externalsignjob) {
            return null;
        }
        obj.debug('main', "External signing job called for file: " + signingArguments.out);
        
        const { spawnSync } = require('child_process');

        var signResult;
        try {
            signResult = spawnSync('"' + obj.config.settings.externalsignjob + '"', ['"' + signingArguments.out + '"'], {
                encoding: 'utf-8',
                shell: true,
                stdio: 'inherit'
            });
        } catch (ex) { signResult = { error: ex }; }

        if (signResult.error || signResult.status !== 0) {
            obj.debug('main', "External signing failed for file: " + signingArguments.out);
            console.error("External signing failed for file: " + signingArguments.out);
            return signResult.error || new Error('External signing failed (' + (signResult.signal ? ('signal ' + signResult.signal) : ('exit code ' + signResult.status)) + ').');
        }
        return null;
    }

    // Late default agent downloads: swap the new paths in, run the signing pass and rebuild the default
    // domain's table, as startup does, so no restart is needed. One pass runs at a time; a request made
    // during a pass queues one more. reloadMeshAgents waits out the signing so it never builds a table
    // from a half-rewritten signedagents folder.
    obj.activateAgentDefaults = function (func) {
        if (typeof func != 'function') func = function () { };
        var state = obj.agentActivationState;
        if (state != null) { state.waiting.push(func); state.rerun = true; return; }
        state = obj.agentActivationState = { waiting: [func], rerun: false };
        function done(err) {
            const callbacks = state.waiting;
            delete obj.agentActivationState;
            for (var i in callbacks) { try { callbacks[i](err); } catch (ex) { } }
        }
        function pass() {
            state.rerun = false;
            if ((obj.agentDefaults == null) || !obj.agentDefaults.activate()) { done(); return; }
            console.log('Default agent files downloaded, loading them.');
            obj.agentActivation = new Promise(function (resolve) { obj.signMeshAgents(obj.config.domains[''], resolve); });
            obj.agentActivation.then(function () {
                obj.agentActivation = null;
                obj.updateMeshTools();
                obj.reloadMeshAgents(obj.config.domains[''], function (err) {
                    if (err) { addServerWarning('Unable to load the downloaded default agent files: ' + (err.message || err)); }
                    else { console.log('Default agent files loaded.'); }
                    if (state.rerun) { pass(); return; }
                    done(err);
                });
            });
        }
        Promise.resolve(obj.agentTablesReady).then(pass, pass);
    }

    // Coalesce reloads so an older hash read cannot overwrite a newer selection.
    obj.reloadMeshAgents = function (domain, func) {
        const key = domain.id || '';
        if (obj.agentReloadState == null) obj.agentReloadState = {};
        var state = obj.agentReloadState[key];
        if (state != null) { state.waiting.push(typeof func == 'function' ? func : function () { }); state.rerun = true; return; }
        state = obj.agentReloadState[key] = { waiting: [typeof func == 'function' ? func : function () { }], rerun: false };
        function done(err) {
            const callbacks = state.waiting;
            delete obj.agentReloadState[key];
            for (var i in callbacks) { try { callbacks[i](err); } catch (ex) { } }
        }
        function pass() {
            state.rerun = false;
            Promise.resolve(obj.agentActivation).then(function () {
                obj.updateMeshAgentsTable(domain, function () {
                    if (domain.id == '') { try { obj.updateMeshAgentInstallScripts(); } catch (ex) { } }
                    if (state.rerun) { pass(); return; }
                    // Connected agents still hold the entries they looked up in the old table.
                    if (obj.webserver != null) { for (var i in obj.webserver.wsagents) { try { obj.webserver.wsagents[i].updateAgentExeInfo(); } catch (ex) { } } }
                    done();
                });
            }).catch(done);
        }
        pass();
    }

    // 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; }
        const agentTable = {};
        function done() { objx.meshAgentBinaries = agentTable; if (func != null) func(); }

        // Load agent information file. This includes the data & time of the agent.
        var 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;
            const override = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
            // Files placed in "meshcentral-data/agents" are served ahead of the release files, as before.
            if (((domain.id != '') || (obj.meshAgentsArchitectureNumbers[archid].unsigned !== true)) && obj.fs.existsSync(override)) {
                agentpath = override;
            } else if (domain.id == '') {
                // Load all agents when processing the default domain
                agentpath = obj.getAgentBinaryPath(obj.meshAgentsArchitectureNumbers[archid].base || obj.meshAgentsArchitectureNumbers[archid].localname);
                if (obj.meshAgentsArchitectureNumbers[archid].unsigned !== true) {
                    const agentpath2 = obj.path.join(obj.datapath, 'signedagents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
                    // A failed signing attempt must not reuse a copy from another release.
                    if (agentpath && obj.fs.existsSync(agentpath2) && ((obj.signedAgentSources || {})[archid] === agentpath)) { agentpath = agentpath2; }
                    else if (obj.meshAgentsArchitectureNumbers[archid].base != null) { agentpath = null; } // MeshCmd only exists signed. Without it the download merges the script on the fly.
                }
            } else {
                // When processing an extra domain, only load agents that are specific to that domain
                continue;
            }

            if (!agentpath) continue;

            // Fetch agent binary information
            var stats = null;
            try { stats = obj.fs.statSync(agentpath); } catch (ex) { }
            if ((stats == null) || !stats.isFile()) continue;

            // Setup agent information
            archcount++;
            agentTable[archid] = Object.assign({}, obj.meshAgentsArchitectureNumbers[archid]);
            if (domain.id != '') delete agentTable[archid].codesign;
            agentTable[archid].path = agentpath;
            const release = obj.agentDefaults && obj.agentDefaults.info(obj.meshAgentsArchitectureNumbers[archid].base || obj.meshAgentsArchitectureNumbers[archid].localname);
            if (release && domain.id === '' && agentpath !== override) agentTable[archid].release = release;
            agentTable[archid].url = 'http://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?id=' + archid;
            agentTable[archid].size = stats.size;
            if ((agentInfo[archid] != null) && (agentInfo[archid].mtime != null)) { agentTable[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 { agentTable[archid].pe = obj.exeHandler.parseWindowsExecutable(agentpath); } catch (ex) { }
            }

            // If agents must be stored in RAM or if this is a Windows 32/64/ARM64 agent, load the agent in RAM.
            if ((obj.args.agentsinram === true) || (((archid == 3) || (archid == 4) || (archid == 43)) && (obj.args.agentsinram !== false))) {
                // The agent hash skips the certificate table, so only signed agents can carry the random msh. Appended to an
                // unsigned agent it ends up in the hash the agent reports after updating, and every connection updates it again.
                if (((archid == 3) || (archid == 4) || (archid == 43)) && (agentTable[archid].pe != null) && (agentTable[archid].pe.CertificateTableAddress != 0)) {
                    // Load the agent with a random msh added to it.
                    const outStream = new require('stream').Duplex();
                    outStream.meshAgentBinary = agentTable[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');

						const zip = require('@zip.js/zip.js');

						// Compress the agent using ZIP
						const x = this.meshAgentBinary;

						const zipWriter = new zip.ZipWriter(new zip.Uint8ArrayWriter(), { level: 9, zip64: false });

						zipWriter.add(
							'meshagent',
							new zip.Uint8ArrayReader(x.data)
						).then(function () {
							return zipWriter.close();
						}).then(function (zipData) {
							// Convert zip.js Uint8Array output to Buffer
							const concatData = Buffer.from(zipData);

							// Hash the compressed binary
							const hash = obj.crypto.createHash('sha384').update(concatData);
							x.zhash = hash.digest('binary');
							x.zhashhex = Buffer.from(x.zhash, 'binary').toString('hex');

							// Set the agent
							x.zdata = concatData;
							x.zsize = concatData.length;
						}).catch(function (err) {
							console.error('ZIP compression failed', err);
						});
                    })
                    obj.exeHandler.streamExeWithMeshPolicy(
                        {
                            platform: 'win32',
                            sourceFileName: agentpath,
                            destinationStream: outStream,
                            randomPolicy: true, // Indicates that the msh policy is random data.
                            msh: outStream.meshAgentBinary.randomMsh,
                            peinfo: agentTable[archid].pe
                        });
                } else {
                    // Load the agent as-is
                    agentTable[archid].data = obj.fs.readFileSync(agentpath);

					// Compress the agent using ZIP
					const zip = require('@zip.js/zip.js');
					const x = agentTable[archid];
					const zipWriter = new zip.ZipWriter(new zip.Uint8ArrayWriter(), { level: 9, zip64: false });

					zipWriter.add(
						'meshagent',
						new zip.Uint8ArrayReader(x.data)
					).then(function () {
						return zipWriter.close();
					}).then(function (zipData) {
						// Convert zip.js Uint8Array output to Buffer
						const concatData = Buffer.from(zipData);

						// Hash the compressed binary
						const hash = obj.crypto.createHash('sha384').update(concatData);
						x.zhash = hash.digest('binary');
						x.zhashhex = Buffer.from(x.zhash, 'binary').toString('hex');

						// Set the agent
						x.zdata = concatData;
						x.zsize = concatData.length;

						//console.log('Packed', x.size, x.zsize);
					}).catch(function (err) {
						console.error('ZIP compression failed', err);
					});
                }
            }

            // Hash the binary
            const hashStream = obj.crypto.createHash('sha384');
            hashStream.archid = archid;
            hashStream.on('data', function (data) {
                agentTable[this.archid].hash = data.toString('binary');
                agentTable[this.archid].hashhex = data.toString('hex');
                if (--archcount == 0) done();
            });
            const options = { sourcePath: agentpath, targetStream: hashStream, platform: obj.meshAgentsArchitectureNumbers[archid].platform };
            if (agentTable[archid].pe != null) { options.peinfo = agentTable[archid].pe; }
            obj.exeHandler.hashExecutableFile(options);

            // If Windows binaries are served without a random msh, compute the RAW file hash here.
            if ((agentTable[archid].randomMsh == null) && ((archid == 3) || (archid == 4) || (archid == 43))) {
                const hash = obj.crypto.createHash('sha384').update(obj.fs.readFileSync(agentpath));
                agentTable[archid].fileHash = hash.digest('binary');
                agentTable[archid].fileHashHex = Buffer.from(agentTable[archid].fileHash, 'binary').toString('hex');
            }
        }
        if (archcount === 0) done();
    };

    // 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').replace(/\+/g, '@').replace(/\//g, '$');
        } 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.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : '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) || (obj.args.log[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'); }
                        const formattedArgs = args.map(function (arg) { return (typeof arg === 'object' && arg !== null) ? JSON.stringify(arg) : arg; });
                        obj.fs.writeSync(obj.xxLogFile, new Date().toLocaleTimeString() + ' - ' + source + ': ' + formattedArgs.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) { 
            const line = lines[i].trim();
            if (line.length === 0) continue;
            if (line.charAt(0) === '#') continue;
            const par