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