EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
meshcentral.js
Go to the documentation of this file.
1/**
2* @description MeshCentral main module
3* @author Ylian Saint-Hilaire
4* @copyright Intel Corporation 2018-2022
5* @license Apache-2.0
6* @version v0.0.1
7*/
8
9/*xjslint node: true */
10/*xjslint plusplus: true */
11/*xjslint maxlen: 256 */
12/*jshint node: true */
13/*jshint strict: false */
14/*jshint esversion: 6 */
15"use strict";
16
17const common = require('./common.js');
18
19// If app metrics is available
20if (process.argv[2] == '--launch') { try { require('appmetrics-dash').monitor({ url: '/', title: 'MeshCentral', port: 88, host: '127.0.0.1' }); } catch (ex) { } }
21
22function CreateMeshCentralServer(config, args) {
23 const obj = {};
24 obj.db = null;
25 obj.webserver = null; // HTTPS main web server, typically on port 443
26 obj.redirserver = null; // HTTP relay web server, typically on port 80
27 obj.mpsserver = null; // Intel AMT CIRA server, typically on port 4433
28 obj.mqttbroker = null; // MQTT server, not is not often used
29 obj.swarmserver = null; // Swarm server, this is used only to update older MeshCentral v1 agents
30 obj.smsserver = null; // SMS server, used to send user SMS messages
31 obj.msgserver = null; // Messaging server, used to sent used messages
32 obj.amtEventHandler = null;
33 obj.pluginHandler = null;
34 obj.amtScanner = null;
35 obj.amtManager = null; // Intel AMT manager, used to oversee all Intel AMT devices, activate them and sync policies
36 obj.meshScanner = null;
37 obj.taskManager = null;
38 obj.letsencrypt = null; // Let's encrypt server, used to get and renew TLS certificates
39 obj.eventsDispatch = {};
40 obj.fs = require('fs');
41 obj.path = require('path');
42 obj.crypto = require('crypto');
43 obj.exeHandler = require('./exeHandler.js');
44 obj.platform = require('os').platform();
45 obj.args = args;
46 obj.common = common;
47 obj.configurationFiles = null;
48 obj.certificates = null;
49 obj.connectivityByNode = {}; // This object keeps a list of all connected CIRA and agents, by nodeid->value (value: 1 = Agent, 2 = CIRA, 4 = AmtDirect)
50 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)
51 obj.debugSources = [];
52 obj.debugRemoteSources = null;
53 obj.config = config; // Configuration file
54 obj.dbconfig = {}; // Persistance values, loaded from database
55 obj.certificateOperations = null;
56 obj.defaultMeshCmd = null;
57 obj.defaultMeshCores = {};
58 obj.defaultMeshCoresDeflate = {};
59 obj.defaultMeshCoresHash = {};
60 obj.meshToolsBinaries = {}; // Mesh Tools Binaries, ToolName --> { hash:(sha384 hash), size:(binary size), path:(binary path) }
61 obj.meshAgentBinaries = {}; // Mesh Agent Binaries, Architecture type --> { hash:(sha384 hash), size:(binary size), path:(binary path) }
62 obj.meshAgentInstallScripts = {}; // Mesh Install Scripts, Script ID -- { hash:(sha384 hash), size:(binary size), path:(binary path) }
63 obj.multiServer = null;
64 obj.ipKvmManager = null;
65 obj.maintenanceTimer = null;
66 obj.serverId = null;
67 obj.serverKey = Buffer.from(obj.crypto.randomBytes(48), 'binary');
68 obj.loginCookieEncryptionKey = null;
69 obj.invitationLinkEncryptionKey = null;
70 obj.serverSelfWriteAllowed = true;
71 obj.serverStatsCounter = Math.floor(Math.random() * 1000);
72 obj.taskLimiter = obj.common.createTaskLimiterQueue(50, 20, 60); // (maxTasks, maxTaskTime, cleaningInterval) This is a task limiter queue to smooth out server work.
73 obj.agentUpdateBlockSize = 65531; // MeshAgent update block size
74 obj.serverWarnings = []; // List of warnings that should be shown to administrators
75 obj.cookieUseOnceTable = {}; // List of cookies that are already expired
76 obj.cookieUseOnceTableCleanCounter = 0; // Clean the cookieUseOnceTable each 20 additions
77 obj.firstStats = true; // True until this server saves it's not stats to the database
78
79 // Server version
80 obj.currentVer = null;
81 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
82 getCurrentVersion();
83
84 // Setup the default configuration and files paths
85 if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
86 obj.parentpath = obj.path.join(__dirname, '../..');
87 obj.datapath = obj.path.join(__dirname, '../../meshcentral-data');
88 obj.filespath = obj.path.join(__dirname, '../../meshcentral-files');
89 obj.backuppath = obj.path.join(__dirname, '../../meshcentral-backups');
90 obj.recordpath = obj.path.join(__dirname, '../../meshcentral-recordings');
91 obj.webViewsPath = obj.path.join(__dirname, 'views');
92 obj.webPublicPath = obj.path.join(__dirname, 'public');
93 obj.webEmailsPath = obj.path.join(__dirname, 'emails');
94 if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/views'))) { obj.webViewsOverridePath = obj.path.join(__dirname, '../../meshcentral-web/views'); }
95 if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/public'))) { obj.webPublicOverridePath = obj.path.join(__dirname, '../../meshcentral-web/public'); }
96 if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/emails'))) { obj.webEmailsOverridePath = obj.path.join(__dirname, '../../meshcentral-web/emails'); }
97 } else {
98 obj.parentpath = __dirname;
99 obj.datapath = obj.path.join(__dirname, '../meshcentral-data');
100 obj.filespath = obj.path.join(__dirname, '../meshcentral-files');
101 obj.backuppath = obj.path.join(__dirname, '../meshcentral-backups');
102 obj.recordpath = obj.path.join(__dirname, '../meshcentral-recordings');
103 obj.webViewsPath = obj.path.join(__dirname, 'views');
104 obj.webPublicPath = obj.path.join(__dirname, 'public');
105 obj.webEmailsPath = obj.path.join(__dirname, 'emails');
106 if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/views'))) { obj.webViewsOverridePath = obj.path.join(__dirname, '../meshcentral-web/views'); }
107 if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/public'))) { obj.webPublicOverridePath = obj.path.join(__dirname, '../meshcentral-web/public'); }
108 if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/emails'))) { obj.webEmailsOverridePath = obj.path.join(__dirname, '../meshcentral-web/emails'); }
109 }
110
111 // Clean up any temporary files
112 const removeTime = new Date(Date.now()).getTime() - (30 * 60 * 1000); // 30 minutes
113 const dir = obj.fs.readdir(obj.path.join(obj.filespath, 'tmp'), function (err, files) {
114 if (err != null) return;
115 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) { } }
116 });
117
118 // Look to see if data and/or file path is specified
119 if (obj.config.settings && (typeof obj.config.settings.datapath == 'string')) { obj.datapath = obj.config.settings.datapath; }
120 if (obj.config.settings && (typeof obj.config.settings.filespath == 'string')) { obj.filespath = obj.config.settings.filespath; }
121
122 // Create data and files folders if needed
123 try { obj.fs.mkdirSync(obj.datapath); } catch (ex) { }
124 try { obj.fs.mkdirSync(obj.filespath); } catch (ex) { }
125
126 // Windows Specific Code, setup service and event log
127 obj.service = null;
128 obj.servicelog = null;
129 if (obj.platform == 'win32') {
130 const nodewindows = require('node-windows');
131 obj.service = nodewindows.Service;
132 const eventlogger = nodewindows.EventLogger;
133 obj.servicelog = new eventlogger('MeshCentral');
134 }
135
136 // Start the Meshcentral server
137 obj.Start = function () {
138 var i;
139 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.
140
141 // Check for invalid arguments
142 const validArguments = [
143 '_', 'user', 'port', 'portbind', 'relayport', 'relayaliasport', 'agentport',
144 'agentportbind', 'agentaliasport', 'aliasport', 'mpsport', 'mpsaliasport',
145 'redirport', 'redirportbind', 'rediraliasport', 'cert', 'mpscert',
146 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'showitem',
147 'listuserids', 'showusergroups', 'shownodes', 'showallmeshes', 'showmeshes',
148 'showevents', 'showsmbios', 'showpower', 'clearpower', 'showiplocations',
149 'help', 'exactports', 'xinstall', 'xuninstall', 'install', 'uninstall',
150 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate',
151 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly',
152 'nousers', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport',
153 'dbmerge', 'dbfix', 'dbencryptkey', 'selfupdate', 'tlsoffload',
154 'usenodedefaulttlsciphers', 'tlsciphers', 'userallowedip', 'userblockedip',
155 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport',
156 'logintoken', 'logintokenkey', 'logintokengen', 'mailtokengen', 'admin',
157 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore',
158 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'oldencrypt',
159 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'vaultpushconfigfiles',
160 'vaultpullconfigfiles', 'vaultdeleteconfigfiles', 'configkey',
161 'loadconfigfromdb', 'npmpath', 'serverid', 'recordencryptionrecode', 'vault',
162 'token', 'unsealkey', 'name', 'log', 'dbstats', 'translate', 'createaccount',
163 'setuptelegram', 'resetaccount', 'pass', 'removesubdomain', 'adminaccount',
164 'domain', 'email', 'configfile', 'maintenancemode', 'nedbtodb',
165 'removetestagents', 'agentupdatetest', 'hashpassword', 'hashpass',
166 'indexmcrec', 'mpsdebug', 'dumpcores', 'dev', 'mysql', 'mariadb', 'trustedproxy'
167 ];
168 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; } }
169 const ENVVAR_PREFIX = "meshcentral_"
170 let envArgs = []
171 for (let [envvar, envval] of Object.entries(process.env)) {
172 if (envvar.toLocaleLowerCase().startsWith(ENVVAR_PREFIX)) {
173 let argname = envvar.slice(ENVVAR_PREFIX.length).toLocaleLowerCase()
174 if (!!argname && !(validArguments.indexOf(argname) == -1)) {
175 envArgs = envArgs.concat([`--${argname}`, envval])
176 }
177 }
178 }
179 envArgs = require('minimist')(envArgs)
180 obj.args = Object.assign(envArgs, obj.args)
181 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; }
182 if (obj.args.mysql == true) { console.log('Must specify: --mysql [connectionstring] \r\nExample mysql://user:password@127.0.0.1:3306/database'); return; }
183 if (obj.args.mariadb == true) { console.log('Must specify: --mariadb [connectionstring] \r\nExample mariadb://user:password@127.0.0.1:3306/database'); return; }
184 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.
185
186 if ((obj.args.help == true) || (obj.args['?'] == true)) {
187 console.log('MeshCentral v' + getCurrentVersion() + ', remote computer management web portal.');
188 console.log('This software is open source under Apache 2.0 license.');
189 console.log('Details at: https://www.meshcentral.com\r\n');
190 if ((obj.platform == 'win32') || (obj.platform == 'linux')) {
191 console.log('Run as a background service');
192 console.log(' --install/uninstall Install MeshCentral as a background service.');
193 console.log(' --start/stop/restart Control MeshCentral background service.');
194 console.log('');
195 console.log('Run standalone, console application');
196 }
197 console.log(' --user [username] Always login as [username] if account exists.');
198 console.log(' --port [number] Web server port number.');
199 console.log(' --redirport [number] Creates an additional HTTP server to redirect users to the HTTPS server.');
200 console.log(' --exactports Server must run with correct ports or exit.');
201 console.log(' --noagentupdate Server will not update mesh agent native binaries.');
202 console.log(' --nedbtodb Transfer all NeDB records into current database.');
203 console.log(' --listuserids Show a list of a user identifiers in the database.');
204 console.log(' --cert [name], (country), (org) Create a web server certificate with [name] server name.');
205 console.log(' country and organization can optionally be set.');
206 console.log('');
207 console.log('Server recovery commands, use only when MeshCentral is offline.');
208 console.log(' --createaccount [userid] Create a new user account.');
209 console.log(' --resetaccount [userid] Unlock an account, disable 2FA and set a new account password.');
210 console.log(' --adminaccount [userid] Promote account to site administrator.');
211 return;
212 }
213
214 // Fix a NeDB database
215 if (obj.args.dbfix) {
216 var lines = null, badJsonCount = 0, fieldNames = [], fixedDb = [];
217 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(); }
218 for (var i = 0; i < lines.length; i++) {
219 var x = null;
220 try { x = JSON.parse(lines[i]); } catch (ex) { badJsonCount++; }
221 if (x != null) { fixedDb.push(lines[i]); for (var j in x) { if (fieldNames.indexOf(j) == -1) { fieldNames.push(j); } } }
222 }
223 console.log('Lines: ' + lines.length + ', badJSON: ' + badJsonCount + ', Feilds: ' + fieldNames);
224 obj.fs.writeFileSync(obj.getConfigFilePath(obj.args.dbfix) + '-fixed', fixedDb.join('\n'), { encoding: 'utf8' });
225 return;
226 }
227
228 // Check for invalid cert name
229 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; }
230
231 // Perform a password hash
232 if (obj.args.hashpassword) { require('./pass').hash(obj.args.hashpassword, function (err, salt, hash, tag) { console.log(salt + ',' + hash); process.exit(); }); return; }
233
234 // Dump to mesh cores
235 if (obj.args.dumpcores) { obj.updateMeshCore(function () { console.log('Done.'); }, true); return; }
236
237 // Setup Telegram
238 if (obj.args.setuptelegram) { require('./meshmessaging.js').SetupTelegram(obj); return; }
239
240 // Perform web site translations into different languages
241 if (obj.args.translate) {
242 // Check NodeJS version
243 const NodeJSVer = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
244 if (NodeJSVer < 8) { console.log("Translation feature requires Node v8 or above, current version is " + process.version + "."); process.exit(); return; }
245
246 // Check if translate.json is in the "meshcentral-data" folder, if so use that and translate default pages.
247 var translationFile = null, customTranslation = false;
248 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; }
249 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."); } }
250 if (translationFile == null) { console.log("Unable to find translate.json."); process.exit(); return; }
251
252 // Perform translation operations
253 var didSomething = false;
254 process.chdir(obj.path.join(__dirname, 'translate'));
255 const translateEngine = require('./translate/translate.js')
256 if (customTranslation == true) {
257 // Translate all of the default files using custom translation file
258 translateEngine.startEx(['', '', 'minifyall']);
259 translateEngine.startEx(['', '', 'translateall', translationFile]);
260 translateEngine.startEx(['', '', 'extractall', translationFile]);
261 didSomething = true;
262 } else {
263 // Translate all of the default files
264 translateEngine.startEx(['', '', 'minifyall']);
265 translateEngine.startEx(['', '', 'translateall']);
266 translateEngine.startEx(['', '', 'extractall']);
267 didSomething = true;
268 }
269
270 // Check if "meshcentral-web" exists, if so, translate all pages in that folder.
271 if (obj.webViewsOverridePath != null) {
272 didSomething = true;
273 var files = obj.fs.readdirSync(obj.webViewsOverridePath);
274 for (var i in files) {
275 var file = obj.path.join(obj.webViewsOverridePath, files[i]);
276 if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
277 translateEngine.startEx(['', '', 'minify', file]);
278 }
279 }
280 files = obj.fs.readdirSync(obj.webViewsOverridePath);
281 for (var i in files) {
282 var file = obj.path.join(obj.webViewsOverridePath, files[i]);
283 if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
284 translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']);
285 }
286 }
287 }
288
289 // Check domains and see if "meshcentral-web-DOMAIN" exists, if so, translate all pages in that folder
290 for (i in obj.config.domains) {
291 if (i == "") continue;
292 var path = obj.path.join(obj.datapath, '..', 'meshcentral-web-' + i, 'views');
293 if (require('fs').existsSync(path)) {
294 didSomething = true;
295 var files = obj.fs.readdirSync(path);
296 for (var a in files) {
297 var file = obj.path.join(path, files[a]);
298 if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
299 translateEngine.startEx(['', '', 'minify', file]);
300 }
301 }
302 files = obj.fs.readdirSync(path);
303 for (var a in files) {
304 var file = obj.path.join(path, files[a]);
305 if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
306 translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']);
307 }
308 }
309 }
310 }
311 /*
312 if (obj.webPublicOverridePath != null) {
313 didSomething = true;
314 var files = obj.fs.readdirSync(obj.webPublicOverridePath);
315 for (var i in files) {
316 var file = obj.path.join(obj.webPublicOverridePath, files[i]);
317 if (file.endsWith('.htm') && !file.endsWith('-min.htm')) {
318 translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']);
319 }
320 }
321 }
322 */
323
324 if (didSomething == false) { console.log("Nothing to do."); }
325 console.log('Finished Translating.')
326 process.exit();
327 return;
328 }
329
330 // 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.
331 if (obj.args.npmpath == null) {
332 try {
333 var nodepath = process.argv[0];
334 var npmpath = obj.path.join(obj.path.dirname(process.argv[0]), 'npm');
335 if (obj.fs.existsSync(nodepath) && obj.fs.existsSync(npmpath)) {
336 if (nodepath.indexOf(' ') >= 0) { nodepath = '"' + nodepath + '"'; }
337 if (npmpath.indexOf(' ') >= 0) { npmpath = '"' + npmpath + '"'; }
338 if (obj.platform == 'win32') { obj.args.npmpath = npmpath; } else { obj.args.npmpath = (nodepath + ' ' + npmpath); }
339 }
340 } catch (ex) { }
341 }
342
343 // Linux background service systemd handling
344 if (obj.platform == 'linux') {
345 if (obj.args.install == true) {
346 // Install MeshCentral in Systemd
347 console.log('Installing MeshCentral as background Service...');
348 var systemdConf = null;
349 const userinfo = require('os').userInfo();
350 if (require('fs').existsSync('/etc/systemd/system')) { systemdConf = '/etc/systemd/system/meshcentral.service'; }
351 else if (require('fs').existsSync('/lib/systemd/system')) { systemdConf = '/lib/systemd/system/meshcentral.service'; }
352 else if (require('fs').existsSync('/usr/lib/systemd/system')) { systemdConf = '/usr/lib/systemd/system/meshcentral.service'; }
353 else { console.log('Unable to find systemd configuration folder.'); process.exit(); return; }
354 console.log('Writing config file...');
355 require('child_process').exec('which node', {}, function (error, stdout, stderr) {
356 if ((error != null) || (stdout.indexOf('\n') == -1)) { console.log('ERROR: Unable to get node location: ' + error); process.exit(); return; }
357 const nodePath = stdout.substring(0, stdout.indexOf('\n'));
358 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';
359 require('child_process').exec('echo \"' + config + '\" | sudo tee ' + systemdConf, {}, function (error, stdout, stderr) {
360 if ((error != null) && (error != '')) { console.log('ERROR: Unable to write config file: ' + error); process.exit(); return; }
361 console.log('Enabling service...');
362 require('child_process').exec('sudo systemctl enable meshcentral.service', {}, function (error, stdout, stderr) {
363 if ((error != null) && (error != '')) { console.log('ERROR: Unable to enable MeshCentral as a service: ' + error); process.exit(); return; }
364 if (stdout.length > 0) { console.log(stdout); }
365 console.log('Starting service...');
366 require('child_process').exec('sudo systemctl start meshcentral.service', {}, function (error, stdout, stderr) {
367 if ((error != null) && (error != '')) { console.log('ERROR: Unable to start MeshCentral as a service: ' + error); process.exit(); return; }
368 if (stdout.length > 0) { console.log(stdout); }
369 console.log('Done.');
370 });
371 });
372 });
373 });
374 return;
375 } else if (obj.args.uninstall == true) {
376 // Uninstall MeshCentral in Systemd
377 console.log('Uninstalling MeshCentral background service...');
378 var systemdConf = null;
379 if (require('fs').existsSync('/etc/systemd/system')) { systemdConf = '/etc/systemd/system/meshcentral.service'; }
380 else if (require('fs').existsSync('/lib/systemd/system')) { systemdConf = '/lib/systemd/system/meshcentral.service'; }
381 else if (require('fs').existsSync('/usr/lib/systemd/system')) { systemdConf = '/usr/lib/systemd/system/meshcentral.service'; }
382 else { console.log('Unable to find systemd configuration folder.'); process.exit(); return; }
383 console.log('Stopping service...');
384 require('child_process').exec('sudo systemctl stop meshcentral.service', {}, function (err, stdout, stderr) {
385 if ((err != null) && (err != '')) { console.log('ERROR: Unable to stop MeshCentral as a service: ' + err); }
386 if (stdout.length > 0) { console.log(stdout); }
387 console.log('Disabling service...');
388 require('child_process').exec('sudo systemctl disable meshcentral.service', {}, function (err, stdout, stderr) {
389 if ((err != null) && (err != '')) { console.log('ERROR: Unable to disable MeshCentral as a service: ' + err); }
390 if (stdout.length > 0) { console.log(stdout); }
391 console.log('Removing config file...');
392 require('child_process').exec('sudo rm ' + systemdConf, {}, function (err, stdout, stderr) {
393 if ((err != null) && (err != '')) { console.log('ERROR: Unable to delete MeshCentral config file: ' + err); }
394 console.log('Done.');
395 });
396 });
397 });
398 return;
399 } else if (obj.args.start == true) {
400 // Start MeshCentral in Systemd
401 require('child_process').exec('sudo systemctl start meshcentral.service', {}, function (err, stdout, stderr) {
402 if ((err != null) && (err != '')) { console.log('ERROR: Unable to start MeshCentral: ' + err); process.exit(); return; }
403 console.log('Done.');
404 });
405 return;
406 } else if (obj.args.stop == true) {
407 // Stop MeshCentral in Systemd
408 require('child_process').exec('sudo systemctl stop meshcentral.service', {}, function (err, stdout, stderr) {
409 if ((err != null) && (err != '')) { console.log('ERROR: Unable to stop MeshCentral: ' + err); process.exit(); return; }
410 console.log('Done.');
411 });
412 return;
413 } else if (obj.args.restart == true) {
414 // Restart MeshCentral in Systemd
415 require('child_process').exec('sudo systemctl restart meshcentral.service', {}, function (err, stdout, stderr) {
416 if ((err != null) && (err != '')) { console.log('ERROR: Unable to restart MeshCentral: ' + err); process.exit(); return; }
417 console.log('Done.');
418 });
419 return;
420 }
421 }
422
423 // FreeBSD background service handling, MUST USE SPAWN FOR SERVICE COMMANDS!
424 if (obj.platform == 'freebsd') {
425 if (obj.args.install == true) {
426 // Install MeshCentral in rc.d
427 console.log('Installing MeshCentral as background Service...');
428 var systemdConf = "/usr/local/etc/rc.d/meshcentral";
429 const userinfo = require('os').userInfo();
430 console.log('Writing config file...');
431 require('child_process').exec('which node', {}, function (error, stdout, stderr) {
432 if ((error != null) || (stdout.indexOf('\n') == -1)) { console.log('ERROR: Unable to get node location: ' + error); process.exit(); return; }
433 const nodePath = stdout.substring(0, stdout.indexOf('\n'));
434 const config = '#!/bin/sh\n# MeshCentral FreeBSD Service Script\n# PROVIDE: meshcentral\n# REQUIRE: NETWORKING\n# KEYWORD: shutdown\n. /etc/rc.subr\nname=meshcentral\nuser=' + userinfo.username + '\nrcvar=meshcentral_enable\n: \\${meshcentral_enable:=\\"NO\\"}\n: \\${meshcentral_args:=\\"\\"}\npidfile=/var/run/meshcentral/meshcentral.pid\ncommand=\\"/usr/sbin/daemon\\"\nmeshcentral_chdir=\\"' + obj.parentpath + '\\"\ncommand_args=\\"-r -u \\${user} -P \\${pidfile} -S -T meshcentral -m 3 ' + nodePath + ' ' + __dirname + ' \\${meshcentral_args}\\"\nload_rc_config \\$name\nrun_rc_command \\"\\$1\\"\n';
435 require('child_process').exec('echo \"' + config + '\" | tee ' + systemdConf + ' && chmod +x ' + systemdConf, {}, function (error, stdout, stderr) {
436 if ((error != null) && (error != '')) { console.log('ERROR: Unable to write config file: ' + error); process.exit(); return; }
437 console.log('Enabling service...');
438 require('child_process').exec('sysrc meshcentral_enable="YES"', {}, function (error, stdout, stderr) {
439 if ((error != null) && (error != '')) { console.log('ERROR: Unable to enable MeshCentral as a service: ' + error); process.exit(); return; }
440 if (stdout.length > 0) { console.log(stdout); }
441 console.log('Starting service...');
442 const service = require('child_process').spawn('service', ['meshcentral', 'start']);
443 service.stdout.on('data', function (data) { console.log(data.toString()); });
444 service.stderr.on('data', function (data) { console.log(data.toString()); });
445 service.on('exit', function (code) {
446 console.log((code === 0) ? 'Done.' : 'ERROR: Unable to start MeshCentral as a service');
447 process.exit(); // Must exit otherwise we just hang
448 });
449 });
450 });
451 });
452 return;
453 } else if (obj.args.uninstall == true) {
454 // Uninstall MeshCentral in rc.d
455 console.log('Uninstalling MeshCentral background service...');
456 var systemdConf = "/usr/local/etc/rc.d/meshcentral";
457 console.log('Stopping service...');
458 const service = require('child_process').spawn('service', ['meshcentral', 'stop']);
459 service.stdout.on('data', function (data) { console.log(data.toString()); });
460 service.stderr.on('data', function (data) { console.log(data.toString()); });
461 service.on('exit', function (code) {
462 if (code !== 0) { console.log('ERROR: Unable to stop MeshCentral as a service'); }
463 console.log('Disabling service...');
464 require('child_process').exec('sysrc -x meshcentral_enable', {}, function (err, stdout, stderr) {
465 if ((err != null) && (err != '')) { console.log('ERROR: Unable to disable MeshCentral as a service: ' + err); }
466 if (stdout.length > 0) { console.log(stdout); }
467 console.log('Removing config file...');
468 require('child_process').exec('rm ' + systemdConf, {}, function (err, stdout, stderr) {
469 if ((err != null) && (err != '')) { console.log('ERROR: Unable to delete MeshCentral config file: ' + err); }
470 console.log('Done.');
471 process.exit(); // Must exit otherwise we just hang
472 });
473 });
474 });
475 return;
476 } else if (obj.args.start == true) {
477 // Start MeshCentral in rc.d
478 const service = require('child_process').spawn('service', ['meshcentral', 'start']);
479 service.stdout.on('data', function (data) { console.log(data.toString()); });
480 service.stderr.on('data', function (data) { console.log(data.toString()); });
481 service.on('exit', function (code) {
482 console.log((code === 0) ? 'Done.' : 'ERROR: Unable to start MeshCentral as a service: ' + error);
483 process.exit(); // Must exit otherwise we just hang
484 });
485 return;
486 } else if (obj.args.stop == true) {
487 // Stop MeshCentral in rc.d
488 const service = require('child_process').spawn('service', ['meshcentral', 'stop']);
489 service.stdout.on('data', function (data) { console.log(data.toString()); });
490 service.stderr.on('data', function (data) { console.log(data.toString()); });
491 service.on('exit', function (code) {
492 console.log((code === 0) ? 'Done.' : 'ERROR: Unable to stop MeshCentral as a service: ' + error);
493 process.exit(); // Must exit otherwise we just hang
494 });
495 return;
496 } else if (obj.args.restart == true) {
497 // Restart MeshCentral in rc.d
498 const service = require('child_process').spawn('service', ['meshcentral', 'restart']);
499 service.stdout.on('data', function (data) { console.log(data.toString()); });
500 service.stderr.on('data', function (data) { console.log(data.toString()); });
501 service.on('exit', function (code) {
502 console.log((code === 0) ? 'Done.' : 'ERROR: Unable to restart MeshCentral as a service: ' + error);
503 process.exit(); // Must exit otherwise we just hang
504 });
505 return;
506 }
507 }
508
509 // Index a recorded file
510 if (obj.args.indexmcrec != null) {
511 if (typeof obj.args.indexmcrec != 'string') {
512 console.log('Usage: --indexmrec [filename.mcrec]');
513 } else if (obj.fs.existsSync(obj.args.indexmcrec)) {
514 console.log('Indexing file: ' + obj.args.indexmcrec);
515 require(require('path').join(__dirname, 'mcrec.js')).indexFile(obj.args.indexmcrec);
516 } else {
517 console.log('Unable to find file: ' + obj.args.indexmcrec);
518 }
519 return;
520 }
521
522 // Windows background service handling
523 if ((obj.platform == 'win32') && (obj.service != null)) {
524 // Build MeshCentral parent path and Windows Service path
525 var mcpath = __dirname;
526 if (mcpath.endsWith('\\node_modules\\meshcentral') || mcpath.endsWith('/node_modules/meshcentral')) { mcpath = require('path').join(mcpath, '..', '..'); }
527 const servicepath = obj.path.join(mcpath, 'WinService');
528
529 // Check if we need to install, start, stop, remove ourself as a background service
530 if (((obj.args.xinstall == true) || (obj.args.xuninstall == true) || (obj.args.start == true) || (obj.args.stop == true) || (obj.args.restart == true))) {
531 var env = [], xenv = ['user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'exactport', 'rediraliasport', 'debug'];
532 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.
533
534 var serviceFilePath = null;
535 if (obj.fs.existsSync(obj.path.join(servicepath, 'winservice.js'))) { serviceFilePath = obj.path.join(servicepath, 'winservice.js'); }
536 else if (obj.fs.existsSync(obj.path.join(__dirname, '../WinService/winservice.js'))) { serviceFilePath = obj.path.join(__dirname, '../WinService/winservice.js'); }
537 else if (obj.fs.existsSync(obj.path.join(__dirname, 'winservice.js'))) { serviceFilePath = obj.path.join(__dirname, 'winservice.js'); }
538 if (serviceFilePath == null) { console.log('Unable to find winservice.js'); return; }
539
540 const svc = new obj.service({ name: 'MeshCentral', description: 'MeshCentral Remote Management Server', script: servicepath, env: env, wait: 2, grow: 0.5 });
541 svc.on('install', function () { console.log('MeshCentral service installed.'); svc.start(); });
542 svc.on('uninstall', function () { console.log('MeshCentral service uninstalled.'); process.exit(); });
543 svc.on('start', function () { console.log('MeshCentral service started.'); process.exit(); });
544 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); } });
545 svc.on('alreadyinstalled', function () { console.log('MeshCentral service already installed.'); process.exit(); });
546 svc.on('invalidinstallation', function () { console.log('Invalid MeshCentral service installation.'); process.exit(); });
547
548 if (obj.args.xinstall == true) { try { svc.install(); } catch (ex) { logException(ex); } }
549 if (obj.args.stop == true || obj.args.restart == true) { try { svc.stop(); } catch (ex) { logException(ex); } }
550 if (obj.args.start == true) { try { svc.start(); } catch (ex) { logException(ex); } }
551 if (obj.args.xuninstall == true) { try { svc.uninstall(); } catch (ex) { logException(ex); } }
552 return;
553 }
554
555 // Windows service install using the external winservice.js
556 if (obj.args.install == true) {
557 console.log('Installing MeshCentral as Windows Service...');
558 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; } }
559 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; }
560 require('child_process').exec('node winservice.js --install', { maxBuffer: 512000, timeout: 120000, cwd: servicepath }, function (error, stdout, stderr) {
561 if ((error != null) && (error != '')) { console.log('ERROR: Unable to install MeshCentral as a service: ' + error); process.exit(); return; }
562 console.log(stdout);
563 });
564 return;
565 } else if (obj.args.uninstall == true) {
566 console.log('Uninstalling MeshCentral Windows Service...');
567 if (obj.fs.existsSync(servicepath) == true) {
568 require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: servicepath }, function (error, stdout, stderr) {
569 if ((error != null) && (error != '')) { console.log('ERROR: Unable to uninstall MeshCentral service: ' + error); process.exit(); return; }
570 console.log(stdout);
571 try { obj.fs.unlinkSync(obj.path.join(servicepath, 'winservice.js')); } catch (ex) { }
572 try { obj.fs.rmdirSync(servicepath); } catch (ex) { }
573 });
574 } else if (obj.fs.existsSync(obj.path.join(__dirname, '../WinService')) == true) {
575 require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: obj.path.join(__dirname, '../WinService') }, function (error, stdout, stderr) {
576 if ((error != null) && (error != '')) { console.log('ERROR: Unable to uninstall MeshCentral service: ' + error); process.exit(); return; }
577 console.log(stdout);
578 try { obj.fs.unlinkSync(obj.path.join(__dirname, '../WinService/winservice.js')); } catch (ex) { }
579 try { obj.fs.rmdirSync(obj.path.join(__dirname, '../WinService')); } catch (ex) { }
580 });
581 } else {
582 require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: __dirname }, function (error, stdout, stderr) {
583 if ((error != null) && (error != '')) { console.log('ERROR: Unable to uninstall MeshCentral service: ' + error); process.exit(); return; }
584 console.log(stdout);
585 });
586 }
587 return;
588 }
589 }
590
591 // If "--launch" is in the arguments, launch now
592 if (obj.args.launch) {
593 if (obj.args.vault) { obj.StartVault(); } else { obj.StartEx(); }
594 } else {
595 // if "--launch" is not specified, launch the server as a child process.
596 const startArgs = [];
597 for (const flag of process.execArgv) { startArgs.push(flag); }
598 startArgs.push(process.argv[1]);
599 for (let i = 2; i < process.argv.length; i++) { startArgs.push(process.argv[i]); }
600 startArgs.push('--launch', process.pid.toString());
601 obj.launchChildServer(startArgs);
602 }
603 };
604
605 // Launch MeshCentral as a child server and monitor it.
606 obj.launchChildServer = function (startArgs) {
607 const child_process = require('child_process');
608 const isInspectorAttached = (()=> { try { return require('node:inspector').url() !== undefined; } catch (_) { return false; } }).call();
609 const logFromChildProcess = isInspectorAttached ? () => {} : console.log.bind(console);
610 if (!startArgs.includes('--disable-proto=delete')) { startArgs.unshift('--disable-proto=delete'); }
611 childProcess = child_process.execFile(process.argv[0], startArgs, { maxBuffer: Infinity, cwd: obj.parentpath }, function (error, stdout, stderr) {
612 if (childProcess.xrestart == 1) {
613 setTimeout(function () { obj.launchChildServer(startArgs); }, 500); // This is an expected restart.
614 } else if (childProcess.xrestart == 2) {
615 console.log('Expected exit...');
616 process.exit(); // User CTRL-C exit.
617 } else if (childProcess.xrestart == 3) {
618 // Server self-update exit
619 var version = '';
620 if (typeof obj.args.selfupdate == 'string') { version = '@' + obj.args.selfupdate; }
621 else if (typeof obj.args.specificupdate == 'string') { version = '@' + obj.args.specificupdate; delete obj.args.specificupdate; }
622 const child_process = require('child_process');
623 const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
624 const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
625 const env = Object.assign({}, process.env); // Shallow clone
626 if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
627 // always use --save-exact - https://stackoverflow.com/a/64507176/1210734
628 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) {
629 if ((error != null) && (error != '')) { console.log('Update failed: ' + error); }
630 });
631 xxprocess.data = '';
632 xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
633 xxprocess.stderr.on('data', function (data) { xxprocess.data += data; });
634 xxprocess.on('close', function (code) {
635 if (code == 0) { console.log('Update completed...'); }
636
637 // Run the server updated script if present
638 if (typeof obj.config.settings.runonserverupdated == 'string') {
639 const child_process = require('child_process');
640 var parentpath = __dirname;
641 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, '../..'); }
642 child_process.exec(obj.config.settings.runonserverupdated + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
643 }
644
645 if (obj.args.cleannpmcacheonupdate === true) {
646 // Perform NPM cache clean
647 console.log('Cleaning NPM cache...');
648 const xxxprocess = child_process.exec(npmpath + ' cache clean --force', { maxBuffer: Infinity, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
649 xxxprocess.on('close', function (code) { setTimeout(function () { obj.launchChildServer(startArgs); }, 1000); });
650 } else {
651 // Run the updated server
652 setTimeout(function () { obj.launchChildServer(startArgs); }, 1000);
653 }
654 });
655 } else {
656 if (error != null) {
657 // This is an un-expected restart
658 console.log(error);
659 console.log('ERROR: MeshCentral failed with critical error, check mesherrors.txt. Restarting in 5 seconds...');
660 setTimeout(function () { obj.launchChildServer(startArgs); }, 5000);
661
662 // Run the server error script if present
663 if (typeof obj.config.settings.runonservererror == 'string') {
664 const child_process = require('child_process');
665 var parentpath = __dirname;
666 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, '../..'); }
667 child_process.exec(obj.config.settings.runonservererror + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
668 }
669 }
670 }
671 });
672 childProcess.stdout.on('data', function (data) {
673 if (data[data.length - 1] == '\n') { data = data.substring(0, data.length - 1); }
674 if (data.indexOf('Updating settings folder...') >= 0) { childProcess.xrestart = 1; }
675 else if (data.indexOf('Updating server certificates...') >= 0) { childProcess.xrestart = 1; }
676 else if (data.indexOf('Server Ctrl-C exit...') >= 0) { childProcess.xrestart = 2; }
677 else if (data.indexOf('Starting self upgrade...') >= 0) { childProcess.xrestart = 3; }
678 else if (data.indexOf('Server restart...') >= 0) { childProcess.xrestart = 1; }
679 else if (data.indexOf('Starting self upgrade to: ') >= 0) { obj.args.specificupdate = data.substring(26).split('\r')[0].split('\n')[0]; childProcess.xrestart = 3; }
680 var datastr = data;
681 while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
682 logFromChildProcess(datastr);
683 });
684 childProcess.stderr.on('data', function (data) {
685 var datastr = data;
686 while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
687 logFromChildProcess('ERR: ' + datastr);
688 if (data.startsWith('le.challenges[tls-sni-01].loopback')) { return; } // Ignore this error output from GreenLock
689 if (data[data.length - 1] == '\n') { data = data.substring(0, data.length - 1); }
690 obj.logError(data);
691 });
692 childProcess.on('close', function (code) { if ((code != 0) && (code != 123)) { /* console.log("Exited with code " + code); */ } });
693 };
694
695 obj.logError = function (err) {
696 try {
697 var errlogpath = null;
698 if (typeof obj.args.mesherrorlogpath == 'string') { errlogpath = obj.path.join(obj.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = obj.getConfigFilePath('mesherrors.txt'); }
699 obj.fs.appendFileSync(errlogpath, '-------- ' + new Date().toLocaleString() + ' ---- ' + getCurrentVersion() + ' --------\r\n\r\n' + err + '\r\n\r\n\r\n');
700 } catch (ex) { console.log('ERROR: Unable to write to mesherrors.txt.'); }
701 };
702
703 // Get current and latest MeshCentral server versions using NPM
704 obj.getLatestServerVersion = function (callback) {
705 if (callback == null) return;
706 try {
707 if (typeof obj.args.selfupdate == 'string') { callback(getCurrentVersion(), obj.args.selfupdate); return; } // If we are targetting a specific version, return that one as current.
708 const child_process = require('child_process');
709 const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
710 const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
711 const env = Object.assign({}, process.env); // Shallow clone
712 if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
713 const xxprocess = child_process.exec(npmpath + npmproxy + ' view meshcentral dist-tags.latest', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
714 xxprocess.data = '';
715 xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
716 xxprocess.stderr.on('data', function (data) { });
717 xxprocess.on('close', function (code) {
718 var latestVer = null;
719 if (code == 0) { try { latestVer = xxprocess.data.split(' ').join('').split('\r').join('').split('\n').join(''); } catch (ex) { } }
720 callback(getCurrentVersion(), latestVer);
721 });
722 } catch (ex) { callback(getCurrentVersion(), null, ex); } // If the system is running out of memory, an exception here can easily happen.
723 };
724
725 // Get current version and all MeshCentral server tags using NPM
726 obj.getServerTags = function (callback) {
727 if (callback == null) return;
728 try {
729 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.
730 const child_process = require('child_process');
731 const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
732 const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
733 const env = Object.assign({}, process.env); // Shallow clone
734 if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
735 const xxprocess = child_process.exec(npmpath + npmproxy + ' dist-tag ls meshcentral', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
736 xxprocess.data = '';
737 xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
738 xxprocess.stderr.on('data', function (data) { });
739 xxprocess.on('close', function (code) {
740 var tags = { current: getCurrentVersion() };
741 if (code == 0) {
742 try {
743 var lines = xxprocess.data.split('\r\n').join('\n').split('\n');
744 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]; } }
745 } catch (ex) { }
746 }
747 callback(tags);
748 });
749 } catch (ex) { callback({ current: getCurrentVersion() }, ex); } // If the system is running out of memory, an exception here can easily happen.
750 };
751
752 // Use NPM to get list of versions
753 obj.getServerVersions = function (callback) {
754 try {
755 const child_process = require('child_process');
756 const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
757 const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
758 const env = Object.assign({}, process.env); // Shallow clone
759 if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
760 const xxprocess = child_process.exec(npmpath + npmproxy + ' view meshcentral versions --json', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
761 xxprocess.data = '';
762 xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
763 xxprocess.stderr.on('data', function (data) { });
764 xxprocess.on('close', function (code) {
765 (code == 0) ? callback(xxprocess.data) : callback('{}');
766 });
767 } catch (ex) { callback('{}'); }
768 };
769
770 // Initiate server self-update
771 obj.performServerUpdate = function (version) {
772 if (obj.serverSelfWriteAllowed != true) return false;
773 if ((version == null) || (version == '') || (typeof version != 'string')) { console.log('Starting self upgrade...'); } else { console.log('Starting self upgrade to: ' + version); }
774 process.exit(200);
775 return true;
776 };
777
778 // Initiate server self-update
779 obj.performServerCertUpdate = function () { console.log('Updating server certificates...'); process.exit(200); };
780
781 // Start by loading configuration from Vault
782 obj.StartVault = function () {
783 // Check that the configuration can only be loaded from one place
784 if ((obj.args.vault != null) && (obj.args.loadconfigfromdb != null)) { console.log("Can't load configuration from both database and Vault."); process.exit(); return; }
785
786 // Fix arguments if needed
787 if (typeof obj.args.vault == 'string') {
788 obj.args.vault = { endpoint: obj.args.vault };
789 if (typeof obj.args.token == 'string') { obj.args.vault.token = obj.args.token; }
790 if (typeof obj.args.unsealkey == 'string') { obj.args.vault.unsealkey = obj.args.unsealkey; }
791 if (typeof obj.args.name == 'string') { obj.args.vault.name = obj.args.name; }
792 }
793
794 // Load configuration for HashiCorp's Vault if needed
795 if (obj.args.vault) {
796 if (obj.args.vault.endpoint == null) { console.log('Missing Vault endpoint.'); process.exit(); return; }
797 if (obj.args.vault.token == null) { console.log('Missing Vault token.'); process.exit(); return; }
798 if (obj.args.vault.unsealkey == null) { console.log('Missing Vault unsealkey.'); process.exit(); return; }
799 if (obj.args.vault.name == null) { obj.args.vault.name = 'meshcentral'; }
800
801 // Get new instance of the client
802 const vault = require("node-vault")({ endpoint: obj.args.vault.endpoint, token: obj.args.vault.token });
803 vault.unseal({ key: obj.args.vault.unsealkey })
804 .then(function () {
805 if (obj.args.vaultdeleteconfigfiles) {
806 vault.delete('secret/data/' + obj.args.vault.name)
807 .then(function (r) { console.log('Done.'); process.exit(); })
808 .catch(function (x) { console.log(x); process.exit(); });
809 } else if (obj.args.vaultpushconfigfiles) {
810 // Push configuration files into Vault
811 if ((obj.args.vaultpushconfigfiles == '*') || (obj.args.vaultpushconfigfiles === true)) { obj.args.vaultpushconfigfiles = obj.datapath; }
812 obj.fs.readdir(obj.args.vaultpushconfigfiles, function (err, files) {
813 if (err != null) { console.log('ERROR: Unable to read from folder ' + obj.args.vaultpushconfigfiles); process.exit(); return; }
814 var configFound = false;
815 for (var i in files) { if (files[i] == 'config.json') { configFound = true; } }
816 if (configFound == false) { console.log('ERROR: No config.json in folder ' + obj.args.vaultpushconfigfiles); process.exit(); return; }
817 var configFiles = {};
818 for (var i in files) {
819 const file = files[i];
820 if ((file == 'config.json') || file.endsWith('.key') || file.endsWith('.crt') || (file == 'terms.txt') || file.endsWith('.jpg') || file.endsWith('.png')) {
821 const path = obj.path.join(obj.args.vaultpushconfigfiles, files[i]), binary = Buffer.from(obj.fs.readFileSync(path, { encoding: 'binary' }), 'binary');
822 console.log('Pushing ' + file + ', ' + binary.length + ' bytes.');
823 if (file.endsWith('.json') || file.endsWith('.key') || file.endsWith('.crt')) { configFiles[file] = binary.toString(); } else { configFiles[file] = binary.toString('base64'); }
824 }
825 }
826 vault.write('secret/data/' + obj.args.vault.name, { "data": configFiles })
827 .then(function (r) { console.log('Done.'); process.exit(); })
828 .catch(function (x) { console.log(x); process.exit(); });
829 });
830 } else {
831 // Read configuration files from Vault
832 vault.read('secret/data/' + obj.args.vault.name)
833 .then(function (r) {
834 if ((r == null) || (r.data == null) || (r.data.data == null)) { console.log('Unable to read configuration from Vault.'); process.exit(); return; }
835 var configFiles = obj.configurationFiles = r.data.data;
836
837 // Decode Base64 when needed
838 for (var file in configFiles) { if (!file.endsWith('.json') && !file.endsWith('.key') && !file.endsWith('.crt')) { configFiles[file] = Buffer.from(configFiles[file], 'base64'); } }
839
840 // Save all of the files
841 if (obj.args.vaultpullconfigfiles) {
842 for (var i in configFiles) {
843 var fullFileName = obj.path.join(obj.args.vaultpullconfigfiles, i);
844 try { obj.fs.writeFileSync(fullFileName, configFiles[i]); } catch (ex) { console.log('Unable to write to ' + fullFileName); process.exit(); return; }
845 console.log('Pulling ' + i + ', ' + configFiles[i].length + ' bytes.');
846 }
847 console.log('Done.');
848 process.exit();
849 }
850
851 // Parse the new configuration file
852 var config2 = null;
853 try { config2 = JSON.parse(configFiles['config.json']); } catch (ex) { console.log('Error, unable to parse config.json from Vault.'); process.exit(); return; }
854
855 // Set the command line arguments to the config file if they are not present
856 if (!config2.settings) { config2.settings = {}; }
857 for (var i in args) { config2.settings[i] = args[i]; }
858 obj.args = args = config2.settings;
859
860 // Lower case all keys in the config file
861 obj.common.objKeysToLower(config2, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
862
863 // Grad some of the values from the original config.json file if present.
864 if ((config.settings.vault != null) && (config2.settings != null)) { config2.settings.vault = config.settings.vault; }
865
866 // We got a new config.json from the database, let's use it.
867 config = obj.config = config2;
868 obj.StartEx();
869 })
870 .catch(function (x) { console.log(x); process.exit(); });
871 }
872 }).catch(function (x) { console.log(x); process.exit(); });
873 return;
874 }
875 }
876
877 // Look for easy command line instructions and do them here.
878 obj.StartEx = async function () {
879 var i;
880 //var wincmd = require('node-windows');
881 //wincmd.list(function (svc) { console.log(svc); }, true);
882
883 // Setup syslog support. Not supported on Windows.
884 if ((require('os').platform() != 'win32') && ((config.settings.syslog != null) || (config.settings.syslogjson != null) || (config.settings.syslogauth != null))) {
885 if (config.settings.syslog === true) { config.settings.syslog = 'meshcentral'; }
886 if (config.settings.syslogjson === true) { config.settings.syslogjson = 'meshcentral-json'; }
887 if (config.settings.syslogauth === true) { config.settings.syslogauth = 'meshcentral-auth'; }
888 if (typeof config.settings.syslog == 'string') {
889 obj.syslog = require('modern-syslog');
890 console.log('Starting ' + config.settings.syslog + ' syslog.');
891 obj.syslog.init(config.settings.syslog, obj.syslog.LOG_PID | obj.syslog.LOG_ODELAY, obj.syslog.LOG_LOCAL0);
892 obj.syslog.log(obj.syslog.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
893 }
894 if (typeof config.settings.syslogjson == 'string') {
895 obj.syslogjson = require('modern-syslog');
896 console.log('Starting ' + config.settings.syslogjson + ' JSON syslog.');
897 obj.syslogjson.init(config.settings.syslogjson, obj.syslogjson.LOG_PID | obj.syslogjson.LOG_ODELAY, obj.syslogjson.LOG_LOCAL0);
898 obj.syslogjson.log(obj.syslogjson.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
899 }
900 if (typeof config.settings.syslogauth == 'string') {
901 obj.syslogauth = require('modern-syslog');
902 console.log('Starting ' + config.settings.syslogauth + ' auth syslog.');
903 obj.syslogauth.init(config.settings.syslogauth, obj.syslogauth.LOG_PID | obj.syslogauth.LOG_ODELAY, obj.syslogauth.LOG_LOCAL0);
904 obj.syslogauth.log(obj.syslogauth.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
905 }
906 }
907 // Setup TCP syslog support, this works on all OS's.
908 if (config.settings.syslogtcp != null) {
909 const syslog = require('syslog');
910 if (config.settings.syslogtcp === true) {
911 obj.syslogtcp = syslog.createClient(514, 'localhost');
912 } else {
913 const sp = config.settings.syslogtcp.split(':');
914 obj.syslogtcp = syslog.createClient(parseInt(sp[1]), sp[0]);
915 }
916 obj.syslogtcp.log("MeshCentral v" + getCurrentVersion() + " Server Start", obj.syslogtcp.LOG_INFO);
917 }
918
919 // Check top level configuration for any unrecognized values
920 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]); } } }
921
922 // Read IP lists from files if applicable
923 config.settings.userallowedip = obj.args.userallowedip = readIpListFromFile(obj.args.userallowedip);
924 config.settings.userblockedip = obj.args.userblockedip = readIpListFromFile(obj.args.userblockedip);
925 config.settings.agentallowedip = obj.args.agentallowedip = readIpListFromFile(obj.args.agentallowedip);
926 config.settings.agentblockedip = obj.args.agentblockedip = readIpListFromFile(obj.args.agentblockedip);
927 config.settings.swarmallowedip = obj.args.swarmallowedip = readIpListFromFile(obj.args.swarmallowedip);
928
929 // Check IP lists and ranges
930 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(','); } }
931 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(','); } }
932 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(','); } }
933 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(','); } }
934 if (typeof obj.args.swarmallowedip == 'string') { if (obj.args.swarmallowedip == '') { obj.args.swarmallowedip = null; } else { obj.args.swarmallowedip = obj.args.swarmallowedip.split(' ').join('').split(','); } }
935 if ((typeof obj.args.agentupdateblocksize == 'number') && (obj.args.agentupdateblocksize >= 1024) && (obj.args.agentupdateblocksize <= 65531)) { obj.agentUpdateBlockSize = obj.args.agentupdateblocksize; }
936 if (typeof obj.args.trustedproxy == 'string') { obj.args.trustedproxy = obj.args.trustedproxy.split(' ').join('').split(','); }
937 if (typeof obj.args.tlsoffload == 'string') { obj.args.tlsoffload = obj.args.tlsoffload.split(' ').join('').split(','); }
938
939 // Check IP lists and ranges and if DNS return IP addresses
940 config.settings.userallowedip = await resolveDomainsToIps(config.settings.userallowedip);
941 config.settings.userblockedip = await resolveDomainsToIps(config.settings.userblockedip);
942 config.settings.agentallowedip = await resolveDomainsToIps(config.settings.agentallowedip);
943 config.settings.agentblockedip = await resolveDomainsToIps(config.settings.agentblockedip);
944 config.settings.swarmallowedip = await resolveDomainsToIps(config.settings.swarmallowedip);
945
946 // Check the "cookieIpCheck" value
947 if ((obj.args.cookieipcheck === false) || (obj.args.cookieipcheck == 'none')) { obj.args.cookieipcheck = 'none'; }
948 else if ((typeof obj.args.cookieipcheck != 'string') || (obj.args.cookieipcheck.toLowerCase() != 'strict')) { obj.args.cookieipcheck = 'lax'; }
949 else { obj.args.cookieipcheck = 'strict'; }
950
951 // Check the "cookieSameSite" value
952 if (typeof obj.args.cookiesamesite != 'string') { delete obj.args.cookiesamesite; }
953 else if (['none', 'lax', 'strict'].indexOf(obj.args.cookiesamesite.toLowerCase()) == -1) { delete obj.args.cookiesamesite; } else { obj.args.cookiesamesite = obj.args.cookiesamesite.toLowerCase(); }
954
955 // Check if WebSocket compression is supported. It's known to be broken in NodeJS v11.11 to v12.15, and v13.2
956 const verSplit = process.version.substring(1).split('.');
957 const ver = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
958 if (((ver >= 11.11) && (ver <= 12.15)) || (ver == 13.2)) {
959 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); }
960 obj.args.wscompression = obj.args.agentwscompression = false;
961 obj.config.settings.wscompression = obj.config.settings.agentwscompression = false;
962 }
963
964 // Local console tracing
965 if (typeof obj.args.debug == 'string') { obj.debugSources = obj.args.debug.toLowerCase().split(','); }
966 else if (typeof obj.args.debug == 'object') { obj.debugSources = obj.args.debug; }
967 else if (obj.args.debug === true) { obj.debugSources = '*'; }
968
969 require('./db.js').CreateDB(obj,
970 function (db) {
971 obj.db = db;
972 obj.db.SetupDatabase(function (dbversion) {
973 // See if any database operations needs to be completed
974 if (obj.args.deletedomain) { obj.db.DeleteDomain(obj.args.deletedomain, function () { console.log('Deleted domain ' + obj.args.deletedomain + '.'); process.exit(); }); return; }
975 if (obj.args.deletedefaultdomain) { obj.db.DeleteDomain('', function () { console.log('Deleted default domain.'); process.exit(); }); return; }
976 if (obj.args.showall) { obj.db.GetAll(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
977 if (obj.args.showusers) { obj.db.GetAllType('user', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
978 if (obj.args.showitem) { obj.db.Get(obj.args.showitem, function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
979 if (obj.args.listuserids) { obj.db.GetAllType('user', function (err, docs) { for (var i in docs) { console.log(docs[i]._id); } process.exit(); }); return; }
980 if (obj.args.showusergroups) { obj.db.GetAllType('ugrp', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
981 if (obj.args.shownodes) { obj.db.GetAllType('node', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
982 if (obj.args.showallmeshes) { obj.db.GetAllType('mesh', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
983 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; }
984 if (obj.args.showevents) { obj.db.GetAllEvents(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
985 if (obj.args.showsmbios) { obj.db.GetAllSMBIOS(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
986 if (obj.args.showpower) { obj.db.getAllPower(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
987 if (obj.args.clearpower) { obj.db.removeAllPowerEvents(function () { process.exit(); }); return; }
988 if (obj.args.showiplocations) { obj.db.GetAllType('iploc', function (err, docs) { console.log(docs); process.exit(); }); return; }
989 if (obj.args.logintoken) { obj.getLoginToken(obj.args.logintoken, function (r) { console.log(r); process.exit(); }); return; }
990 if (obj.args.logintokenkey) { obj.showLoginTokenKey(function (r) { console.log(r); process.exit(); }); return; }
991 if (obj.args.recordencryptionrecode) { obj.db.performRecordEncryptionRecode(function (count) { console.log('Re-encoded ' + count + ' record(s).'); process.exit(); }); return; }
992 if (obj.args.dbstats) { obj.db.getDbStats(function (stats) { console.log(stats); process.exit(); }); return; }
993 if (obj.args.createaccount) { // Create a new user account
994 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; }
995 var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.createaccount.toLowerCase(), domainid = obj.args.domain ? obj.args.domain : '';
996 if (obj.args.createaccount.startsWith('user/')) { userid = obj.args.createaccount; domainid = obj.args.createaccount.split('/')[1]; }
997 if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
998 obj.db.Get(userid, function (err, docs) {
999 if (err != null) { console.log("Database error: " + err); process.exit(); return; }
1000 if ((docs != null) && (docs.length != 0)) { console.log('User already exists.'); process.exit(); return; }
1001 if ((domainid != '') && ((config.domains == null) || (config.domains[domainid] == null))) { console.log("Invalid domain."); process.exit(); return; }
1002 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: {} };
1003 if (typeof obj.args.email == 'string') { user.email = obj.args.email; user.emailVerified = true; }
1004 if (obj.args.hashpass) {
1005 // Create an account using a pre-hashed password. Use --hashpassword to pre-hash a password.
1006 var hashpasssplit = obj.args.hashpass.split(',');
1007 if (hashpasssplit.length != 2) { console.log("Invalid hashed password."); process.exit(); return; }
1008 user.salt = hashpasssplit[0];
1009 user.hash = hashpasssplit[1];
1010 obj.db.Set(user, function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
1011 } else {
1012 // Hash the password and create the account.
1013 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);
1014 }
1015 });
1016 return;
1017 }
1018 if (obj.args.resetaccount) { // Unlock a user account, set a new password and remove 2FA
1019 if ((typeof obj.args.resetaccount != 'string') || (obj.args.resetaccount.indexOf(' ') >= 0)) { console.log("Usage: --resetaccount [userid] --domain (domain) --pass [password]."); process.exit(); return; }
1020 var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.resetaccount.toLowerCase();
1021 if (obj.args.resetaccount.startsWith('user/')) { userid = obj.args.resetaccount; }
1022 if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
1023 obj.db.Get(userid, function (err, docs) {
1024 if (err != null) { console.log("Database error: " + err); process.exit(); return; }
1025 if ((docs == null) || (docs.length == 0)) { console.log("Unknown userid, usage: --resetaccount [userid] --domain (domain) --pass [password]."); process.exit(); return; }
1026 const user = docs[0]; if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { user.siteadmin -= 32; } // Unlock the account.
1027 delete user.phone; delete user.otpekey; delete user.otpsecret; delete user.otpkeys; delete user.otphkeys; delete user.otpdev; delete user.otpsms; delete user.otpmsg; user.otpduo; // Disable 2FA
1028 delete user.msghandle; // Disable users 2fa messaging too
1029 var config = getConfig(false);
1030 if (config.domains[user.domain].auth || config.domains[user.domain].authstrategies) {
1031 console.log('This users domain has external authentication methods enabled so the password will not be changed if you set one')
1032 obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
1033 } else {
1034 if (obj.args.hashpass && (typeof obj.args.hashpass == 'string')) {
1035 // Reset an account using a pre-hashed password. Use --hashpassword to pre-hash a password.
1036 var hashpasssplit = obj.args.hashpass.split(',');
1037 if (hashpasssplit.length != 2) { console.log("Invalid hashed password."); process.exit(); return; }
1038 user.salt = hashpasssplit[0];
1039 user.hash = hashpasssplit[1];
1040 obj.db.Set(user, function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
1041 } else if (obj.args.pass && (typeof obj.args.pass == 'string')) {
1042 // Hash the password and reset the account.
1043 require('./pass').hash(String(obj.args.pass), user.salt, function (err, hash, tag) {
1044 if (err) { console.log("Unable to reset password: " + err); process.exit(); return; }
1045 user.hash = hash;
1046 obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
1047 }, 0);
1048 } else {
1049 console.log('Not setting a users password');
1050 obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
1051 }
1052 }
1053 });
1054 return;
1055 }
1056 if (obj.args.adminaccount) { // Set a user account to server administrator
1057 if ((typeof obj.args.adminaccount != 'string') || (obj.args.adminaccount.indexOf(' ') >= 0)) { console.log("Invalid userid, usage: --adminaccount [username] --domain (domain)"); process.exit(); return; }
1058 var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.adminaccount.toLowerCase();
1059 if (obj.args.adminaccount.startsWith('user/')) { userid = obj.args.adminaccount; }
1060 if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
1061 obj.db.Get(userid, function (err, docs) {
1062 if (err != null) { console.log("Database error: " + err); process.exit(); return; }
1063 if ((docs == null) || (docs.length == 0)) { console.log("Unknown userid, usage: --adminaccount [userid] --domain (domain)."); process.exit(); return; }
1064 docs[0].siteadmin = 0xFFFFFFFF; // Set user as site administrator
1065 obj.db.Set(docs[0], function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
1066 });
1067 return;
1068 }
1069 if (obj.args.removesubdomain) { // Remove all references to a sub domain from the database
1070 if ((typeof obj.args.removesubdomain != 'string') || (obj.args.removesubdomain.indexOf(' ') >= 0)) { console.log("Invalid sub domain, usage: --removesubdomain [domain]"); process.exit(); return; }
1071 obj.db.removeDomain(obj.args.removesubdomain, function () { console.log("Done."); process.exit(); return; });
1072 return;
1073 }
1074 if (obj.args.removetestagents) { // Remove all test agents from the database
1075 db.GetAllType('node', function (err, docs) {
1076 if ((err != null) || (docs.length == 0)) {
1077 console.log('Unable to get any nodes from the database');
1078 process.exit(0);
1079 } else {
1080 // Load all users
1081 const allusers = {}, removeCount = 0;
1082 obj.db.GetAllType('user', function (err, docs) {
1083 obj.common.unEscapeAllLinksFieldName(docs);
1084 for (i in docs) { allusers[docs[i]._id] = docs[i]; }
1085 });
1086
1087 // Look at all devices
1088 for (var i in docs) {
1089 if ((docs[i] != null) && (docs[i].agent != null) && (docs[i].agent.id == 23)) {
1090 // Remove this test node
1091 const node = docs[i];
1092
1093 // Delete this node including network interface information, events and timeline
1094 removeCount++;
1095 db.Remove(node._id); // Remove node with that id
1096 db.Remove('if' + node._id); // Remove interface information
1097 db.Remove('nt' + node._id); // Remove notes
1098 db.Remove('lc' + node._id); // Remove last connect time
1099 db.Remove('si' + node._id); // Remove system information
1100 if (db.RemoveSMBIOS) { db.RemoveSMBIOS(node._id); } // Remove SMBios data
1101 db.RemoveAllNodeEvents(node._id); // Remove all events for this node
1102 db.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
1103 if (typeof node.pmt == 'string') { db.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
1104 db.Get('ra' + node._id, function (err, nodes) {
1105 if ((nodes != null) && (nodes.length == 1)) { db.Remove('da' + nodes[0].daid); } // Remove diagnostic agent to real agent link
1106 db.Remove('ra' + node._id); // Remove real agent to diagnostic agent link
1107 });
1108
1109 // Remove any user node links
1110 if (node.links != null) {
1111 for (var i in node.links) {
1112 if (i.startsWith('user/')) {
1113 var cuser = allusers[i];
1114 if ((cuser != null) && (cuser.links != null) && (cuser.links[node._id] != null)) {
1115 // Remove the user link & save the user
1116 delete cuser.links[node._id];
1117 if (Object.keys(cuser.links).length == 0) { delete cuser.links; }
1118 db.SetUser(cuser);
1119 }
1120 }
1121 }
1122 }
1123
1124 }
1125 }
1126 if (removeCount == 0) {
1127 console.log("Done, no devices removed.");
1128 process.exit(0);
1129 } else {
1130 console.log("Removed " + removeCount + " device(s), holding 10 seconds...");
1131 setTimeout(function () { console.log("Done."); process.exit(0); }, 10000)
1132 }
1133 }
1134 });
1135 return;
1136 }
1137
1138 // Import NeDB data into database
1139 if (obj.args.nedbtodb) {
1140 if (db.databaseType == 1) { console.log("NeDB is current database, can't perform transfer."); process.exit(); return; }
1141 console.log("Transfering NeDB data into database...");
1142 db.nedbtodb(function (msg) { console.log(msg); process.exit(); })
1143 return;
1144 }
1145
1146 // Show a list of all configuration files in the database
1147 if (obj.args.dblistconfigfiles) {
1148 obj.db.GetAllType('cfile', function (err, docs) {
1149 if (err == null) {
1150 if (docs.length == 0) {
1151 console.log("No files found.");
1152 } else {
1153 for (var i in docs) {
1154 if (typeof obj.args.dblistconfigfiles == 'string') {
1155 const data = obj.db.decryptData(obj.args.dblistconfigfiles, docs[i].data);
1156 if (data == null) {
1157 console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' encrypted bytes - Unable to decrypt.');
1158 } else {
1159 console.log(docs[i]._id.split('/')[1] + ', ' + data.length + ' bytes, decoded correctly.');
1160 }
1161 } else {
1162 console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' encrypted bytes.');
1163 }
1164 }
1165 }
1166 } else { console.log('Unable to read from database.'); } process.exit();
1167 });
1168 return;
1169 }
1170
1171 // Display the content of a configuration file in the database
1172 if (obj.args.dbshowconfigfile) {
1173 if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
1174 obj.db.getConfigFile(obj.args.dbshowconfigfile, function (err, docs) {
1175 if (err == null) {
1176 if (docs.length == 0) { console.log("File not found."); } else {
1177 const data = obj.db.decryptData(obj.args.configkey, docs[0].data);
1178 if (data == null) { console.log("Invalid config key."); } else { console.log(data); }
1179 }
1180 } else { console.log("Unable to read from database."); }
1181 process.exit();
1182 }); return;
1183 }
1184
1185 // Delete all configuration files from database
1186 if (obj.args.dbdeleteconfigfiles) {
1187 console.log("Deleting all configuration files from the database..."); obj.db.RemoveAllOfType('cfile', function () { console.log('Done.'); process.exit(); });
1188 }
1189
1190 // Push all relevent files from meshcentral-data into the database
1191 if (obj.args.dbpushconfigfiles) {
1192 if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
1193 if ((obj.args.dbpushconfigfiles !== true) && (typeof obj.args.dbpushconfigfiles != 'string')) {
1194 console.log("Usage: --dbpulldatafiles (path) This will import files from folder into the database");
1195 console.log(" --dbpulldatafiles This will import files from meshcentral-data into the db.");
1196 process.exit();
1197 } else {
1198 if ((obj.args.dbpushconfigfiles == '*') || (obj.args.dbpushconfigfiles === true)) { obj.args.dbpushconfigfiles = obj.datapath; }
1199 obj.fs.readdir(obj.args.dbpushconfigfiles, function (err, files) {
1200 if (err != null) { console.log('ERROR: Unable to read from folder ' + obj.args.dbpushconfigfiles); process.exit(); return; }
1201 var configFound = false;
1202 for (var i in files) { if (files[i] == 'config.json') { configFound = true; } }
1203 if (configFound == false) { console.log('ERROR: No config.json in folder ' + obj.args.dbpushconfigfiles); process.exit(); return; }
1204 obj.db.RemoveAllOfType('cfile', function () {
1205 obj.fs.readdir(obj.args.dbpushconfigfiles, function (err, files) {
1206 var lockCount = 1
1207 for (var i in files) {
1208 const file = files[i];
1209 if ((file == 'config.json') || file.endsWith('.key') || file.endsWith('.crt') || (file == 'terms.txt') || file.endsWith('.jpg') || file.endsWith('.png')) {
1210 const path = obj.path.join(obj.args.dbpushconfigfiles, files[i]), binary = Buffer.from(obj.fs.readFileSync(path, { encoding: 'binary' }), 'binary');
1211 console.log('Pushing ' + file + ', ' + binary.length + ' bytes.');
1212 lockCount++;
1213 if (obj.args.oldencrypt) {
1214 obj.db.setConfigFile(file, obj.db.oldEncryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } });
1215 } else {
1216 obj.db.setConfigFile(file, obj.db.encryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } });
1217 }
1218 }
1219 }
1220 if (--lockCount == 0) { process.exit(); }
1221 });
1222 });
1223 });
1224 }
1225 return;
1226 }
1227
1228 // Pull all database files into meshcentral-data
1229 if (obj.args.dbpullconfigfiles) {
1230 if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
1231 if (typeof obj.args.dbpullconfigfiles != 'string') {
1232 console.log("Usage: --dbpulldatafiles (path)");
1233 process.exit();
1234 } else {
1235 obj.db.GetAllType('cfile', function (err, docs) {
1236 if (err == null) {
1237 if (docs.length == 0) {
1238 console.log("File not found.");
1239 } else {
1240 for (var i in docs) {
1241 const file = docs[i]._id.split('/')[1], binary = obj.db.decryptData(obj.args.configkey, docs[i].data);
1242 if (binary == null) {
1243 console.log("Invalid config key.");
1244 } else {
1245 const fullFileName = obj.path.join(obj.args.dbpullconfigfiles, file);
1246 try { obj.fs.writeFileSync(fullFileName, binary); } catch (ex) { console.log('Unable to write to ' + fullFileName); process.exit(); return; }
1247 console.log('Pulling ' + file + ', ' + binary.length + ' bytes.');
1248 }
1249 }
1250 }
1251 } else {
1252 console.log("Unable to read from database.");
1253 }
1254 process.exit();
1255 });
1256 }
1257 return;
1258 }
1259
1260 if (obj.args.dbexport) {
1261 // Export the entire database to a JSON file
1262 if (obj.args.dbexport == true) { obj.args.dbexport = obj.getConfigFilePath('meshcentral.db.json'); }
1263 obj.db.GetAll(function (err, docs) {
1264 obj.fs.writeFileSync(obj.args.dbexport, JSON.stringify(docs));
1265 console.log('Exported ' + docs.length + ' objects(s) to ' + obj.args.dbexport + '.'); process.exit();
1266 });
1267 return;
1268 }
1269 if (obj.args.dbexportmin) {
1270 // Export a minimal database to a JSON file. Export only users, meshes and nodes.
1271 // This is a useful command to look at the database.
1272 if (obj.args.dbexportmin == true) { obj.args.dbexportmin = obj.getConfigFilePath('meshcentral.db.json'); }
1273 obj.db.GetAllType({ $in: ['user', 'node', 'mesh'] }, function (err, docs) {
1274 obj.fs.writeFileSync(obj.args.dbexportmin, JSON.stringify(docs));
1275 console.log('Exported ' + docs.length + ' objects(s) to ' + obj.args.dbexportmin + '.'); process.exit();
1276 });
1277 return;
1278 }
1279 if (obj.args.dbimport) {
1280 // Import the entire database from a JSON file
1281 if (obj.args.dbimport == true) { obj.args.dbimport = obj.getConfigFilePath('meshcentral.db.json'); }
1282 var json = null, json2 = '', badCharCount = 0;
1283 try { json = obj.fs.readFileSync(obj.args.dbimport, { encoding: 'utf8' }); } catch (ex) { console.log('Invalid JSON file: ' + obj.args.dbimport + ': ' + ex); process.exit(); }
1284 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
1285 if (badCharCount > 0) { console.log(badCharCount + ' invalid character(s) where removed.'); }
1286 try { json = JSON.parse(json2); } catch (ex) { console.log('Invalid JSON format: ' + obj.args.dbimport + ': ' + e); process.exit(); }
1287 if ((json == null) || (typeof json.length != 'number') || (json.length < 1)) { console.log('Invalid JSON format: ' + obj.args.dbimport + '.'); }
1288 // Escape MongoDB invalid field chars
1289 for (i in json) {
1290 const doc = json[i];
1291 for (var j in doc) { if (j.indexOf('.') >= 0) { console.log("Invalid field name (" + j + ") in document: " + json[i]); return; } }
1292 //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]; } } }
1293 //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]; } } }
1294 }
1295 //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
1296 setTimeout(function () { // If the Mongo database is being created for the first time, there is a race condition here. This will get around it.
1297 obj.db.RemoveAll(function () {
1298 obj.db.InsertMany(json, function (err) {
1299 if (err != null) { console.log(err); } else { console.log('Imported ' + json.length + ' objects(s) from ' + obj.args.dbimport + '.'); } process.exit();
1300 });
1301 });
1302 }, 100);
1303 return;
1304 }
1305 /*
1306 if (obj.args.dbimport) {
1307 // Import the entire database from a very large JSON file
1308 obj.db.RemoveAll(function () {
1309 if (obj.args.dbimport == true) { obj.args.dbimport = obj.getConfigFilePath('meshcentral.db.json'); }
1310 var json = null, json2 = "", badCharCount = 0;
1311 const StreamArray = require('stream-json/streamers/StreamArray');
1312 const jsonStream = StreamArray.withParser();
1313 jsonStream.on('data', function (data) { obj.db.Set(data.value); });
1314 jsonStream.on('end', () => { console.log('Done.'); process.exit(); });
1315 obj.fs.createReadStream(obj.args.dbimport).pipe(jsonStream.input);
1316 });
1317 return;
1318 }
1319 */
1320 if (obj.args.dbmerge) {
1321 // Import the entire database from a JSON file
1322 if (obj.args.dbmerge == true) { obj.args.dbmerge = obj.getConfigFilePath('meshcentral.db.json'); }
1323 var json = null, json2 = "", badCharCount = 0;
1324 try { json = obj.fs.readFileSync(obj.args.dbmerge, { encoding: 'utf8' }); } catch (ex) { console.log('Invalid JSON file: ' + obj.args.dbmerge + ': ' + ex); process.exit(); }
1325 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
1326 if (badCharCount > 0) { console.log(badCharCount + ' invalid character(s) where removed.'); }
1327 try { json = JSON.parse(json2); } catch (ex) { console.log('Invalid JSON format: ' + obj.args.dbmerge + ': ' + ex); process.exit(); }
1328 if ((json == null) || (typeof json.length != 'number') || (json.length < 1)) { console.log('Invalid JSON format: ' + obj.args.dbimport + '.'); }
1329
1330 // Get all users from current database
1331 obj.db.GetAllType('user', function (err, docs) {
1332 const users = {}, usersCount = 0;
1333 for (var i in docs) { users[docs[i]._id] = docs[i]; usersCount++; }
1334
1335 // Fetch all meshes from the database
1336 obj.db.GetAllType('mesh', function (err, docs) {
1337 obj.common.unEscapeAllLinksFieldName(docs);
1338 const meshes = {}, meshesCount = 0;
1339 for (var i in docs) { meshes[docs[i]._id] = docs[i]; meshesCount++; }
1340 console.log('Loaded ' + usersCount + ' users and ' + meshesCount + ' meshes.');
1341 // Look at each object in the import file
1342 const objectToAdd = [];
1343 for (var i in json) {
1344 const newobj = json[i];
1345 if (newobj.type == 'user') {
1346 // Check if the user already exists
1347 var existingUser = users[newobj._id];
1348 if (existingUser) {
1349 // Merge the links
1350 if (typeof newobj.links == 'object') {
1351 for (var j in newobj.links) {
1352 if ((existingUser.links == null) || (existingUser.links[j] == null)) {
1353 if (existingUser.links == null) { existingUser.links = {}; }
1354 existingUser.links[j] = newobj.links[j];
1355 }
1356 }
1357 }
1358 if (existingUser.name == 'admin') { existingUser.links = {}; }
1359 objectToAdd.push(existingUser); // Add this user
1360 } else {
1361 objectToAdd.push(newobj); // Add this user
1362 }
1363 } else if (newobj.type == 'mesh') {
1364 // Add this object
1365 objectToAdd.push(newobj);
1366 } // Don't add nodes.
1367 }
1368 console.log('Importing ' + objectToAdd.length + ' object(s)...');
1369 var pendingCalls = 1;
1370 for (var i in objectToAdd) {
1371 pendingCalls++;
1372 obj.db.Set(objectToAdd[i], function (err) { if (err != null) { console.log(err); } else { if (--pendingCalls == 0) { process.exit(); } } });
1373 }
1374 if (--pendingCalls == 0) { process.exit(); }
1375 });
1376 });
1377 return;
1378 }
1379
1380 // Check if the database is capable of performing a backup
1381 // Moved behind autobackup config init in startex4: obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
1382
1383 // Load configuration for database if needed
1384 if (obj.args.loadconfigfromdb) {
1385 var key = null;
1386 if (typeof obj.args.configkey == 'string') { key = obj.args.configkey; }
1387 else if (typeof obj.args.loadconfigfromdb == 'string') { key = obj.args.loadconfigfromdb; }
1388 if (key == null) { console.log("Error, --configkey is required."); process.exit(); return; }
1389 obj.db.getAllConfigFiles(key, function (configFiles) {
1390 if (configFiles == null) { console.log("Error, no configuration files found or invalid configkey."); process.exit(); return; }
1391 if (!configFiles['config.json']) { console.log("Error, could not file config.json from database."); process.exit(); return; }
1392 if (typeof configFiles['config.json'] == 'object') { configFiles['config.json'] = configFiles['config.json'].toString(); }
1393 if (configFiles['config.json'].charCodeAt(0) == 65279) { configFiles['config.json'] = configFiles['config.json'].substring(1); }
1394 obj.configurationFiles = configFiles;
1395
1396 // Parse the new configuration file
1397 var config2 = null;
1398 try { config2 = JSON.parse(configFiles['config.json']); } catch (ex) { console.log('Error, unable to parse config.json from database.', ex); process.exit(); return; }
1399
1400 // Set the command line arguments to the config file if they are not present
1401 if (!config2.settings) { config2.settings = {}; }
1402 for (i in args) { config2.settings[i] = args[i]; }
1403
1404 // Lower case all keys in the config file
1405 common.objKeysToLower(config2, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
1406
1407 // Grab some of the values from the original config.json file if present.
1408 config2['mysql'] = config['mysql'];
1409 config2['mariadb'] = config['mariadb'];
1410 config2['mongodb'] = config['mongodb'];
1411 config2['mongodbcol'] = config['mongodbcol'];
1412 config2['dbencryptkey'] = config['dbencryptkey'];
1413 config2['acebase'] = config['acebase'];
1414 config2['sqlite3'] = config['sqlite3'];
1415
1416 // We got a new config.json from the database, let's use it.
1417 config = obj.config = config2;
1418 obj.StartEx1b();
1419 });
1420 } else {
1421 config = obj.config = getConfig(obj.args.vault == null);
1422 obj.StartEx1b();
1423 }
1424 });
1425 }
1426 );
1427 };
1428
1429 // Time to start the server of real.
1430 obj.StartEx1b = async function () {
1431 var i;
1432
1433 // Add NodeJS version warning if needed
1434 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 + "."); }
1435
1436 // Setup certificate operations
1437 obj.certificateOperations = require('./certoperations.js').CertificateOperations(obj);
1438
1439 // Linux format /var/log/auth.log
1440 if (obj.config.settings.authlog != null) {
1441 obj.fs.open(obj.config.settings.authlog, 'a', function (err, fd) {
1442 if (err == null) { obj.authlogfile = fd; } else { console.log('ERROR: Unable to open: ' + obj.config.settings.authlog); }
1443 })
1444 }
1445
1446 // Start CrowdSec bouncer if needed: https://www.crowdsec.net/
1447 if (typeof obj.args.crowdsec == 'object') { obj.crowdSecBounser = require('./crowdsec.js').CreateCrowdSecBouncer(obj, obj.args.crowdsec); }
1448
1449 // Check if self update is allowed. If running as a Windows service, self-update is not possible.
1450 if (obj.fs.existsSync(obj.path.join(__dirname, 'daemon'))) { obj.serverSelfWriteAllowed = false; }
1451
1452 // If we are targetting a specific version, update now.
1453 if ((obj.serverSelfWriteAllowed == true) && (typeof obj.args.selfupdate == 'string')) {
1454 obj.args.selfupdate = obj.args.selfupdate.toLowerCase();
1455 if (getCurrentVersion() !== obj.args.selfupdate) { obj.performServerUpdate(); return; } // We are targetting a specific version, run self update now.
1456 }
1457
1458 // Write the server state
1459 obj.updateServerState('state', 'starting');
1460 if (process.pid) { obj.updateServerState('server-pid', process.pid); }
1461 if (process.ppid) { obj.updateServerState('server-parent-pid', process.ppid); }
1462
1463 // Read environment variables. For a subset of arguments, we allow them to be read from environment variables.
1464 const xenv = ['user', 'port', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'exactport', 'debug'];
1465 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]]); } }
1466
1467 // Validate the domains, this is used for multi-hosting
1468 if (obj.config.domains == null) { obj.config.domains = {}; }
1469 if (obj.config.domains[''] == null) { obj.config.domains[''] = {}; }
1470 if (obj.config.domains[''].dns != null) { console.log("ERROR: Default domain can't have a DNS name."); return; }
1471 var xdomains = {}; for (i in obj.config.domains) { xdomains[i.toLowerCase()] = obj.config.domains[i]; } obj.config.domains = xdomains;
1472 var bannedDomains = ['public', 'private', 'images', 'scripts', 'styles', 'views']; // List of banned domains
1473 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]; } } }
1474 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]; } }
1475 for (i in obj.config.domains) {
1476 // Remove any domains that start with underscore
1477 if (i.startsWith('_')) { delete obj.config.domains[i]; continue; }
1478
1479 // Apply default domain settings if present
1480 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]; } } }
1481
1482 // Perform domain setup
1483 if (typeof obj.config.domains[i] != 'object') { console.log("ERROR: Invalid domain configuration in config.json."); process.exit(); return; }
1484 if ((i.length > 0) && (i[0] == '_')) { delete obj.config.domains[i]; continue; } // Remove any domains with names that start with _
1485 if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); }
1486 if (obj.config.domains[i].limits == null) { obj.config.domains[i].limits = {}; }
1487 if (obj.config.domains[i].dns == null) { obj.config.domains[i].url = (i == '') ? '/' : ('/' + i + '/'); } else { obj.config.domains[i].url = '/'; }
1488 obj.config.domains[i].id = i;
1489 if ((typeof obj.config.domains[i].maxdeviceview != 'number') || (obj.config.domains[i].maxdeviceview < 1)) { delete obj.config.domains[i].maxdeviceview; }
1490 if (typeof obj.config.domains[i].loginkey == 'string') { obj.config.domains[i].loginkey = [obj.config.domains[i].loginkey]; }
1491 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; }
1492 if (typeof obj.config.domains[i].agentkey == 'string') { obj.config.domains[i].agentkey = [obj.config.domains[i].agentkey]; }
1493 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; }
1494 obj.config.domains[i].userallowedip = obj.config.domains[i].userallowedip = readIpListFromFile(obj.config.domains[i].userallowedip);
1495 obj.config.domains[i].userblockedip = obj.config.domains[i].userblockedip = readIpListFromFile(obj.config.domains[i].userblockedip);
1496 obj.config.domains[i].agentallowedip = obj.config.domains[i].agentallowedip = readIpListFromFile(obj.config.domains[i].agentallowedip);
1497 obj.config.domains[i].agentblockedip = obj.config.domains[i].agentblockedip = readIpListFromFile(obj.config.domains[i].agentblockedip);
1498 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(','); } }
1499 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(','); } }
1500 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(','); } }
1501 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(','); } }
1502 // Check IP lists and ranges and if DNS return IP addresses
1503 obj.config.domains[i].userallowedip = await resolveDomainsToIps(obj.config.domains[i].userallowedip);
1504 obj.config.domains[i].userblockedip = await resolveDomainsToIps(obj.config.domains[i].userblockedip);
1505 obj.config.domains[i].agentallowedip = await resolveDomainsToIps(obj.config.domains[i].agentallowedip);
1506 obj.config.domains[i].agentblockedip = await resolveDomainsToIps(obj.config.domains[i].agentblockedip);
1507 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(','); } }
1508 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(','); } }
1509 if ((obj.config.domains[i].passwordrequirements != null) && (typeof obj.config.domains[i].passwordrequirements == 'object')) {
1510 if (typeof obj.config.domains[i].passwordrequirements.skip2factor == 'string') {
1511 obj.config.domains[i].passwordrequirements.skip2factor = obj.config.domains[i].passwordrequirements.skip2factor.split(',');
1512 } else {
1513 delete obj.config.domains[i].passwordrequirements.skip2factor;
1514 }
1515 // Fix the list of users to add "user/domain/" if needed
1516 if (Array.isArray(obj.config.domains[i].passwordrequirements.logintokens)) {
1517 var newValues = [];
1518 for (var j in obj.config.domains[i].passwordrequirements.logintokens) {
1519 var splitVal = obj.config.domains[i].passwordrequirements.logintokens[j].split('/');;
1520 if (splitVal.length == 1) { newValues.push('user/' + i + '/' + splitVal[0]); }
1521 if (splitVal.length == 2) { newValues.push('user/' + splitVal[0] + '/' + splitVal[1]); }
1522 if (splitVal.length == 3) { newValues.push(splitVal[0] + '/' + splitVal[1] + '/' + splitVal[2]); }
1523 }
1524 obj.config.domains[i].passwordrequirements.logintokens = newValues;
1525 }
1526 }
1527 if ((obj.config.domains[i].auth == 'ldap') && (typeof obj.config.domains[i].ldapoptions != 'object')) {
1528 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."); }
1529 process.exit();
1530 return;
1531 }
1532 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.
1533 if (obj.config.domains[i].sitestyle == null) { obj.config.domains[i].sitestyle = 2; } // Default to site style #2
1534
1535 // Convert newAccountsRights from a array of strings to flags number.
1536 obj.config.domains[i].newaccountsrights = obj.common.meshServerRightsArrayToNumber(obj.config.domains[i].newaccountsrights);
1537 if (typeof (obj.config.domains[i].newaccountsrights) != 'number') { delete obj.config.domains[i].newaccountsrights; }
1538
1539 // Check if there is a web views path and/or web public path for this domain
1540 if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
1541 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'); }
1542 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'); }
1543 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'); }
1544 } else {
1545 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'); }
1546 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'); }
1547 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'); }
1548 }
1549
1550 // Check agent customization if any
1551 if (typeof obj.config.domains[i].agentcustomization == 'object') {
1552 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(''); }
1553 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(''); }
1554 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(''); }
1555 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(''); }
1556 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; } }
1557 } else {
1558 delete obj.config.domains[i].agentcustomization;
1559 }
1560
1561 // Convert user consent flags
1562 if (typeof obj.config.domains[i].userconsentflags == 'object') {
1563 var flags = 0;
1564 if (obj.config.domains[i].userconsentflags.desktopnotify == true) { flags |= 1; }
1565 if (obj.config.domains[i].userconsentflags.terminalnotify == true) { flags |= 2; }
1566 if (obj.config.domains[i].userconsentflags.filenotify == true) { flags |= 4; }
1567 if (obj.config.domains[i].userconsentflags.desktopprompt == true) { flags |= 8; }
1568 if (obj.config.domains[i].userconsentflags.terminalprompt == true) { flags |= 16; }
1569 if (obj.config.domains[i].userconsentflags.fileprompt == true) { flags |= 32; }
1570 if (obj.config.domains[i].userconsentflags.desktopprivacybar == true) { flags |= 64; }
1571 obj.config.domains[i].userconsentflags = flags;
1572 }
1573
1574 // If we have Intel AMT manager settings, take a look at them here.
1575 if (typeof obj.config.domains[i].amtmanager == 'object') {
1576 if (typeof obj.config.domains[i].amtmanager.tlsrootcert == 'object') {
1577 obj.config.domains[i].amtmanager.tlsrootcert2 = obj.certificateOperations.loadGenericCertAndKey(obj.config.domains[i].amtmanager.tlsrootcert);
1578 if (obj.config.domains[i].amtmanager.tlsrootcert2 == null) { // Show an error message if needed
1579 if (i == '') {
1580 addServerWarning("Unable to load Intel AMT TLS root certificate for default domain.", 5);
1581 } else {
1582 addServerWarning("Unable to load Intel AMT TLS root certificate for domain " + i + ".", 6, [i]);
1583 }
1584 }
1585 }
1586 }
1587
1588 // Check agentfileinfo
1589 if (typeof obj.config.domains[i].agentfileinfo == 'object') {
1590 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; }
1591 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; }
1592 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; }
1593 if (typeof obj.config.domains[i].agentfileinfo.icon == 'string') {
1594 // Load the agent .ico file
1595 var icon = null;
1596 try { icon = require('./authenticode.js').loadIcon(obj.path.join(obj.datapath, obj.config.domains[i].agentfileinfo.icon)); } catch (ex) { }
1597 if (icon != null) {
1598 // The icon file was correctly loaded
1599 obj.config.domains[i].agentfileinfo.icon = icon;
1600 } else {
1601 // Failed to load the icon file, display a server warning
1602 addServerWarning("Unable to load agent icon file: " + obj.config.domains[i].agentfileinfo.icon + ".", 23, [obj.config.domains[i].agentfileinfo.icon]);
1603 delete obj.config.domains[i].agentfileinfo.icon;
1604 }
1605 } else {
1606 // Invalid icon file path
1607 delete obj.config.domains[i].agentfileinfo.icon;
1608 }
1609 if (typeof obj.config.domains[i].agentfileinfo.logo == 'string') {
1610 // Load the agent .bmp file
1611 var logo = null;
1612 try { logo = require('./authenticode.js').loadBitmap(obj.path.join(obj.datapath, obj.config.domains[i].agentfileinfo.logo)); } catch (ex) { }
1613 if (logo != null) {
1614 // The logo file was correctly loaded
1615 obj.config.domains[i].agentfileinfo.logo = logo;
1616 } else {
1617 // Failed to load the icon file, display a server warning
1618 addServerWarning("Unable to load agent logo file: " + obj.config.domains[i].agentfileinfo.logo + ".", 24, [obj.config.domains[i].agentfileinfo.logo]);
1619 delete obj.config.domains[i].agentfileinfo.logo;
1620 }
1621 } else {
1622 // Invalid icon file path
1623 delete obj.config.domains[i].agentfileinfo.logo;
1624 }
1625 }
1626 }
1627
1628 // Log passed arguments into Windows Service Log
1629 //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); }
1630
1631 // Look at passed in arguments
1632 if ((obj.args.user != null) && (typeof obj.args.user != 'string')) { delete obj.args.user; }
1633 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); }
1634 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; }
1635 if (obj.args.ignoreagenthashcheck === true) { addServerWarning("Agent hash checking is being skipped, this is unsafe.", 9); }
1636 if (obj.args.port == null || typeof obj.args.port != 'number') { obj.args.port = 443; }
1637 if (obj.args.aliasport != null && (typeof obj.args.aliasport != 'number')) obj.args.aliasport = null;
1638 if (obj.args.mpsport == null || typeof obj.args.mpsport != 'number') obj.args.mpsport = 4433;
1639 if (obj.args.mpsaliasport != null && (typeof obj.args.mpsaliasport != 'number')) obj.args.mpsaliasport = null;
1640 if (obj.args.rediraliasport != null && (typeof obj.args.rediraliasport != 'number')) obj.args.rediraliasport = null;
1641 if (obj.args.redirport == null) obj.args.redirport = 80;
1642 if (obj.args.minifycore == null) obj.args.minifycore = false;
1643 if (typeof obj.args.agentidletimeout != 'number') { obj.args.agentidletimeout = 150000; } else { obj.args.agentidletimeout *= 1000 } // Default agent idle timeout is 2m, 30sec.
1644 if ((obj.args.lanonly != true) && (typeof obj.args.webrtconfig == 'object')) { // fix incase you are using an old mis-spelt webrtconfig
1645 obj.args.webrtcconfig = obj.args.webrtconfig;
1646 delete obj.args.webrtconfig;
1647 }
1648 if ((obj.args.lanonly != true) && (obj.args.webrtcconfig == null)) { obj.args.webrtcconfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.cloudflare.com:3478' }] }; } // Setup default WebRTC STUN servers
1649 else if ((obj.args.lanonly != true) && (typeof obj.args.webrtcconfig == 'object')) {
1650 if (obj.args.webrtcconfig.iceservers) { // webrtc is case-sensitive, so must rename iceservers to iceServers!
1651 obj.args.webrtcconfig.iceServers = obj.args.webrtcconfig.iceservers;
1652 delete obj.args.webrtcconfig.iceservers;
1653 }
1654 }
1655 if (typeof obj.args.ignoreagenthashcheck == 'string') { if (obj.args.ignoreagenthashcheck == '') { delete obj.args.ignoreagenthashcheck; } else { obj.args.ignoreagenthashcheck = obj.args.ignoreagenthashcheck.split(','); } }
1656
1657 // Setup a site administrator
1658 if ((obj.args.admin) && (typeof obj.args.admin == 'string')) {
1659 var adminname = obj.args.admin.split('/');
1660 if (adminname.length == 1) { adminname = 'user//' + adminname[0]; }
1661 else if (adminname.length == 2) { adminname = 'user/' + adminname[0] + '/' + adminname[1]; }
1662 else { console.log("Invalid administrator name."); process.exit(); return; }
1663 obj.db.Get(adminname, function (err, user) {
1664 if (user.length != 1) { console.log("Invalid user name."); process.exit(); return; }
1665 user[0].siteadmin = 4294967295; // 0xFFFFFFFF
1666 obj.db.Set(user[0], function () {
1667 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."); }
1668 process.exit();
1669 return;
1670 });
1671 });
1672 return;
1673 }
1674
1675 // Remove a site administrator
1676 if ((obj.args.unadmin) && (typeof obj.args.unadmin == 'string')) {
1677 var adminname = obj.args.unadmin.split('/');
1678 if (adminname.length == 1) { adminname = 'user//' + adminname[0]; }
1679 else if (adminname.length == 2) { adminname = 'user/' + adminname[0] + '/' + adminname[1]; }
1680 else { console.log("Invalid administrator name."); process.exit(); return; }
1681 obj.db.Get(adminname, function (err, user) {
1682 if (user.length != 1) { console.log("Invalid user name."); process.exit(); return; }
1683 if (user[0].siteadmin) { delete user[0].siteadmin; }
1684 obj.db.Set(user[0], function () {
1685 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."); }
1686 process.exit();
1687 return;
1688 });
1689 });
1690 return;
1691 }
1692
1693 // Setup agent error log
1694 if ((obj.config) && (obj.config.settings) && (obj.config.settings.agentlogdump)) {
1695 obj.fs.open(obj.path.join(obj.datapath, 'agenterrorlogs.txt'), 'a', function (err, fd) { obj.agentErrorLog = fd; })
1696 }
1697
1698 // Perform other database cleanup
1699 obj.db.cleanup();
1700
1701 // Set all nodes to power state of unknown (0)
1702 obj.db.storePowerEvent({ time: new Date(), nodeid: '*', power: 0, s: 1 }, obj.multiServer); // s:1 indicates that the server is starting up.
1703
1704 // Read or setup database configuration values
1705 obj.db.Get('dbconfig', function (err, dbconfig) {
1706 if ((dbconfig != null) && (dbconfig.length == 1)) { obj.dbconfig = dbconfig[0]; } else { obj.dbconfig = { _id: 'dbconfig', version: 1 }; }
1707 if (obj.dbconfig.amtWsEventSecret == null) { obj.crypto.randomBytes(32, function (err, buf) { obj.dbconfig.amtWsEventSecret = buf.toString('hex'); obj.db.Set(obj.dbconfig); }); }
1708
1709 // This is used by the user to create a username/password for a Intel AMT WSMAN event subscription
1710 if (obj.args.getwspass) {
1711 if (obj.args.getwspass.length == 64) {
1712 obj.crypto.randomBytes(6, function (err, buf) {
1713 while (obj.dbconfig.amtWsEventSecret == null) { process.nextTick(); }
1714 const username = buf.toString('hex');
1715 const nodeid = obj.args.getwspass;
1716 const pass = obj.crypto.createHash('sha384').update(username.toLowerCase() + ':' + nodeid + ':' + obj.dbconfig.amtWsEventSecret).digest('base64').substring(0, 12).split('/').join('x').split('\\').join('x');
1717 console.log("--- Intel(r) AMT WSMAN eventing credentials ---");
1718 console.log("Username: " + username);
1719 console.log("Password: " + pass);
1720 console.log("Argument: " + nodeid);
1721 process.exit();
1722 });
1723 } else {
1724 console.log("Invalid NodeID.");
1725 process.exit();
1726 }
1727 return;
1728 }
1729
1730 // Setup the task manager
1731 if ((obj.config) && (obj.config.settings) && (obj.config.settings.taskmanager == true)) {
1732 obj.taskManager = require('./taskmanager').createTaskManager(obj);
1733 }
1734
1735 // Start plugin manager if configuration allows this.
1736 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))) {
1737 obj.pluginHandler = require('./pluginHandler.js').pluginHandler(obj);
1738 }
1739
1740 // Load the default meshcore and meshcmd
1741 obj.updateMeshCore();
1742 obj.updateMeshCmd();
1743
1744 // Setup and start the redirection server if needed. We must start the redirection server before Let's Encrypt.
1745 if ((obj.args.redirport != null) && (typeof obj.args.redirport == 'number') && (obj.args.redirport != 0)) {
1746 obj.redirserver = require('./redirserver.js').CreateRedirServer(obj, obj.db, obj.args, obj.StartEx2);
1747 } else {
1748 obj.StartEx2(); // If not needed, move on.
1749 }
1750 });
1751 }
1752
1753 // Done starting the redirection server, go on to load the server certificates
1754 obj.StartEx2 = function () {
1755 // Load server certificates
1756 obj.certificateOperations.GetMeshServerCertificate(obj.args, obj.config, function (certs) {
1757 // Get the current node version
1758 if ((obj.config.letsencrypt == null) || (obj.redirserver == null)) {
1759 obj.StartEx3(certs); // Just use the configured certificates
1760 } else if ((obj.config.letsencrypt != null) && (obj.config.letsencrypt.nochecks == true)) {
1761 // Use Let's Encrypt with no checking
1762 obj.letsencrypt = require('./letsencrypt.js').CreateLetsEncrypt(obj);
1763 obj.letsencrypt.getCertificate(certs, obj.StartEx3); // Use Let's Encrypt with no checking, use at your own risk.
1764 } else {
1765 // Check Let's Encrypt settings
1766 var leok = true;
1767 if ((typeof obj.config.letsencrypt.names != 'string') && (typeof obj.config.settings.cert == 'string')) { obj.config.letsencrypt.names = obj.config.settings.cert; }
1768 if (typeof obj.config.letsencrypt.email != 'string') { leok = false; addServerWarning("Missing Let's Encrypt email address.", 10); }
1769 else if (typeof obj.config.letsencrypt.names != 'string') { leok = false; addServerWarning("Invalid Let's Encrypt host names.", 11); }
1770 else if (obj.config.letsencrypt.names.indexOf('*') >= 0) { leok = false; addServerWarning("Invalid Let's Encrypt names, can't contain a *.", 12); }
1771 else if (obj.config.letsencrypt.email.split('@').length != 2) { leok = false; addServerWarning("Invalid Let's Encrypt email address.", 10); }
1772 else if (obj.config.letsencrypt.email.trim() !== obj.config.letsencrypt.email) { leok = false; addServerWarning("Invalid Let's Encrypt email address.", 10); }
1773 else {
1774 const le = require('./letsencrypt.js');
1775 try { obj.letsencrypt = le.CreateLetsEncrypt(obj); } catch (ex) { console.log(ex); }
1776 if (obj.letsencrypt == null) { addServerWarning("Unable to setup Let's Encrypt module.", 13); leok = false; }
1777 }
1778 if (leok == true) {
1779 // Check that the email address domain MX resolves.
1780 require('dns').resolveMx(obj.config.letsencrypt.email.split('@')[1], function (err, addresses) {
1781 if (err == null) {
1782 // Check that all names resolve
1783 checkResolveAll(obj.config.letsencrypt.names.split(','), function (err) {
1784 if (err == null) {
1785 obj.letsencrypt.getCertificate(certs, obj.StartEx3); // Use Let's Encrypt
1786 } else {
1787 for (var i in err) { addServerWarning("Invalid Let's Encrypt names, unable to resolve: " + err[i], 14, [err[i]]); }
1788 obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
1789 }
1790 });
1791 } else {
1792 addServerWarning("Invalid Let's Encrypt email address, unable to resolve: " + obj.config.letsencrypt.email.split('@')[1], 15, [obj.config.letsencrypt.email.split('@')[1]]);
1793 obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
1794 }
1795 });
1796 } else {
1797 obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
1798 }
1799 }
1800 });
1801 };
1802
1803 // Start the server with the given certificates, but check if we have web certificates to load
1804 obj.StartEx3 = function (certs) {
1805 obj.certificates = certs;
1806 obj.certificateOperations.acceleratorStart(certs); // Set the state of the accelerators
1807
1808 // Load any domain web certificates
1809 for (var i in obj.config.domains) {
1810 // Load any Intel AMT ACM activation certificates
1811 if (obj.config.domains[i].amtacmactivation == null) { obj.config.domains[i].amtacmactivation = {}; }
1812 obj.certificateOperations.loadIntelAmtAcmCerts(obj.config.domains[i].amtacmactivation);
1813 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]); } }
1814 if (typeof obj.config.domains[i].certurl == 'string') {
1815 obj.supportsProxyCertificatesRequest = true; // If a certurl is set, enable proxy cert requests
1816 // Then, fix the URL and add 'https://' if needed
1817 if (obj.config.domains[i].certurl.indexOf('://') < 0) { obj.config.domains[i].certurl = 'https://' + obj.config.domains[i].certurl; }
1818 }
1819 }
1820
1821 // Load CloudFlare trusted proxies list if needed
1822 if ((obj.config.settings.trustedproxy != null) && (typeof obj.config.settings.trustedproxy == 'string') && (obj.config.settings.trustedproxy.toLowerCase() == 'cloudflare')) {
1823 obj.config.settings.extrascriptsrc = 'ajax.cloudflare.com'; // Add CloudFlare as a trusted script source. This allows for CloudFlare's RocketLoader feature.
1824 delete obj.args.trustedproxy;
1825 delete obj.config.settings.trustedproxy;
1826 obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v4', null, function (url, data, tag) {
1827 if (data != null) {
1828 if (Array.isArray(obj.args.trustedproxy) == false) { obj.args.trustedproxy = []; }
1829 const ipranges = data.split('\n');
1830 for (var i in ipranges) { if (ipranges[i] != '') { obj.args.trustedproxy.push(ipranges[i]); } }
1831 obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v6', null, function (url, data, tag) {
1832 if (data != null) {
1833 var ipranges = data.split('\n');
1834 for (var i in ipranges) { if (ipranges[i] != '') { obj.args.trustedproxy.push(ipranges[i]); } }
1835 obj.config.settings.trustedproxy = obj.args.trustedproxy;
1836 } else {
1837 addServerWarning("Unable to load CloudFlare trusted proxy IPv6 address list.", 16);
1838 }
1839 obj.StartEx4(); // Keep going
1840 });
1841 } else {
1842 addServerWarning("Unable to load CloudFlare trusted proxy IPv4 address list.", 16);
1843 obj.StartEx4(); // Keep going
1844 }
1845 });
1846 } else {
1847 obj.StartEx4(); // Keep going
1848 }
1849 }
1850
1851 // Start the server with the given certificates
1852 obj.StartEx4 = function () {
1853 var i;
1854
1855 // If the certificate is un-configured, force LAN-only mode
1856 if (obj.certificates.CommonName.indexOf('.') == -1) { /*console.log('Server name not configured, running in LAN-only mode.');*/ obj.args.lanonly = true; }
1857
1858 // Write server version and run mode
1859 const productionMode = (process.env.NODE_ENV && (process.env.NODE_ENV == 'production'));
1860 const runmode = (obj.args.lanonly ? 2 : (obj.args.wanonly ? 1 : 0));
1861 console.log("MeshCentral v" + getCurrentVersion() + ', ' + (["Hybrid (LAN + WAN) mode", "WAN mode", "LAN mode"][runmode]) + (productionMode ? ", Production mode." : '.'));
1862
1863 // Check that no sub-domains have the same DNS as the parent
1864 for (i in obj.config.domains) {
1865 if ((obj.config.domains[i].dns != null) && (obj.certificates.CommonName.toLowerCase() === obj.config.domains[i].dns.toLowerCase())) {
1866 console.log("ERROR: Server sub-domain can't have same DNS name as the parent."); process.exit(0); return;
1867 }
1868 }
1869
1870 // Load the list of MeshCentral tools
1871 obj.updateMeshTools();
1872
1873 // Load MeshAgent translation strings
1874 try {
1875 var translationpath = obj.path.join(__dirname, 'agents', 'agent-translations.json');
1876 const translationpath2 = obj.path.join(obj.datapath, 'agents', 'agent-translations.json');
1877 if (obj.fs.existsSync(translationpath2)) { translationpath = translationpath2; } // If the agent is present in "meshcentral-data/agents", use that one instead.
1878 var translations = JSON.parse(obj.fs.readFileSync(translationpath).toString());
1879 if (translations['zh-chs']) { translations['zh-hans'] = translations['zh-chs']; delete translations['zh-chs']; }
1880 if (translations['zh-cht']) {
1881 translations['zh-hant'] = translations['zh-cht'];
1882 translations['zh-tw'] = translations['zh-cht'];
1883 translations['zh-hk'] = translations['zh-cht'];
1884 delete translations['zh-cht'];
1885 }
1886 // If there is domain customizations to the agent strings, do this here.
1887 for (var i in obj.config.domains) {
1888 var domainTranslations = translations;
1889 if ((typeof obj.config.domains[i].agentcustomization == 'object') && (typeof obj.config.domains[i].agentcustomization.installtext == 'string')) {
1890 domainTranslations = Object.assign({}, domainTranslations); // Shallow clone
1891 for (var j in domainTranslations) { delete domainTranslations[j].description; }
1892 domainTranslations.en.description = obj.config.domains[i].agentcustomization.installtext;
1893 }
1894 obj.config.domains[i].agentTranslations = JSON.stringify(domainTranslations);
1895 }
1896 } catch (ex) { }
1897
1898 // Load any domain specific agents
1899 for (var i in obj.config.domains) { if ((i != '') && (obj.config.domains[i].share == null)) { obj.updateMeshAgentsTable(obj.config.domains[i], function () { }); } }
1900
1901 // Load the list of mesh agents and install scripts
1902 if ((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true)) { for (i in obj.meshAgentsArchitectureNumbers) { obj.meshAgentsArchitectureNumbers[i].update = false; } }
1903 obj.signMeshAgents(obj.config.domains[''], function () {
1904 obj.updateMeshAgentsTable(obj.config.domains[''], function () {
1905 obj.updateMeshAgentInstallScripts();
1906
1907 // Setup and start the web server
1908 obj.crypto.randomBytes(48, function (err, buf) {
1909 // Setup Mesh Multi-Server if needed
1910 obj.multiServer = require('./multiserver.js').CreateMultiServer(obj, obj.args);
1911 if (obj.multiServer != null) {
1912 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; }
1913 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; }
1914 obj.serverId = obj.multiServer.serverid;
1915 for (var serverid in obj.config.peers.servers) { obj.peerConnectivityByNode[serverid] = {}; }
1916 }
1917
1918 // If the server is set to "nousers", allow only loopback unless IP filter is set
1919 if ((obj.args.nousers == true) && (obj.args.userallowedip == null)) { obj.args.userallowedip = "::1,127.0.0.1"; }
1920
1921 // Set the session length to 60 minutes if not set and set a random key if needed
1922 if ((obj.args.sessiontime != null) && ((typeof obj.args.sessiontime != 'number') || (obj.args.sessiontime < 1))) { delete obj.args.sessiontime; }
1923 if (typeof obj.args.sessionkey != 'string') { obj.args.sessionkey = buf.toString('hex').toUpperCase(); }
1924
1925 // Create MQTT Broker to hook into webserver and mpsserver
1926 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); }
1927
1928 // Initialize JWT Authentication Module (RMM+PSA Integration)
1929 const fs = require('fs');
1930 const jwtDebugLog = (msg) => {
1931 console.log('[JWT-DEBUG] ' + msg);
1932 try {
1933 fs.appendFileSync('/tmp/jwt-debug.log', new Date().toISOString() + ' ' + msg + '\n');
1934 } catch (e) {}
1935 };
1936
1937 jwtDebugLog('===== JWT AUTH CHECK =====');
1938 jwtDebugLog('obj.config.settings exists: ' + !!obj.config.settings);
1939 jwtDebugLog('jwtauth value: ' + (obj.config.settings ? obj.config.settings.jwtauth : 'N/A'));
1940 jwtDebugLog('jwtauth type: ' + (obj.config.settings ? typeof obj.config.settings.jwtauth : 'N/A'));
1941 jwtDebugLog('jwtauth === true: ' + (obj.config.settings && obj.config.settings.jwtauth === true));
1942
1943 if (obj.config.settings && obj.config.settings.jwtauth === true) {
1944 jwtDebugLog('Attempting to load JWT auth module...');
1945 try {
1946 jwtDebugLog('Requiring jwt-auth.js...');
1947 obj.jwtAuth = require('./jwt-auth.js').CreateJWTAuth(obj);
1948 jwtDebugLog('JWT module created successfully');
1949 if (obj.jwtAuth && typeof obj.jwtAuth.init === 'function') {
1950 jwtDebugLog('Calling jwt init()...');
1951 obj.jwtAuth.init();
1952 jwtDebugLog('JWT init() complete');
1953 }
1954 console.log('✅ JWT Authentication enabled with PostgreSQL backend');
1955 } catch (ex) {
1956 jwtDebugLog('Failed to initialize JWT: ' + ex.message);
1957 console.log('❌ Failed to initialize JWT Authentication:', ex.message);
1958 console.error('JWT Auth Error Details:', ex);
1959 console.error('Stack:', ex.stack);
1960 }
1961 } else {
1962 jwtDebugLog('JWT auth NOT enabled - condition failed');
1963 }
1964
1965 // Start the web server and if needed, the redirection web server.
1966 obj.webserver = require('./webserver.js').CreateWebServer(obj, obj.db, obj.args, obj.certificates, obj.StartEx5);
1967 if (obj.redirserver != null) { obj.redirserver.hookMainWebServer(obj.certificates); }
1968
1969 // Change RelayDNS to a array of strings
1970 if (typeof obj.args.relaydns == 'string') { obj.args.relaydns = [obj.args.relaydns]; }
1971 if (obj.common.validateStrArray(obj.args.relaydns, 1) == false) { delete obj.args.relaydns; }
1972
1973 // Start the HTTP relay web server if needed
1974 if ((obj.args.relaydns == null) && (typeof obj.args.relayport == 'number') && (obj.args.relayport != 0)) {
1975 obj.webrelayserver = require('./webrelayserver.js').CreateWebRelayServer(obj, obj.db, obj.args, obj.certificates, function () { });
1976 }
1977
1978 // Update proxy certificates
1979 if (obj.supportsProxyCertificatesRequest == true) { obj.updateProxyCertificates(true); }
1980
1981 // Setup the Intel AMT event handler
1982 obj.amtEventHandler = require('./amtevents.js').CreateAmtEventsHandler(obj);
1983
1984 // Setup the Intel AMT local network scanner
1985 if (obj.args.wanonly != true) {
1986 if (obj.args.amtscanner != false) { obj.amtScanner = require('./amtscanner.js').CreateAmtScanner(obj).start(); }
1987 if (obj.args.meshscanner != false) { obj.meshScanner = require('./meshscanner.js').CreateMeshScanner(obj).start(); }
1988 }
1989
1990 // Setup and start the MPS server
1991 obj.mpsserver = require('./mpsserver.js').CreateMpsServer(obj, obj.db, obj.args, obj.certificates);
1992
1993 // Setup the Intel AMT manager
1994 if (obj.args.amtmanager !== false) {
1995 obj.amtManager = require('./amtmanager.js').CreateAmtManager(obj);
1996 }
1997
1998 // Setup and start the legacy swarm server
1999 if ((obj.certificates.swarmserver != null) && (obj.args.swarmport != null) && (obj.args.swarmport !== 0)) {
2000 obj.swarmserver = require('./swarmserver.js').CreateSwarmServer(obj, obj.db, obj.args, obj.certificates);
2001 }
2002
2003 // Setup the main email server
2004 if (obj.config.sendgrid != null) {
2005 // Sendgrid server
2006 obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
2007 obj.mailserver.verify();
2008 if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode.", 17); }
2009 } else if (obj.config.smtp != null) {
2010 // SMTP server
2011 obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
2012 obj.mailserver.verify();
2013 if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
2014 } else if (obj.config.sendmail != null) {
2015 // Sendmail server
2016 obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
2017 obj.mailserver.verify();
2018 if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
2019 }
2020
2021 // Setup the email server for each domain
2022 for (i in obj.config.domains) {
2023 if (obj.config.domains[i].sendgrid != null) {
2024 // Sendgrid server
2025 obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
2026 obj.config.domains[i].mailserver.verify();
2027 if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode.", 17); }
2028 } else if ((obj.config.domains[i].smtp != null) && (obj.config.domains[i].smtp.host != null) && (obj.config.domains[i].smtp.from != null)) {
2029 // SMTP server
2030 obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
2031 obj.config.domains[i].mailserver.verify();
2032 if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
2033 } else if (obj.config.domains[i].sendmail != null) {
2034 // Sendmail server
2035 obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
2036 obj.config.domains[i].mailserver.verify();
2037 if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
2038 } else {
2039 // Setup the parent mail server for this domain
2040 if (obj.mailserver != null) { obj.config.domains[i].mailserver = obj.mailserver; }
2041 }
2042 }
2043
2044 // Setup SMS gateway
2045 if (config.sms != null) {
2046 obj.smsserver = require('./meshsms.js').CreateMeshSMS(obj);
2047 if ((obj.smsserver != null) && (obj.args.lanonly == true)) { addServerWarning("SMS gateway has limited use in LAN mode.", 19); }
2048 }
2049
2050 // Setup user messaging
2051 if (config.messaging != null) {
2052 obj.msgserver = require('./meshmessaging.js').CreateServer(obj);
2053 }
2054
2055 // Setup web based push notifications
2056 if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) {
2057 obj.webpush = require('web-push');
2058 var vapidKeys = null;
2059 try { vapidKeys = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, 'vapid.json')).toString()); } catch (ex) { }
2060 if ((vapidKeys == null) || (typeof vapidKeys.publicKey != 'string') || (typeof vapidKeys.privateKey != 'string')) {
2061 console.log("Generating web push VAPID keys...");
2062 vapidKeys = obj.webpush.generateVAPIDKeys();
2063 obj.common.moveOldFiles([obj.path.join(obj.datapath, 'vapid.json')]);
2064 obj.fs.writeFileSync(obj.path.join(obj.datapath, 'vapid.json'), JSON.stringify(vapidKeys));
2065 }
2066 obj.webpush.vapidPublicKey = vapidKeys.publicKey;
2067 obj.webpush.setVapidDetails('mailto:' + config.settings.webpush.email, vapidKeys.publicKey, vapidKeys.privateKey);
2068 if (typeof config.settings.webpush.gcmapi == 'string') { webpush.setGCMAPIKey(config.settings.webpush.gcmapi); }
2069 }
2070
2071 // Get the current node version
2072 const verSplit = process.version.substring(1).split('.');
2073 var nodeVersion = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
2074
2075 // Setup Firebase
2076 if ((config.firebase != null) && (typeof config.firebase.senderid == 'string') && (typeof config.firebase.serverkey == 'string')) {
2077 addServerWarning('Firebase now requires a service account JSON file, Firebase disabled.', 27);
2078 } else if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) {
2079 var serviceAccount;
2080 try { serviceAccount = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, config.firebase.serviceaccountfile)).toString()); } catch (ex) { console.log(ex); }
2081 if (serviceAccount != null) { obj.firebase = require('./firebase').CreateFirebase(obj, serviceAccount); }
2082 } else if ((typeof config.firebaserelay == 'object') && (typeof config.firebaserelay.url == 'string')) {
2083 // Setup the push messaging relay
2084 obj.firebase = require('./firebase').CreateFirebaseRelay(obj, config.firebaserelay.url, config.firebaserelay.key);
2085 } else if (obj.config.settings.publicpushnotifications === true) {
2086 // Setup the Firebase push messaging relay using https://alt.meshcentral.com, this is the public push notification server.
2087 obj.firebase = require('./firebase').CreateFirebaseRelay(obj, 'https://alt.meshcentral.com/firebaserelay.aspx');
2088 }
2089
2090 // Setup monitoring
2091 if (obj.config.settings.prometheus != null) { obj.monitoring = require('./monitoring.js').CreateMonitoring(obj, obj.args); }
2092
2093 // Start periodic maintenance
2094 obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 60 * 60); // Run this every hour
2095 //obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 10 * 1); // DEBUG: Run this more often
2096
2097 // Dispatch an event that the server is now running
2098 obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'started', msg: 'Server started' });
2099
2100 // Plugin hook. Need to run something at server startup? This is the place.
2101 if (obj.pluginHandler) { obj.pluginHandler.callHook('server_startup'); }
2102
2103 // Setup the login cookie encryption key
2104 if ((obj.config) && (obj.config.settings) && (typeof obj.config.settings.logincookieencryptionkey == 'string')) {
2105 // We have a string, hash it and use that as a key
2106 try { obj.loginCookieEncryptionKey = Buffer.from(obj.config.settings.logincookieencryptionkey, 'hex'); } catch (ex) { }
2107 if ((obj.loginCookieEncryptionKey == null) || (obj.loginCookieEncryptionKey.length != 80)) { addServerWarning("Invalid \"LoginCookieEncryptionKey\" in config.json.", 20); obj.loginCookieEncryptionKey = null; }
2108 }
2109
2110 // Login cookie encryption key not set, use one from the database
2111 if (obj.loginCookieEncryptionKey == null) {
2112 obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
2113 if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
2114 obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
2115 } else {
2116 obj.loginCookieEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() });
2117 }
2118 });
2119 }
2120
2121 // Load the invitation link encryption key from the database
2122 obj.db.Get('InvitationLinkEncryptionKey', function (err, docs) {
2123 if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (docs[0].key.length >= 160)) {
2124 obj.invitationLinkEncryptionKey = Buffer.from(docs[0].key, 'hex');
2125 } else {
2126 obj.invitationLinkEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'InvitationLinkEncryptionKey', key: obj.invitationLinkEncryptionKey.toString('hex'), time: Date.now() });
2127 }
2128 });
2129
2130 // Setup Intel AMT hello server
2131 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')) {
2132 obj.amtProvisioningServer = require('./amtprovisioningserver').CreateAmtProvisioningServer(obj, config.settings.amtprovisioningserver);
2133 }
2134
2135 // Start collecting server stats every 5 minutes
2136 obj.trafficStats = obj.webserver.getTrafficStats();
2137 setInterval(function () {
2138 obj.serverStatsCounter++;
2139 var hours = 720; // Start with all events lasting 30 days.
2140 if (((obj.serverStatsCounter) % 2) == 1) { hours = 3; } // Half of the event get removed after 3 hours.
2141 else if ((Math.floor(obj.serverStatsCounter / 2) % 2) == 1) { hours = 8; } // Another half of the event get removed after 8 hours.
2142 else if ((Math.floor(obj.serverStatsCounter / 4) % 2) == 1) { hours = 24; } // Another half of the event get removed after 24 hours.
2143 else if ((Math.floor(obj.serverStatsCounter / 8) % 2) == 1) { hours = 48; } // Another half of the event get removed after 48 hours.
2144 else if ((Math.floor(obj.serverStatsCounter / 16) % 2) == 1) { hours = 72; } // Another half of the event get removed after 72 hours.
2145 const expire = new Date();
2146 expire.setTime(expire.getTime() + (60 * 60 * 1000 * hours));
2147
2148 // Get traffic data
2149 var trafficStats = obj.webserver.getTrafficDelta(obj.trafficStats);
2150 obj.trafficStats = trafficStats.current;
2151
2152 var data = {
2153 time: new Date(),
2154 expire: expire,
2155 mem: process.memoryUsage(),
2156 conn: {
2157 ca: Object.keys(obj.webserver.wsagents).length,
2158 cu: Object.keys(obj.webserver.wssessions).length,
2159 us: Object.keys(obj.webserver.wssessions2).length,
2160 rs: obj.webserver.relaySessionCount,
2161 am: 0
2162 },
2163 traffic: trafficStats.delta
2164 };
2165 try { data.cpu = require('os').loadavg(); } catch (ex) { }
2166 if (obj.mpsserver != null) {
2167 data.conn.amc = 0;
2168 for (var i in obj.mpsserver.ciraConnections) { data.conn.amc += obj.mpsserver.ciraConnections[i].length; }
2169 }
2170 for (var i in obj.connectivityByNode) {
2171 const node = obj.connectivityByNode[i];
2172 if (node && typeof node.connectivity !== 'undefined' && node.connectivity === 4) { data.conn.am++; }
2173 }
2174 if (obj.firstStats === true) { delete obj.firstStats; data.first = true; }
2175 if (obj.multiServer != null) { data.s = obj.multiServer.serverid; }
2176 obj.db.SetServerStats(data); // Save the stats to the database
2177 obj.DispatchEvent(['*'], obj, { action: 'servertimelinestats', data: data }); // Event the server stats
2178 }, 300000);
2179
2180 obj.debug('main', "Server started");
2181 if (obj.args.nousers == true) { obj.updateServerState('nousers', '1'); }
2182 obj.updateServerState('state', "running");
2183
2184 // Setup auto-backup defaults. Unless autobackup is set to false try to make a backup.
2185 if (obj.config.settings.autobackup == false || obj.config.settings.autobackup == 'false') { obj.config.settings.autobackup = {backupintervalhours: -1}; } //block all autobackup functions
2186 else {
2187 if (typeof obj.config.settings.autobackup != 'object') { obj.config.settings.autobackup = {}; };
2188 if (typeof obj.config.settings.autobackup.backupintervalhours != 'number') { obj.config.settings.autobackup.backupintervalhours = 24; };
2189 if (typeof obj.config.settings.autobackup.keeplastdaysbackup != 'number') { obj.config.settings.autobackup.keeplastdaysbackup = 10; };
2190 if (obj.config.settings.autobackup.backuphour != null ) { obj.config.settings.autobackup.backupintervalhours = 24; if ((typeof obj.config.settings.autobackup.backuphour != 'number') || (obj.config.settings.autobackup.backuphour > 23 || obj.config.settings.autobackup.backuphour < 0 )) { obj.config.settings.autobackup.backuphour = 0; }}
2191 else {obj.config.settings.autobackup.backuphour = -1 };
2192 //arrayfi in case of string and remove possible ', ' space. !! If a string instead of an array is passed, it will be split by ',' so *{.txt,.log} won't work in that case !!
2193 if (!obj.config.settings.autobackup.backupignorefilesglob) {obj.config.settings.autobackup.backupignorefilesglob = []}
2194 else if (typeof obj.config.settings.autobackup.backupignorefilesglob == 'string') { obj.config.settings.autobackup.backupignorefilesglob = obj.config.settings.autobackup.backupignorefilesglob.replaceAll(', ', ',').split(','); };
2195 if (!obj.config.settings.autobackup.backupskipfoldersglob) {obj.config.settings.autobackup.backupskipfoldersglob = []}
2196 else if (typeof obj.config.settings.autobackup.backupskipfoldersglob == 'string') { obj.config.settings.autobackup.backupskipfoldersglob = obj.config.settings.autobackup.backupskipfoldersglob.replaceAll(', ', ',').split(','); };
2197 if (typeof obj.config.settings.autobackup.backuppath == 'string') { obj.backuppath = (obj.config.settings.autobackup.backuppath = (obj.path.resolve(obj.config.settings.autobackup.backuppath))) } else { obj.config.settings.autobackup.backuppath = obj.backuppath };
2198 if (typeof obj.config.settings.autobackup.backupname != 'string') { obj.config.settings.autobackup.backupname = 'meshcentral-autobackup-'};
2199 if (typeof obj.config.settings.autobackup.webdav == 'object') {
2200 //make webdav compliant: http://www.webdav.org/specs/rfc4918.html#rfc.section.5.2, http://www.webdav.org/specs/rfc2518.html#METHOD_MKCOL
2201 // So with leading and trailing slash in the foldername, and no double and backslashes
2202 if (typeof obj.config.settings.autobackup.webdav.foldername != 'string') {obj.config.settings.autobackup.webdav.foldername = '/MeshCentral-Backups/'}
2203 else {obj.config.settings.autobackup.webdav.foldername = ('/' + obj.config.settings.autobackup.webdav.foldername + '/').replaceAll("\\", "/").replaceAll("//", "/").replaceAll("//", "/")};
2204 }
2205 }
2206
2207 // Check if the database is capable of performing a backup
2208 obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
2209
2210 // Load Intel AMT passwords from the "amtactivation.log" file
2211 obj.loadAmtActivationLogPasswords(function (amtPasswords) {
2212 obj.amtPasswords = amtPasswords;
2213 });
2214
2215 // Setup users that can see all device groups
2216 if (typeof obj.config.settings.managealldevicegroups == 'string') { obj.config.settings.managealldevicegroups = obj.config.settings.managealldevicegroups.split(','); }
2217 else if (Array.isArray(obj.config.settings.managealldevicegroups) == false) { obj.config.settings.managealldevicegroups = []; }
2218 for (i in obj.config.domains) {
2219 if (Array.isArray(obj.config.domains[i].managealldevicegroups)) {
2220 for (var j in obj.config.domains[i].managealldevicegroups) {
2221 if (typeof obj.config.domains[i].managealldevicegroups[j] == 'string') {
2222 const u = 'user/' + i + '/' + obj.config.domains[i].managealldevicegroups[j];
2223 if (obj.config.settings.managealldevicegroups.indexOf(u) == -1) { obj.config.settings.managealldevicegroups.push(u); }
2224 }
2225 }
2226 }
2227 }
2228 obj.config.settings.managealldevicegroups.sort();
2229
2230 // Start watchdog timer if needed
2231 // 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
2232 // "watchDog": { "interval": 100, "timeout": 150 }
2233 // This will check every 100ms, if the timer is more than 150ms late, it will warn.
2234 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)) {
2235 obj.watchdogtime = Date.now();
2236 obj.watchdogmax = 0;
2237 obj.watchdogmaxtime = null;
2238 obj.watchdogtable = [];
2239 obj.watchdog = setInterval(function () {
2240 const now = Date.now(), delta = now - obj.watchdogtime - config.settings.watchdog.interval;
2241 if (delta > obj.watchdogmax) { obj.watchdogmax = delta; obj.watchdogmaxtime = new Date().toLocaleString(); }
2242 if (delta > config.settings.watchdog.timeout) {
2243 const msg = obj.common.format("Watchdog timer timeout, {0}ms.", delta);
2244 obj.watchdogtable.push(new Date().toLocaleString() + ', ' + delta + 'ms');
2245 while (obj.watchdogtable.length > 10) { obj.watchdogtable.shift(); }
2246 obj.debug('main', msg);
2247 try {
2248 var errlogpath = null;
2249 if (typeof obj.args.mesherrorlogpath == 'string') { errlogpath = obj.path.join(obj.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = obj.getConfigFilePath('mesherrors.txt'); }
2250 obj.fs.appendFileSync(errlogpath, new Date().toLocaleString() + ': ' + msg + '\r\n');
2251 } catch (ex) { console.log('ERROR: Unable to write to mesherrors.txt.'); }
2252 }
2253 obj.watchdogtime = now;
2254 }, config.settings.watchdog.interval);
2255 obj.debug('main', "Started watchdog timer.");
2256 }
2257
2258 });
2259 });
2260 });
2261 };
2262
2263 // Called when the web server finished loading
2264 obj.StartEx5 = function () {
2265 // Setup the email server for each domain
2266 var ipKvmSupport = false;
2267 for (var i in obj.config.domains) { if (obj.config.domains[i].ipkvm == true) { ipKvmSupport = true; } }
2268 if (ipKvmSupport) { obj.ipKvmManager = require('./meshipkvm').CreateIPKVMManager(obj); }
2269
2270 // Run the server start script if present
2271 if (typeof obj.config.settings.runonserverstarted == 'string') {
2272 const child_process = require('child_process');
2273 var parentpath = __dirname;
2274 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, '../..'); }
2275 child_process.exec(obj.config.settings.runonserverstarted + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
2276 }
2277 }
2278
2279 // Refresh any certificate hashs from the reverse proxy
2280 obj.pendingProxyCertificatesRequests = 0;
2281 obj.lastProxyCertificatesRequest = null;
2282 obj.supportsProxyCertificatesRequest = false;
2283 obj.updateProxyCertificates = function (force) {
2284 if (force !== true) {
2285 if ((obj.pendingProxyCertificatesRequests > 0) || (obj.supportsProxyCertificatesRequest == false)) return;
2286 if ((obj.lastProxyCertificatesRequest != null) && ((Date.now() - obj.lastProxyCertificatesRequest) < 120000)) return; // Don't allow this call more than every 2 minutes.
2287 obj.lastProxyCertificatesRequest = Date.now();
2288 }
2289
2290 // Load any domain web certificates
2291 for (var i in obj.config.domains) {
2292 if (obj.config.domains[i].certurl != null) {
2293 // Load web certs
2294 obj.pendingProxyCertificatesRequests++;
2295 var dnsname = obj.config.domains[i].dns;
2296 if ((dnsname == null) && (obj.config.settings.cert != null)) { dnsname = obj.config.settings.cert; }
2297 obj.certificateOperations.loadCertificate(obj.config.domains[i].certurl, dnsname, obj.config.domains[i], function (url, cert, xhostname, xdomain) {
2298 obj.pendingProxyCertificatesRequests--;
2299 if (cert != null) {
2300 // Hash the entire cert
2301 const hash = obj.crypto.createHash('sha384').update(Buffer.from(cert, 'binary')).digest('hex');
2302 if (xdomain.certhash != hash) { // The certificate has changed.
2303 xdomain.certkeyhash = hash;
2304 xdomain.certhash = hash;
2305
2306 try {
2307 // Decode a RSA certificate and hash the public key, if this is not RSA, skip this.
2308 const forgeCert = obj.certificateOperations.forge.pki.certificateFromAsn1(obj.certificateOperations.forge.asn1.fromDer(cert));
2309 xdomain.certkeyhash = obj.certificateOperations.forge.pki.getPublicKeyFingerprint(forgeCert.publicKey, { md: obj.certificateOperations.forge.md.sha384.create(), encoding: 'hex' });
2310 obj.webserver.webCertificateExpire[xdomain.id] = Date.parse(forgeCert.validity.notAfter); // Update certificate expire time
2311 //console.log('V1: ' + xdomain.certkeyhash);
2312 } catch (ex) {
2313 delete obj.webserver.webCertificateExpire[xdomain.id]; // Remove certificate expire time
2314 delete xdomain.certkeyhash;
2315 }
2316
2317 if (obj.webserver) {
2318 obj.webserver.webCertificateHashs[xdomain.id] = obj.webserver.webCertificateFullHashs[xdomain.id] = Buffer.from(hash, 'hex').toString('binary');
2319 if (xdomain.certkeyhash != null) { obj.webserver.webCertificateHashs[xdomain.id] = Buffer.from(xdomain.certkeyhash, 'hex').toString('binary'); }
2320
2321 // Disconnect all agents with bad web certificates
2322 for (var i in obj.webserver.wsagentsWithBadWebCerts) { obj.webserver.wsagentsWithBadWebCerts[i].close(1); }
2323 }
2324
2325 console.log(obj.common.format("Loaded web certificate from \"{0}\", host: \"{1}\"", url, xhostname));
2326 console.log(obj.common.format(" SHA384 cert hash: {0}", xdomain.certhash));
2327 if ((xdomain.certkeyhash != null) && (xdomain.certhash != xdomain.certkeyhash)) { console.log(obj.common.format(" SHA384 key hash: {0}", xdomain.certkeyhash)); }
2328 }
2329 } else {
2330 console.log(obj.common.format("Failed to load web certificate at: \"{0}\", host: \"{1}\"", url, xhostname));
2331 }
2332 });
2333 }
2334 }
2335 }
2336
2337 // Perform maintenance operations (called every hour)
2338 obj.maintenanceActions = function () {
2339 // Perform database maintenance
2340 obj.db.maintenance();
2341
2342 // Clean up any temporary files
2343 const removeTime = new Date(Date.now()).getTime() - (30 * 60 * 1000); // 30 minutes
2344 const dir = obj.fs.readdir(obj.path.join(obj.filespath, 'tmp'), function (err, files) {
2345 if (err != null) return;
2346 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) { } }
2347 });
2348
2349 // Check for self-update that targets a specific version
2350 if ((typeof obj.args.selfupdate == 'string') && (getCurrentVersion() === obj.args.selfupdate)) { obj.args.selfupdate = false; }
2351
2352 // Check if we need to perform server self-update
2353 if ((obj.args.selfupdate) && (obj.serverSelfWriteAllowed == true)) {
2354 obj.db.getValueOfTheDay('performSelfUpdate', 1, function (performSelfUpdate) {
2355 if (performSelfUpdate.value > 0) {
2356 performSelfUpdate.value--;
2357 obj.db.Set(performSelfUpdate);
2358 obj.getLatestServerVersion(function (currentVer, latestVer) { if (currentVer != latestVer) { obj.performServerUpdate(); return; } });
2359 } else {
2360 checkAutobackup();
2361 }
2362 });
2363 } else {
2364 checkAutobackup();
2365 }
2366 };
2367
2368 // Check if we need to perform an automatic backup
2369 function checkAutobackup() {
2370 if (obj.config.settings.autobackup.backupintervalhours >= 1 ) {
2371 obj.db.Get('LastAutoBackupTime', function (err, docs) {
2372 if (err != null) { console.error("checkAutobackup: Error getting LastBackupTime from DB"); return}
2373 var lastBackup = 0;
2374 const currentdate = new Date();
2375 let currentHour = currentdate.getHours();
2376 let now = currentdate.getTime();
2377 if (docs.length == 1) { lastBackup = docs[0].value; }
2378 const delta = now - lastBackup;
2379 //const delta = 9999999999; // DEBUG: backup always
2380 obj.debug ('backup', 'Entering checkAutobackup, lastAutoBackupTime: ' + new Date(lastBackup).toLocaleString('default', { dateStyle: 'medium', timeStyle: 'short' }) + ', delta: ' + (delta/(1000*60*60)).toFixed(2) + ' hours');
2381 //start autobackup if interval has passed or at configured hour, whichever comes first. When an hour schedule is missed, it will make a backup immediately.
2382 if ((delta > (obj.config.settings.autobackup.backupintervalhours * 60 * 60 * 1000)) || ((currentHour == obj.config.settings.autobackup.backuphour) && (delta >= 2 * 60 * 60 * 1000))) {
2383 // A new auto-backup is required.
2384 obj.db.Set({ _id: 'LastAutoBackupTime', value: now }); // Save the current time in the database
2385 obj.db.performBackup(); // Perform the backup
2386 }
2387 });
2388 }
2389 }
2390
2391 // Stop the Meshcentral server
2392 obj.Stop = function (restoreFile) {
2393 // If the database is not setup, exit now.
2394 if (!obj.db) return;
2395
2396 // Dispatch an event saying the server is now stopping
2397 obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'stopped', msg: "Server stopped" });
2398
2399 // Set all nodes to power state of unknown (0)
2400 obj.db.storePowerEvent({ time: new Date(), nodeid: '*', power: 0, s: 2 }, obj.multiServer, function () { // s:2 indicates that the server is shutting down.
2401 if (restoreFile) {
2402 obj.debug('main', obj.common.format("Server stopped, updating settings: {0}", restoreFile));
2403 console.log("Updating settings folder...");
2404
2405 const yauzl = require('yauzl');
2406 yauzl.open(restoreFile, { lazyEntries: true }, function (err, zipfile) {
2407 if (err) throw err;
2408 zipfile.readEntry();
2409 zipfile.on('entry', function (entry) {
2410 if (/\/$/.test(entry.fileName)) {
2411 // Directory file names end with '/'.
2412 // Note that entires for directories themselves are optional.
2413 // An entry's fileName implicitly requires its parent directories to exist.
2414 zipfile.readEntry();
2415 } else {
2416 // File entry
2417 zipfile.openReadStream(entry, function (err, readStream) {
2418 if (err) throw err;
2419 readStream.on('end', function () { zipfile.readEntry(); });
2420 var directory = obj.path.dirname(entry.fileName);
2421 if (directory != '.') {
2422 directory = obj.getConfigFilePath(directory)
2423 if (obj.fs.existsSync(directory) == false) { obj.fs.mkdirSync(directory); }
2424 }
2425 //console.log('Extracting:', obj.getConfigFilePath(entry.fileName));
2426 readStream.pipe(obj.fs.createWriteStream(obj.getConfigFilePath(entry.fileName)));
2427 });
2428 }
2429 });
2430 zipfile.on('end', function () { setTimeout(function () { obj.fs.unlinkSync(restoreFile); process.exit(123); }); });
2431 });
2432 } else {
2433 obj.debug('main', "Server stopped");
2434 process.exit(0);
2435 }
2436 });
2437
2438 // Update the server state
2439 obj.updateServerState('state', "stopped");
2440 };
2441
2442 // Event Dispatch
2443 obj.AddEventDispatch = function (ids, target) {
2444 obj.debug('dispatch', 'AddEventDispatch', ids);
2445 for (var i in ids) { var id = ids[i]; if (!obj.eventsDispatch[id]) { obj.eventsDispatch[id] = [target]; } else { obj.eventsDispatch[id].push(target); } }
2446 };
2447 obj.RemoveEventDispatch = function (ids, target) {
2448 obj.debug('dispatch', 'RemoveEventDispatch', ids);
2449 for (var i in ids) {
2450 const id = ids[i];
2451 if (obj.eventsDispatch[id]) {
2452 var j = obj.eventsDispatch[id].indexOf(target);
2453 if (j >= 0) {
2454 if (obj.eventsDispatch[id].length == 1) {
2455 delete obj.eventsDispatch[id];
2456 } else {
2457 const newList = []; // We create a new list so not to modify the original list. Allows this function to be called during an event dispatch.
2458 for (var k in obj.eventsDispatch[i]) { if (obj.eventsDispatch[i][k] != target) { newList.push(obj.eventsDispatch[i][k]); } }
2459 obj.eventsDispatch[i] = newList;
2460 }
2461 }
2462 }
2463 }
2464 };
2465 obj.RemoveEventDispatchId = function (id) {
2466 obj.debug('dispatch', 'RemoveEventDispatchId', id);
2467 if (obj.eventsDispatch[id] != null) { delete obj.eventsDispatch[id]; }
2468 };
2469 obj.RemoveAllEventDispatch = function (target) {
2470 obj.debug('dispatch', 'RemoveAllEventDispatch');
2471 for (var i in obj.eventsDispatch) {
2472 const j = obj.eventsDispatch[i].indexOf(target);
2473 if (j >= 0) {
2474 if (obj.eventsDispatch[i].length == 1) {
2475 delete obj.eventsDispatch[i];
2476 } else {
2477 const newList = []; // We create a new list so not to modify the original list. Allows this function to be called during an event dispatch.
2478 for (var k in obj.eventsDispatch[i]) { if (obj.eventsDispatch[i][k] != target) { newList.push(obj.eventsDispatch[i][k]); } }
2479 obj.eventsDispatch[i] = newList;
2480 }
2481 }
2482 }
2483 };
2484 obj.DispatchEvent = function (ids, source, event, fromPeerServer) {
2485 // If the database is not setup, exit now.
2486 if (!obj.db) return;
2487
2488 // Send event to syslog if needed
2489 if (obj.syslog && event.msg) { obj.syslog.log(obj.syslog.LOG_INFO, event.msg); }
2490 if (obj.syslogjson) { obj.syslogjson.log(obj.syslogjson.LOG_INFO, JSON.stringify(event)); }
2491 if (obj.syslogtcp && event.msg) { obj.syslogtcp.log(event.msg, obj.syslogtcp.LOG_INFO); }
2492
2493 obj.debug('dispatch', 'DispatchEvent', ids);
2494 if ((typeof event == 'object') && (!event.nolog)) {
2495 event.time = new Date();
2496 // The event we store is going to skip some of the fields so we don't store too much stuff in the database.
2497 const storeEvent = Object.assign({}, event);
2498 if (storeEvent.node) { delete storeEvent.node; } // Skip the "node" field. May skip more in the future.
2499 if (storeEvent.links) {
2500 // Escape "links" names that may have "." and/or "$"
2501 storeEvent.links = Object.assign({}, storeEvent.links);
2502 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]; } }
2503 }
2504 if (storeEvent.mesh) {
2505 // Escape "mesh" names that may have "." and/or "$"
2506 storeEvent.mesh = obj.common.escapeLinksFieldNameEx(storeEvent.mesh);
2507 }
2508 storeEvent.ids = ids;
2509 obj.db.StoreEvent(storeEvent);
2510 }
2511 const targets = []; // List of targets we dispatched the event to, we don't want to dispatch to the same target twice.
2512 for (var j in ids) {
2513 const id = ids[j];
2514 const eventsDispatch = obj.eventsDispatch[id];
2515 if (eventsDispatch) {
2516 for (var i in eventsDispatch) {
2517 if (targets.indexOf(eventsDispatch[i]) == -1) { // Check if we already displatched to this target
2518 targets.push(eventsDispatch[i]);
2519 try { eventsDispatch[i].HandleEvent(source, event, ids, id); } catch (ex) { console.log(ex, eventsDispatch[i]); }
2520 }
2521 }
2522 }
2523 }
2524 if ((fromPeerServer == null) && (obj.multiServer != null) && ((typeof event != 'object') || (event.nopeers != 1))) { obj.multiServer.DispatchEvent(ids, source, event); }
2525 };
2526
2527 // Get the connection state of a node
2528 obj.GetConnectivityState = function (nodeid) { return obj.connectivityByNode[nodeid]; };
2529
2530 // Get the routing server id for a given node and connection type, can never be self.
2531 obj.GetRoutingServerIdNotSelf = function (nodeid, connectType) {
2532 if (obj.multiServer == null) return null;
2533 for (var serverid in obj.peerConnectivityByNode) {
2534 if (serverid == obj.serverId) continue;
2535 var state = obj.peerConnectivityByNode[serverid][nodeid];
2536 if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: serverid, meshid: state.meshid }; }
2537 }
2538 return null;
2539 };
2540
2541 // Get the routing server id for a given node and connection type, self first
2542 obj.GetRoutingServerId = function (nodeid, connectType) {
2543 if (obj.multiServer == null) return null;
2544
2545 // Look at our own server first
2546 var connections = obj.peerConnectivityByNode[obj.serverId];
2547 if (connections != null) {
2548 var state = connections[nodeid];
2549 if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: obj.serverId, meshid: state.meshid }; }
2550 }
2551
2552 // Look at other servers
2553 for (var serverid in obj.peerConnectivityByNode) {
2554 if (serverid == obj.serverId) continue;
2555 var state = obj.peerConnectivityByNode[serverid][nodeid];
2556 if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: serverid, meshid: state.meshid }; }
2557 }
2558 return null;
2559 };
2560
2561 // Update the connection state of a node when in multi-server mode
2562 // Update obj.connectivityByNode using obj.peerConnectivityByNode for the list of nodes in argument
2563 obj.UpdateConnectivityState = function (nodeids) {
2564 for (var nodeid in nodeids) {
2565 var meshid = null, state = null, oldConnectivity = 0, oldPowerState = 0, newConnectivity = 0, newPowerState = 0;
2566 var oldState = obj.connectivityByNode[nodeid];
2567 if (oldState != null) { meshid = oldState.meshid; oldConnectivity = oldState.connectivity; oldPowerState = oldState.powerState; }
2568 for (var serverid in obj.peerConnectivityByNode) {
2569 var peerState = obj.peerConnectivityByNode[serverid][nodeid];
2570 if (peerState != null) {
2571 if (state == null) {
2572 // Copy the state
2573 state = {};
2574 newConnectivity = state.connectivity = peerState.connectivity;
2575 newPowerState = state.powerState = peerState.powerState;
2576 meshid = state.meshid = peerState.meshid;
2577 //if (peerState.agentPower) { state.agentPower = peerState.agentPower; }
2578 //if (peerState.ciraPower) { state.ciraPower = peerState.ciraPower; }
2579 //if (peerState.amtPower) { state.amtPower = peerState.amtPower; }
2580 } else {
2581 // Merge the state
2582 state.connectivity |= peerState.connectivity;
2583 newConnectivity = state.connectivity;
2584 if ((peerState.powerState != 0) && ((state.powerState == 0) || (peerState.powerState < state.powerState))) { newPowerState = state.powerState = peerState.powerState; }
2585 meshid = state.meshid = peerState.meshid;
2586 //if (peerState.agentPower) { state.agentPower = peerState.agentPower; }
2587 //if (peerState.ciraPower) { state.ciraPower = peerState.ciraPower; }
2588 //if (peerState.amtPower) { state.amtPower = peerState.amtPower; }
2589 }
2590 }
2591 }
2592 obj.connectivityByNode[nodeid] = state;
2593
2594 //console.log('xx', nodeid, meshid, newConnectivity, oldPowerState, newPowerState, oldPowerState);
2595
2596 // Event any changes on this server only
2597 if ((newConnectivity != oldPowerState) || (newPowerState != oldPowerState)) {
2598 obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: newConnectivity, pwr: newPowerState, nolog: 1, nopeers: 1, id: Math.random() });
2599 }
2600 }
2601 };
2602
2603 // See if we need to notifiy any user of device state change
2604 obj.NotifyUserOfDeviceStateChange = function (meshid, nodeid, connectTime, connectType, powerState, serverid, stateSet, extraInfo) {
2605 // Check if there is a email server for this domain
2606 const meshSplit = meshid.split('/');
2607 if (meshSplit.length != 3) return;
2608 const domainId = meshSplit[1];
2609 if (obj.config.domains[domainId] == null) return;
2610 const mailserver = obj.config.domains[domainId].mailserver;
2611 if ((mailserver == null) && (obj.msgserver == null)) return;
2612
2613 // Get the device group for this device
2614 const mesh = obj.webserver.meshes[meshid];
2615 if ((mesh == null) || (mesh.links == null)) return;
2616
2617 // Get the list of users that have visibility to this device
2618 // This includes users that are part of user groups
2619 const users = [];
2620 for (var i in mesh.links) {
2621 if (i.startsWith('user/') && (users.indexOf(i) < 0)) { users.push(i); }
2622 if (i.startsWith('ugrp/')) {
2623 var usergrp = obj.webserver.userGroups[i];
2624 if (usergrp.links != null) { for (var j in usergrp.links) { if (j.startsWith('user/') && (users.indexOf(j) < 0)) { users.push(j); } } }
2625 }
2626 }
2627
2628 // Check if any user needs email notification
2629 for (var i in users) {
2630 const user = obj.webserver.users[users[i]];
2631 if (user != null) {
2632 var notify = 0;
2633
2634 // Device group notifications
2635 const meshLinks = user.links[meshid];
2636 if ((meshLinks != null) && (meshLinks.notify != null)) { notify |= meshLinks.notify; }
2637
2638 // User notifications
2639 if (user.notify != null) {
2640 if (user.notify[meshid] != null) { notify |= user.notify[meshid]; }
2641 if (user.notify[nodeid] != null) { notify |= user.notify[nodeid]; }
2642 }
2643
2644 // Email notifications
2645 if ((user.email != null) && (user.emailVerified == true) && (mailserver != null) && ((notify & 48) != 0)) {
2646 if (stateSet == true) {
2647 if ((notify & 16) != 0) {
2648 mailserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2649 } else {
2650 mailserver.cancelNotifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2651 }
2652 }
2653 else if (stateSet == false) {
2654 if ((notify & 32) != 0) {
2655 mailserver.notifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2656 } else {
2657 mailserver.cancelNotifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2658 }
2659 }
2660 }
2661
2662 // Messaging notifications
2663 if ((obj.msgserver != null) && ((notify & 384) != 0)) {
2664 if (stateSet == true) {
2665 if ((notify & 128) != 0) {
2666 obj.msgserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2667 } else {
2668 obj.msgserver.cancelNotifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2669 }
2670 }
2671 else if (stateSet == false) {
2672 if ((notify & 256) != 0) {
2673 obj.msgserver.notifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2674 } else {
2675 obj.msgserver.cancelNotifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2676 }
2677 }
2678 }
2679 }
2680 }
2681 }
2682
2683 // See if we need to notifiy any user of device requested help
2684 //if (typeof device.name == 'string') { parent.parent.NotifyUserOfDeviceHelpRequest(domain, device._id, device.meshid, device.name, command.msgArgs[0], command.msgArgs[1]); }
2685
2686 obj.NotifyUserOfDeviceHelpRequest = function (domain, meshid, nodeid, devicename, helpusername, helprequest) {
2687 // Check if there is a email server for this domain
2688 const meshSplit = meshid.split('/');
2689 if (meshSplit.length != 3) return;
2690 const domainId = meshSplit[1];
2691 if (obj.config.domains[domainId] == null) return;
2692 const mailserver = obj.config.domains[domainId].mailserver;
2693 if ((mailserver == null) && (obj.msgserver == null)) return;
2694
2695 // Get the device group for this device
2696 const mesh = obj.webserver.meshes[meshid];
2697 if ((mesh == null) || (mesh.links == null)) return;
2698
2699 // Get the list of users that have visibility to this device
2700 // This includes users that are part of user groups
2701 const users = [];
2702 for (var i in mesh.links) {
2703 if (i.startsWith('user/') && (users.indexOf(i) < 0)) { users.push(i); }
2704 if (i.startsWith('ugrp/')) {
2705 var usergrp = obj.webserver.userGroups[i];
2706 if (usergrp.links != null) { for (var j in usergrp.links) { if (j.startsWith('user/') && (users.indexOf(j) < 0)) { users.push(j); } } }
2707 }
2708 }
2709
2710 // Check if any user needs email notification
2711 for (var i in users) {
2712 const user = obj.webserver.users[users[i]];
2713 if (user != null) {
2714 var notify = 0;
2715
2716 // Device group notifications
2717 const meshLinks = user.links[meshid];
2718 if ((meshLinks != null) && (meshLinks.notify != null)) { notify |= meshLinks.notify; }
2719
2720 // User notifications
2721 if (user.notify != null) {
2722 if (user.notify[meshid] != null) { notify |= user.notify[meshid]; }
2723 if (user.notify[nodeid] != null) { notify |= user.notify[nodeid]; }
2724 }
2725
2726 // Mail help request
2727 if ((user.email != null) && (user.emailVerified == true) && ((notify & 64) != 0)) { mailserver.sendDeviceHelpMail(domain, user.name, user.email, devicename, nodeid, helpusername, helprequest, user.llang); }
2728
2729 // Message help request
2730 if ((user.msghandle != null) && ((notify & 512) != 0)) { obj.msgserver.sendDeviceHelpRequest(domain, user.name, user.msghandle, devicename, nodeid, helpusername, helprequest, user.llang); }
2731 }
2732 }
2733 }
2734
2735 // Set the connectivity state of a node and setup the server so that messages can be routed correctly.
2736 // meshId: mesh identifier of format mesh/domain/meshidhex
2737 // nodeId: node identifier of format node/domain/nodeidhex
2738 // connectTime: time of connection, milliseconds elapsed since the UNIX epoch.
2739 // connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 4 = Intel AMT local, 8 = Intel AMT Relay, 16 = MQTT
2740 // 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
2741 //var connectTypeStrings = ['', 'MeshAgent', 'Intel AMT CIRA', '', 'Intel AMT local', '', '', '', 'Intel AMT Relay', '', '', '', '', '', '', '', 'MQTT'];
2742 //var powerStateStrings = ['Unknown', 'Powered', 'Sleep', 'Sleep', 'Deep Sleep', 'Hibernating', 'Soft-Off', 'Present', 'Off'];
2743 obj.SetConnectivityState = function (meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
2744 //console.log('SetConnectivity for ' + nodeid.substring(0, 16) + ', Type: ' + connectTypeStrings[connectType] + ', Power: ' + powerStateStrings[powerState] + (serverid == null ? ('') : (', ServerId: ' + serverid)));
2745 if ((serverid == null) && (obj.multiServer != null)) { obj.multiServer.DispatchMessage({ action: 'SetConnectivityState', meshid: meshid, nodeid: nodeid, connectTime: connectTime, connectType: connectType, powerState: powerState, extraInfo: extraInfo }); }
2746
2747 if (obj.multiServer == null) {
2748 // Single server mode
2749
2750 // Change the node connection state
2751 var eventConnectChange = 0;
2752 var state = obj.connectivityByNode[nodeid];
2753 if (state) {
2754 // Change the connection in the node and mesh state lists
2755 if ((state.connectivity & connectType) == 0) { state.connectivity |= connectType; eventConnectChange = 1; }
2756 state.meshid = meshid;
2757 } else {
2758 // Add the connection to the node and mesh state list
2759 obj.connectivityByNode[nodeid] = state = { connectivity: connectType, meshid: meshid };
2760 eventConnectChange = 1;
2761 }
2762
2763 // Set node power state
2764 if (connectType == 1) { state.agentPower = powerState; } else if (connectType == 2) { state.ciraPower = powerState; } else if (connectType == 4) { state.amtPower = powerState; }
2765 var powerState = 0, oldPowerState = state.powerState;
2766 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; }
2767 if ((state.powerState == null) || (state.powerState == undefined) || (state.powerState != powerState)) {
2768 state.powerState = powerState;
2769 eventConnectChange = 1;
2770
2771 // Set new power state in database
2772 const record = { time: new Date(connectTime), nodeid: nodeid, power: powerState };
2773 if (oldPowerState != null) { record.oldPower = oldPowerState; }
2774 obj.db.storePowerEvent(record, obj.multiServer);
2775 }
2776
2777 // Event the node connection change
2778 if (eventConnectChange == 1) {
2779 obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: state.connectivity, pwr: state.powerState, ct: connectTime, nolog: 1, nopeers: 1, id: Math.random() });
2780
2781 // Save indication of node connection change
2782 const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 1, connectType: connectType };
2783 if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
2784 obj.db.Set(lc);
2785
2786 // Notify any users of device connection
2787 obj.NotifyUserOfDeviceStateChange(meshid, nodeid, connectTime, connectType, powerState, serverid, true, extraInfo);
2788 }
2789 } else {
2790 // Multi server mode
2791
2792 // Change the node connection state
2793 if (serverid == null) { serverid = obj.serverId; }
2794 if (obj.peerConnectivityByNode[serverid] == null) return; // Guard against unknown serverid's
2795 var eventConnectChange = 0;
2796 var state = obj.peerConnectivityByNode[serverid][nodeid];
2797 if (state) {
2798 // Change the connection in the node and mesh state lists
2799 if ((state.connectivity & connectType) == 0) { state.connectivity |= connectType; eventConnectChange = 1; }
2800 state.meshid = meshid;
2801 } else {
2802 // Add the connection to the node and mesh state list
2803 obj.peerConnectivityByNode[serverid][nodeid] = state = { connectivity: connectType, meshid: meshid };
2804 eventConnectChange = 1;
2805 }
2806
2807 // Set node power state
2808 if (connectType == 1) { state.agentPower = powerState; } else if (connectType == 2) { state.ciraPower = powerState; } else if (connectType == 4) { state.amtPower = powerState; }
2809 var powerState = 0, oldPowerState = state.powerState;
2810 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; }
2811 if ((state.powerState == null) || (state.powerState == undefined) || (state.powerState != powerState)) {
2812 state.powerState = powerState;
2813 eventConnectChange = 1;
2814
2815 // Set new power state in database
2816 var record = { time: new Date(connectTime), nodeid: nodeid, power: powerState, server: obj.multiServer.serverid };
2817 if (oldPowerState != null) { record.oldPower = oldPowerState; }
2818 obj.db.storePowerEvent(record, obj.multiServer);
2819 }
2820
2821 if (eventConnectChange == 1) {
2822 // Update the combined node state
2823 var x = {}; x[nodeid] = 1;
2824 obj.UpdateConnectivityState(x);
2825
2826 // Save indication of node connection change
2827 if (serverid == obj.serverId) {
2828 const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 1, connectType: connectType, serverid: obj.serverId };
2829 if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
2830 obj.db.Set(lc);
2831 }
2832
2833 // Notify any users of device connection
2834 obj.NotifyUserOfDeviceStateChange(meshid, nodeid, connectTime, connectType, powerState, serverid, true, extraInfo);
2835 }
2836 }
2837 };
2838
2839 // Clear the connectivity state of a node and setup the server so that messages can be routed correctly.
2840 // meshId: mesh identifier of format mesh/domain/meshidhex
2841 // nodeId: node identifier of format node/domain/nodeidhex
2842 // connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 4 = Intel AMT local.
2843 obj.ClearConnectivityState = function (meshid, nodeid, connectType, serverid, extraInfo) {
2844 //console.log('ClearConnectivity for ' + nodeid.substring(0, 16) + ', Type: ' + connectTypeStrings[connectType] + (serverid == null?(''):(', ServerId: ' + serverid)));
2845 if ((serverid == null) && (obj.multiServer != null)) { obj.multiServer.DispatchMessage({ action: 'ClearConnectivityState', meshid: meshid, nodeid: nodeid, connectType: connectType, extraInfo: extraInfo }); }
2846
2847 if (obj.multiServer == null) {
2848 // Single server mode
2849 var eventConnectChange = 0;
2850
2851 // Remove the agent connection from the nodes connection list
2852 const state = obj.connectivityByNode[nodeid];
2853 if (state == null) return;
2854
2855 if ((state.connectivity & connectType) != 0) {
2856 state.connectivity -= connectType;
2857
2858 // Save indication of node connection change
2859 const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 0, connectType: connectType };
2860 if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
2861 obj.db.Set(lc);
2862
2863 // If the node is completely disconnected, clean it up completely
2864 if (state.connectivity == 0) { delete obj.connectivityByNode[nodeid]; }
2865 eventConnectChange = 1;
2866 }
2867
2868 // Clear node power state
2869 var powerState = 0;
2870 const oldPowerState = state.powerState;
2871 if (connectType == 1) { state.agentPower = 0; } else if (connectType == 2) { state.ciraPower = 0; } else if (connectType == 4) { state.amtPower = 0; }
2872 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; }
2873 if ((state.powerState == null) || (state.powerState != powerState)) {
2874 state.powerState = powerState;
2875 eventConnectChange = 1;
2876
2877 // Set new power state in database
2878 obj.db.storePowerEvent({ time: new Date(), nodeid: nodeid, power: powerState, oldPower: oldPowerState }, obj.multiServer);
2879 }
2880
2881 // Event the node connection change
2882 if (eventConnectChange == 1) {
2883 obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: state.connectivity, pwr: state.powerState, nolog: 1, nopeers: 1, id: Math.random() });
2884
2885 // Notify any users of device disconnection
2886 obj.NotifyUserOfDeviceStateChange(meshid, nodeid, Date.now(), connectType, -1, serverid, false, extraInfo);
2887 }
2888 } else {
2889 // Multi server mode
2890
2891 // Remove the agent connection from the nodes connection list
2892 if (serverid == null) { serverid = obj.serverId; }
2893 if (obj.peerConnectivityByNode[serverid] == null) return; // Guard against unknown serverid's
2894 var state = obj.peerConnectivityByNode[serverid][nodeid];
2895 if (state == null) return;
2896
2897 // If existing state exist, remove this connection
2898 if ((state.connectivity & connectType) != 0) {
2899 state.connectivity -= connectType; // Remove one connectivity mode
2900
2901 // Save indication of node connection change
2902 if (serverid == obj.serverId) {
2903 const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 0, connectType: connectType, serverid: obj.serverId };
2904 if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
2905 obj.db.Set(lc);
2906 }
2907
2908 // If the node is completely disconnected, clean it up completely
2909 if (state.connectivity == 0) { delete obj.peerConnectivityByNode[serverid][nodeid]; state.powerState = 0; }
2910
2911 // Notify any users of device disconnection
2912 obj.NotifyUserOfDeviceStateChange(meshid, nodeid, Date.now(), connectType, -1, serverid, false, extraInfo);
2913 }
2914
2915 // Clear node power state
2916 if (connectType == 1) { state.agentPower = 0; } else if (connectType == 2) { state.ciraPower = 0; } else if (connectType == 4) { state.amtPower = 0; }
2917 var powerState = 0;
2918 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; }
2919 if ((state.powerState == null) || (state.powerState != powerState)) { state.powerState = powerState; }
2920
2921 // Update the combined node state
2922 var x = {}; x[nodeid] = 1;
2923 obj.UpdateConnectivityState(x);
2924 }
2925 };
2926
2927 // Escape a code string
2928 obj.escapeCodeString = function (str, keepUtf8) {
2929 const escapeCodeStringTable = { '\'': '\\\'', '\"': '\\"', '\\': '\\\\', '\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t' };
2930 var r = '', c, cr, table;
2931 for (var i = 0; i < str.length; i++) {
2932 c = str[i];
2933 table = escapeCodeStringTable[c];
2934 if (table != null) {
2935 r += table;
2936 } else if (keepUtf8 === true) {
2937 r += c;
2938 } else {
2939 cr = c.charCodeAt(0);
2940 if ((cr >= 32) && (cr <= 127)) { r += c; }
2941 }
2942 }
2943 return r;
2944 }
2945
2946 // Update the default mesh core
2947 obj.updateMeshCore = function (func, dumpToFile) {
2948 // Figure out where meshcore.js is
2949 var meshcorePath = obj.datapath;
2950 if (obj.fs.existsSync(obj.path.join(meshcorePath, 'meshcore.js')) == false) {
2951 meshcorePath = obj.path.join(__dirname, 'agents');
2952 if (obj.fs.existsSync(obj.path.join(meshcorePath, 'meshcore.js')) == false) {
2953 obj.defaultMeshCores = obj.defaultMeshCoresHash = {}; if (func != null) { func(false); } // meshcore.js not found
2954 }
2955 }
2956
2957 // Read meshcore.js and all .js files in the modules folder.
2958 var meshCore = null, modulesDir = null;
2959 const modulesAdd = {
2960 'windows-amt': ['var addedModules = [];\r\n'],
2961 'linux-amt': ['var addedModules = [];\r\n'],
2962 'linux-noamt': ['var addedModules = [];\r\n']
2963 };
2964
2965 // Read the recovery core if present
2966 var meshRecoveryCore = null;
2967 if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'recoverycore.js')) == true) {
2968 try { meshRecoveryCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'recoverycore.js')).toString(); } catch (ex) { }
2969 if (meshRecoveryCore != null) {
2970 modulesAdd['windows-recovery'] = ['var addedModules = [];\r\n'];
2971 modulesAdd['linux-recovery'] = ['var addedModules = [];\r\n'];
2972 }
2973 }
2974
2975 // Read the agent recovery core if present
2976 var meshAgentRecoveryCore = null;
2977 if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'meshcore_diagnostic.js')) == true) {
2978 try { meshAgentRecoveryCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'meshcore_diagnostic.js')).toString(); } catch (ex) { }
2979 if (meshAgentRecoveryCore != null) {
2980 modulesAdd['windows-agentrecovery'] = ['var addedModules = [];\r\n'];
2981 modulesAdd['linux-agentrecovery'] = ['var addedModules = [];\r\n'];
2982 }
2983 }
2984
2985 // Read the tiny core if present
2986 var meshTinyCore = null;
2987 if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'tinycore.js')) == true) {
2988 try { meshTinyCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'tinycore.js')).toString(); } catch (ex) { }
2989 if (meshTinyCore != null) {
2990 modulesAdd['windows-tiny'] = ['var addedModules = [];\r\n'];
2991 modulesAdd['linux-tiny'] = ['var addedModules = [];\r\n'];
2992 }
2993 }
2994
2995 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.
2996 if (meshCore == null) { try { meshCore = obj.fs.readFileSync(obj.path.join(meshcorePath, 'meshcore.js')).toString(); } catch (ex) { } } // Use non-minified meshcore.
2997 if (meshCore != null) {
2998 var moduleDirPath = null;
2999 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.
3000 if (modulesDir == null) { try { moduleDirPath = obj.path.join(meshcorePath, 'modules_meshcore'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
3001 if (modulesDir != null) {
3002 for (var i in modulesDir) {
3003 if (modulesDir[i].toLowerCase().endsWith('.json')) {
3004 // We are adding a JSON file to the meshcores
3005 var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 5);
3006 if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 6); } // Remove the ".min" for ".min.json" files.
3007 const jsonData = obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('utf8'), true);
3008 const moduleData = ['var ', moduleName, ' = JSON.parse(\'', jsonData, '\');\r\n'];
3009
3010 // Add to all major cores
3011 modulesAdd['windows-amt'].push(...moduleData);
3012 modulesAdd['linux-amt'].push(...moduleData);
3013 modulesAdd['linux-noamt'].push(...moduleData);
3014 }
3015 if (modulesDir[i].toLowerCase().endsWith('.js')) {
3016 // We are adding a JS file to the meshcores
3017 var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 3);
3018 if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 4); } // Remove the ".min" for ".min.js" files.
3019 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'];
3020
3021 // Merge this module
3022 // NOTE: "smbios" module makes some non-AI Linux segfault, only include for IA platforms.
3023 if (moduleName.startsWith('amt-') || (moduleName == 'smbios')) {
3024 // Add to IA / Intel AMT cores only
3025 modulesAdd['windows-amt'].push(...moduleData);
3026 modulesAdd['linux-amt'].push(...moduleData);
3027 } else if (moduleName.startsWith('win-')) {
3028 // Add to Windows cores only
3029 modulesAdd['windows-amt'].push(...moduleData);
3030 } else if (moduleName.startsWith('linux-')) {
3031 // Add to Linux cores only
3032 modulesAdd['linux-amt'].push(...moduleData);
3033 modulesAdd['linux-noamt'].push(...moduleData);
3034 } else {
3035 // Add to all cores
3036 modulesAdd['windows-amt'].push(...moduleData);
3037 modulesAdd['linux-amt'].push(...moduleData);
3038 modulesAdd['linux-noamt'].push(...moduleData);
3039 }
3040
3041 // Merge this module to recovery modules if needed
3042 if (modulesAdd['windows-recovery'] != null) {
3043 if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal') || (moduleName == 'win-virtual-terminal')) {
3044 modulesAdd['windows-recovery'].push(...moduleData);
3045 }
3046 }
3047
3048 // Merge this module to agent recovery modules if needed
3049 if (modulesAdd['windows-agentrecovery'] != null) {
3050 if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal') || (moduleName == 'win-virtual-terminal')) {
3051 modulesAdd['windows-agentrecovery'].push(...moduleData);
3052 }
3053 }
3054 }
3055 }
3056 }
3057
3058 // Add plugins to cores
3059 if (obj.pluginHandler) { obj.pluginHandler.addMeshCoreModules(modulesAdd); }
3060
3061 // If we need to dump modules to file, create a meshcores folder
3062 if (dumpToFile) { try { obj.fs.mkdirSync('meshcores'); } catch (ex) { } }
3063
3064 // Merge the cores and compute the hashes
3065 for (var i in modulesAdd) {
3066 if ((i == 'windows-recovery') || (i == 'linux-recovery')) {
3067 obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshRecoveryCore].join('');
3068 } else if ((i == 'windows-agentrecovery') || (i == 'linux-agentrecovery')) {
3069 obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshAgentRecoveryCore].join('');
3070 } else if ((i == 'windows-tiny') || (i == 'linux-tiny')) {
3071 obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshTinyCore].join('');
3072 } else {
3073 obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshCore].join('');
3074 }
3075 obj.defaultMeshCores[i] = Buffer.from(obj.defaultMeshCores[i], 'utf8');
3076 obj.defaultMeshCoresHash[i] = obj.crypto.createHash('sha384').update(obj.defaultMeshCores[i]).digest('binary');
3077 obj.debug('main', 'Core module ' + i + ' is ' + obj.defaultMeshCores[i].length + ' bytes.');
3078
3079 // Write all modules to files. Great for debugging.
3080 if (dumpToFile) {
3081 console.log('Core module ' + i + ' is ' + obj.defaultMeshCores[i].length + ' bytes, saving to meshcores/' + i + '.js.'); // Print the core size and filename
3082 obj.fs.writeFile('meshcores/' + i + '.js', obj.defaultMeshCores[i].slice(4), function () { }); // Write the core to file
3083 }
3084
3085 // Compress the mesh cores with DEFLATE
3086 const callback = function MeshCoreDeflateCb(err, buffer) { if (err == null) { obj.defaultMeshCoresDeflate[MeshCoreDeflateCb.i] = buffer; } }
3087 callback.i = i;
3088 require('zlib').deflate(obj.defaultMeshCores[i], { level: require('zlib').Z_BEST_COMPRESSION }, callback);
3089 }
3090 }
3091
3092 // We are done creating all the mesh cores.
3093 if (func != null) { func(true); }
3094 };
3095
3096 // Update the default meshcmd
3097 obj.updateMeshCmdTimer = 'notset';
3098 obj.updateMeshCmd = function (func) {
3099 // Figure out where meshcmd.js is and read it.
3100 var meshCmd = null, meshcmdPath, moduleAdditions = ['var addedModules = [];\r\n'], moduleDirPath, modulesDir = null;
3101 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(); }
3102 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(); }
3103 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(); }
3104 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(); }
3105 else { obj.defaultMeshCmd = null; if (func != null) { func(false); } return; } // meshcmd.js not found
3106 meshCmd = meshCmd.replace("'***Mesh*Cmd*Version***'", '\'' + getCurrentVersion() + '\'');
3107
3108 // Figure out where the modules_meshcmd folder is.
3109 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.
3110 if (modulesDir == null) { try { moduleDirPath = obj.path.join(meshcmdPath, 'modules_meshcmd'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
3111 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.
3112 if (modulesDir == null) { try { moduleDirPath = obj.path.join(__dirname, 'agents', 'modules_meshcmd'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
3113
3114 // Read all .js files in the meshcmd modules folder.
3115 if (modulesDir != null) {
3116 for (var i in modulesDir) {
3117 if (modulesDir[i].toLowerCase().endsWith('.js')) {
3118 // Merge this module
3119 var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 3);
3120 if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 4); } // Remove the ".min" for ".min.js" files.
3121 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');
3122 }
3123 }
3124 }
3125
3126 // Set the new default meshcmd.js
3127 moduleAdditions.push(meshCmd);
3128 obj.defaultMeshCmd = moduleAdditions.join('');
3129 //console.log('MeshCmd is ' + obj.defaultMeshCmd.length + ' bytes.'); // DEBUG, Print the merged meshcmd.js size
3130 //obj.fs.writeFile("C:\\temp\\meshcmd.js", obj.defaultMeshCmd.substring(4)); // DEBUG, Write merged meshcmd.js to file
3131 if (func != null) { func(true); }
3132
3133 // Monitor for changes in meshcmd.js
3134 if (obj.updateMeshCmdTimer === 'notset') {
3135 obj.updateMeshCmdTimer = null;
3136 obj.fs.watch(meshcmdPath, function (eventType, filename) {
3137 if (obj.updateMeshCmdTimer != null) { clearTimeout(obj.updateMeshCmdTimer); obj.updateMeshCmdTimer = null; }
3138 obj.updateMeshCmdTimer = setTimeout(function () { obj.updateMeshCmd(); }, 5000);
3139 });
3140 }
3141 };
3142
3143 // List of possible mesh agent install scripts
3144 const meshToolsList = {
3145 'MeshCentralRouter': { localname: 'MeshCentralRouter.exe', dlname: 'winrouter' },
3146 'MeshCentralAssistant': { localname: 'MeshCentralAssistant.exe', dlname: 'winassistant', winhash: true }
3147 //'MeshCentralRouterMacOS': { localname: 'MeshCentralRouter.dmg', dlname: 'MeshCentralRouter.dmg' }
3148 };
3149
3150 // Update the list of available mesh agents
3151 obj.updateMeshTools = function () {
3152 for (var toolname in meshToolsList) {
3153 if (meshToolsList[toolname].winhash === true) {
3154 var toolpath = obj.path.join(__dirname, 'agents', meshToolsList[toolname].localname);
3155 const toolpath2 = obj.path.join(obj.datapath, 'agents', meshToolsList[toolname].localname);
3156 if (obj.fs.existsSync(toolpath2)) { toolpath = toolpath2; } // If the tool is present in "meshcentral-data/agents", use that one instead.
3157
3158 var hashStream = obj.crypto.createHash('sha384');
3159 hashStream.toolname = toolname;
3160 hashStream.toolpath = toolpath;
3161 hashStream.dlname = meshToolsList[toolname].dlname;
3162 hashStream.hashx = 0;
3163 hashStream.on('data', function (data) {
3164 obj.meshToolsBinaries[this.toolname] = { hash: data.toString('hex'), hashx: this.hashx, path: this.toolpath, dlname: this.dlname, url: this.url };
3165 obj.meshToolsBinaries[this.toolname].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?meshaction=' + this.dlname;
3166 var stats = null;
3167 try { stats = obj.fs.statSync(this.toolpath); } catch (ex) { }
3168 if (stats != null) { obj.meshToolsBinaries[this.toolname].size = stats.size; }
3169 });
3170 const options = { sourcePath: toolpath, targetStream: hashStream };
3171 obj.exeHandler.hashExecutableFile(options);
3172 } else {
3173 var toolpath = obj.path.join(__dirname, 'agents', meshToolsList[toolname].localname);
3174 const toolpath2 = obj.path.join(obj.datapath, 'agents', meshToolsList[toolname].localname);
3175 if (obj.fs.existsSync(toolpath2)) { toolpath = toolpath2; } // If the tool is present in "meshcentral-data/agents", use that one instead.
3176
3177 var stream = null;
3178 try {
3179 stream = obj.fs.createReadStream(toolpath);
3180 stream.on('data', function (data) { this.hash.update(data, 'binary'); this.hashx += data.length; });
3181 stream.on('error', function (data) {
3182 // If there is an error reading this file, make sure this agent is not in the agent table
3183 if (obj.meshToolsBinaries[this.toolname] != null) { delete obj.meshToolsBinaries[this.toolname]; }
3184 });
3185 stream.on('end', function () {
3186 // Add the agent to the agent table with all information and the hash
3187 obj.meshToolsBinaries[this.toolname] = {};
3188 obj.meshToolsBinaries[this.toolname].hash = this.hash.digest('hex');
3189 obj.meshToolsBinaries[this.toolname].hashx = this.hashx;
3190 obj.meshToolsBinaries[this.toolname].path = this.agentpath;
3191 obj.meshToolsBinaries[this.toolname].dlname = this.dlname;
3192 obj.meshToolsBinaries[this.toolname].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?meshaction=' + this.dlname;
3193 var stats = null;
3194 try { stats = obj.fs.statSync(this.agentpath); } catch (ex) { }
3195 if (stats != null) { obj.meshToolsBinaries[this.toolname].size = stats.size; }
3196 });
3197 stream.toolname = toolname;
3198 stream.agentpath = toolpath;
3199 stream.dlname = meshToolsList[toolname].dlname;
3200 stream.hash = obj.crypto.createHash('sha384', stream);
3201 stream.hashx = 0;
3202 } catch (ex) { }
3203 }
3204 }
3205 };
3206
3207 // List of possible mesh agent install scripts
3208 const meshAgentsInstallScriptList = {
3209 1: { id: 1, localname: 'meshinstall-linux.sh', rname: 'meshinstall.sh', linux: true },
3210 2: { id: 2, localname: 'meshinstall-initd.sh', rname: 'meshagent', linux: true },
3211 5: { id: 5, localname: 'meshinstall-bsd-rcd.sh', rname: 'meshagent', linux: true },
3212 6: { id: 6, localname: 'meshinstall-linux.js', rname: 'meshinstall.js', linux: true }
3213 };
3214
3215 // Update the list of available mesh agents
3216 obj.updateMeshAgentInstallScripts = function () {
3217 for (var scriptid in meshAgentsInstallScriptList) {
3218 var scriptpath = obj.path.join(__dirname, 'agents', meshAgentsInstallScriptList[scriptid].localname);
3219 var stream = null;
3220 try {
3221 stream = obj.fs.createReadStream(scriptpath);
3222 stream.xdata = '';
3223 stream.on('data', function (data) { this.hash.update(data, 'binary'); this.xdata += data; });
3224 stream.on('error', function (data) {
3225 // If there is an error reading this file, make sure this agent is not in the agent table
3226 if (obj.meshAgentInstallScripts[this.info.id] != null) { delete obj.meshAgentInstallScripts[this.info.id]; }
3227 });
3228 stream.on('end', function () {
3229 // Add the agent to the agent table with all information and the hash
3230 obj.meshAgentInstallScripts[this.info.id] = Object.assign({}, this.info);
3231 obj.meshAgentInstallScripts[this.info.id].hash = this.hash.digest('hex');
3232 obj.meshAgentInstallScripts[this.info.id].path = this.agentpath;
3233 obj.meshAgentInstallScripts[this.info.id].data = this.xdata;
3234 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;
3235 var stats = null;
3236 try { stats = obj.fs.statSync(this.agentpath); } catch (ex) { }
3237 if (stats != null) { obj.meshAgentInstallScripts[this.info.id].size = stats.size; }
3238
3239 // Place Unit line breaks on Linux scripts if not already present.
3240 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') }
3241 });
3242 stream.info = meshAgentsInstallScriptList[scriptid];
3243 stream.agentpath = scriptpath;
3244 stream.hash = obj.crypto.createHash('sha384', stream);
3245 } catch (ex) { }
3246 }
3247 };
3248
3249 // List of possible mesh agents
3250 obj.meshAgentsArchitectureNumbers = {
3251 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' },
3252 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' },
3253 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' },
3254 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 },
3255 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 },
3256 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' },
3257 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' },
3258 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' },
3259 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' },
3260 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' },
3261 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' },
3262 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.
3263 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' },
3264 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' },
3265 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
3266 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' },
3267 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
3268 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
3269 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' },
3270 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' },
3271 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' },
3272 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' },
3273 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' },
3274 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
3275 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' },
3276 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"
3277 26: { id: 26, localname: 'meshagent_aarch64', rname: 'meshagent', desc: 'Linux ARM 64 bit', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // This is replaced by ARCHID 32
3278 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.
3279 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
3280 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
3281 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
3282 32: { id: 32, localname: 'meshagent_aarch64', rname: 'meshagent', desc: 'Linux ARM 64 bit', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
3283 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.
3284 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
3285 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
3286 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
3287 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
3288 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
3289 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
3290 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' },
3291 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 },
3292 // 44: { id: 44, localname: 'meshagent_armvirt32', rname: 'meshagent', desc: 'ARMVIRT32 (OpenWRT)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // OpenWRT Routers (agent to be built)
3293 45: { id: 45, localname: 'meshagent_riscv64', rname: 'meshagent', desc: 'RISC-V x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // RISC-V 64bit
3294 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 },
3295 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 },
3296 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
3297 10006: { id: 10006, localname: 'MeshCentralAssistant.exe', rname: 'MeshCentralAssistant.exe', desc: 'MeshCentral Assistant for Windows', update: false, amt: false, platform: 'win32' }, // MeshCentral Assistant
3298 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
3299 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
3300 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
3301 };
3302
3303 // Sign windows agents
3304 obj.signMeshAgents = function (domain, func) {
3305 // Setup the domain is specified
3306 var objx = domain, suffix = '';
3307 if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
3308
3309 // Check if a custom agent signing certificate is available
3310 var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
3311
3312 // If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
3313 if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
3314 agentSignCertInfo = {
3315 cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
3316 key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
3317 extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
3318 }
3319 }
3320 if (agentSignCertInfo == null) { func(); return; } // No code signing certificate, nothing to do.
3321
3322 // Setup the domain is specified
3323 var objx = domain, suffix = '';
3324 if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
3325
3326 // Generate the agent signature description and URL
3327 const serverSignedAgentsPath = obj.path.join(obj.datapath, 'signedagents' + suffix);
3328 const signDesc = (domain.title ? domain.title : agentSignCertInfo.cert.subject.hash);
3329 const httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
3330 var httpsHost = ((domain.dns != null) ? domain.dns : obj.certificates.CommonName);
3331 if (obj.args.agentaliasdns != null) { httpsHost = obj.args.agentaliasdns; }
3332 var signUrl = 'https://' + httpsHost;
3333 if (httpsPort != 443) { signUrl += ':' + httpsPort; }
3334 var xdomain = (domain.dns == null) ? domain.id : '';
3335 if (xdomain != '') xdomain += '/';
3336 signUrl += '/' + xdomain;
3337
3338 // If requested, lock the agent to this server
3339 if (obj.config.settings.agentsignlock) { signUrl += '?ServerID=' + obj.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert).toUpperCase(); }
3340
3341 // Setup the time server
3342 var timeStampUrl = 'http://timestamp.comodoca.com/authenticode';
3343 if (obj.args.agenttimestampserver === false) { timeStampUrl = null; }
3344 else if (typeof obj.args.agenttimestampserver == 'string') { timeStampUrl = obj.args.agenttimestampserver; }
3345
3346 // Setup the time server proxy
3347 var timeStampProxy = null;
3348 if (typeof obj.args.agenttimestampproxy == 'string') { timeStampProxy = obj.args.agenttimestampproxy; }
3349 else if ((obj.args.agenttimestampproxy !== false) && (typeof obj.args.npmproxy == 'string')) { timeStampProxy = obj.args.npmproxy; }
3350
3351 // Setup the pending operations counter
3352 var pendingOperations = 1;
3353
3354 for (var archid in obj.meshAgentsArchitectureNumbers) {
3355 if (obj.meshAgentsArchitectureNumbers[archid].codesign !== true) continue;
3356
3357 var agentpath;
3358 if (domain.id == '') {
3359 // Load all agents when processing the default domain
3360 agentpath = obj.path.join(__dirname, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
3361 var agentpath2 = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
3362 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.
3363 } else {
3364 // When processing an extra domain, only load agents that are specific to that domain
3365 agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
3366 if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
3367 }
3368
3369 // Open the original agent with authenticode
3370 const signeedagentpath = obj.path.join(serverSignedAgentsPath, obj.meshAgentsArchitectureNumbers[archid].localname);
3371 const originalAgent = require('./authenticode.js').createAuthenticodeHandler(agentpath);
3372 if (originalAgent != null) {
3373 // Check if the agent is already signed correctly
3374 const destinationAgent = require('./authenticode.js').createAuthenticodeHandler(signeedagentpath);
3375 var destinationAgentOk = (
3376 (destinationAgent != null) &&
3377 (destinationAgent.fileHashSigned != null) &&
3378 (Buffer.compare(destinationAgent.fileHashSigned, destinationAgent.fileHashActual) == 0) &&
3379 (destinationAgent.signingAttribs.indexOf(signUrl) >= 0) &&
3380 (destinationAgent.signingAttribs.indexOf(signDesc) >= 0)
3381 );
3382
3383 if (destinationAgent != null) {
3384 // If the agent is signed correctly, look to see if the resources in the destination agent are correct
3385 var orgVersionStrings = originalAgent.getVersionInfo();
3386 if (destinationAgentOk == true) {
3387 const versionStrings = destinationAgent.getVersionInfo();
3388 const versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
3389 for (var i in versionProperties) {
3390 const prop = versionProperties[i], propl = prop.toLowerCase();
3391 if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo[propl] == 'string')) {
3392 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.
3393 } else {
3394 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.
3395 }
3396 }
3397
3398 // Check file version number
3399 if (destinationAgentOk == true) {
3400 if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['fileversionnumber'] == 'string')) {
3401 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.
3402 } else {
3403 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.
3404 }
3405 }
3406
3407 // Check product version number
3408 if (destinationAgentOk == true) {
3409 if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['productversionnumber'] == 'string')) {
3410 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.
3411 } else {
3412 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.
3413 }
3414 }
3415
3416 // Check the agent icon
3417 if (destinationAgentOk == true) {
3418 if ((domain.agentfileinfo != null) && (domain.agentfileinfo.icon != null)) {
3419 // Check if the destination agent matches the icon we want
3420 const agentIconGroups = destinationAgent.getIconInfo();
3421 if (agentIconGroups != null) {
3422 const agentIconGroupNames = Object.keys(agentIconGroups);
3423 if (agentIconGroupNames.length > 0) {
3424 const agentMainIconGroup = agentIconGroups[agentIconGroupNames[0]];
3425 if (agentMainIconGroup.resCount != domain.agentfileinfo.icon.resCount) {
3426 destinationAgentOk = false; // The icon image count is different, don't bother hashing to see if the icons are different.
3427 } else {
3428 const agentMainIconGroupHash = require('./authenticode.js').hashObject(agentMainIconGroup);
3429 const iconHash = require('./authenticode.js').hashObject(domain.agentfileinfo.icon);
3430 if (agentMainIconGroupHash != iconHash) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
3431 }
3432 }
3433 }
3434 } else {
3435 // Check if the destination agent has the default icon
3436 const agentIconGroups1 = destinationAgent.getIconInfo();
3437 const agentIconGroups2 = originalAgent.getIconInfo();
3438 if (agentIconGroups1.resCount != agentIconGroups2.resCount) {
3439 destinationAgentOk = false; // The icon image count is different, don't bother hashing to see if the icons are different.
3440 } else {
3441 const iconHash1 = require('./authenticode.js').hashObject(agentIconGroups1);
3442 const iconHash2 = require('./authenticode.js').hashObject(agentIconGroups2);
3443 if (iconHash1 != iconHash2) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
3444 }
3445 }
3446 }
3447
3448 // Check the agent logo
3449 if (destinationAgentOk == true) {
3450 if ((domain.agentfileinfo != null) && (domain.agentfileinfo.logo != null)) {
3451 // Check if the destination agent matches the logo we want
3452 const agentBitmaps = destinationAgent.getBitmapInfo();
3453 if (agentBitmaps != null) {
3454 const agentBitmapNames = Object.keys(agentBitmaps);
3455 if (agentBitmapNames.length > 0) {
3456 const agentMainBitmap = agentBitmaps[agentBitmapNames[0]];
3457 const agentMainBitmapHash = require('./authenticode.js').hashObject(agentMainBitmap);
3458 const bitmapHash = require('./authenticode.js').hashObject(domain.agentfileinfo.logo);
3459 if (agentMainBitmapHash != bitmapHash) { destinationAgentOk = false; } // If the existing agent logo does not match the desired logo, we need to re-sign the agent.
3460 }
3461 }
3462 } else {
3463 // Check if the destination agent has the default icon
3464 const agentBitmaps1 = destinationAgent.getBitmapInfo();
3465 const agentBitmaps2 = originalAgent.getBitmapInfo();
3466 const agentBitmapNames = Object.keys(agentBitmaps1);
3467 if (agentBitmapNames.length == 0) {
3468 destinationAgentOk = false;
3469 } else {
3470 const iconHash1 = require('./authenticode.js').hashObject(agentBitmaps1[agentBitmapNames[0]]);
3471 const iconHash2 = require('./authenticode.js').hashObject(agentBitmaps2[agentBitmapNames[0]]);
3472 if (iconHash1 != iconHash2) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
3473 }
3474 }
3475 }
3476 }
3477
3478 // If everything looks ok, runs a hash of the original and destination agent .text, .data and .rdata sections. If different, sign the agent again.
3479 if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.text').compare(destinationAgent.getHashOfSection('sha384', '.text')) != 0)) { destinationAgentOk = false; }
3480 if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.data').compare(destinationAgent.getHashOfSection('sha384', '.data')) != 0)) { destinationAgentOk = false; }
3481 if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.rdata').compare(destinationAgent.getHashOfSection('sha384', '.rdata')) != 0)) { destinationAgentOk = false; }
3482
3483 // We are done comparing the destination agent, close it.
3484 destinationAgent.close();
3485 }
3486
3487 if (destinationAgentOk == false) {
3488 // If not signed correctly, sign it. First, create the server signed agent folder if needed
3489 try { obj.fs.mkdirSync(serverSignedAgentsPath); } catch (ex) { }
3490 const xagentSignedFunc = function agentSignedFunc(err, size) {
3491 if (err == null) {
3492 // Agent was signed succesfuly
3493 console.log(obj.common.format('Code signed {0}.', agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname));
3494 } else {
3495 // Failed to sign agent
3496 addServerWarning('Failed to sign \"' + agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname + '\": ' + err, 22, [agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname, err]);
3497 }
3498 obj.callExternalSignJob(agentSignedFunc.signingArguments); // Call external signing job regardless of success or failure
3499 if (--pendingOperations === 0) { agentSignedFunc.func(); }
3500 }
3501 pendingOperations++;
3502 xagentSignedFunc.func = func;
3503 xagentSignedFunc.objx = objx;
3504 xagentSignedFunc.archid = archid;
3505 xagentSignedFunc.signeedagentpath = signeedagentpath;
3506
3507 // Parse the resources in the executable and make any required changes
3508 var resChanges = false, versionStrings = null;
3509 if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object')) {
3510 versionStrings = originalAgent.getVersionInfo();
3511 var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
3512 // Change the agent string properties
3513 for (var i in versionProperties) {
3514 const prop = versionProperties[i], propl = prop.toLowerCase();
3515 if (domain.agentfileinfo[propl] && (domain.agentfileinfo[propl] != versionStrings[prop])) { versionStrings[prop] = domain.agentfileinfo[propl]; resChanges = true; }
3516 }
3517 // Change the agent file version
3518 if (domain.agentfileinfo['fileversionnumber'] && (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion'])) {
3519 versionStrings['~FileVersion'] = domain.agentfileinfo['fileversionnumber']; resChanges = true;
3520 }
3521 // Change the agent product version
3522 if (domain.agentfileinfo['productversionnumber'] && (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion'])) {
3523 versionStrings['~ProductVersion'] = domain.agentfileinfo['productversionnumber']; resChanges = true;
3524 }
3525 if (resChanges == true) { originalAgent.setVersionInfo(versionStrings); }
3526
3527 // Change the agent icon
3528 if (domain.agentfileinfo.icon != null) {
3529 const agentIconGroups = originalAgent.getIconInfo();
3530 if (agentIconGroups != null) {
3531 const agentIconGroupNames = Object.keys(agentIconGroups);
3532 if (agentIconGroupNames.length > 0) {
3533 const agentMainIconGroupName = agentIconGroupNames[0];
3534 agentIconGroups[agentIconGroupNames[0]] = domain.agentfileinfo.icon;
3535 originalAgent.setIconInfo(agentIconGroups);
3536 }
3537 }
3538 }
3539
3540 // Change the agent logo
3541 if (domain.agentfileinfo.logo != null) {
3542 const agentBitmaps = originalAgent.getBitmapInfo();
3543 if (agentBitmaps != null) {
3544 const agentBitmapNames = Object.keys(agentBitmaps);
3545 if (agentBitmapNames.length > 0) {
3546 agentBitmaps[agentBitmapNames[0]] = domain.agentfileinfo.logo;
3547 originalAgent.setBitmapInfo(agentBitmaps);
3548 }
3549 }
3550 }
3551 }
3552
3553 const signingArguments = { out: signeedagentpath, desc: signDesc, url: signUrl, time: timeStampUrl, proxy: timeStampProxy }; // Shallow clone
3554 signingArguments.resChanges = resChanges;
3555
3556 obj.debug('main', "Code signing with arguments: " + JSON.stringify(signingArguments));
3557 xagentSignedFunc.signingArguments = signingArguments; // Attach the signing arguments to the callback function
3558 if (resChanges == false) {
3559 // Sign the agent the simple way, without changing any resources.
3560 originalAgent.sign(agentSignCertInfo, signingArguments, xagentSignedFunc);
3561 } else {
3562 // Change the agent resources and sign the agent, this is a much more involved process.
3563 // NOTE: This is experimental and could corupt the agent.
3564 originalAgent.writeExecutable(signingArguments, agentSignCertInfo, xagentSignedFunc);
3565 }
3566
3567 } else {
3568 // Signed agent is already ok, use it.
3569 originalAgent.close();
3570 }
3571
3572
3573 }
3574 }
3575
3576 if (--pendingOperations === 0) { func(); }
3577 }
3578
3579 obj.callExternalSignJob = function (signingArguments) {
3580 if (obj.config.settings && !obj.config.settings.externalsignjob) {
3581 return;
3582 }
3583 obj.debug('main', "External signing job called for file: " + signingArguments.out);
3584
3585 const { spawnSync } = require('child_process');
3586
3587 const signResult = spawnSync('"' + obj.config.settings.externalsignjob + '"', ['"' + signingArguments.out + '"'], {
3588 encoding: 'utf-8',
3589 shell: true,
3590 stdio: 'inherit'
3591 });
3592
3593 if (signResult.error || signResult.status !== 0) {
3594 obj.debug('main', "External signing failed for file: " + signingArguments.out);
3595 console.error("External signing failed for file: " + signingArguments.out);
3596 return;
3597 }
3598 }
3599
3600 // Update the list of available mesh agents
3601 obj.updateMeshAgentsTable = function (domain, func) {
3602 // Check if a custom agent signing certificate is available
3603 var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
3604
3605 // If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
3606 if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
3607 agentSignCertInfo = {
3608 cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
3609 key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
3610 extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
3611 }
3612 }
3613
3614 // Setup the domain is specified
3615 var objx = domain, suffix = '';
3616 if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
3617
3618 // Load agent information file. This includes the data & time of the agent.
3619 const agentInfo = [];
3620 try { agentInfo = JSON.parse(obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'hashagents.json'), 'utf8')); } catch (ex) { }
3621
3622 var archcount = 0;
3623 for (var archid in obj.meshAgentsArchitectureNumbers) {
3624 var agentpath;
3625 if (domain.id == '') {
3626 // Load all agents when processing the default domain
3627 agentpath = obj.path.join(__dirname, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
3628 if (obj.meshAgentsArchitectureNumbers[archid].unsigned !== true) {
3629 const agentpath2 = obj.path.join(obj.datapath, 'signedagents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
3630 if (obj.fs.existsSync(agentpath2)) { agentpath = agentpath2; } // If the agent is present in "meshcentral-data/signedagents", use that one instead.
3631 const agentpath3 = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
3632 if (obj.fs.existsSync(agentpath3)) { agentpath = agentpath3; } // If the agent is present in "meshcentral-data/agents", use that one instead.
3633 }
3634 } else {
3635 // When processing an extra domain, only load agents that are specific to that domain
3636 agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
3637 if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
3638 }
3639
3640 // Fetch agent binary information
3641 var stats = null;
3642 try { stats = obj.fs.statSync(agentpath); } catch (ex) { }
3643 if ((stats == null)) continue; // If this agent does not exist, skip it.
3644
3645 // Setup agent information
3646 archcount++;
3647 objx.meshAgentBinaries[archid] = Object.assign({}, obj.meshAgentsArchitectureNumbers[archid]);
3648 objx.meshAgentBinaries[archid].path = agentpath;
3649 objx.meshAgentBinaries[archid].url = 'http://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?id=' + archid;
3650 objx.meshAgentBinaries[archid].size = stats.size;
3651 if ((agentInfo[archid] != null) && (agentInfo[archid].mtime != null)) { objx.meshAgentBinaries[archid].mtime = new Date(agentInfo[archid].mtime); } // Set agent time if available
3652
3653 // If this is a windows binary, pull binary information
3654 if (obj.meshAgentsArchitectureNumbers[archid].platform == 'win32') {
3655 try { objx.meshAgentBinaries[archid].pe = obj.exeHandler.parseWindowsExecutable(agentpath); } catch (ex) { }
3656 }
3657
3658 // If agents must be stored in RAM or if this is a Windows 32/64/ARM64 agent, load the agent in RAM.
3659 if ((obj.args.agentsinram === true) || (((archid == 3) || (archid == 4) || (archid == 43)) && (obj.args.agentsinram !== false))) {
3660 if ((archid == 3) || (archid == 4) || (archid == 43)) {
3661 // Load the agent with a random msh added to it.
3662 const outStream = new require('stream').Duplex();
3663 outStream.meshAgentBinary = objx.meshAgentBinaries[archid];
3664 if (agentSignCertInfo) { outStream.meshAgentBinary.randomMsh = agentSignCertInfo.cert.subject.hash; } else { outStream.meshAgentBinary.randomMsh = obj.crypto.randomBytes(16).toString('hex'); }
3665 outStream.bufferList = [];
3666 outStream._write = function (chunk, encoding, callback) { this.bufferList.push(chunk); if (callback) callback(); }; // Append the chuck.
3667 outStream._read = function (size) { }; // Do nothing, this is not going to be called.
3668 outStream.on('finish', function () {
3669 // Merge all chunks
3670 this.meshAgentBinary.data = Buffer.concat(this.bufferList);
3671 this.meshAgentBinary.size = this.meshAgentBinary.data.length;
3672 delete this.bufferList;
3673
3674 // Hash the uncompressed binary
3675 const hash = obj.crypto.createHash('sha384').update(this.meshAgentBinary.data);
3676 this.meshAgentBinary.fileHash = hash.digest('binary');
3677 this.meshAgentBinary.fileHashHex = Buffer.from(this.meshAgentBinary.fileHash, 'binary').toString('hex');
3678
3679 // Compress the agent using ZIP
3680 const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method.
3681 const onZipData = function onZipData(buffer) { onZipData.x.zacc.push(buffer); }
3682 const onZipEnd = function onZipEnd() {
3683 // Concat all the buffer for create compressed zip agent
3684 const concatData = Buffer.concat(onZipData.x.zacc);
3685 delete onZipData.x.zacc;
3686
3687 // Hash the compressed binary
3688 const hash = obj.crypto.createHash('sha384').update(concatData);
3689 onZipData.x.zhash = hash.digest('binary');
3690 onZipData.x.zhashhex = Buffer.from(onZipData.x.zhash, 'binary').toString('hex');
3691
3692 // Set the agent
3693 onZipData.x.zdata = concatData;
3694 onZipData.x.zsize = concatData.length;
3695 }
3696 const onZipError = function onZipError() { delete onZipData.x.zacc; }
3697 this.meshAgentBinary.zacc = [];
3698 onZipData.x = this.meshAgentBinary;
3699 onZipEnd.x = this.meshAgentBinary;
3700 onZipError.x = this.meshAgentBinary;
3701 archive.on('data', onZipData);
3702 archive.on('end', onZipEnd);
3703 archive.on('error', onZipError);
3704
3705 // 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.
3706 // archive.append(this.meshAgentBinary.data, { name: 'meshagent' }); // This is the version that does not work on NodeJS v16.
3707 const ReadableStream = require('stream').Readable;
3708 const zipInputStream = new ReadableStream();
3709 zipInputStream.push(this.meshAgentBinary.data);
3710 zipInputStream.push(null);
3711 archive.append(zipInputStream, { name: 'meshagent' });
3712
3713 archive.finalize();
3714 })
3715 obj.exeHandler.streamExeWithMeshPolicy(
3716 {
3717 platform: 'win32',
3718 sourceFileName: agentpath,
3719 destinationStream: outStream,
3720 randomPolicy: true, // Indicates that the msh policy is random data.
3721 msh: outStream.meshAgentBinary.randomMsh,
3722 peinfo: objx.meshAgentBinaries[archid].pe
3723 });
3724 } else {
3725 // Load the agent as-is
3726 objx.meshAgentBinaries[archid].data = obj.fs.readFileSync(agentpath);
3727
3728 // Compress the agent using ZIP
3729 const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method.
3730
3731 const onZipData = function onZipData(buffer) { onZipData.x.zacc.push(buffer); }
3732 const onZipEnd = function onZipEnd() {
3733 // Concat all the buffer for create compressed zip agent
3734 const concatData = Buffer.concat(onZipData.x.zacc);
3735 delete onZipData.x.zacc;
3736
3737 // Hash the compressed binary
3738 const hash = obj.crypto.createHash('sha384').update(concatData);
3739 onZipData.x.zhash = hash.digest('binary');
3740 onZipData.x.zhashhex = Buffer.from(onZipData.x.zhash, 'binary').toString('hex');
3741
3742 // Set the agent
3743 onZipData.x.zdata = concatData;
3744 onZipData.x.zsize = concatData.length;
3745
3746 //console.log('Packed', onZipData.x.size, onZipData.x.zsize);
3747 }
3748 const onZipError = function onZipError() { delete onZipData.x.zacc; }
3749 objx.meshAgentBinaries[archid].zacc = [];
3750 onZipData.x = objx.meshAgentBinaries[archid];
3751 onZipEnd.x = objx.meshAgentBinaries[archid];
3752 onZipError.x = objx.meshAgentBinaries[archid];
3753 archive.on('data', onZipData);
3754 archive.on('end', onZipEnd);
3755 archive.on('error', onZipError);
3756 archive.append(objx.meshAgentBinaries[archid].data, { name: 'meshagent' });
3757 archive.finalize();
3758 }
3759 }
3760
3761 // Hash the binary
3762 const hashStream = obj.crypto.createHash('sha384');
3763 hashStream.archid = archid;
3764 hashStream.on('data', function (data) {
3765 objx.meshAgentBinaries[this.archid].hash = data.toString('binary');
3766 objx.meshAgentBinaries[this.archid].hashhex = data.toString('hex');
3767 if ((--archcount == 0) && (func != null)) { func(); }
3768 });
3769 const options = { sourcePath: agentpath, targetStream: hashStream, platform: obj.meshAgentsArchitectureNumbers[archid].platform };
3770 if (objx.meshAgentBinaries[archid].pe != null) { options.peinfo = objx.meshAgentBinaries[archid].pe; }
3771 obj.exeHandler.hashExecutableFile(options);
3772
3773 // If we are not loading Windows binaries to RAM, compute the RAW file hash of the signed binaries here.
3774 if ((obj.args.agentsinram === false) && ((archid == 3) || (archid == 4) || (archid == 43))) {
3775 const hash = obj.crypto.createHash('sha384').update(obj.fs.readFileSync(agentpath));
3776 objx.meshAgentBinaries[archid].fileHash = hash.digest('binary');
3777 objx.meshAgentBinaries[archid].fileHashHex = Buffer.from(objx.meshAgentBinaries[archid].fileHash, 'binary').toString('hex');
3778 }
3779 }
3780 };
3781
3782 // Generate a time limited user login token
3783 obj.getLoginToken = function (userid, func) {
3784 if ((userid == null) || (typeof userid != 'string')) { func('Invalid userid.'); return; }
3785 const x = userid.split('/');
3786 if (x == null || x.length != 3 || x[0] != 'user') { func('Invalid userid.'); return; }
3787 obj.db.Get(userid, function (err, docs) {
3788 if (err != null || docs == null || docs.length == 0) {
3789 func('User ' + userid + ' not found.'); return;
3790 } else {
3791 // Load the login cookie encryption key from the database
3792 obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
3793 if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
3794 // Key is present, use it.
3795 obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
3796 func(obj.encodeCookie({ u: userid, a: 3 }, obj.loginCookieEncryptionKey));
3797 } else {
3798 // Key is not present, generate one.
3799 obj.loginCookieEncryptionKey = obj.generateCookieKey();
3800 obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() }, function () { func(obj.encodeCookie({ u: userid, a: 3 }, obj.loginCookieEncryptionKey)); });
3801 }
3802 });
3803 }
3804 });
3805 };
3806
3807 // Show the user login token generation key
3808 obj.showLoginTokenKey = function (func) {
3809 // Load the login cookie encryption key from the database
3810 obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
3811 if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
3812 // Key is present, use it.
3813 func(docs[0].key);
3814 } else {
3815 // Key is not present, generate one.
3816 obj.loginCookieEncryptionKey = obj.generateCookieKey();
3817 obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() }, function () { func(obj.loginCookieEncryptionKey.toString('hex')); });
3818 }
3819 });
3820 };
3821
3822 // Load the list of Intel AMT UUID and passwords from "amtactivation.log"
3823 obj.loadAmtActivationLogPasswords = function (func) {
3824 const amtlogfilename = obj.path.join(obj.datapath, 'amtactivation.log');
3825 obj.fs.readFile(amtlogfilename, 'utf8', function (err, data) {
3826 const amtPasswords = {}; // UUID --> [Passwords]
3827 if ((err == null) && (data != null)) {
3828 const lines = data.split('\n');
3829 for (var i in lines) {
3830 const line = lines[i];
3831 if (line.startsWith('{')) {
3832 var j = null;
3833 try { j = JSON.parse(line); } catch (ex) { }
3834 if ((j != null) && (typeof j == 'object')) {
3835 if ((typeof j.amtUuid == 'string') && (typeof j.password == 'string')) {
3836 if (amtPasswords[j.amtUuid] == null) {
3837 amtPasswords[j.amtUuid] = [j.password]; // Add password to array
3838 } else {
3839 amtPasswords[j.amtUuid].unshift(j.password); // Add password at the start of the array
3840 }
3841 }
3842 }
3843 }
3844 }
3845 // Remove all duplicates and only keep the 3 last passwords for any given device
3846 for (var i in amtPasswords) {
3847 amtPasswords[i] = [...new Set(amtPasswords[i])];
3848 while (amtPasswords[i].length > 3) { amtPasswords[i].pop(); }
3849 }
3850 }
3851 func(obj.common.sortObj(amtPasswords)); // Sort by UUID
3852 });
3853 }
3854
3855 // Encrypt session data
3856 obj.encryptSessionData = function (data, key) {
3857 if (data == null) return null;
3858 if (key == null) { key = obj.loginCookieEncryptionKey; }
3859 try {
3860 const iv = Buffer.from(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
3861 const crypted = Buffer.concat([cipher.update(JSON.stringify(data), 'utf8'), cipher.final()]);
3862 return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64').replace(/\+/g, '@').replace(/\//g, '$');
3863 } catch (ex) { return null; }
3864 }
3865
3866 // Decrypt the session data
3867 obj.decryptSessionData = function (data, key) {
3868 if ((typeof data != 'string') || (data.length < 13)) return {};
3869 if (key == null) { key = obj.loginCookieEncryptionKey; }
3870 try {
3871 const buf = Buffer.from(data.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
3872 const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), buf.slice(0, 12));
3873 decipher.setAuthTag(buf.slice(12, 28));
3874 return JSON.parse(decipher.update(buf.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
3875 } catch (ex) { return {}; }
3876 }
3877
3878 // Generate a cryptographic key used to encode and decode cookies
3879 obj.generateCookieKey = function () {
3880 return Buffer.from(obj.crypto.randomBytes(80), 'binary');
3881 //return Buffer.alloc(80, 0); // Sets the key to zeros, debug only.
3882 };
3883
3884 // Encode an object as a cookie using a key using AES-GCM. (key must be 32 bytes or more)
3885 obj.encodeCookie = function (o, key) {
3886 try {
3887 if (key == null) { key = obj.serverKey; }
3888 o.time = Math.floor(Date.now() / 1000); // Add the cookie creation time
3889 const iv = Buffer.from(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
3890 const crypted = Buffer.concat([cipher.update(JSON.stringify(o), 'utf8'), cipher.final()]);
3891 const r = Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64').replace(/\+/g, '@').replace(/\//g, '$');
3892 obj.debug('cookie', 'Encoded AESGCM cookie: ' + JSON.stringify(o));
3893 return r;
3894 } catch (ex) { obj.debug('cookie', 'ERR: Failed to encode AESGCM cookie due to exception: ' + ex); return null; }
3895 };
3896
3897 // 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)
3898 obj.decodeCookie = function (cookie, key, timeout) {
3899 if (cookie == null) return null;
3900 var r = obj.decodeCookieAESGCM(cookie, key, timeout);
3901 if (r === -1) { r = obj.decodeCookieAESSHA(cookie, key, timeout); } // If decodeCookieAESGCM() failed to decode, try decodeCookieAESSHA()
3902 if ((r == null) && (obj.args.cookieencoding == null) && (cookie.length != 64) && ((cookie == cookie.toLowerCase()) || (cookie == cookie.toUpperCase()))) {
3903 obj.debug('cookie', 'Upper/Lowercase cookie, try "CookieEncoding":"hex" in settings section of config.json.');
3904 console.log('Upper/Lowercase cookie, try "CookieEncoding":"hex" in settings section of config.json.');
3905 }
3906 if ((r != null) && (typeof r.once == 'string') && (r.once.length > 0)) {
3907 // This cookie must only be used once.
3908 if (timeout == null) { timeout = 2; }
3909 if (obj.cookieUseOnceTable[r.once] == null) {
3910 const ctimeout = (((r.expire) == null || (typeof r.expire != 'number')) ? (r.time + ((timeout + 3) * 60000)) : (r.time + ((r.expire + 3) * 60000)));
3911
3912 // Store the used cookie in RAM
3913 obj.cookieUseOnceTable[r.once] = ctimeout;
3914
3915 // Store the used cookie in the database
3916 // TODO
3917
3918 // Send the used cookie to peer servers
3919 // TODO
3920
3921 // Clean up the used table
3922 if (++obj.cookieUseOnceTableCleanCounter > 20) {
3923 const now = Date.now();
3924 for (var i in obj.cookieUseOnceTable) { if (obj.cookieUseOnceTable[i] < now) { delete obj.cookieUseOnceTable[i]; } }
3925 obj.cookieUseOnceTableCleanCounter = 0;
3926 }
3927 } else { return null; }
3928 }
3929 return r;
3930 }
3931
3932 // 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)
3933 obj.decodeCookieAESGCM = function (cookie, key, timeout) {
3934 try {
3935 if (key == null) { key = obj.serverKey; }
3936 cookie = Buffer.from(cookie.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
3937 const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), cookie.slice(0, 12));
3938 decipher.setAuthTag(cookie.slice(12, 28));
3939 const o = JSON.parse(decipher.update(cookie.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
3940 if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { obj.debug('cookie', 'ERR: Bad cookie due to invalid time'); return null; }
3941 o.time = o.time * 1000; // Decode the cookie creation time
3942 o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds)
3943 if ((o.expire) == null || (typeof o.expire != 'number')) {
3944 // Use a fixed cookie expire time
3945 if (timeout == null) { timeout = 2; }
3946 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)
3947 } else {
3948 // An expire time is included in the cookie (in minutes), use this.
3949 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)
3950 }
3951 obj.debug('cookie', 'Decoded AESGCM cookie: ' + JSON.stringify(o));
3952 return o;
3953 } catch (ex) { obj.debug('cookie', 'ERR: Bad AESGCM cookie due to exception: ' + ex); return -1; }
3954 };
3955
3956 // 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)
3957 // We do this because poor .NET does not support AES256-GCM.
3958 obj.decodeCookieAESSHA = function (cookie, key, timeout) {
3959 try {
3960 if (key == null) { key = obj.serverKey; }
3961 if (key.length < 80) { return null; }
3962 cookie = Buffer.from(cookie.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
3963 const decipher = obj.crypto.createDecipheriv('aes-256-cbc', key.slice(48, 80), cookie.slice(0, 16));
3964 const rawmsg = decipher.update(cookie.slice(16), 'binary', 'binary') + decipher.final('binary');
3965 const hmac = obj.crypto.createHmac('sha384', key.slice(0, 48));
3966 hmac.update(rawmsg.slice(48));
3967 if (Buffer.compare(hmac.digest(), Buffer.from(rawmsg.slice(0, 48))) == false) { return null; }
3968 const o = JSON.parse(rawmsg.slice(48).toString('utf8'));
3969 if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { obj.debug('cookie', 'ERR: Bad cookie due to invalid time'); return null; }
3970 o.time = o.time * 1000; // Decode the cookie creation time
3971 o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds)
3972 if ((o.expire) == null || (typeof o.expire != 'number')) {
3973 // Use a fixed cookie expire time
3974 if (timeout == null) { timeout = 2; }
3975 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)
3976 } else {
3977 // An expire time is included in the cookie (in minutes), use this.
3978 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)
3979 }
3980 obj.debug('cookie', 'Decoded AESSHA cookie: ' + JSON.stringify(o));
3981 return o;
3982 } catch (ex) { obj.debug('cookie', 'ERR: Bad AESSHA cookie due to exception: ' + ex); return null; }
3983 };
3984
3985 // Debug
3986 obj.debug = function (source, ...args) {
3987 // Send event to console
3988 if ((obj.debugSources != null) && ((obj.debugSources == '*') || (obj.debugSources.indexOf(source) >= 0))) { console.log(source.toUpperCase() + ':', ...args); }
3989
3990 // Send event to log file
3991 if (obj.config.settings && obj.config.settings.log) {
3992 if (typeof obj.args.log == 'string') { obj.args.log = obj.args.log.split(','); }
3993 if ((obj.args.log.indexOf(source) >= 0) || (obj.args.log[0] == '*')) {
3994 const d = new Date();
3995 if (obj.xxLogFile == null) {
3996 try {
3997 obj.xxLogFile = obj.fs.openSync(obj.getConfigFilePath('log.txt'), 'a+', 0o666);
3998 obj.fs.writeSync(obj.xxLogFile, '---- Log start at ' + new Date().toLocaleString() + ' ----\r\n');
3999 obj.xxLogDateStr = d.toLocaleDateString();
4000 } catch (ex) { }
4001 }
4002 if (obj.xxLogFile != null) {
4003 try {
4004 if (obj.xxLogDateStr != d.toLocaleDateString()) { obj.xxLogDateStr = d.toLocaleDateString(); obj.fs.writeSync(obj.xxLogFile, '---- ' + d.toLocaleDateString() + ' ----\r\n'); }
4005 const formattedArgs = args.map(function (arg) { return (typeof arg === 'object' && arg !== null) ? JSON.stringify(arg) : arg; });
4006 obj.fs.writeSync(obj.xxLogFile, new Date().toLocaleTimeString() + ' - ' + source + ': ' + formattedArgs.join(', ') + '\r\n');
4007 } catch (ex) { }
4008 }
4009 }
4010 }
4011
4012 // Send the event to logged in administrators
4013 if ((obj.debugRemoteSources != null) && ((obj.debugRemoteSources == '*') || (obj.debugRemoteSources.indexOf(source) >= 0))) {
4014 var sendcount = 0;
4015 for (var sessionid in obj.webserver.wssessions2) {
4016 const ws = obj.webserver.wssessions2[sessionid];
4017 if ((ws != null) && (ws.userid != null)) {
4018 const user = obj.webserver.users[ws.userid];
4019 if ((user != null) && (user.siteadmin == 4294967295)) {
4020 try { ws.send(JSON.stringify({ action: 'trace', source: source, args: args, time: Date.now() })); sendcount++; } catch (ex) { }
4021 }
4022 }
4023 }
4024 if (sendcount == 0) { obj.debugRemoteSources = null; } // If there are no listeners, remove debug sources.
4025 }
4026 };
4027
4028 // Update server state. Writes a server state file.
4029 const meshServerState = {};
4030 obj.updateServerState = function (name, val) {
4031 //console.log('updateServerState', name, val);
4032 try {
4033 if ((name != null) && (val != null)) {
4034 var changed = false;
4035 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; } } }
4036 if (changed == false) return;
4037 }
4038 var r = 'time=' + Date.now() + '\r\n';
4039 for (var i in meshServerState) { r += (i + '=' + meshServerState[i] + '\r\n'); }
4040 try {
4041 obj.fs.writeFileSync(obj.getConfigFilePath('serverstate.txt'), r); // Try to write the server state, this may fail if we don't have permission.
4042 } catch (ex) { obj.serverSelfWriteAllowed = false; }
4043 } catch (ex) { } // Do nothing since this is not a critical feature.
4044 };
4045
4046 // Read a list of IP addresses from a file
4047 function readIpListFromFile(arg) {
4048 if ((typeof arg != 'string') || (!arg.startsWith('file:'))) return arg;
4049 var lines = null;
4050 try { lines = obj.fs.readFileSync(obj.path.join(obj.datapath, arg.substring(5))).toString().split(/\r?\n/).join('\r').split('\r'); } catch (ex) { }
4051 if (lines == null) return null;
4052 const validLines = [];
4053 for (var i in lines) {
4054 const line = lines[i].trim();
4055 if (line.length === 0) continue;
4056 if (line.charAt(0) === '#') continue;
4057 const parts = line.split('#');
4058 const candidate = parts[0].trim();
4059 if (candidate.length > 0 && candidate.indexOf('@') === -1 && (candidate.indexOf('.') > -1 || candidate.charAt(0) === ':')) {
4060 validLines.push(candidate);
4061 }
4062 }
4063 return validLines;
4064 }
4065
4066 // Logging funtions
4067 function logException(e) { e += ''; logErrorEvent(e); }
4068 function logInfoEvent(msg) { if (obj.servicelog != null) { obj.servicelog.info(msg); } console.log(msg); }
4069 function logWarnEvent(msg) { if (obj.servicelog != null) { obj.servicelog.warn(msg); } console.log(msg); }
4070 function logErrorEvent(msg) { if (obj.servicelog != null) { obj.servicelog.error(msg); } console.error(msg); }
4071 obj.getServerWarnings = function () { return serverWarnings; }
4072 // TODO: migrate from other addServerWarning function and add timestamp
4073 obj.addServerWarning = function (msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
4074
4075 // auth.log functions
4076 obj.authLog = function (server, msg, args) {
4077 if (typeof msg != 'string') return;
4078 var str = msg;
4079 if (args != null) {
4080 if (typeof args.sessionid == 'string') { str += ', SessionID: ' + args.sessionid; }
4081 if (typeof args.useragent == 'string') { const userAgentInfo = obj.webserver.getUserAgentInfo(args.useragent); str += ', Browser: ' + userAgentInfo.browserStr + ', OS: ' + userAgentInfo.osStr; }
4082 }
4083 obj.debug('authlog', str);
4084 if (obj.syslogauth != null) { try { obj.syslogauth.log(obj.syslogauth.LOG_INFO, str); } catch (ex) { } }
4085 if (obj.authlogfile != null) { // Write authlog to file
4086 try {
4087 const d = new Date(), month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()];
4088 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');
4089 obj.fs.write(obj.authlogfile, str, function (err, written, string) { if (err) { console.error(err); } });
4090 } catch (ex) { console.error(ex); }
4091 }
4092 }
4093
4094 // Return the path of a file into the meshcentral-data path
4095 obj.getConfigFilePath = function (filename) {
4096 if ((obj.config != null) && (obj.config.configfiles != null) && (obj.config.configfiles[filename] != null) && (typeof obj.config.configfiles[filename] == 'string')) {
4097 //console.log('getConfigFilePath(\"' + filename + '\") = ' + obj.config.configfiles[filename]);
4098 return obj.config.configfiles[filename];
4099 }
4100 //console.log('getConfigFilePath(\"' + filename + '\") = ' + obj.path.join(obj.datapath, filename));
4101 return obj.path.join(obj.datapath, filename);
4102 };
4103
4104 return obj;
4105}
4106
4107// Resolve a list of names, call back with list of failed resolves.
4108function checkResolveAll(names, func) {
4109 const dns = require('dns'), state = { func: func, count: names.length, err: null };
4110 for (var i in names) {
4111 dns.lookup(names[i], { all: true }, function (err, records) {
4112 if (err != null) { if (this.state.err == null) { this.state.err = [this.name]; } else { this.state.err.push(this.name); } }
4113 if (--this.state.count == 0) { this.state.func(this.state.err); }
4114 }.bind({ name: names[i], state: state }))
4115 }
4116}
4117
4118// Resolve a list of domains to IP addresses, return a flat array of IPs.
4119async function resolveDomainsToIps(originalArray) {
4120 if (!Array.isArray(originalArray)) { return undefined; }
4121 const flatResult = [];
4122 for (const item of originalArray) {
4123 if (new require('ipcheck')(item).valid) {
4124 flatResult.push(item);
4125 continue;
4126 }
4127 try {
4128 const results = await require('dns').promises.lookup(item, { all: true });
4129 flatResult.push(...results.map(r => r.address));
4130 } catch (err) {
4131 console.log(`Could not resolve ${item}`);
4132 }
4133 }
4134 if (flatResult.length == 0) { return undefined; }
4135 return flatResult;
4136}
4137
4138// Return the server configuration
4139function getConfig(createSampleConfig) {
4140 // Figure out the datapath location
4141 var i, datapath = null;
4142 const fs = require('fs'), path = require('path'), args = require('minimist')(process.argv.slice(2));
4143 if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
4144 datapath = path.join(__dirname, '../../meshcentral-data');
4145 } else {
4146 datapath = path.join(__dirname, '../meshcentral-data');
4147 }
4148 if (args.datapath) { datapath = args.datapath; }
4149 try { fs.mkdirSync(datapath); } catch (ex) { }
4150
4151 // Read configuration file if present and change arguments.
4152 var config = {}, configFilePath = path.join(datapath, 'config.json');
4153 if (args.configfile) { configFilePath = common.joinPath(datapath, args.configfile); }
4154 if (fs.existsSync(configFilePath)) {
4155 // Load and validate the configuration file
4156 try { config = require(configFilePath); } catch (ex) { console.log('ERROR: Unable to parse ' + configFilePath + '.'); return null; }
4157 if (config.domains == null) { config.domains = {}; }
4158 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; } }
4159 } else {
4160 if (createSampleConfig === true) {
4161 // Copy the "sample-config.json" to give users a starting point
4162 const sampleConfigPath = path.join(__dirname, 'sample-config.json');
4163 if (fs.existsSync(sampleConfigPath)) { fs.createReadStream(sampleConfigPath).pipe(fs.createWriteStream(configFilePath)); }
4164 }
4165 }
4166
4167 // Set the command line arguments to the config file if they are not present
4168 if (!config.settings) { config.settings = {}; }
4169 for (i in args) { config.settings[i] = args[i]; }
4170
4171 // Lower case all keys in the config file
4172 try {
4173 require('./common.js').objKeysToLower(config, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
4174 } catch (ex) {
4175 console.log('CRITICAL ERROR: Unable to access the file \"./common.js\".\r\nCheck folder & file permissions.');
4176 process.exit();
4177 }
4178
4179 return config;
4180}
4181
4182// Check if a list of modules are present and install any missing ones
4183function InstallModules(modules, args, func) {
4184 var missingModules = [];
4185 if (modules.length > 0) {
4186 const dependencies = require('./package.json').dependencies;
4187 for (var i in modules) {
4188 // Modules may contain a version tag (foobar@1.0.0), remove it so the module can be found using require
4189 const moduleNameAndVersion = modules[i];
4190 const moduleInfo = moduleNameAndVersion.split('@', 3);
4191 var moduleName = null;
4192 var moduleVersion = null;
4193 if(moduleInfo.length == 1){ // normal package without version
4194 moduleName = moduleInfo[0];
4195 } else if (moduleInfo.length == 2) { // normal package with a version OR custom repo package with no version
4196 moduleName = moduleInfo[0] === '' ? moduleNameAndVersion : moduleInfo[0];
4197 moduleVersion = moduleInfo[0] === '' ? null : moduleInfo[1];
4198 } else if (moduleInfo.length == 3) { // custom repo package and package with a version
4199 moduleName = "@" + moduleInfo[1];
4200 moduleVersion = moduleInfo[2];
4201 }
4202 try {
4203 // Does the module need a specific version?
4204 if (moduleVersion) {
4205 var versionMatch = false;
4206 var modulePath = null;
4207 // This is the first way to test if a module is already installed.
4208 try { versionMatch = (require(`${moduleName}/package.json`).version == moduleVersion) } catch (ex) {
4209 if (ex.code == "ERR_PACKAGE_PATH_NOT_EXPORTED") { modulePath = ("" + ex).split(' ').at(-1); } else { throw new Error(); }
4210 }
4211 // If the module is not installed, but we get the ERR_PACKAGE_PATH_NOT_EXPORTED error, try a second way.
4212 if ((versionMatch == false) && (modulePath != null)) {
4213 if (JSON.parse(require('fs').readFileSync(modulePath, 'utf8')).version != moduleVersion) { throw new Error(); }
4214 } else if (versionMatch == false) {
4215 throw new Error();
4216 }
4217 } else {
4218 // For all other modules, do the check here.
4219 // Is the module in package.json? Install exact version.
4220 if (typeof dependencies[moduleName] != null) { moduleVersion = dependencies[moduleName]; }
4221 require(moduleName);
4222 }
4223 } catch (ex) {
4224 missingModules.push(moduleNameAndVersion);
4225 }
4226 }
4227
4228 if (missingModules.length > 0) { if (args.debug) { console.log('Missing Modules: ' + missingModules.join(', ')); } InstallModuleEx(missingModules, args, func); } else { func(); }
4229 }
4230}
4231
4232// 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,
4233// 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.
4234function InstallModuleEx(modulenames, args, func) {
4235 var names = modulenames.join(' ');
4236 console.log('Installing modules', modulenames);
4237 const child_process = require('child_process');
4238 var parentpath = __dirname;
4239 function getCurrentVersion() { try { return JSON.parse(require('fs').readFileSync(require('path').join(__dirname, 'package.json'), 'utf8')).version; } catch (ex) { } return null; } // Fetch server version
4240 //const meshCentralVersion = getCurrentVersion();
4241 //if ((meshCentralVersion != null) && (args.dev == null)) { names = 'meshcentral@' + getCurrentVersion() + ' ' + names; }
4242
4243 // Get the working directory
4244 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, '../..'); }
4245
4246 if (args.debug) { console.log('NPM Command Line: ' + npmpath + ` install --save-exact --no-audit --omit=optional --no-fund ${names}`); }
4247 // always use --save-exact - https://stackoverflow.com/a/64507176/1210734
4248 child_process.exec(npmpath + ` install --save-exact --no-audit --no-optional --omit=optional ${names}`, { maxBuffer: 512000, timeout: 300000, cwd: parentpath }, function (error, stdout, stderr) {
4249 if ((error != null) && (error != '')) {
4250 var mcpath = __dirname;
4251 if (mcpath.endsWith('\\node_modules\\meshcentral') || mcpath.endsWith('/node_modules/meshcentral')) { mcpath = require('path').join(mcpath, '..', '..'); }
4252 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');
4253 process.exit();
4254 return;
4255 }
4256 func();
4257 return;
4258 });
4259}
4260
4261// Detect CTRL-C on Linux and stop nicely
4262process.on('SIGINT', function () { if (meshserver != null) { meshserver.Stop(); meshserver = null; } console.log('Server Ctrl-C exit...'); process.exit(); });
4263
4264// Add a server warning, warnings will be shown to the administrator on the web application
4265// TODO: migrate to obj.addServerWarning?
4266const serverWarnings = [];
4267function addServerWarning(msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
4268
4269/*
4270var ServerWarnings = {
4271 1: "",
4272 2: "Missing WebDAV parameters.",
4273 3: "Unrecognized configuration option \"{0}\".",
4274 4: "WebSocket compression is disabled, this feature is broken in NodeJS v11.11 to v12.15 and v13.2",
4275 5: "Unable to load Intel AMT TLS root certificate for default domain.",
4276 6: "Unable to load Intel AMT TLS root certificate for domain {0}.",
4277 7: "CIRA local FQDN's ignored when server in LAN-only or WAN-only mode.",
4278 8: "Can't have more than 4 CIRA local FQDN's. Ignoring value.",
4279 9: "Agent hash checking is being skipped, this is unsafe.",
4280 10: "Missing Let's Encrypt email address.",
4281 11: "Invalid Let's Encrypt host names.",
4282 12: "Invalid Let's Encrypt names, can't contain a *.",
4283 13: "Unable to setup Let's Encrypt module.",
4284 14: "Invalid Let's Encrypt names, unable to resolve: {0}",
4285 15: "Invalid Let's Encrypt email address, unable to resolve: {0}",
4286 16: "Unable to load CloudFlare trusted proxy IPv6 address list.",
4287 17: "SendGrid server has limited use in LAN mode.",
4288 18: "SMTP server has limited use in LAN mode.",
4289 19: "SMS gateway has limited use in LAN mode.",
4290 20: "Invalid \"LoginCookieEncryptionKey\" in config.json.",
4291 21: "Backup path can't be set within meshcentral-data folder, backup settings ignored.",
4292 22: "Failed to sign agent {0}: {1}",
4293 23: "Unable to load agent icon file: {0}.",
4294 24: "Unable to load agent logo file: {0}.",
4295 25: "This NodeJS version does not support OpenID.",
4296 26: "This NodeJS version does not support Discord.js.",
4297 27: "Firebase now requires a service account JSON file, Firebase disabled."
4298};
4299*/
4300
4301// Load the really basic modules
4302var npmpath = 'npm';
4303var meshserver = null;
4304var childProcess = null;
4305var previouslyInstalledModules = {};
4306function mainStart() {
4307 // Check the NodeJS is version 16 or better.
4308 if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) < 16) { console.log("MeshCentral requires Node v16 or above, current version is " + process.version + "."); return; }
4309
4310 // If running within the node_modules folder, move working directory to the parent of the node_modules folder.
4311 if (__dirname.endsWith('\\node_modules\\meshcentral') || __dirname.endsWith('/node_modules/meshcentral')) { process.chdir(require('path').join(__dirname, '..', '..')); }
4312
4313 // Check for any missing modules.
4314 InstallModules(['minimist'], {}, function () {
4315 // Parse inbound arguments
4316 const args = require('minimist')(process.argv.slice(2));
4317
4318 // Setup the NPM path
4319 if (args.npmpath == null) {
4320 try {
4321 var xnodepath = process.argv[0];
4322 var xnpmpath = require('path').join(require('path').dirname(process.argv[0]), 'npm');
4323 if (require('fs').existsSync(xnodepath) && require('fs').existsSync(xnpmpath)) {
4324 if (xnodepath.indexOf(' ') >= 0) { xnodepath = '"' + xnodepath + '"'; }
4325 if (xnpmpath.indexOf(' ') >= 0) { xnpmpath = '"' + xnpmpath + '"'; }
4326 if (require('os').platform() == 'win32') { npmpath = xnpmpath; } else { npmpath = (xnodepath + ' ' + xnpmpath); }
4327 }
4328 } catch (ex) { console.log(ex); }
4329 } else {
4330 npmpath = args.npmpath;
4331 }
4332
4333 // Get the server configuration
4334 var config = getConfig(false);
4335 if (config == null) { process.exit(); }
4336
4337 // Lowercase the auth value if present
4338 for (var i in config.domains) { if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); } }
4339
4340 // Get the current node version
4341 const verSplit = process.version.substring(1).split('.');
4342 var nodeVersion = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
4343
4344 // Check if RDP support if present
4345 var mstsc = true;
4346 try { require('./rdp') } catch (ex) { mstsc = false; }
4347
4348 // Check if Windows SSPI, LDAP, Passport and YubiKey OTP will be used
4349 var sspi = false;
4350 var ldap = false;
4351 var passport = [];
4352 var allsspi = true;
4353 var yubikey = false;
4354 var ssh = false;
4355 var sessionRecording = false;
4356 var domainCount = 0;
4357 var wildleek = false;
4358 var nodemailer = false;
4359 var sendgrid = false;
4360 var captcha = false;
4361 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; }
4362 if (domainCount == 0) { allsspi = false; }
4363 for (var i in config.domains) {
4364 if (i.startsWith('_')) continue;
4365 if (((config.domains[i].smtp != null) && (config.domains[i].smtp.name != 'console')) || (config.domains[i].sendmail != null)) { nodemailer = true; }
4366 if (config.domains[i].sendgrid != null) { sendgrid = true; }
4367 if (config.domains[i].yubikey != null) { yubikey = true; }
4368 if (config.domains[i].auth == 'ldap') { ldap = true; }
4369 if (mstsc == false) { config.domains[i].mstsc = false; }
4370 if (config.domains[i].ssh == true) { ssh = true; }
4371 if ((typeof config.domains[i].authstrategies == 'object')) {
4372 if (passport.indexOf('passport') == -1) { passport.push('passport@0.7.0','connect-flash@0.1.1'); } // Passport v0.6.0 requires a patch, see https://github.com/jaredhanson/passport/issues/904 and include connect-flash here to display errors
4373 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@1.0.4'); }
4374 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@2.0.0'); }
4375 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@0.1.12'); }
4376 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@0.1.0'); passport.push('jwt-simple@0.5.6'); }
4377 if ((typeof config.domains[i].authstrategies.oidc == 'object') && (passport.indexOf('openid-client@5.7.1') == -1)) {
4378 if ((nodeVersion >= 17)
4379 || ((Math.floor(nodeVersion) == 16) && (nodeVersion >= 16.13))
4380 || ((Math.floor(nodeVersion) == 14) && (nodeVersion >= 14.15))
4381 || ((Math.floor(nodeVersion) == 12) && (nodeVersion >= 12.19))) {
4382 passport.push('openid-client@5.7.1');
4383 } else {
4384 addServerWarning('This NodeJS version does not support OpenID Connect on MeshCentral.', 25);
4385 delete config.domains[i].authstrategies.oidc;
4386 }
4387 }
4388 if ((typeof config.domains[i].authstrategies.saml == 'object') || (typeof config.domains[i].authstrategies.jumpcloud == 'object')) { passport.push('passport-saml'); }
4389 }
4390 if (config.domains[i].sessionrecording != null) { sessionRecording = true; }
4391 if ((config.domains[i].passwordrequirements != null) && (config.domains[i].passwordrequirements.bancommonpasswords == true)) { wildleek = true; }
4392 if ((config.domains[i].newaccountscaptcha != null) && (config.domains[i].newaccountscaptcha !== false)) { captcha = true; }
4393 if ((typeof config.domains[i].duo2factor == 'object') && (passport.indexOf('@duosecurity/duo_universal') == -1)) { passport.push('@duosecurity/duo_universal@2.1.0'); }
4394 }
4395
4396 // Build the list of required modules
4397 // NOTE: ALL MODULES MUST HAVE A VERSION NUMBER AND THE VERSION MUST MATCH THAT USED IN Dockerfile
4398 var modules = ['archiver@7.0.1', 'body-parser@1.20.4', 'cbor@5.2.0', 'compression@1.8.1', 'cookie-session@2.1.1', 'express@4.22.1', 'express-handlebars@7.1.3', 'express-ws@5.0.2', 'ipcheck@0.1.0', 'minimist@1.2.8', 'multiparty@4.2.3', '@seald-io/nedb@4.1.2', 'node-forge@1.3.2', 'ua-parser-js@1.0.40', 'ua-client-hints-js@0.1.2', 'ws@8.18.3', 'yauzl@2.10.0'];
4399 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
4400 if (ldap == true) { modules.push('ldapauth-fork@5.0.5'); }
4401 if (ssh == true) { modules.push('ssh2@1.17.0'); }
4402 if (passport != null) { modules.push(...passport); }
4403 if (captcha == true) { modules.push('svg-captcha@1.4.0'); }
4404
4405 if (sessionRecording == true) { modules.push('image-size@2.0.2'); } // Need to get the remote desktop JPEG sizes to index the recording file.
4406 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.
4407 if (config.settings.mqtt != null) { modules.push('aedes@0.51.3'); } // Add MQTT Modules
4408 if (config.settings.mysql != null) { modules.push('mysql2@3.15.1'); } // Add MySQL.
4409 //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/)
4410 if (config.settings.mongodb != null) { modules.push('mongodb@4.17.2'); modules.push('@mongodb-js/saslprep@1.3.1')} // Add MongoDB, official driver.
4411 if (config.settings.postgres != null) { modules.push('pg@8.16.3') } // Add Postgres, official driver.
4412 if (config.settings.mariadb != null) { modules.push('mariadb@3.4.5'); } // Add MariaDB, official driver.
4413 if (config.settings.acebase != null) { modules.push('acebase@1.29.5'); } // Add AceBase, official driver.
4414 if (config.settings.sqlite3 != null) { modules.push('sqlite3@5.1.7'); } // Add sqlite3, official driver.
4415 if (config.settings.vault != null) { modules.push('node-vault@0.10.5'); } // Add official HashiCorp's Vault module.
4416 const hasExistingProxy = process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy'];
4417 if (((config.settings.plugins != null) && (config.settings.plugins.proxy != null)) || (hasExistingProxy)) { modules.push('https-proxy-agent@7.0.6'); } // Required for HTTP/HTTPS proxy support
4418 else if (config.settings.xmongodb != null) { modules.push('mongojs@3.1.0'); } // Add MongoJS, old driver.
4419 if (nodemailer || ((config.smtp != null) && (config.smtp.name != 'console')) || (config.sendmail != null)) { modules.push('nodemailer@6.10.1'); } // Add SMTP support
4420 if (sendgrid || (config.sendgrid != null)) { modules.push('@sendgrid/mail@8.1.6'); } // Add SendGrid support
4421 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('html-minifier-terser@7.2.0'); } // Translation support
4422 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)
4423 if (config.settings.prometheus != null) { modules.push('prom-client@15.1.3'); } // Add Prometheus Metrics support
4424
4425 if (typeof config.settings.autobackup == 'object') {
4426 // Setup encrypted zip support if needed
4427 if (config.settings.autobackup.zippassword) { modules.push('archiver-zip-encrypted@2.0.0'); }
4428 // Enable Google Drive Support
4429 if (typeof config.settings.autobackup.googledrive == 'object') { modules.push('googleapis@128.0.0'); }
4430 // Enable WebDAV Support
4431 if (typeof config.settings.autobackup.webdav == 'object') {
4432 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@5.8.0'); }
4433 }
4434 // Enable S3 Support
4435 if (typeof config.settings.autobackup.s3 == 'object') { modules.push('minio@8.0.6'); }
4436 }
4437
4438 // Setup common password blocking
4439 if (wildleek == true) { modules.push('wildleek@2.0.0'); }
4440
4441 // Setup 2nd factor authentication
4442 if (config.settings.no2factorauth !== true) {
4443 // Setup YubiKey OTP if configured
4444 if (yubikey == true) { modules.push('yub@0.11.1'); } // Add YubiKey OTP support (replaced yubikeyotp due to form-data issues)
4445 if (allsspi == false) { modules.push('otplib@12.0.1'); } // Google Authenticator support (v10 supports older NodeJS versions).
4446 }
4447
4448 // Desktop multiplexor support
4449 if (config.settings.desktopmultiplex === true) { modules.push('image-size@2.0.2'); }
4450
4451 // SMS support
4452 if (config.sms != null) {
4453 if (config.sms.provider == 'twilio') { modules.push('twilio@4.23.0'); }
4454 if (config.sms.provider == 'plivo') { modules.push('plivo@4.75.1'); }
4455 if (config.sms.provider == 'telnyx') { modules.push('telnyx@1.25.5'); }
4456 }
4457
4458 // Messaging support
4459 if (config.messaging != null) {
4460 if (config.messaging.telegram != null) { modules.push('telegram@2.26.22'); modules.push('input@1.0.1'); }
4461 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); } }
4462 if (config.messaging.xmpp != null) { modules.push('@xmpp/client@0.13.6'); }
4463 if (config.messaging.pushover != null) { modules.push('node-pushover@1.0.0'); }
4464 if (config.messaging.zulip != null) { modules.push('zulip@0.1.0'); }
4465 }
4466
4467 // Setup web based push notifications
4468 if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) { modules.push('web-push@3.6.7'); }
4469
4470 // Firebase Support
4471 if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) { modules.push('firebase-admin@12.7.0'); }
4472
4473 // Syslog support
4474 if ((require('os').platform() != 'win32') && (config.settings.syslog || config.settings.syslogjson)) { modules.push('modern-syslog@1.2.0'); }
4475 if (config.settings.syslogtcp) { modules.push('syslog@0.1.1-1'); }
4476
4477 // Setup heapdump support if needed, useful for memory leak debugging
4478 // https://www.arbazsiddiqui.me/a-practical-guide-to-memory-leaks-in-nodejs/
4479 if (config.settings.heapdump === true) { modules.push('heapdump@0.3.15'); }
4480
4481 // Install any missing modules and launch the server
4482 InstallModules(modules, args, function () {
4483 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; } }
4484 meshserver = CreateMeshCentralServer(config, args);
4485 meshserver.Start();
4486 });
4487
4488 // On exit, also terminate the child process if applicable
4489 process.on('exit', function () { if (childProcess) { childProcess.kill(); childProcess = null; } });
4490
4491 // If our parent exits, we also exit
4492 if (args.launch) {
4493 process.stderr.on('end', function () { process.exit(); });
4494 process.stdout.on('end', function () { process.exit(); });
4495 process.stdin.on('end', function () { process.exit(); });
4496 process.stdin.on('data', function (data) { });
4497 }
4498 });
4499}
4500
4501if (require.main === module) {
4502 mainStart(); // Called directly, launch normally.
4503} else {
4504 module.exports.mainStart = mainStart; // Required as a module, useful for winservice.js
4505}