master
js 4,460 lines 321 KB
Raw
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
17 const common = require('./common.js');
18
19 // If app metrics is available
20 if (process.argv[2] == '--launch') { try { require('appmetrics-dash').monitor({ url: '/', title: 'MeshCentral', port: 88, host: '127.0.0.1' }); } catch (ex) { } }
21
22 function 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; delete 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.domain, 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]) < 20) { addServerWarning("MeshCentral will require Node v20 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 const trustedproxyIsCloudflareString = (obj.args.trustedproxy != null) && (typeof obj.args.trustedproxy == 'string') && (obj.args.trustedproxy.toLowerCase() == 'cloudflare');
1823 const trustedproxyIsCloudflareArray = Array.isArray(obj.args.trustedproxy) && obj.args.trustedproxy.some(function (x) { return (typeof x == 'string') && (x.toLowerCase() == 'cloudflare'); });
1824 if (trustedproxyIsCloudflareString || trustedproxyIsCloudflareArray) {
1825 obj.config.settings.extrascriptsrc = 'ajax.cloudflare.com'; // Add CloudFlare as a trusted script source. This allows for CloudFlare's RocketLoader feature.
1826 // Preserve any non-'cloudflare' entries already in the array
1827 const existingProxies = trustedproxyIsCloudflareArray ? obj.args.trustedproxy.filter(function (x) { return !(typeof x == 'string' && x.toLowerCase() == 'cloudflare'); }) : [];
1828 obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v4', null, function (url, data, tag) {
1829 if (data != null) {
1830 const newProxies = existingProxies.slice();
1831 const ipranges = data.split('\n');
1832 for (var i in ipranges) { if (ipranges[i] != '') { newProxies.push(ipranges[i]); } }
1833 obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v6', null, function (url, data, tag) {
1834 if (data != null) {
1835 var ipranges = data.split('\n');
1836 for (var i in ipranges) { if (ipranges[i] != '') { newProxies.push(ipranges[i]); } }
1837 } else {
1838 addServerWarning("Unable to load CloudFlare trusted proxy IPv6 address list.", 16);
1839 }
1840 obj.args.trustedproxy = newProxies;
1841 obj.StartEx4(); // Keep going
1842 });
1843 } else {
1844 addServerWarning("Unable to load CloudFlare trusted proxy IPv4 address list.", 16);
1845 obj.StartEx4(); // Keep going
1846 }
1847 });
1848 } else {
1849 obj.StartEx4(); // Keep going
1850 }
1851 }
1852
1853 // Start the server with the given certificates
1854 obj.StartEx4 = function () {
1855 var i;
1856
1857 // If the certificate is un-configured, force LAN-only mode
1858 if (obj.certificates.CommonName.indexOf('.') == -1) { /*console.log('Server name not configured, running in LAN-only mode.');*/ obj.args.lanonly = true; }
1859
1860 // Write server version and run mode
1861 const productionMode = (process.env.NODE_ENV && (process.env.NODE_ENV == 'production'));
1862 const runmode = (obj.args.lanonly ? 2 : (obj.args.wanonly ? 1 : 0));
1863 console.log("MeshCentral v" + getCurrentVersion() + ', ' + (["Hybrid (LAN + WAN) mode", "WAN mode", "LAN mode"][runmode]) + (productionMode ? ", Production mode." : '.'));
1864
1865 // Check that no sub-domains have the same DNS as the parent
1866 for (i in obj.config.domains) {
1867 if ((obj.config.domains[i].dns != null) && (obj.certificates.CommonName.toLowerCase() === obj.config.domains[i].dns.toLowerCase())) {
1868 console.log("ERROR: Server sub-domain can't have same DNS name as the parent."); process.exit(0); return;
1869 }
1870 }
1871
1872 // Load the list of MeshCentral tools
1873 obj.updateMeshTools();
1874
1875 // Load MeshAgent translation strings
1876 try {
1877 var translationpath = obj.path.join(__dirname, 'agents', 'agent-translations.json');
1878 const translationpath2 = obj.path.join(obj.datapath, 'agents', 'agent-translations.json');
1879 if (obj.fs.existsSync(translationpath2)) { translationpath = translationpath2; } // If the agent is present in "meshcentral-data/agents", use that one instead.
1880 var translations = JSON.parse(obj.fs.readFileSync(translationpath).toString());
1881 if (translations['zh-chs']) { translations['zh-hans'] = translations['zh-chs']; delete translations['zh-chs']; }
1882 if (translations['zh-cht']) {
1883 translations['zh-hant'] = translations['zh-cht'];
1884 translations['zh-tw'] = translations['zh-cht'];
1885 translations['zh-hk'] = translations['zh-cht'];
1886 delete translations['zh-cht'];
1887 }
1888 // If there is domain customizations to the agent strings, do this here.
1889 for (var i in obj.config.domains) {
1890 var domainTranslations = translations;
1891 if ((typeof obj.config.domains[i].agentcustomization == 'object') && (typeof obj.config.domains[i].agentcustomization.installtext == 'string')) {
1892 domainTranslations = Object.assign({}, domainTranslations); // Shallow clone
1893 for (var j in domainTranslations) { delete domainTranslations[j].description; }
1894 domainTranslations.en.description = obj.config.domains[i].agentcustomization.installtext;
1895 }
1896 obj.config.domains[i].agentTranslations = JSON.stringify(domainTranslations);
1897 }
1898 } catch (ex) { }
1899
1900 // Load any domain specific agents
1901 for (var i in obj.config.domains) { if ((i != '') && (obj.config.domains[i].share == null)) { obj.updateMeshAgentsTable(obj.config.domains[i], function () { }); } }
1902
1903 // Load the list of mesh agents and install scripts
1904 if ((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true)) { for (i in obj.meshAgentsArchitectureNumbers) { obj.meshAgentsArchitectureNumbers[i].update = false; } }
1905 obj.signMeshAgents(obj.config.domains[''], function () {
1906 obj.updateMeshAgentsTable(obj.config.domains[''], function () {
1907 obj.updateMeshAgentInstallScripts();
1908
1909 // Setup and start the web server
1910 obj.crypto.randomBytes(48, function (err, buf) {
1911 // Setup Mesh Multi-Server if needed
1912 obj.multiServer = require('./multiserver.js').CreateMultiServer(obj, obj.args);
1913 if (obj.multiServer != null) {
1914 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; }
1915 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; }
1916 obj.serverId = obj.multiServer.serverid;
1917 for (var serverid in obj.config.peers.servers) { obj.peerConnectivityByNode[serverid] = {}; }
1918 }
1919
1920 // If the server is set to "nousers", allow only loopback unless IP filter is set
1921 if ((obj.args.nousers == true) && (obj.args.userallowedip == null)) { obj.args.userallowedip = "::1,127.0.0.1"; }
1922
1923 // Set the session length to 60 minutes if not set and set a random key if needed
1924 if ((obj.args.sessiontime != null) && ((typeof obj.args.sessiontime != 'number') || (obj.args.sessiontime < 1))) { delete obj.args.sessiontime; }
1925 if (typeof obj.args.sessionkey != 'string') { obj.args.sessionkey = buf.toString('hex').toUpperCase(); }
1926
1927 // Create MQTT Broker to hook into webserver and mpsserver
1928 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); }
1929
1930 // Start the web server and if needed, the redirection web server.
1931 obj.webserver = require('./webserver.js').CreateWebServer(obj, obj.db, obj.args, obj.certificates, obj.StartEx5);
1932 if (obj.redirserver != null) { obj.redirserver.hookMainWebServer(obj.certificates); }
1933
1934 // Change RelayDNS to a array of strings
1935 if (typeof obj.args.relaydns == 'string') { obj.args.relaydns = [obj.args.relaydns]; }
1936 if (obj.common.validateStrArray(obj.args.relaydns, 1) == false) { delete obj.args.relaydns; }
1937
1938 // Start the HTTP relay web server if needed
1939 if ((obj.args.relaydns == null) && (typeof obj.args.relayport == 'number') && (obj.args.relayport != 0)) {
1940 obj.webrelayserver = require('./webrelayserver.js').CreateWebRelayServer(obj, obj.db, obj.args, obj.certificates, function () { });
1941 }
1942
1943 // Update proxy certificates
1944 if (obj.supportsProxyCertificatesRequest == true) { obj.updateProxyCertificates(true); }
1945
1946 // Setup the Intel AMT event handler
1947 obj.amtEventHandler = require('./amtevents.js').CreateAmtEventsHandler(obj);
1948
1949 // Setup the Intel AMT local network scanner
1950 if (obj.args.wanonly != true) {
1951 if (obj.args.amtscanner != false) { obj.amtScanner = require('./amtscanner.js').CreateAmtScanner(obj).start(); }
1952 if (obj.args.meshscanner != false) { obj.meshScanner = require('./meshscanner.js').CreateMeshScanner(obj).start(); }
1953 }
1954
1955 // Setup and start the MPS server
1956 obj.mpsserver = require('./mpsserver.js').CreateMpsServer(obj, obj.db, obj.args, obj.certificates);
1957
1958 // Setup the Intel AMT manager
1959 if (obj.args.amtmanager !== false) {
1960 obj.amtManager = require('./amtmanager.js').CreateAmtManager(obj);
1961 }
1962
1963 // Setup and start the legacy swarm server
1964 if ((obj.certificates.swarmserver != null) && (obj.args.swarmport != null) && (obj.args.swarmport !== 0)) {
1965 obj.swarmserver = require('./swarmserver.js').CreateSwarmServer(obj, obj.db, obj.args, obj.certificates);
1966 }
1967
1968 // Setup the main email server
1969 if (obj.config.sendgrid != null) {
1970 // Sendgrid server
1971 obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
1972 obj.mailserver.verify();
1973 if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode.", 17); }
1974 } else if (obj.config.smtp != null) {
1975 // SMTP server
1976 obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
1977 obj.mailserver.verify();
1978 if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
1979 } else if (obj.config.sendmail != null) {
1980 // Sendmail server
1981 obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
1982 obj.mailserver.verify();
1983 if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
1984 }
1985
1986 // Setup the email server for each domain
1987 for (i in obj.config.domains) {
1988 if (obj.config.domains[i].sendgrid != null) {
1989 // Sendgrid server
1990 obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
1991 obj.config.domains[i].mailserver.verify();
1992 if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode.", 17); }
1993 } else if ((obj.config.domains[i].smtp != null) && (obj.config.domains[i].smtp.host != null) && (obj.config.domains[i].smtp.from != null)) {
1994 // SMTP server
1995 obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
1996 obj.config.domains[i].mailserver.verify();
1997 if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
1998 } else if (obj.config.domains[i].sendmail != null) {
1999 // Sendmail server
2000 obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
2001 obj.config.domains[i].mailserver.verify();
2002 if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
2003 } else {
2004 // Setup the parent mail server for this domain
2005 if (obj.mailserver != null) { obj.config.domains[i].mailserver = obj.mailserver; }
2006 }
2007 }
2008
2009 // Setup SMS gateway
2010 if (config.sms != null) {
2011 obj.smsserver = require('./meshsms.js').CreateMeshSMS(obj);
2012 if ((obj.smsserver != null) && (obj.args.lanonly == true)) { addServerWarning("SMS gateway has limited use in LAN mode.", 19); }
2013 }
2014
2015 // Setup user messaging
2016 if (config.messaging != null) {
2017 obj.msgserver = require('./meshmessaging.js').CreateServer(obj);
2018 }
2019
2020 // Setup web based push notifications
2021 if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) {
2022 obj.webpush = require('web-push');
2023 var vapidKeys = null;
2024 try { vapidKeys = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, 'vapid.json')).toString()); } catch (ex) { }
2025 if ((vapidKeys == null) || (typeof vapidKeys.publicKey != 'string') || (typeof vapidKeys.privateKey != 'string')) {
2026 console.log("Generating web push VAPID keys...");
2027 vapidKeys = obj.webpush.generateVAPIDKeys();
2028 obj.common.moveOldFiles([obj.path.join(obj.datapath, 'vapid.json')]);
2029 obj.fs.writeFileSync(obj.path.join(obj.datapath, 'vapid.json'), JSON.stringify(vapidKeys));
2030 }
2031 obj.webpush.vapidPublicKey = vapidKeys.publicKey;
2032 obj.webpush.setVapidDetails('mailto:' + config.settings.webpush.email, vapidKeys.publicKey, vapidKeys.privateKey);
2033 if (typeof config.settings.webpush.gcmapi == 'string') { webpush.setGCMAPIKey(config.settings.webpush.gcmapi); }
2034 }
2035
2036 // Get the current node version
2037 const verSplit = process.version.substring(1).split('.');
2038 var nodeVersion = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
2039
2040 // Setup Firebase
2041 if ((config.firebase != null) && (typeof config.firebase.senderid == 'string') && (typeof config.firebase.serverkey == 'string')) {
2042 addServerWarning('Firebase now requires a service account JSON file, Firebase disabled.', 27);
2043 } else if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) {
2044 var serviceAccount;
2045 try { serviceAccount = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, config.firebase.serviceaccountfile)).toString()); } catch (ex) { console.log(ex); }
2046 if (serviceAccount != null) { obj.firebase = require('./firebase').CreateFirebase(obj, serviceAccount); }
2047 } else if ((typeof config.firebaserelay == 'object') && (typeof config.firebaserelay.url == 'string')) {
2048 // Setup the push messaging relay
2049 obj.firebase = require('./firebase').CreateFirebaseRelay(obj, config.firebaserelay.url, config.firebaserelay.key);
2050 } else if (obj.config.settings.publicpushnotifications === true) {
2051 // Setup the Firebase push messaging relay using https://alt.meshcentral.com, this is the public push notification server.
2052 obj.firebase = require('./firebase').CreateFirebaseRelay(obj, 'https://alt.meshcentral.com/firebaserelay.aspx');
2053 }
2054
2055 // Setup monitoring
2056 if (obj.config.settings.prometheus != null) { obj.monitoring = require('./monitoring.js').CreateMonitoring(obj, obj.args); }
2057
2058 // Start periodic maintenance
2059 obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 60 * 60); // Run this every hour
2060 //obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 10 * 1); // DEBUG: Run this more often
2061
2062 // Dispatch an event that the server is now running
2063 obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'started', msg: 'Server started' });
2064
2065 // Plugin hook. Need to run something at server startup? This is the place.
2066 if (obj.pluginHandler) { obj.pluginHandler.callHook('server_startup'); }
2067
2068 // Setup the login cookie encryption key
2069 if ((obj.config) && (obj.config.settings) && (typeof obj.config.settings.logincookieencryptionkey == 'string')) {
2070 // We have a string, hash it and use that as a key
2071 try { obj.loginCookieEncryptionKey = Buffer.from(obj.config.settings.logincookieencryptionkey, 'hex'); } catch (ex) { }
2072 if ((obj.loginCookieEncryptionKey == null) || (obj.loginCookieEncryptionKey.length != 80)) { addServerWarning("Invalid \"LoginCookieEncryptionKey\" in config.json.", 20); obj.loginCookieEncryptionKey = null; }
2073 }
2074
2075 // Login cookie encryption key not set, use one from the database
2076 if (obj.loginCookieEncryptionKey == null) {
2077 obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
2078 if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
2079 obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
2080 } else {
2081 obj.loginCookieEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() });
2082 }
2083 });
2084 }
2085
2086 // Load the invitation link encryption key from the database
2087 obj.db.Get('InvitationLinkEncryptionKey', function (err, docs) {
2088 if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (docs[0].key.length >= 160)) {
2089 obj.invitationLinkEncryptionKey = Buffer.from(docs[0].key, 'hex');
2090 } else {
2091 obj.invitationLinkEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'InvitationLinkEncryptionKey', key: obj.invitationLinkEncryptionKey.toString('hex'), time: Date.now() });
2092 }
2093 });
2094
2095 // Setup Intel AMT hello server
2096 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')) {
2097 obj.amtProvisioningServer = require('./amtprovisioningserver').CreateAmtProvisioningServer(obj, config.settings.amtprovisioningserver);
2098 }
2099
2100 // Start collecting server stats every 5 minutes
2101 obj.trafficStats = obj.webserver.getTrafficStats();
2102 setInterval(function () {
2103 obj.serverStatsCounter++;
2104 var hours = 720; // Start with all events lasting 30 days.
2105 if (((obj.serverStatsCounter) % 2) == 1) { hours = 3; } // Half of the event get removed after 3 hours.
2106 else if ((Math.floor(obj.serverStatsCounter / 2) % 2) == 1) { hours = 8; } // Another half of the event get removed after 8 hours.
2107 else if ((Math.floor(obj.serverStatsCounter / 4) % 2) == 1) { hours = 24; } // Another half of the event get removed after 24 hours.
2108 else if ((Math.floor(obj.serverStatsCounter / 8) % 2) == 1) { hours = 48; } // Another half of the event get removed after 48 hours.
2109 else if ((Math.floor(obj.serverStatsCounter / 16) % 2) == 1) { hours = 72; } // Another half of the event get removed after 72 hours.
2110 const expire = new Date();
2111 expire.setTime(expire.getTime() + (60 * 60 * 1000 * hours));
2112
2113 // Get traffic data
2114 var trafficStats = obj.webserver.getTrafficDelta(obj.trafficStats);
2115 obj.trafficStats = trafficStats.current;
2116
2117 var data = {
2118 time: new Date(),
2119 expire: expire,
2120 mem: process.memoryUsage(),
2121 conn: {
2122 ca: Object.keys(obj.webserver.wsagents).length,
2123 cu: Object.keys(obj.webserver.wssessions).length,
2124 us: Object.keys(obj.webserver.wssessions2).length,
2125 rs: obj.webserver.relaySessionCount,
2126 am: 0
2127 },
2128 traffic: trafficStats.delta
2129 };
2130 try { data.cpu = require('os').loadavg(); } catch (ex) { }
2131 if (obj.mpsserver != null) {
2132 data.conn.amc = 0;
2133 for (var i in obj.mpsserver.ciraConnections) { data.conn.amc += obj.mpsserver.ciraConnections[i].length; }
2134 }
2135 for (var i in obj.connectivityByNode) {
2136 const node = obj.connectivityByNode[i];
2137 if (node && typeof node.connectivity !== 'undefined' && node.connectivity === 4) { data.conn.am++; }
2138 }
2139 if (obj.firstStats === true) { delete obj.firstStats; data.first = true; }
2140 if (obj.multiServer != null) { data.s = obj.multiServer.serverid; }
2141 obj.db.SetServerStats(data); // Save the stats to the database
2142 obj.DispatchEvent(['*'], obj, { action: 'servertimelinestats', data: data }); // Event the server stats
2143 }, 300000);
2144
2145 obj.debug('main', "Server started");
2146 if (obj.args.nousers == true) { obj.updateServerState('nousers', '1'); }
2147 obj.updateServerState('state', "running");
2148
2149 // Setup auto-backup defaults. Unless autobackup is set to false try to make a backup.
2150 if (obj.config.settings.autobackup == false || obj.config.settings.autobackup == 'false') { obj.config.settings.autobackup = {backupintervalhours: -1}; } //block all autobackup functions
2151 else {
2152 if (typeof obj.config.settings.autobackup != 'object') { obj.config.settings.autobackup = {}; };
2153 if (typeof obj.config.settings.autobackup.backupintervalhours != 'number') { obj.config.settings.autobackup.backupintervalhours = 24; };
2154 if (typeof obj.config.settings.autobackup.keeplastdaysbackup != 'number') { obj.config.settings.autobackup.keeplastdaysbackup = 10; };
2155 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; }}
2156 else {obj.config.settings.autobackup.backuphour = -1 };
2157 //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 !!
2158 if (!obj.config.settings.autobackup.backupignorefilesglob) {obj.config.settings.autobackup.backupignorefilesglob = []}
2159 else if (typeof obj.config.settings.autobackup.backupignorefilesglob == 'string') { obj.config.settings.autobackup.backupignorefilesglob = obj.config.settings.autobackup.backupignorefilesglob.replaceAll(', ', ',').split(','); };
2160 if (!obj.config.settings.autobackup.backupskipfoldersglob) {obj.config.settings.autobackup.backupskipfoldersglob = []}
2161 else if (typeof obj.config.settings.autobackup.backupskipfoldersglob == 'string') { obj.config.settings.autobackup.backupskipfoldersglob = obj.config.settings.autobackup.backupskipfoldersglob.replaceAll(', ', ',').split(','); };
2162 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 };
2163 if (typeof obj.config.settings.autobackup.backupname != 'string') { obj.config.settings.autobackup.backupname = 'meshcentral-autobackup-'};
2164 if (typeof obj.config.settings.autobackup.webdav == 'object') {
2165 //make webdav compliant: http://www.webdav.org/specs/rfc4918.html#rfc.section.5.2, http://www.webdav.org/specs/rfc2518.html#METHOD_MKCOL
2166 // So with leading and trailing slash in the foldername, and no double and backslashes
2167 if (typeof obj.config.settings.autobackup.webdav.foldername != 'string') {obj.config.settings.autobackup.webdav.foldername = '/MeshCentral-Backups/'}
2168 else {obj.config.settings.autobackup.webdav.foldername = ('/' + obj.config.settings.autobackup.webdav.foldername + '/').replaceAll("\\", "/").replaceAll("//", "/").replaceAll("//", "/")};
2169 }
2170 }
2171
2172 // Check if the database is capable of performing a backup
2173 obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
2174
2175 // Load Intel AMT passwords from the "amtactivation.log" file
2176 obj.loadAmtActivationLogPasswords(function (amtPasswords) {
2177 obj.amtPasswords = amtPasswords;
2178 });
2179
2180 // Setup users that can see all device groups
2181 if (typeof obj.config.settings.managealldevicegroups == 'string') { obj.config.settings.managealldevicegroups = obj.config.settings.managealldevicegroups.split(','); }
2182 else if (Array.isArray(obj.config.settings.managealldevicegroups) == false) { obj.config.settings.managealldevicegroups = []; }
2183 for (i in obj.config.domains) {
2184 if (Array.isArray(obj.config.domains[i].managealldevicegroups)) {
2185 for (var j in obj.config.domains[i].managealldevicegroups) {
2186 if (typeof obj.config.domains[i].managealldevicegroups[j] == 'string') {
2187 const u = 'user/' + i + '/' + obj.config.domains[i].managealldevicegroups[j];
2188 if (obj.config.settings.managealldevicegroups.indexOf(u) == -1) { obj.config.settings.managealldevicegroups.push(u); }
2189 }
2190 }
2191 }
2192 }
2193 obj.config.settings.managealldevicegroups.sort();
2194
2195 // Start watchdog timer if needed
2196 // 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
2197 // "watchDog": { "interval": 100, "timeout": 150 }
2198 // This will check every 100ms, if the timer is more than 150ms late, it will warn.
2199 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)) {
2200 obj.watchdogtime = Date.now();
2201 obj.watchdogmax = 0;
2202 obj.watchdogmaxtime = null;
2203 obj.watchdogtable = [];
2204 obj.watchdog = setInterval(function () {
2205 const now = Date.now(), delta = now - obj.watchdogtime - config.settings.watchdog.interval;
2206 if (delta > obj.watchdogmax) { obj.watchdogmax = delta; obj.watchdogmaxtime = new Date().toLocaleString(); }
2207 if (delta > config.settings.watchdog.timeout) {
2208 const msg = obj.common.format("Watchdog timer timeout, {0}ms.", delta);
2209 obj.watchdogtable.push(new Date().toLocaleString() + ', ' + delta + 'ms');
2210 while (obj.watchdogtable.length > 10) { obj.watchdogtable.shift(); }
2211 obj.debug('main', msg);
2212 try {
2213 var errlogpath = null;
2214 if (typeof obj.args.mesherrorlogpath == 'string') { errlogpath = obj.path.join(obj.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = obj.getConfigFilePath('mesherrors.txt'); }
2215 obj.fs.appendFileSync(errlogpath, new Date().toLocaleString() + ': ' + msg + '\r\n');
2216 } catch (ex) { console.log('ERROR: Unable to write to mesherrors.txt.'); }
2217 }
2218 obj.watchdogtime = now;
2219 }, config.settings.watchdog.interval);
2220 obj.debug('main', "Started watchdog timer.");
2221 }
2222
2223 });
2224 });
2225 });
2226 };
2227
2228 // Called when the web server finished loading
2229 obj.StartEx5 = function () {
2230 // Setup the email server for each domain
2231 var ipKvmSupport = false;
2232 for (var i in obj.config.domains) { if (obj.config.domains[i].ipkvm == true) { ipKvmSupport = true; } }
2233 if (ipKvmSupport) { obj.ipKvmManager = require('./meshipkvm').CreateIPKVMManager(obj); }
2234
2235 // Run the server start script if present
2236 if (typeof obj.config.settings.runonserverstarted == 'string') {
2237 const child_process = require('child_process');
2238 var parentpath = __dirname;
2239 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, '../..'); }
2240 child_process.exec(obj.config.settings.runonserverstarted + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
2241 }
2242 }
2243
2244 // Refresh any certificate hashs from the reverse proxy
2245 obj.pendingProxyCertificatesRequests = 0;
2246 obj.lastProxyCertificatesRequest = null;
2247 obj.supportsProxyCertificatesRequest = false;
2248 obj.updateProxyCertificates = function (force) {
2249 if (force !== true) {
2250 if ((obj.pendingProxyCertificatesRequests > 0) || (obj.supportsProxyCertificatesRequest == false)) return;
2251 if ((obj.lastProxyCertificatesRequest != null) && ((Date.now() - obj.lastProxyCertificatesRequest) < 120000)) return; // Don't allow this call more than every 2 minutes.
2252 obj.lastProxyCertificatesRequest = Date.now();
2253 }
2254
2255 // Load any domain web certificates
2256 for (var i in obj.config.domains) {
2257 if (obj.config.domains[i].certurl != null) {
2258 // Load web certs
2259 obj.pendingProxyCertificatesRequests++;
2260 var dnsname = obj.config.domains[i].dns;
2261 if ((dnsname == null) && (obj.config.settings.cert != null)) { dnsname = obj.config.settings.cert; }
2262 obj.certificateOperations.loadCertificate(obj.config.domains[i].certurl, dnsname, obj.config.domains[i], function (url, cert, xhostname, xdomain) {
2263 obj.pendingProxyCertificatesRequests--;
2264 if (cert != null) {
2265 // Hash the entire cert
2266 const hash = obj.crypto.createHash('sha384').update(Buffer.from(cert, 'binary')).digest('hex');
2267 if (xdomain.certhash != hash) { // The certificate has changed.
2268 xdomain.certkeyhash = hash;
2269 xdomain.certhash = hash;
2270
2271 try {
2272 // Decode a RSA certificate and hash the public key, if this is not RSA, skip this.
2273 const forgeCert = obj.certificateOperations.forge.pki.certificateFromAsn1(obj.certificateOperations.forge.asn1.fromDer(cert));
2274 xdomain.certkeyhash = obj.certificateOperations.forge.pki.getPublicKeyFingerprint(forgeCert.publicKey, { md: obj.certificateOperations.forge.md.sha384.create(), encoding: 'hex' });
2275 obj.webserver.webCertificateExpire[xdomain.id] = Date.parse(forgeCert.validity.notAfter); // Update certificate expire time
2276 //console.log('V1: ' + xdomain.certkeyhash);
2277 } catch (ex) {
2278 delete obj.webserver.webCertificateExpire[xdomain.id]; // Remove certificate expire time
2279 delete xdomain.certkeyhash;
2280 }
2281
2282 if (obj.webserver) {
2283 obj.webserver.webCertificateHashs[xdomain.id] = obj.webserver.webCertificateFullHashs[xdomain.id] = Buffer.from(hash, 'hex').toString('binary');
2284 if (xdomain.certkeyhash != null) { obj.webserver.webCertificateHashs[xdomain.id] = Buffer.from(xdomain.certkeyhash, 'hex').toString('binary'); }
2285
2286 // Disconnect all agents with bad web certificates
2287 for (var i in obj.webserver.wsagentsWithBadWebCerts) { obj.webserver.wsagentsWithBadWebCerts[i].close(1); }
2288 }
2289
2290 console.log(obj.common.format("Loaded web certificate from \"{0}\", host: \"{1}\"", url, xhostname));
2291 console.log(obj.common.format(" SHA384 cert hash: {0}", xdomain.certhash));
2292 if ((xdomain.certkeyhash != null) && (xdomain.certhash != xdomain.certkeyhash)) { console.log(obj.common.format(" SHA384 key hash: {0}", xdomain.certkeyhash)); }
2293 }
2294 } else {
2295 console.log(obj.common.format("Failed to load web certificate at: \"{0}\", host: \"{1}\"", url, xhostname));
2296 }
2297 });
2298 }
2299 }
2300 }
2301
2302 // Perform maintenance operations (called every hour)
2303 obj.maintenanceActions = function () {
2304 // Perform database maintenance
2305 obj.db.maintenance();
2306
2307 // Clean up any temporary files
2308 const removeTime = new Date(Date.now()).getTime() - (30 * 60 * 1000); // 30 minutes
2309 const dir = obj.fs.readdir(obj.path.join(obj.filespath, 'tmp'), function (err, files) {
2310 if (err != null) return;
2311 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) { } }
2312 });
2313
2314 // Check for self-update that targets a specific version
2315 if ((typeof obj.args.selfupdate == 'string') && (getCurrentVersion() === obj.args.selfupdate)) { obj.args.selfupdate = false; }
2316
2317 // Check if we need to perform server self-update
2318 if ((obj.args.selfupdate) && (obj.serverSelfWriteAllowed == true)) {
2319 obj.db.getValueOfTheDay('performSelfUpdate', 1, function (performSelfUpdate) {
2320 if (performSelfUpdate.value > 0) {
2321 performSelfUpdate.value--;
2322 obj.db.Set(performSelfUpdate);
2323 obj.getLatestServerVersion(function (currentVer, latestVer) { if (currentVer != latestVer) { obj.performServerUpdate(); return; } });
2324 } else {
2325 checkAutobackup();
2326 }
2327 });
2328 } else {
2329 checkAutobackup();
2330 }
2331 };
2332
2333 // Check if we need to perform an automatic backup
2334 function checkAutobackup() {
2335 if (obj.config.settings.autobackup.backupintervalhours >= 1 ) {
2336 obj.db.Get('LastAutoBackupTime', function (err, docs) {
2337 if (err != null) { console.error("checkAutobackup: Error getting LastBackupTime from DB"); return}
2338 var lastBackup = 0;
2339 const currentdate = new Date();
2340 let currentHour = currentdate.getHours();
2341 let now = currentdate.getTime();
2342 if (docs.length == 1) { lastBackup = docs[0].value; }
2343 const delta = now - lastBackup;
2344 //const delta = 9999999999; // DEBUG: backup always
2345 obj.debug ('backup', 'Entering checkAutobackup, lastAutoBackupTime: ' + new Date(lastBackup).toLocaleString('default', { dateStyle: 'medium', timeStyle: 'short' }) + ', delta: ' + (delta/(1000*60*60)).toFixed(2) + ' hours');
2346 //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.
2347 if ((delta > (obj.config.settings.autobackup.backupintervalhours * 60 * 60 * 1000)) || ((currentHour == obj.config.settings.autobackup.backuphour) && (delta >= 2 * 60 * 60 * 1000))) {
2348 // A new auto-backup is required.
2349 obj.db.Set({ _id: 'LastAutoBackupTime', value: now }); // Save the current time in the database
2350 obj.db.performBackup(); // Perform the backup
2351 }
2352 });
2353 }
2354 }
2355
2356 // Stop the Meshcentral server
2357 obj.Stop = function (restoreFile) {
2358 // If the database is not setup, exit now.
2359 if (!obj.db) return;
2360
2361 // Dispatch an event saying the server is now stopping
2362 obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'stopped', msg: "Server stopped" });
2363
2364 // Set all nodes to power state of unknown (0)
2365 obj.db.storePowerEvent({ time: new Date(), nodeid: '*', power: 0, s: 2 }, obj.multiServer, function () { // s:2 indicates that the server is shutting down.
2366 if (restoreFile) {
2367 obj.debug('main', obj.common.format("Server stopped, updating settings: {0}", restoreFile));
2368 console.log("Updating settings folder...");
2369
2370 const yauzl = require('yauzl');
2371 yauzl.open(restoreFile, { lazyEntries: true }, function (err, zipfile) {
2372 if (err) throw err;
2373 zipfile.readEntry();
2374 zipfile.on('entry', function (entry) {
2375 if (/\/$/.test(entry.fileName)) {
2376 // Directory file names end with '/'.
2377 // Note that entires for directories themselves are optional.
2378 // An entry's fileName implicitly requires its parent directories to exist.
2379 zipfile.readEntry();
2380 } else {
2381 // File entry
2382 zipfile.openReadStream(entry, function (err, readStream) {
2383 if (err) throw err;
2384 readStream.on('end', function () { zipfile.readEntry(); });
2385 var directory = obj.path.dirname(entry.fileName);
2386 if (directory != '.') {
2387 directory = obj.getConfigFilePath(directory)
2388 if (obj.fs.existsSync(directory) == false) { obj.fs.mkdirSync(directory); }
2389 }
2390 //console.log('Extracting:', obj.getConfigFilePath(entry.fileName));
2391 readStream.pipe(obj.fs.createWriteStream(obj.getConfigFilePath(entry.fileName)));
2392 });
2393 }
2394 });
2395 zipfile.on('end', function () { setTimeout(function () { obj.fs.unlinkSync(restoreFile); process.exit(123); }); });
2396 });
2397 } else {
2398 obj.debug('main', "Server stopped");
2399 process.exit(0);
2400 }
2401 });
2402
2403 // Update the server state
2404 obj.updateServerState('state', "stopped");
2405 };
2406
2407 // Event Dispatch
2408 obj.AddEventDispatch = function (ids, target) {
2409 obj.debug('dispatch', 'AddEventDispatch', ids);
2410 for (var i in ids) { var id = ids[i]; if (!obj.eventsDispatch[id]) { obj.eventsDispatch[id] = [target]; } else { obj.eventsDispatch[id].push(target); } }
2411 };
2412 obj.RemoveEventDispatch = function (ids, target) {
2413 obj.debug('dispatch', 'RemoveEventDispatch', ids);
2414 for (var i in ids) {
2415 const id = ids[i];
2416 if (obj.eventsDispatch[id]) {
2417 var j = obj.eventsDispatch[id].indexOf(target);
2418 if (j >= 0) {
2419 if (obj.eventsDispatch[id].length == 1) {
2420 delete obj.eventsDispatch[id];
2421 } else {
2422 const newList = []; // We create a new list so not to modify the original list. Allows this function to be called during an event dispatch.
2423 for (var k in obj.eventsDispatch[id]) { if (obj.eventsDispatch[id][k] != target) { newList.push(obj.eventsDispatch[id][k]); } }
2424 obj.eventsDispatch[id] = newList;
2425 }
2426 }
2427 }
2428 }
2429 };
2430 obj.RemoveEventDispatchId = function (id) {
2431 obj.debug('dispatch', 'RemoveEventDispatchId', id);
2432 if (obj.eventsDispatch[id] != null) { delete obj.eventsDispatch[id]; }
2433 };
2434 obj.RemoveAllEventDispatch = function (target) {
2435 obj.debug('dispatch', 'RemoveAllEventDispatch');
2436 for (var i in obj.eventsDispatch) {
2437 const j = obj.eventsDispatch[i].indexOf(target);
2438 if (j >= 0) {
2439 if (obj.eventsDispatch[i].length == 1) {
2440 delete obj.eventsDispatch[i];
2441 } else {
2442 const newList = []; // We create a new list so not to modify the original list. Allows this function to be called during an event dispatch.
2443 for (var k in obj.eventsDispatch[i]) { if (obj.eventsDispatch[i][k] != target) { newList.push(obj.eventsDispatch[i][k]); } }
2444 obj.eventsDispatch[i] = newList;
2445 }
2446 }
2447 }
2448 };
2449 obj.DispatchEvent = function (ids, source, event, fromPeerServer) {
2450 // If the database is not setup, exit now.
2451 if (!obj.db) return;
2452
2453 // Send event to syslog if needed
2454 if (obj.syslog && event.msg) { obj.syslog.log(obj.syslog.LOG_INFO, event.msg); }
2455 if (obj.syslogjson) { obj.syslogjson.log(obj.syslogjson.LOG_INFO, JSON.stringify(event)); }
2456 if (obj.syslogtcp && event.msg) { obj.syslogtcp.log(event.msg, obj.syslogtcp.LOG_INFO); }
2457
2458 obj.debug('dispatch', 'DispatchEvent', ids);
2459 if ((typeof event == 'object') && (!event.nolog)) {
2460 event.time = new Date();
2461 // The event we store is going to skip some of the fields so we don't store too much stuff in the database.
2462 const storeEvent = Object.assign({}, event);
2463 if (storeEvent.node) { delete storeEvent.node; } // Skip the "node" field. May skip more in the future.
2464 if (storeEvent.links) {
2465 // Escape "links" names that may have "." and/or "$"
2466 storeEvent.links = Object.assign({}, storeEvent.links);
2467 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]; } }
2468 }
2469 if (storeEvent.mesh) {
2470 // Escape "mesh" names that may have "." and/or "$"
2471 storeEvent.mesh = obj.common.escapeLinksFieldNameEx(storeEvent.mesh);
2472 }
2473 storeEvent.ids = ids;
2474 obj.db.StoreEvent(storeEvent);
2475 }
2476 const targets = []; // List of targets we dispatched the event to, we don't want to dispatch to the same target twice.
2477 for (var j in ids) {
2478 const id = ids[j];
2479 const eventsDispatch = obj.eventsDispatch[id];
2480 if (eventsDispatch) {
2481 for (var i in eventsDispatch) {
2482 if (targets.indexOf(eventsDispatch[i]) == -1) { // Check if we already displatched to this target
2483 targets.push(eventsDispatch[i]);
2484 try { eventsDispatch[i].HandleEvent(source, event, ids, id); } catch (ex) { console.log(ex, eventsDispatch[i]); }
2485 }
2486 }
2487 }
2488 }
2489 if ((fromPeerServer == null) && (obj.multiServer != null) && ((typeof event != 'object') || (event.nopeers != 1))) { obj.multiServer.DispatchEvent(ids, source, event); }
2490 };
2491
2492 // Get the connection state of a node
2493 obj.GetConnectivityState = function (nodeid) { return obj.connectivityByNode[nodeid]; };
2494
2495 // Get the routing server id for a given node and connection type, can never be self.
2496 obj.GetRoutingServerIdNotSelf = function (nodeid, connectType) {
2497 if (obj.multiServer == null) return null;
2498 for (var serverid in obj.peerConnectivityByNode) {
2499 if (serverid == obj.serverId) continue;
2500 var state = obj.peerConnectivityByNode[serverid][nodeid];
2501 if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: serverid, meshid: state.meshid }; }
2502 }
2503 return null;
2504 };
2505
2506 // Get the routing server id for a given node and connection type, self first
2507 obj.GetRoutingServerId = function (nodeid, connectType) {
2508 if (obj.multiServer == null) return null;
2509
2510 // Look at our own server first
2511 var connections = obj.peerConnectivityByNode[obj.serverId];
2512 if (connections != null) {
2513 var state = connections[nodeid];
2514 if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: obj.serverId, meshid: state.meshid }; }
2515 }
2516
2517 // Look at other servers
2518 for (var serverid in obj.peerConnectivityByNode) {
2519 if (serverid == obj.serverId) continue;
2520 var state = obj.peerConnectivityByNode[serverid][nodeid];
2521 if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: serverid, meshid: state.meshid }; }
2522 }
2523 return null;
2524 };
2525
2526 // Update the connection state of a node when in multi-server mode
2527 // Update obj.connectivityByNode using obj.peerConnectivityByNode for the list of nodes in argument
2528 obj.UpdateConnectivityState = function (nodeids) {
2529 for (var nodeid in nodeids) {
2530 var meshid = null, state = null, oldConnectivity = 0, oldPowerState = 0, newConnectivity = 0, newPowerState = 0;
2531 var oldState = obj.connectivityByNode[nodeid];
2532 if (oldState != null) { meshid = oldState.meshid; oldConnectivity = oldState.connectivity; oldPowerState = oldState.powerState; }
2533 for (var serverid in obj.peerConnectivityByNode) {
2534 var peerState = obj.peerConnectivityByNode[serverid][nodeid];
2535 if (peerState != null) {
2536 if (state == null) {
2537 // Copy the state
2538 state = {};
2539 newConnectivity = state.connectivity = peerState.connectivity;
2540 newPowerState = state.powerState = peerState.powerState;
2541 meshid = state.meshid = peerState.meshid;
2542 //if (peerState.agentPower) { state.agentPower = peerState.agentPower; }
2543 //if (peerState.ciraPower) { state.ciraPower = peerState.ciraPower; }
2544 //if (peerState.amtPower) { state.amtPower = peerState.amtPower; }
2545 } else {
2546 // Merge the state
2547 state.connectivity |= peerState.connectivity;
2548 newConnectivity = state.connectivity;
2549 if ((peerState.powerState != 0) && ((state.powerState == 0) || (peerState.powerState < state.powerState))) { newPowerState = state.powerState = peerState.powerState; }
2550 meshid = state.meshid = peerState.meshid;
2551 //if (peerState.agentPower) { state.agentPower = peerState.agentPower; }
2552 //if (peerState.ciraPower) { state.ciraPower = peerState.ciraPower; }
2553 //if (peerState.amtPower) { state.amtPower = peerState.amtPower; }
2554 }
2555 }
2556 }
2557 obj.connectivityByNode[nodeid] = state;
2558
2559 //console.log('xx', nodeid, meshid, newConnectivity, oldPowerState, newPowerState, oldPowerState);
2560
2561 // Event any changes on this server only
2562 if ((newConnectivity != oldPowerState) || (newPowerState != oldPowerState)) {
2563 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() });
2564 }
2565 }
2566 };
2567
2568 // See if we need to notifiy any user of device state change
2569 obj.NotifyUserOfDeviceStateChange = function (meshid, nodeid, connectTime, connectType, powerState, serverid, stateSet, extraInfo) {
2570 // Check if there is a email server for this domain
2571 const meshSplit = meshid.split('/');
2572 if (meshSplit.length != 3) return;
2573 const domainId = meshSplit[1];
2574 if (obj.config.domains[domainId] == null) return;
2575 const mailserver = obj.config.domains[domainId].mailserver;
2576 if ((mailserver == null) && (obj.msgserver == null)) return;
2577
2578 // Get the device group for this device
2579 const mesh = obj.webserver.meshes[meshid];
2580 if ((mesh == null) || (mesh.links == null)) return;
2581
2582 // Get the list of users that have visibility to this device
2583 // This includes users that are part of user groups
2584 const users = [];
2585 for (var i in mesh.links) {
2586 if (i.startsWith('user/') && (users.indexOf(i) < 0)) { users.push(i); }
2587 if (i.startsWith('ugrp/')) {
2588 var usergrp = obj.webserver.userGroups[i];
2589 if (usergrp.links != null) { for (var j in usergrp.links) { if (j.startsWith('user/') && (users.indexOf(j) < 0)) { users.push(j); } } }
2590 }
2591 }
2592
2593 // Check if any user needs email notification
2594 for (var i in users) {
2595 const user = obj.webserver.users[users[i]];
2596 if (user != null) {
2597 var notify = 0;
2598
2599 // Device group notifications
2600 const meshLinks = user.links[meshid];
2601 if ((meshLinks != null) && (meshLinks.notify != null)) { notify |= meshLinks.notify; }
2602
2603 // User notifications
2604 if (user.notify != null) {
2605 if (user.notify[meshid] != null) { notify |= user.notify[meshid]; }
2606 if (user.notify[nodeid] != null) { notify |= user.notify[nodeid]; }
2607 }
2608
2609 // Email notifications
2610 if ((user.email != null) && (user.emailVerified == true) && (mailserver != null) && ((notify & 48) != 0)) {
2611 if (stateSet == true) {
2612 if ((notify & 16) != 0) {
2613 mailserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2614 } else {
2615 mailserver.cancelNotifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2616 }
2617 }
2618 else if (stateSet == false) {
2619 if ((notify & 32) != 0) {
2620 mailserver.notifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2621 } else {
2622 mailserver.cancelNotifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2623 }
2624 }
2625 }
2626
2627 // Messaging notifications
2628 if ((obj.msgserver != null) && ((notify & 384) != 0)) {
2629 if (stateSet == true) {
2630 if ((notify & 128) != 0) {
2631 obj.msgserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2632 } else {
2633 obj.msgserver.cancelNotifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2634 }
2635 }
2636 else if (stateSet == false) {
2637 if ((notify & 256) != 0) {
2638 obj.msgserver.notifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2639 } else {
2640 obj.msgserver.cancelNotifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
2641 }
2642 }
2643 }
2644 }
2645 }
2646 }
2647
2648 // See if we need to notifiy any user of device requested help
2649 //if (typeof device.name == 'string') { parent.parent.NotifyUserOfDeviceHelpRequest(domain, device._id, device.meshid, device.name, command.msgArgs[0], command.msgArgs[1]); }
2650
2651 obj.NotifyUserOfDeviceHelpRequest = function (domain, meshid, nodeid, devicename, helpusername, helprequest) {
2652 // Check if there is a email server for this domain
2653 const meshSplit = meshid.split('/');
2654 if (meshSplit.length != 3) return;
2655 const domainId = meshSplit[1];
2656 if (obj.config.domains[domainId] == null) return;
2657 const mailserver = obj.config.domains[domainId].mailserver;
2658 if ((mailserver == null) && (obj.msgserver == null)) return;
2659
2660 // Get the device group for this device
2661 const mesh = obj.webserver.meshes[meshid];
2662 if ((mesh == null) || (mesh.links == null)) return;
2663
2664 // Get the list of users that have visibility to this device
2665 // This includes users that are part of user groups
2666 const users = [];
2667 for (var i in mesh.links) {
2668 if (i.startsWith('user/') && (users.indexOf(i) < 0)) { users.push(i); }
2669 if (i.startsWith('ugrp/')) {
2670 var usergrp = obj.webserver.userGroups[i];
2671 if (usergrp.links != null) { for (var j in usergrp.links) { if (j.startsWith('user/') && (users.indexOf(j) < 0)) { users.push(j); } } }
2672 }
2673 }
2674
2675 // Check if any user needs email notification
2676 for (var i in users) {
2677 const user = obj.webserver.users[users[i]];
2678 if (user != null) {
2679 var notify = 0;
2680
2681 // Device group notifications
2682 const meshLinks = user.links[meshid];
2683 if ((meshLinks != null) && (meshLinks.notify != null)) { notify |= meshLinks.notify; }
2684
2685 // User notifications
2686 if (user.notify != null) {
2687 if (user.notify[meshid] != null) { notify |= user.notify[meshid]; }
2688 if (user.notify[nodeid] != null) { notify |= user.notify[nodeid]; }
2689 }
2690
2691 // Mail help request
2692 if ((user.email != null) && (user.emailVerified == true) && ((notify & 64) != 0)) { mailserver.sendDeviceHelpMail(domain, user.name, user.email, devicename, nodeid, helpusername, helprequest, user.llang); }
2693
2694 // Message help request
2695 if ((user.msghandle != null) && ((notify & 512) != 0)) { obj.msgserver.sendDeviceHelpRequest(domain, user.name, user.msghandle, devicename, nodeid, helpusername, helprequest, user.llang); }
2696 }
2697 }
2698 }
2699
2700 // Set the connectivity state of a node and setup the server so that messages can be routed correctly.
2701 // meshId: mesh identifier of format mesh/domain/meshidhex
2702 // nodeId: node identifier of format node/domain/nodeidhex
2703 // connectTime: time of connection, milliseconds elapsed since the UNIX epoch.
2704 // connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 4 = Intel AMT local, 8 = Intel AMT Relay, 16 = MQTT
2705 // 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
2706 //var connectTypeStrings = ['', 'MeshAgent', 'Intel AMT CIRA', '', 'Intel AMT local', '', '', '', 'Intel AMT Relay', '', '', '', '', '', '', '', 'MQTT'];
2707 //var powerStateStrings = ['Unknown', 'Powered', 'Sleep', 'Sleep', 'Deep Sleep', 'Hibernating', 'Soft-Off', 'Present', 'Off'];
2708 obj.SetConnectivityState = function (meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
2709 //console.log('SetConnectivity for ' + nodeid.substring(0, 16) + ', Type: ' + connectTypeStrings[connectType] + ', Power: ' + powerStateStrings[powerState] + (serverid == null ? ('') : (', ServerId: ' + serverid)));
2710 if ((serverid == null) && (obj.multiServer != null)) { obj.multiServer.DispatchMessage({ action: 'SetConnectivityState', meshid: meshid, nodeid: nodeid, connectTime: connectTime, connectType: connectType, powerState: powerState, extraInfo: extraInfo }); }
2711
2712 if (obj.multiServer == null) {
2713 // Single server mode
2714
2715 // Change the node connection state
2716 var eventConnectChange = 0;
2717 var state = obj.connectivityByNode[nodeid];
2718 if (state) {
2719 // Change the connection in the node and mesh state lists
2720 if ((state.connectivity & connectType) == 0) { state.connectivity |= connectType; eventConnectChange = 1; }
2721 state.meshid = meshid;
2722 } else {
2723 // Add the connection to the node and mesh state list
2724 obj.connectivityByNode[nodeid] = state = { connectivity: connectType, meshid: meshid };
2725 eventConnectChange = 1;
2726 }
2727
2728 // Set node power state
2729 if (connectType == 1) { state.agentPower = powerState; } else if (connectType == 2) { state.ciraPower = powerState; } else if (connectType == 4) { state.amtPower = powerState; }
2730 var powerState = 0, oldPowerState = state.powerState;
2731 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; }
2732 if ((state.powerState == null) || (state.powerState == undefined) || (state.powerState != powerState)) {
2733 state.powerState = powerState;
2734 eventConnectChange = 1;
2735
2736 // Set new power state in database
2737 const record = { time: new Date(connectTime), nodeid: nodeid, power: powerState };
2738 if (oldPowerState != null) { record.oldPower = oldPowerState; }
2739 obj.db.storePowerEvent(record, obj.multiServer);
2740 }
2741
2742 // Event the node connection change
2743 if (eventConnectChange == 1) {
2744 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() });
2745
2746 // Save indication of node connection change
2747 const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 1, connectType: connectType };
2748 if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
2749 obj.db.Set(lc);
2750
2751 // Notify any users of device connection
2752 obj.NotifyUserOfDeviceStateChange(meshid, nodeid, connectTime, connectType, powerState, serverid, true, extraInfo);
2753 }
2754 } else {
2755 // Multi server mode
2756
2757 // Change the node connection state
2758 if (serverid == null) { serverid = obj.serverId; }
2759 if (obj.peerConnectivityByNode[serverid] == null) return; // Guard against unknown serverid's
2760 var eventConnectChange = 0;
2761 var state = obj.peerConnectivityByNode[serverid][nodeid];
2762 if (state) {
2763 // Change the connection in the node and mesh state lists
2764 if ((state.connectivity & connectType) == 0) { state.connectivity |= connectType; eventConnectChange = 1; }
2765 state.meshid = meshid;
2766 } else {
2767 // Add the connection to the node and mesh state list
2768 obj.peerConnectivityByNode[serverid][nodeid] = state = { connectivity: connectType, meshid: meshid };
2769 eventConnectChange = 1;
2770 }
2771
2772 // Set node power state
2773 if (connectType == 1) { state.agentPower = powerState; } else if (connectType == 2) { state.ciraPower = powerState; } else if (connectType == 4) { state.amtPower = powerState; }
2774 var powerState = 0, oldPowerState = state.powerState;
2775 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; }
2776 if ((state.powerState == null) || (state.powerState == undefined) || (state.powerState != powerState)) {
2777 state.powerState = powerState;
2778 eventConnectChange = 1;
2779
2780 // Set new power state in database
2781 var record = { time: new Date(connectTime), nodeid: nodeid, power: powerState, server: obj.multiServer.serverid };
2782 if (oldPowerState != null) { record.oldPower = oldPowerState; }
2783 obj.db.storePowerEvent(record, obj.multiServer);
2784 }
2785
2786 if (eventConnectChange == 1) {
2787 // Update the combined node state
2788 var x = {}; x[nodeid] = 1;
2789 obj.UpdateConnectivityState(x);
2790
2791 // Save indication of node connection change
2792 if (serverid == obj.serverId) {
2793 const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 1, connectType: connectType, serverid: obj.serverId };
2794 if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
2795 obj.db.Set(lc);
2796 }
2797
2798 // Notify any users of device connection
2799 obj.NotifyUserOfDeviceStateChange(meshid, nodeid, connectTime, connectType, powerState, serverid, true, extraInfo);
2800 }
2801 }
2802 };
2803
2804 // Clear the connectivity state of a node and setup the server so that messages can be routed correctly.
2805 // meshId: mesh identifier of format mesh/domain/meshidhex
2806 // nodeId: node identifier of format node/domain/nodeidhex
2807 // connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 4 = Intel AMT local.
2808 obj.ClearConnectivityState = function (meshid, nodeid, connectType, serverid, extraInfo) {
2809 //console.log('ClearConnectivity for ' + nodeid.substring(0, 16) + ', Type: ' + connectTypeStrings[connectType] + (serverid == null?(''):(', ServerId: ' + serverid)));
2810 if ((serverid == null) && (obj.multiServer != null)) { obj.multiServer.DispatchMessage({ action: 'ClearConnectivityState', meshid: meshid, nodeid: nodeid, connectType: connectType, extraInfo: extraInfo }); }
2811
2812 if (obj.multiServer == null) {
2813 // Single server mode
2814 var eventConnectChange = 0;
2815
2816 // Remove the agent connection from the nodes connection list
2817 const state = obj.connectivityByNode[nodeid];
2818 if (state == null) return;
2819
2820 if ((state.connectivity & connectType) != 0) {
2821 state.connectivity -= connectType;
2822
2823 // Save indication of node connection change
2824 const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 0, connectType: connectType };
2825 if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
2826 obj.db.Set(lc);
2827
2828 // If the node is completely disconnected, clean it up completely
2829 if (state.connectivity == 0) { delete obj.connectivityByNode[nodeid]; }
2830 eventConnectChange = 1;
2831 }
2832
2833 // Clear node power state
2834 var powerState = 0;
2835 const oldPowerState = state.powerState;
2836 if (connectType == 1) { state.agentPower = 0; } else if (connectType == 2) { state.ciraPower = 0; } else if (connectType == 4) { state.amtPower = 0; }
2837 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; }
2838 if ((state.powerState == null) || (state.powerState != powerState)) {
2839 state.powerState = powerState;
2840 eventConnectChange = 1;
2841
2842 // Set new power state in database
2843 obj.db.storePowerEvent({ time: new Date(), nodeid: nodeid, power: powerState, oldPower: oldPowerState }, obj.multiServer);
2844 }
2845
2846 // Event the node connection change
2847 if (eventConnectChange == 1) {
2848 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() });
2849
2850 // Notify any users of device disconnection
2851 obj.NotifyUserOfDeviceStateChange(meshid, nodeid, Date.now(), connectType, -1, serverid, false, extraInfo);
2852 }
2853 } else {
2854 // Multi server mode
2855
2856 // Remove the agent connection from the nodes connection list
2857 if (serverid == null) { serverid = obj.serverId; }
2858 if (obj.peerConnectivityByNode[serverid] == null) return; // Guard against unknown serverid's
2859 var state = obj.peerConnectivityByNode[serverid][nodeid];
2860 if (state == null) return;
2861
2862 // If existing state exist, remove this connection
2863 if ((state.connectivity & connectType) != 0) {
2864 state.connectivity -= connectType; // Remove one connectivity mode
2865
2866 // Save indication of node connection change
2867 if (serverid == obj.serverId) {
2868 const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 0, connectType: connectType, serverid: obj.serverId };
2869 if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
2870 obj.db.Set(lc);
2871 }
2872
2873 // If the node is completely disconnected, clean it up completely
2874 if (state.connectivity == 0) { delete obj.peerConnectivityByNode[serverid][nodeid]; state.powerState = 0; }
2875
2876 // Notify any users of device disconnection
2877 obj.NotifyUserOfDeviceStateChange(meshid, nodeid, Date.now(), connectType, -1, serverid, false, extraInfo);
2878 }
2879
2880 // Clear node power state
2881 if (connectType == 1) { state.agentPower = 0; } else if (connectType == 2) { state.ciraPower = 0; } else if (connectType == 4) { state.amtPower = 0; }
2882 var powerState = 0;
2883 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; }
2884 if ((state.powerState == null) || (state.powerState != powerState)) { state.powerState = powerState; }
2885
2886 // Update the combined node state
2887 var x = {}; x[nodeid] = 1;
2888 obj.UpdateConnectivityState(x);
2889 }
2890 };
2891
2892 // Escape a code string
2893 obj.escapeCodeString = function (str, keepUtf8) {
2894 const escapeCodeStringTable = { '\'': '\\\'', '\"': '\\"', '\\': '\\\\', '\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t' };
2895 var r = '', c, cr, table;
2896 for (var i = 0; i < str.length; i++) {
2897 c = str[i];
2898 table = escapeCodeStringTable[c];
2899 if (table != null) {
2900 r += table;
2901 } else if (keepUtf8 === true) {
2902 r += c;
2903 } else {
2904 cr = c.charCodeAt(0);
2905 if ((cr >= 32) && (cr <= 127)) { r += c; }
2906 }
2907 }
2908 return r;
2909 }
2910
2911 // Update the default mesh core
2912 obj.updateMeshCore = function (func, dumpToFile) {
2913 // Figure out where meshcore.js is
2914 var meshcorePath = obj.datapath;
2915 if (obj.fs.existsSync(obj.path.join(meshcorePath, 'meshcore.js')) == false) {
2916 meshcorePath = obj.path.join(__dirname, 'agents');
2917 if (obj.fs.existsSync(obj.path.join(meshcorePath, 'meshcore.js')) == false) {
2918 obj.defaultMeshCores = obj.defaultMeshCoresHash = {}; if (func != null) { func(false); } // meshcore.js not found
2919 }
2920 }
2921
2922 // Read meshcore.js and all .js files in the modules folder.
2923 var meshCore = null, modulesDir = null;
2924 const modulesAdd = {
2925 'windows-amt': ['var addedModules = [];\r\n'],
2926 'linux-amt': ['var addedModules = [];\r\n'],
2927 'linux-noamt': ['var addedModules = [];\r\n']
2928 };
2929
2930 // Read the recovery core if present
2931 var meshRecoveryCore = null;
2932 if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'recoverycore.js')) == true) {
2933 try { meshRecoveryCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'recoverycore.js')).toString(); } catch (ex) { }
2934 if (meshRecoveryCore != null) {
2935 modulesAdd['windows-recovery'] = ['var addedModules = [];\r\n'];
2936 modulesAdd['linux-recovery'] = ['var addedModules = [];\r\n'];
2937 }
2938 }
2939
2940 // Read the agent recovery core if present
2941 var meshAgentRecoveryCore = null;
2942 if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'meshcore_diagnostic.js')) == true) {
2943 try { meshAgentRecoveryCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'meshcore_diagnostic.js')).toString(); } catch (ex) { }
2944 if (meshAgentRecoveryCore != null) {
2945 modulesAdd['windows-agentrecovery'] = ['var addedModules = [];\r\n'];
2946 modulesAdd['linux-agentrecovery'] = ['var addedModules = [];\r\n'];
2947 }
2948 }
2949
2950 // Read the tiny core if present
2951 var meshTinyCore = null;
2952 if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'tinycore.js')) == true) {
2953 try { meshTinyCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'tinycore.js')).toString(); } catch (ex) { }
2954 if (meshTinyCore != null) {
2955 modulesAdd['windows-tiny'] = ['var addedModules = [];\r\n'];
2956 modulesAdd['linux-tiny'] = ['var addedModules = [];\r\n'];
2957 }
2958 }
2959
2960 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.
2961 if (meshCore == null) { try { meshCore = obj.fs.readFileSync(obj.path.join(meshcorePath, 'meshcore.js')).toString(); } catch (ex) { } } // Use non-minified meshcore.
2962 if (meshCore != null) {
2963 var moduleDirPath = null;
2964 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.
2965 if (modulesDir == null) { try { moduleDirPath = obj.path.join(meshcorePath, 'modules_meshcore'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
2966 if (modulesDir != null) {
2967 for (var i in modulesDir) {
2968 if (modulesDir[i].toLowerCase().endsWith('.json')) {
2969 // We are adding a JSON file to the meshcores
2970 var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 5);
2971 if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 6); } // Remove the ".min" for ".min.json" files.
2972 const jsonData = obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('utf8'), true);
2973 const moduleData = ['var ', moduleName, ' = JSON.parse(\'', jsonData, '\');\r\n'];
2974
2975 // Add to all major cores
2976 modulesAdd['windows-amt'].push(...moduleData);
2977 modulesAdd['linux-amt'].push(...moduleData);
2978 modulesAdd['linux-noamt'].push(...moduleData);
2979 }
2980 if (modulesDir[i].toLowerCase().endsWith('.js')) {
2981 // We are adding a JS file to the meshcores
2982 var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 3);
2983 if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 4); } // Remove the ".min" for ".min.js" files.
2984 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'];
2985
2986 // Merge this module
2987 // NOTE: "smbios" module makes some non-AI Linux segfault, only include for IA platforms.
2988 if (moduleName.startsWith('amt-') || (moduleName == 'smbios')) {
2989 // Add to IA / Intel AMT cores only
2990 modulesAdd['windows-amt'].push(...moduleData);
2991 modulesAdd['linux-amt'].push(...moduleData);
2992 } else if (moduleName.startsWith('win-')) {
2993 // Add to Windows cores only
2994 modulesAdd['windows-amt'].push(...moduleData);
2995 } else if (moduleName.startsWith('linux-')) {
2996 // Add to Linux cores only
2997 modulesAdd['linux-amt'].push(...moduleData);
2998 modulesAdd['linux-noamt'].push(...moduleData);
2999 } else {
3000 // Add to all cores
3001 modulesAdd['windows-amt'].push(...moduleData);
3002 modulesAdd['linux-amt'].push(...moduleData);
3003 modulesAdd['linux-noamt'].push(...moduleData);
3004 }
3005
3006 // Merge this module to recovery modules if needed
3007 if (modulesAdd['windows-recovery'] != null) {
3008 if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal') || (moduleName == 'win-virtual-terminal')) {
3009 modulesAdd['windows-recovery'].push(...moduleData);
3010 }
3011 }
3012
3013 // Merge this module to agent recovery modules if needed
3014 if (modulesAdd['windows-agentrecovery'] != null) {
3015 if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal') || (moduleName == 'win-virtual-terminal')) {
3016 modulesAdd['windows-agentrecovery'].push(...moduleData);
3017 }
3018 }
3019 }
3020 }
3021 }
3022
3023 // Add plugins to cores
3024 if (obj.pluginHandler) { obj.pluginHandler.addMeshCoreModules(modulesAdd); }
3025
3026 // If we need to dump modules to file, create a meshcores folder
3027 if (dumpToFile) { try { obj.fs.mkdirSync('meshcores'); } catch (ex) { } }
3028
3029 // Merge the cores and compute the hashes
3030 for (var i in modulesAdd) {
3031 if ((i == 'windows-recovery') || (i == 'linux-recovery')) {
3032 obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshRecoveryCore].join('');
3033 } else if ((i == 'windows-agentrecovery') || (i == 'linux-agentrecovery')) {
3034 obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshAgentRecoveryCore].join('');
3035 } else if ((i == 'windows-tiny') || (i == 'linux-tiny')) {
3036 obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshTinyCore].join('');
3037 } else {
3038 obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshCore].join('');
3039 }
3040 obj.defaultMeshCores[i] = Buffer.from(obj.defaultMeshCores[i], 'utf8');
3041 obj.defaultMeshCoresHash[i] = obj.crypto.createHash('sha384').update(obj.defaultMeshCores[i]).digest('binary');
3042 obj.debug('main', 'Core module ' + i + ' is ' + obj.defaultMeshCores[i].length + ' bytes.');
3043
3044 // Write all modules to files. Great for debugging.
3045 if (dumpToFile) {
3046 console.log('Core module ' + i + ' is ' + obj.defaultMeshCores[i].length + ' bytes, saving to meshcores/' + i + '.js.'); // Print the core size and filename
3047 obj.fs.writeFile('meshcores/' + i + '.js', obj.defaultMeshCores[i].slice(4), function () { }); // Write the core to file
3048 }
3049
3050 // Compress the mesh cores with DEFLATE
3051 const callback = function MeshCoreDeflateCb(err, buffer) { if (err == null) { obj.defaultMeshCoresDeflate[MeshCoreDeflateCb.i] = buffer; } }
3052 callback.i = i;
3053 require('zlib').deflate(obj.defaultMeshCores[i], { level: require('zlib').Z_BEST_COMPRESSION }, callback);
3054 }
3055 }
3056
3057 // We are done creating all the mesh cores.
3058 if (func != null) { func(true); }
3059 };
3060
3061 // Update the default meshcmd
3062 obj.updateMeshCmdTimer = 'notset';
3063 obj.updateMeshCmd = function (func) {
3064 // Figure out where meshcmd.js is and read it.
3065 var meshCmd = null, meshcmdPath, moduleAdditions = ['var addedModules = [];\r\n'], moduleDirPath, modulesDir = null;
3066 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(); }
3067 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(); }
3068 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(); }
3069 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(); }
3070 else { obj.defaultMeshCmd = null; if (func != null) { func(false); } return; } // meshcmd.js not found
3071 meshCmd = meshCmd.replace("'***Mesh*Cmd*Version***'", '\'' + getCurrentVersion() + '\'');
3072
3073 // Figure out where the modules_meshcmd folder is.
3074 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.
3075 if (modulesDir == null) { try { moduleDirPath = obj.path.join(meshcmdPath, 'modules_meshcmd'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
3076 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.
3077 if (modulesDir == null) { try { moduleDirPath = obj.path.join(__dirname, 'agents', 'modules_meshcmd'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
3078
3079 // Read all .js files in the meshcmd modules folder.
3080 if (modulesDir != null) {
3081 for (var i in modulesDir) {
3082 if (modulesDir[i].toLowerCase().endsWith('.js')) {
3083 // Merge this module
3084 var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 3);
3085 if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 4); } // Remove the ".min" for ".min.js" files.
3086 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');
3087 }
3088 }
3089 }
3090
3091 // Set the new default meshcmd.js
3092 moduleAdditions.push(meshCmd);
3093 obj.defaultMeshCmd = moduleAdditions.join('');
3094 //console.log('MeshCmd is ' + obj.defaultMeshCmd.length + ' bytes.'); // DEBUG, Print the merged meshcmd.js size
3095 //obj.fs.writeFile("C:\\temp\\meshcmd.js", obj.defaultMeshCmd.substring(4)); // DEBUG, Write merged meshcmd.js to file
3096 if (func != null) { func(true); }
3097
3098 // Monitor for changes in meshcmd.js
3099 if (obj.updateMeshCmdTimer === 'notset') {
3100 obj.updateMeshCmdTimer = null;
3101 obj.fs.watch(meshcmdPath, function (eventType, filename) {
3102 if (obj.updateMeshCmdTimer != null) { clearTimeout(obj.updateMeshCmdTimer); obj.updateMeshCmdTimer = null; }
3103 obj.updateMeshCmdTimer = setTimeout(function () { obj.updateMeshCmd(); }, 5000);
3104 });
3105 }
3106 };
3107
3108 // List of possible mesh agent install scripts
3109 const meshToolsList = {
3110 'MeshCentralRouter': { localname: 'MeshCentralRouter.exe', dlname: 'winrouter' },
3111 'MeshCentralAssistant': { localname: 'MeshCentralAssistant.exe', dlname: 'winassistant', winhash: true }
3112 //'MeshCentralRouterMacOS': { localname: 'MeshCentralRouter.dmg', dlname: 'MeshCentralRouter.dmg' }
3113 };
3114
3115 // Update the list of available mesh agents
3116 obj.updateMeshTools = function () {
3117 for (var toolname in meshToolsList) {
3118 if (meshToolsList[toolname].winhash === true) {
3119 var toolpath = obj.path.join(__dirname, 'agents', meshToolsList[toolname].localname);
3120 const toolpath2 = obj.path.join(obj.datapath, 'agents', meshToolsList[toolname].localname);
3121 if (obj.fs.existsSync(toolpath2)) { toolpath = toolpath2; } // If the tool is present in "meshcentral-data/agents", use that one instead.
3122
3123 var hashStream = obj.crypto.createHash('sha384');
3124 hashStream.toolname = toolname;
3125 hashStream.toolpath = toolpath;
3126 hashStream.dlname = meshToolsList[toolname].dlname;
3127 hashStream.hashx = 0;
3128 hashStream.on('data', function (data) {
3129 obj.meshToolsBinaries[this.toolname] = { hash: data.toString('hex'), hashx: this.hashx, path: this.toolpath, dlname: this.dlname, url: this.url };
3130 obj.meshToolsBinaries[this.toolname].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?meshaction=' + this.dlname;
3131 var stats = null;
3132 try { stats = obj.fs.statSync(this.toolpath); } catch (ex) { }
3133 if (stats != null) { obj.meshToolsBinaries[this.toolname].size = stats.size; }
3134 });
3135 const options = { sourcePath: toolpath, targetStream: hashStream };
3136 obj.exeHandler.hashExecutableFile(options);
3137 } else {
3138 var toolpath = obj.path.join(__dirname, 'agents', meshToolsList[toolname].localname);
3139 const toolpath2 = obj.path.join(obj.datapath, 'agents', meshToolsList[toolname].localname);
3140 if (obj.fs.existsSync(toolpath2)) { toolpath = toolpath2; } // If the tool is present in "meshcentral-data/agents", use that one instead.
3141
3142 var stream = null;
3143 try {
3144 stream = obj.fs.createReadStream(toolpath);
3145 stream.on('data', function (data) { this.hash.update(data, 'binary'); this.hashx += data.length; });
3146 stream.on('error', function (data) {
3147 // If there is an error reading this file, make sure this agent is not in the agent table
3148 if (obj.meshToolsBinaries[this.toolname] != null) { delete obj.meshToolsBinaries[this.toolname]; }
3149 });
3150 stream.on('end', function () {
3151 // Add the agent to the agent table with all information and the hash
3152 obj.meshToolsBinaries[this.toolname] = {};
3153 obj.meshToolsBinaries[this.toolname].hash = this.hash.digest('hex');
3154 obj.meshToolsBinaries[this.toolname].hashx = this.hashx;
3155 obj.meshToolsBinaries[this.toolname].path = this.agentpath;
3156 obj.meshToolsBinaries[this.toolname].dlname = this.dlname;
3157 obj.meshToolsBinaries[this.toolname].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?meshaction=' + this.dlname;
3158 var stats = null;
3159 try { stats = obj.fs.statSync(this.agentpath); } catch (ex) { }
3160 if (stats != null) { obj.meshToolsBinaries[this.toolname].size = stats.size; }
3161 });
3162 stream.toolname = toolname;
3163 stream.agentpath = toolpath;
3164 stream.dlname = meshToolsList[toolname].dlname;
3165 stream.hash = obj.crypto.createHash('sha384', stream);
3166 stream.hashx = 0;
3167 } catch (ex) { }
3168 }
3169 }
3170 };
3171
3172 // List of possible mesh agent install scripts
3173 const meshAgentsInstallScriptList = {
3174 1: { id: 1, localname: 'meshinstall-linux.sh', rname: 'meshinstall.sh', linux: true },
3175 2: { id: 2, localname: 'meshinstall-initd.sh', rname: 'meshagent', linux: true },
3176 5: { id: 5, localname: 'meshinstall-bsd-rcd.sh', rname: 'meshagent', linux: true },
3177 6: { id: 6, localname: 'meshinstall-linux.js', rname: 'meshinstall.js', linux: true }
3178 };
3179
3180 // Update the list of available mesh agents
3181 obj.updateMeshAgentInstallScripts = function () {
3182 for (var scriptid in meshAgentsInstallScriptList) {
3183 var scriptpath = obj.path.join(__dirname, 'agents', meshAgentsInstallScriptList[scriptid].localname);
3184 var stream = null;
3185 try {
3186 stream = obj.fs.createReadStream(scriptpath);
3187 stream.xdata = '';
3188 stream.on('data', function (data) { this.hash.update(data, 'binary'); this.xdata += data; });
3189 stream.on('error', function (data) {
3190 // If there is an error reading this file, make sure this agent is not in the agent table
3191 if (obj.meshAgentInstallScripts[this.info.id] != null) { delete obj.meshAgentInstallScripts[this.info.id]; }
3192 });
3193 stream.on('end', function () {
3194 // Add the agent to the agent table with all information and the hash
3195 obj.meshAgentInstallScripts[this.info.id] = Object.assign({}, this.info);
3196 obj.meshAgentInstallScripts[this.info.id].hash = this.hash.digest('hex');
3197 obj.meshAgentInstallScripts[this.info.id].path = this.agentpath;
3198 obj.meshAgentInstallScripts[this.info.id].data = this.xdata;
3199 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;
3200 var stats = null;
3201 try { stats = obj.fs.statSync(this.agentpath); } catch (ex) { }
3202 if (stats != null) { obj.meshAgentInstallScripts[this.info.id].size = stats.size; }
3203
3204 // Place Unit line breaks on Linux scripts if not already present.
3205 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') }
3206 });
3207 stream.info = meshAgentsInstallScriptList[scriptid];
3208 stream.agentpath = scriptpath;
3209 stream.hash = obj.crypto.createHash('sha384', stream);
3210 } catch (ex) { }
3211 }
3212 };
3213
3214 // List of possible mesh agents
3215 obj.meshAgentsArchitectureNumbers = {
3216 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' },
3217 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' },
3218 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' },
3219 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 },
3220 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 },
3221 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' },
3222 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' },
3223 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' },
3224 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' },
3225 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' },
3226 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' },
3227 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.
3228 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' },
3229 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' },
3230 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
3231 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' },
3232 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
3233 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
3234 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' },
3235 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' },
3236 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' },
3237 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' },
3238 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' },
3239 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
3240 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' },
3241 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"
3242 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
3243 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.
3244 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
3245 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
3246 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
3247 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' },
3248 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.
3249 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
3250 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
3251 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
3252 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
3253 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
3254 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
3255 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' },
3256 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 },
3257 // 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)
3258 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
3259 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 },
3260 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 },
3261 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
3262 10006: { id: 10006, localname: 'MeshCentralAssistant.exe', rname: 'MeshCentralAssistant.exe', desc: 'MeshCentral Assistant for Windows', update: false, amt: false, platform: 'win32' }, // MeshCentral Assistant
3263 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
3264 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
3265 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
3266 };
3267
3268 // Sign windows agents
3269 obj.signMeshAgents = function (domain, func) {
3270 // Setup the domain is specified
3271 var objx = domain, suffix = '';
3272 if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
3273
3274 // Check if a custom agent signing certificate is available
3275 var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
3276
3277 // If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
3278 if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
3279 agentSignCertInfo = {
3280 cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
3281 key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
3282 extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
3283 }
3284 }
3285 if (agentSignCertInfo == null) { func(); return; } // No code signing certificate, nothing to do.
3286
3287 // Setup the domain is specified
3288 var objx = domain, suffix = '';
3289 if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
3290
3291 // Generate the agent signature description and URL
3292 const serverSignedAgentsPath = obj.path.join(obj.datapath, 'signedagents' + suffix);
3293 const signDesc = (domain.title ? domain.title : agentSignCertInfo.cert.subject.hash);
3294 const httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
3295 var httpsHost = ((domain.dns != null) ? domain.dns : obj.certificates.CommonName);
3296 if (obj.args.agentaliasdns != null) { httpsHost = obj.args.agentaliasdns; }
3297 var signUrl = 'https://' + httpsHost;
3298 if (httpsPort != 443) { signUrl += ':' + httpsPort; }
3299 var xdomain = (domain.dns == null) ? domain.id : '';
3300 if (xdomain != '') xdomain += '/';
3301 signUrl += '/' + xdomain;
3302
3303 // If requested, lock the agent to this server
3304 if (obj.config.settings.agentsignlock) { signUrl += '?ServerID=' + obj.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert).toUpperCase(); }
3305
3306 // Setup the time server
3307 var timeStampUrl = 'http://timestamp.comodoca.com/authenticode';
3308 if (obj.args.agenttimestampserver === false) { timeStampUrl = null; }
3309 else if (typeof obj.args.agenttimestampserver == 'string') { timeStampUrl = obj.args.agenttimestampserver; }
3310
3311 // Setup the time server proxy
3312 var timeStampProxy = null;
3313 if (typeof obj.args.agenttimestampproxy == 'string') { timeStampProxy = obj.args.agenttimestampproxy; }
3314 else if ((obj.args.agenttimestampproxy !== false) && (typeof obj.args.npmproxy == 'string')) { timeStampProxy = obj.args.npmproxy; }
3315
3316 // Setup the pending operations counter
3317 var pendingOperations = 1;
3318
3319 for (var archid in obj.meshAgentsArchitectureNumbers) {
3320 if (obj.meshAgentsArchitectureNumbers[archid].codesign !== true) continue;
3321
3322 var agentpath;
3323 if (domain.id == '') {
3324 // Load all agents when processing the default domain
3325 agentpath = obj.path.join(__dirname, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
3326 var agentpath2 = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
3327 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.
3328 } else {
3329 // When processing an extra domain, only load agents that are specific to that domain
3330 agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
3331 if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
3332 }
3333
3334 // Open the original agent with authenticode
3335 const signeedagentpath = obj.path.join(serverSignedAgentsPath, obj.meshAgentsArchitectureNumbers[archid].localname);
3336 const originalAgent = require('./authenticode.js').createAuthenticodeHandler(agentpath);
3337 if (originalAgent != null) {
3338 // Check if the agent is already signed correctly
3339 const destinationAgent = require('./authenticode.js').createAuthenticodeHandler(signeedagentpath);
3340 var destinationAgentOk = (
3341 (destinationAgent != null) &&
3342 (destinationAgent.fileHashSigned != null) &&
3343 (Buffer.compare(destinationAgent.fileHashSigned, destinationAgent.fileHashActual) == 0) &&
3344 (destinationAgent.signingAttribs.indexOf(signUrl) >= 0) &&
3345 (destinationAgent.signingAttribs.indexOf(signDesc) >= 0)
3346 );
3347
3348 if (destinationAgent != null) {
3349 // If the agent is signed correctly, look to see if the resources in the destination agent are correct
3350 var orgVersionStrings = originalAgent.getVersionInfo();
3351 if (destinationAgentOk == true) {
3352 const versionStrings = destinationAgent.getVersionInfo();
3353 const versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
3354 for (var i in versionProperties) {
3355 const prop = versionProperties[i], propl = prop.toLowerCase();
3356 if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo[propl] == 'string')) {
3357 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.
3358 } else {
3359 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.
3360 }
3361 }
3362
3363 // Check file version number
3364 if (destinationAgentOk == true) {
3365 if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['fileversionnumber'] == 'string')) {
3366 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.
3367 } else {
3368 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.
3369 }
3370 }
3371
3372 // Check product version number
3373 if (destinationAgentOk == true) {
3374 if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['productversionnumber'] == 'string')) {
3375 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.
3376 } else {
3377 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.
3378 }
3379 }
3380
3381 // Check the agent icon
3382 if (destinationAgentOk == true) {
3383 if ((domain.agentfileinfo != null) && (domain.agentfileinfo.icon != null)) {
3384 // Check if the destination agent matches the icon we want
3385 const agentIconGroups = destinationAgent.getIconInfo();
3386 if (agentIconGroups != null) {
3387 const agentIconGroupNames = Object.keys(agentIconGroups);
3388 if (agentIconGroupNames.length > 0) {
3389 const agentMainIconGroup = agentIconGroups[agentIconGroupNames[0]];
3390 if (agentMainIconGroup.resCount != domain.agentfileinfo.icon.resCount) {
3391 destinationAgentOk = false; // The icon image count is different, don't bother hashing to see if the icons are different.
3392 } else {
3393 const agentMainIconGroupHash = require('./authenticode.js').hashObject(agentMainIconGroup);
3394 const iconHash = require('./authenticode.js').hashObject(domain.agentfileinfo.icon);
3395 if (agentMainIconGroupHash != iconHash) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
3396 }
3397 }
3398 }
3399 } else {
3400 // Check if the destination agent has the default icon
3401 const agentIconGroups1 = destinationAgent.getIconInfo();
3402 const agentIconGroups2 = originalAgent.getIconInfo();
3403 if (agentIconGroups1.resCount != agentIconGroups2.resCount) {
3404 destinationAgentOk = false; // The icon image count is different, don't bother hashing to see if the icons are different.
3405 } else {
3406 const iconHash1 = require('./authenticode.js').hashObject(agentIconGroups1);
3407 const iconHash2 = require('./authenticode.js').hashObject(agentIconGroups2);
3408 if (iconHash1 != iconHash2) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
3409 }
3410 }
3411 }
3412
3413 // Check the agent logo
3414 if (destinationAgentOk == true) {
3415 if ((domain.agentfileinfo != null) && (domain.agentfileinfo.logo != null)) {
3416 // Check if the destination agent matches the logo we want
3417 const agentBitmaps = destinationAgent.getBitmapInfo();
3418 if (agentBitmaps != null) {
3419 const agentBitmapNames = Object.keys(agentBitmaps);
3420 if (agentBitmapNames.length > 0) {
3421 const agentMainBitmap = agentBitmaps[agentBitmapNames[0]];
3422 const agentMainBitmapHash = require('./authenticode.js').hashObject(agentMainBitmap);
3423 const bitmapHash = require('./authenticode.js').hashObject(domain.agentfileinfo.logo);
3424 if (agentMainBitmapHash != bitmapHash) { destinationAgentOk = false; } // If the existing agent logo does not match the desired logo, we need to re-sign the agent.
3425 }
3426 }
3427 } else {
3428 // Check if the destination agent has the default icon
3429 const agentBitmaps1 = destinationAgent.getBitmapInfo();
3430 const agentBitmaps2 = originalAgent.getBitmapInfo();
3431 const agentBitmapNames = Object.keys(agentBitmaps1);
3432 if (agentBitmapNames.length == 0) {
3433 destinationAgentOk = false;
3434 } else {
3435 const iconHash1 = require('./authenticode.js').hashObject(agentBitmaps1[agentBitmapNames[0]]);
3436 const iconHash2 = require('./authenticode.js').hashObject(agentBitmaps2[agentBitmapNames[0]]);
3437 if (iconHash1 != iconHash2) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
3438 }
3439 }
3440 }
3441 }
3442
3443 // If everything looks ok, runs a hash of the original and destination agent .text, .data and .rdata sections. If different, sign the agent again.
3444 if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.text').compare(destinationAgent.getHashOfSection('sha384', '.text')) != 0)) { destinationAgentOk = false; }
3445 if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.data').compare(destinationAgent.getHashOfSection('sha384', '.data')) != 0)) { destinationAgentOk = false; }
3446 if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.rdata').compare(destinationAgent.getHashOfSection('sha384', '.rdata')) != 0)) { destinationAgentOk = false; }
3447
3448 // We are done comparing the destination agent, close it.
3449 destinationAgent.close();
3450 }
3451
3452 if (destinationAgentOk == false) {
3453 // If not signed correctly, sign it. First, create the server signed agent folder if needed
3454 try { obj.fs.mkdirSync(serverSignedAgentsPath); } catch (ex) { }
3455 const xagentSignedFunc = function agentSignedFunc(err, size) {
3456 if (err == null) {
3457 // Agent was signed succesfuly
3458 console.log(obj.common.format('Code signed {0}.', agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname));
3459 } else {
3460 // Failed to sign agent
3461 addServerWarning('Failed to sign \"' + agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname + '\": ' + err, 22, [agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname, err]);
3462 }
3463 obj.callExternalSignJob(agentSignedFunc.signingArguments); // Call external signing job regardless of success or failure
3464 if (--pendingOperations === 0) { agentSignedFunc.func(); }
3465 }
3466 pendingOperations++;
3467 xagentSignedFunc.func = func;
3468 xagentSignedFunc.objx = objx;
3469 xagentSignedFunc.archid = archid;
3470 xagentSignedFunc.signeedagentpath = signeedagentpath;
3471
3472 // Parse the resources in the executable and make any required changes
3473 var resChanges = false, versionStrings = null;
3474 if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object')) {
3475 versionStrings = originalAgent.getVersionInfo();
3476 var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
3477 // Change the agent string properties
3478 for (var i in versionProperties) {
3479 const prop = versionProperties[i], propl = prop.toLowerCase();
3480 if (domain.agentfileinfo[propl] && (domain.agentfileinfo[propl] != versionStrings[prop])) { versionStrings[prop] = domain.agentfileinfo[propl]; resChanges = true; }
3481 }
3482 // Change the agent file version
3483 if (domain.agentfileinfo['fileversionnumber'] && (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion'])) {
3484 versionStrings['~FileVersion'] = domain.agentfileinfo['fileversionnumber']; resChanges = true;
3485 }
3486 // Change the agent product version
3487 if (domain.agentfileinfo['productversionnumber'] && (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion'])) {
3488 versionStrings['~ProductVersion'] = domain.agentfileinfo['productversionnumber']; resChanges = true;
3489 }
3490 if (resChanges == true) { originalAgent.setVersionInfo(versionStrings); }
3491
3492 // Change the agent icon
3493 if (domain.agentfileinfo.icon != null) {
3494 const agentIconGroups = originalAgent.getIconInfo();
3495 if (agentIconGroups != null) {
3496 const agentIconGroupNames = Object.keys(agentIconGroups);
3497 if (agentIconGroupNames.length > 0) {
3498 const agentMainIconGroupName = agentIconGroupNames[0];
3499 agentIconGroups[agentIconGroupNames[0]] = domain.agentfileinfo.icon;
3500 originalAgent.setIconInfo(agentIconGroups);
3501 }
3502 }
3503 }
3504
3505 // Change the agent logo
3506 if (domain.agentfileinfo.logo != null) {
3507 const agentBitmaps = originalAgent.getBitmapInfo();
3508 if (agentBitmaps != null) {
3509 const agentBitmapNames = Object.keys(agentBitmaps);
3510 if (agentBitmapNames.length > 0) {
3511 agentBitmaps[agentBitmapNames[0]] = domain.agentfileinfo.logo;
3512 originalAgent.setBitmapInfo(agentBitmaps);
3513 }
3514 }
3515 }
3516 }
3517
3518 const signingArguments = { out: signeedagentpath, desc: signDesc, url: signUrl, time: timeStampUrl, proxy: timeStampProxy }; // Shallow clone
3519 signingArguments.resChanges = resChanges;
3520
3521 obj.debug('main', "Code signing with arguments: " + JSON.stringify(signingArguments));
3522 xagentSignedFunc.signingArguments = signingArguments; // Attach the signing arguments to the callback function
3523 if (resChanges == false) {
3524 // Sign the agent the simple way, without changing any resources.
3525 originalAgent.sign(agentSignCertInfo, signingArguments, xagentSignedFunc);
3526 } else {
3527 // Change the agent resources and sign the agent, this is a much more involved process.
3528 // NOTE: This is experimental and could corupt the agent.
3529 originalAgent.writeExecutable(signingArguments, agentSignCertInfo, xagentSignedFunc);
3530 }
3531
3532 } else {
3533 // Signed agent is already ok, use it.
3534 originalAgent.close();
3535 }
3536
3537
3538 }
3539 }
3540
3541 if (--pendingOperations === 0) { func(); }
3542 }
3543
3544 obj.callExternalSignJob = function (signingArguments) {
3545 if (obj.config.settings && !obj.config.settings.externalsignjob) {
3546 return;
3547 }
3548 obj.debug('main', "External signing job called for file: " + signingArguments.out);
3549
3550 const { spawnSync } = require('child_process');
3551
3552 const signResult = spawnSync('"' + obj.config.settings.externalsignjob + '"', ['"' + signingArguments.out + '"'], {
3553 encoding: 'utf-8',
3554 shell: true,
3555 stdio: 'inherit'
3556 });
3557
3558 if (signResult.error || signResult.status !== 0) {
3559 obj.debug('main', "External signing failed for file: " + signingArguments.out);
3560 console.error("External signing failed for file: " + signingArguments.out);
3561 return;
3562 }
3563 }
3564
3565 // Update the list of available mesh agents
3566 obj.updateMeshAgentsTable = function (domain, func) {
3567 // Check if a custom agent signing certificate is available
3568 var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
3569
3570 // If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
3571 if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
3572 agentSignCertInfo = {
3573 cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
3574 key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
3575 extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
3576 }
3577 }
3578
3579 // Setup the domain is specified
3580 var objx = domain, suffix = '';
3581 if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
3582
3583 // Load agent information file. This includes the data & time of the agent.
3584 const agentInfo = [];
3585 try { agentInfo = JSON.parse(obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'hashagents.json'), 'utf8')); } catch (ex) { }
3586
3587 var archcount = 0;
3588 for (var archid in obj.meshAgentsArchitectureNumbers) {
3589 var agentpath;
3590 if (domain.id == '') {
3591 // Load all agents when processing the default domain
3592 agentpath = obj.path.join(__dirname, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
3593 if (obj.meshAgentsArchitectureNumbers[archid].unsigned !== true) {
3594 const agentpath2 = obj.path.join(obj.datapath, 'signedagents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
3595 if (obj.fs.existsSync(agentpath2)) { agentpath = agentpath2; } // If the agent is present in "meshcentral-data/signedagents", use that one instead.
3596 const agentpath3 = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
3597 if (obj.fs.existsSync(agentpath3)) { agentpath = agentpath3; } // If the agent is present in "meshcentral-data/agents", use that one instead.
3598 }
3599 } else {
3600 // When processing an extra domain, only load agents that are specific to that domain
3601 agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
3602 if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
3603 }
3604
3605 // Fetch agent binary information
3606 var stats = null;
3607 try { stats = obj.fs.statSync(agentpath); } catch (ex) { }
3608 if ((stats == null)) continue; // If this agent does not exist, skip it.
3609
3610 // Setup agent information
3611 archcount++;
3612 objx.meshAgentBinaries[archid] = Object.assign({}, obj.meshAgentsArchitectureNumbers[archid]);
3613 objx.meshAgentBinaries[archid].path = agentpath;
3614 objx.meshAgentBinaries[archid].url = 'http://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?id=' + archid;
3615 objx.meshAgentBinaries[archid].size = stats.size;
3616 if ((agentInfo[archid] != null) && (agentInfo[archid].mtime != null)) { objx.meshAgentBinaries[archid].mtime = new Date(agentInfo[archid].mtime); } // Set agent time if available
3617
3618 // If this is a windows binary, pull binary information
3619 if (obj.meshAgentsArchitectureNumbers[archid].platform == 'win32') {
3620 try { objx.meshAgentBinaries[archid].pe = obj.exeHandler.parseWindowsExecutable(agentpath); } catch (ex) { }
3621 }
3622
3623 // If agents must be stored in RAM or if this is a Windows 32/64/ARM64 agent, load the agent in RAM.
3624 if ((obj.args.agentsinram === true) || (((archid == 3) || (archid == 4) || (archid == 43)) && (obj.args.agentsinram !== false))) {
3625 if ((archid == 3) || (archid == 4) || (archid == 43)) {
3626 // Load the agent with a random msh added to it.
3627 const outStream = new require('stream').Duplex();
3628 outStream.meshAgentBinary = objx.meshAgentBinaries[archid];
3629 if (agentSignCertInfo) { outStream.meshAgentBinary.randomMsh = agentSignCertInfo.cert.subject.hash; } else { outStream.meshAgentBinary.randomMsh = obj.crypto.randomBytes(16).toString('hex'); }
3630 outStream.bufferList = [];
3631 outStream._write = function (chunk, encoding, callback) { this.bufferList.push(chunk); if (callback) callback(); }; // Append the chuck.
3632 outStream._read = function (size) { }; // Do nothing, this is not going to be called.
3633 outStream.on('finish', function () {
3634 // Merge all chunks
3635 this.meshAgentBinary.data = Buffer.concat(this.bufferList);
3636 this.meshAgentBinary.size = this.meshAgentBinary.data.length;
3637 delete this.bufferList;
3638
3639 // Hash the uncompressed binary
3640 const hash = obj.crypto.createHash('sha384').update(this.meshAgentBinary.data);
3641 this.meshAgentBinary.fileHash = hash.digest('binary');
3642 this.meshAgentBinary.fileHashHex = Buffer.from(this.meshAgentBinary.fileHash, 'binary').toString('hex');
3643
3644 // Compress the agent using ZIP
3645 const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method.
3646 const onZipData = function onZipData(buffer) { onZipData.x.zacc.push(buffer); }
3647 const onZipEnd = function onZipEnd() {
3648 // Concat all the buffer for create compressed zip agent
3649 const concatData = Buffer.concat(onZipData.x.zacc);
3650 delete onZipData.x.zacc;
3651
3652 // Hash the compressed binary
3653 const hash = obj.crypto.createHash('sha384').update(concatData);
3654 onZipData.x.zhash = hash.digest('binary');
3655 onZipData.x.zhashhex = Buffer.from(onZipData.x.zhash, 'binary').toString('hex');
3656
3657 // Set the agent
3658 onZipData.x.zdata = concatData;
3659 onZipData.x.zsize = concatData.length;
3660 }
3661 const onZipError = function onZipError() { delete onZipData.x.zacc; }
3662 this.meshAgentBinary.zacc = [];
3663 onZipData.x = this.meshAgentBinary;
3664 onZipEnd.x = this.meshAgentBinary;
3665 onZipError.x = this.meshAgentBinary;
3666 archive.on('data', onZipData);
3667 archive.on('end', onZipEnd);
3668 archive.on('error', onZipError);
3669
3670 // 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.
3671 // archive.append(this.meshAgentBinary.data, { name: 'meshagent' }); // This is the version that does not work on NodeJS v16.
3672 const ReadableStream = require('stream').Readable;
3673 const zipInputStream = new ReadableStream();
3674 zipInputStream.push(this.meshAgentBinary.data);
3675 zipInputStream.push(null);
3676 archive.append(zipInputStream, { name: 'meshagent' });
3677
3678 archive.finalize();
3679 })
3680 obj.exeHandler.streamExeWithMeshPolicy(
3681 {
3682 platform: 'win32',
3683 sourceFileName: agentpath,
3684 destinationStream: outStream,
3685 randomPolicy: true, // Indicates that the msh policy is random data.
3686 msh: outStream.meshAgentBinary.randomMsh,
3687 peinfo: objx.meshAgentBinaries[archid].pe
3688 });
3689 } else {
3690 // Load the agent as-is
3691 objx.meshAgentBinaries[archid].data = obj.fs.readFileSync(agentpath);
3692
3693 // Compress the agent using ZIP
3694 const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method.
3695
3696 const onZipData = function onZipData(buffer) { onZipData.x.zacc.push(buffer); }
3697 const onZipEnd = function onZipEnd() {
3698 // Concat all the buffer for create compressed zip agent
3699 const concatData = Buffer.concat(onZipData.x.zacc);
3700 delete onZipData.x.zacc;
3701
3702 // Hash the compressed binary
3703 const hash = obj.crypto.createHash('sha384').update(concatData);
3704 onZipData.x.zhash = hash.digest('binary');
3705 onZipData.x.zhashhex = Buffer.from(onZipData.x.zhash, 'binary').toString('hex');
3706
3707 // Set the agent
3708 onZipData.x.zdata = concatData;
3709 onZipData.x.zsize = concatData.length;
3710
3711 //console.log('Packed', onZipData.x.size, onZipData.x.zsize);
3712 }
3713 const onZipError = function onZipError() { delete onZipData.x.zacc; }
3714 objx.meshAgentBinaries[archid].zacc = [];
3715 onZipData.x = objx.meshAgentBinaries[archid];
3716 onZipEnd.x = objx.meshAgentBinaries[archid];
3717 onZipError.x = objx.meshAgentBinaries[archid];
3718 archive.on('data', onZipData);
3719 archive.on('end', onZipEnd);
3720 archive.on('error', onZipError);
3721 archive.append(objx.meshAgentBinaries[archid].data, { name: 'meshagent' });
3722 archive.finalize();
3723 }
3724 }
3725
3726 // Hash the binary
3727 const hashStream = obj.crypto.createHash('sha384');
3728 hashStream.archid = archid;
3729 hashStream.on('data', function (data) {
3730 objx.meshAgentBinaries[this.archid].hash = data.toString('binary');
3731 objx.meshAgentBinaries[this.archid].hashhex = data.toString('hex');
3732 if ((--archcount == 0) && (func != null)) { func(); }
3733 });
3734 const options = { sourcePath: agentpath, targetStream: hashStream, platform: obj.meshAgentsArchitectureNumbers[archid].platform };
3735 if (objx.meshAgentBinaries[archid].pe != null) { options.peinfo = objx.meshAgentBinaries[archid].pe; }
3736 obj.exeHandler.hashExecutableFile(options);
3737
3738 // If we are not loading Windows binaries to RAM, compute the RAW file hash of the signed binaries here.
3739 if ((obj.args.agentsinram === false) && ((archid == 3) || (archid == 4) || (archid == 43))) {
3740 const hash = obj.crypto.createHash('sha384').update(obj.fs.readFileSync(agentpath));
3741 objx.meshAgentBinaries[archid].fileHash = hash.digest('binary');
3742 objx.meshAgentBinaries[archid].fileHashHex = Buffer.from(objx.meshAgentBinaries[archid].fileHash, 'binary').toString('hex');
3743 }
3744 }
3745 };
3746
3747 // Generate a time limited user login token
3748 obj.getLoginToken = function (userid, func) {
3749 if ((userid == null) || (typeof userid != 'string')) { func('Invalid userid.'); return; }
3750 const x = userid.split('/');
3751 if (x == null || x.length != 3 || x[0] != 'user') { func('Invalid userid.'); return; }
3752 obj.db.Get(userid, function (err, docs) {
3753 if (err != null || docs == null || docs.length == 0) {
3754 func('User ' + userid + ' not found.'); return;
3755 } else {
3756 // Load the login cookie encryption key from the database
3757 obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
3758 if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
3759 // Key is present, use it.
3760 obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
3761 func(obj.encodeCookie({ u: userid, a: 3 }, obj.loginCookieEncryptionKey));
3762 } else {
3763 // Key is not present, generate one.
3764 obj.loginCookieEncryptionKey = obj.generateCookieKey();
3765 obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() }, function () { func(obj.encodeCookie({ u: userid, a: 3 }, obj.loginCookieEncryptionKey)); });
3766 }
3767 });
3768 }
3769 });
3770 };
3771
3772 // Show the user login token generation key
3773 obj.showLoginTokenKey = function (func) {
3774 // Load the login cookie encryption key from the database
3775 obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
3776 if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
3777 // Key is present, use it.
3778 func(docs[0].key);
3779 } else {
3780 // Key is not present, generate one.
3781 obj.loginCookieEncryptionKey = obj.generateCookieKey();
3782 obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() }, function () { func(obj.loginCookieEncryptionKey.toString('hex')); });
3783 }
3784 });
3785 };
3786
3787 // Load the list of Intel AMT UUID and passwords from "amtactivation.log"
3788 obj.loadAmtActivationLogPasswords = function (func) {
3789 const amtlogfilename = obj.path.join(obj.datapath, 'amtactivation.log');
3790 obj.fs.readFile(amtlogfilename, 'utf8', function (err, data) {
3791 const amtPasswords = {}; // UUID --> [Passwords]
3792 if ((err == null) && (data != null)) {
3793 const lines = data.split('\n');
3794 for (var i in lines) {
3795 const line = lines[i];
3796 if (line.startsWith('{')) {
3797 var j = null;
3798 try { j = JSON.parse(line); } catch (ex) { }
3799 if ((j != null) && (typeof j == 'object')) {
3800 if ((typeof j.amtUuid == 'string') && (typeof j.password == 'string')) {
3801 if (amtPasswords[j.amtUuid] == null) {
3802 amtPasswords[j.amtUuid] = [j.password]; // Add password to array
3803 } else {
3804 amtPasswords[j.amtUuid].unshift(j.password); // Add password at the start of the array
3805 }
3806 }
3807 }
3808 }
3809 }
3810 // Remove all duplicates and only keep the 3 last passwords for any given device
3811 for (var i in amtPasswords) {
3812 amtPasswords[i] = [...new Set(amtPasswords[i])];
3813 while (amtPasswords[i].length > 3) { amtPasswords[i].pop(); }
3814 }
3815 }
3816 func(obj.common.sortObj(amtPasswords)); // Sort by UUID
3817 });
3818 }
3819
3820 // Encrypt session data
3821 obj.encryptSessionData = function (data, key) {
3822 if (data == null) return null;
3823 if (key == null) { key = obj.loginCookieEncryptionKey; }
3824 try {
3825 const iv = Buffer.from(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
3826 const crypted = Buffer.concat([cipher.update(JSON.stringify(data), 'utf8'), cipher.final()]);
3827 return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64').replace(/\+/g, '@').replace(/\//g, '$');
3828 } catch (ex) { return null; }
3829 }
3830
3831 // Decrypt the session data
3832 obj.decryptSessionData = function (data, key) {
3833 if ((typeof data != 'string') || (data.length < 13)) return {};
3834 if (key == null) { key = obj.loginCookieEncryptionKey; }
3835 try {
3836 const buf = Buffer.from(data.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
3837 const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), buf.slice(0, 12));
3838 decipher.setAuthTag(buf.slice(12, 28));
3839 return JSON.parse(decipher.update(buf.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
3840 } catch (ex) { return {}; }
3841 }
3842
3843 // Generate a cryptographic key used to encode and decode cookies
3844 obj.generateCookieKey = function () {
3845 return Buffer.from(obj.crypto.randomBytes(80), 'binary');
3846 //return Buffer.alloc(80, 0); // Sets the key to zeros, debug only.
3847 };
3848
3849 // Encode an object as a cookie using a key using AES-GCM. (key must be 32 bytes or more)
3850 obj.encodeCookie = function (o, key) {
3851 try {
3852 if (key == null) { key = obj.serverKey; }
3853 o.time = Math.floor(Date.now() / 1000); // Add the cookie creation time
3854 const iv = Buffer.from(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
3855 const crypted = Buffer.concat([cipher.update(JSON.stringify(o), 'utf8'), cipher.final()]);
3856 const r = Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64').replace(/\+/g, '@').replace(/\//g, '$');
3857 obj.debug('cookie', 'Encoded AESGCM cookie: ' + JSON.stringify(o));
3858 return r;
3859 } catch (ex) { obj.debug('cookie', 'ERR: Failed to encode AESGCM cookie due to exception: ' + ex); return null; }
3860 };
3861
3862 // 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)
3863 obj.decodeCookie = function (cookie, key, timeout) {
3864 if (cookie == null) return null;
3865 var r = obj.decodeCookieAESGCM(cookie, key, timeout);
3866 if (r === -1) { r = obj.decodeCookieAESSHA(cookie, key, timeout); } // If decodeCookieAESGCM() failed to decode, try decodeCookieAESSHA()
3867 if ((r == null) && (obj.args.cookieencoding == null) && (cookie.length != 64) && ((cookie == cookie.toLowerCase()) || (cookie == cookie.toUpperCase()))) {
3868 obj.debug('cookie', 'Upper/Lowercase cookie, try "CookieEncoding":"hex" in settings section of config.json.');
3869 console.log('Upper/Lowercase cookie, try "CookieEncoding":"hex" in settings section of config.json.');
3870 }
3871 if ((r != null) && (typeof r.once == 'string') && (r.once.length > 0)) {
3872 // This cookie must only be used once.
3873 if (timeout == null) { timeout = 2; }
3874 if (obj.cookieUseOnceTable[r.once] == null) {
3875 const ctimeout = (((r.expire) == null || (typeof r.expire != 'number')) ? (r.time + ((timeout + 3) * 60000)) : (r.time + ((r.expire + 3) * 60000)));
3876
3877 // Store the used cookie in RAM
3878 obj.cookieUseOnceTable[r.once] = ctimeout;
3879
3880 // Store the used cookie in the database
3881 // TODO
3882
3883 // Send the used cookie to peer servers
3884 // TODO
3885
3886 // Clean up the used table
3887 if (++obj.cookieUseOnceTableCleanCounter > 20) {
3888 const now = Date.now();
3889 for (var i in obj.cookieUseOnceTable) { if (obj.cookieUseOnceTable[i] < now) { delete obj.cookieUseOnceTable[i]; } }
3890 obj.cookieUseOnceTableCleanCounter = 0;
3891 }
3892 } else { return null; }
3893 }
3894 return r;
3895 }
3896
3897 // 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)
3898 obj.decodeCookieAESGCM = function (cookie, key, timeout) {
3899 try {
3900 if (key == null) { key = obj.serverKey; }
3901 cookie = Buffer.from(cookie.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
3902 const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), cookie.slice(0, 12));
3903 decipher.setAuthTag(cookie.slice(12, 28));
3904 const o = JSON.parse(decipher.update(cookie.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
3905 if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { obj.debug('cookie', 'ERR: Bad cookie due to invalid time'); return null; }
3906 o.time = o.time * 1000; // Decode the cookie creation time
3907 o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds)
3908 if ((o.expire) == null || (typeof o.expire != 'number')) {
3909 // Use a fixed cookie expire time
3910 if (timeout == null) { timeout = 2; }
3911 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)
3912 } else {
3913 // An expire time is included in the cookie (in minutes), use this.
3914 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)
3915 }
3916 obj.debug('cookie', 'Decoded AESGCM cookie: ' + JSON.stringify(o));
3917 return o;
3918 } catch (ex) { obj.debug('cookie', 'ERR: Bad AESGCM cookie due to exception: ' + ex); return -1; }
3919 };
3920
3921 // 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)
3922 // We do this because poor .NET does not support AES256-GCM.
3923 obj.decodeCookieAESSHA = function (cookie, key, timeout) {
3924 try {
3925 if (key == null) { key = obj.serverKey; }
3926 if (key.length < 80) { return null; }
3927 cookie = Buffer.from(cookie.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
3928 const decipher = obj.crypto.createDecipheriv('aes-256-cbc', key.slice(48, 80), cookie.slice(0, 16));
3929 const rawmsg = decipher.update(cookie.slice(16), 'binary', 'binary') + decipher.final('binary');
3930 const hmac = obj.crypto.createHmac('sha384', key.slice(0, 48));
3931 hmac.update(rawmsg.slice(48));
3932 if (Buffer.compare(hmac.digest(), Buffer.from(rawmsg.slice(0, 48))) == false) { return null; }
3933 const o = JSON.parse(rawmsg.slice(48).toString('utf8'));
3934 if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { obj.debug('cookie', 'ERR: Bad cookie due to invalid time'); return null; }
3935 o.time = o.time * 1000; // Decode the cookie creation time
3936 o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds)
3937 if ((o.expire) == null || (typeof o.expire != 'number')) {
3938 // Use a fixed cookie expire time
3939 if (timeout == null) { timeout = 2; }
3940 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)
3941 } else {
3942 // An expire time is included in the cookie (in minutes), use this.
3943 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)
3944 }
3945 obj.debug('cookie', 'Decoded AESSHA cookie: ' + JSON.stringify(o));
3946 return o;
3947 } catch (ex) { obj.debug('cookie', 'ERR: Bad AESSHA cookie due to exception: ' + ex); return null; }
3948 };
3949
3950 // Debug
3951 obj.debug = function (source, ...args) {
3952 // Send event to console
3953 if ((obj.debugSources != null) && ((obj.debugSources == '*') || (obj.debugSources.indexOf(source) >= 0))) { console.log(source.toUpperCase() + ':', ...args); }
3954
3955 // Send event to log file
3956 if (obj.config.settings && obj.config.settings.log) {
3957 if (typeof obj.args.log == 'string') { obj.args.log = obj.args.log.split(','); }
3958 if ((obj.args.log.indexOf(source) >= 0) || (obj.args.log[0] == '*')) {
3959 const d = new Date();
3960 if (obj.xxLogFile == null) {
3961 try {
3962 obj.xxLogFile = obj.fs.openSync(obj.getConfigFilePath('log.txt'), 'a+', 0o666);
3963 obj.fs.writeSync(obj.xxLogFile, '---- Log start at ' + new Date().toLocaleString() + ' ----\r\n');
3964 obj.xxLogDateStr = d.toLocaleDateString();
3965 } catch (ex) { }
3966 }
3967 if (obj.xxLogFile != null) {
3968 try {
3969 if (obj.xxLogDateStr != d.toLocaleDateString()) { obj.xxLogDateStr = d.toLocaleDateString(); obj.fs.writeSync(obj.xxLogFile, '---- ' + d.toLocaleDateString() + ' ----\r\n'); }
3970 const formattedArgs = args.map(function (arg) { return (typeof arg === 'object' && arg !== null) ? JSON.stringify(arg) : arg; });
3971 obj.fs.writeSync(obj.xxLogFile, new Date().toLocaleTimeString() + ' - ' + source + ': ' + formattedArgs.join(', ') + '\r\n');
3972 } catch (ex) { }
3973 }
3974 }
3975 }
3976
3977 // Send the event to logged in administrators
3978 if ((obj.debugRemoteSources != null) && ((obj.debugRemoteSources == '*') || (obj.debugRemoteSources.indexOf(source) >= 0))) {
3979 var sendcount = 0;
3980 for (var sessionid in obj.webserver.wssessions2) {
3981 const ws = obj.webserver.wssessions2[sessionid];
3982 if ((ws != null) && (ws.userid != null)) {
3983 const user = obj.webserver.users[ws.userid];
3984 if ((user != null) && (user.siteadmin == 4294967295)) {
3985 try { ws.send(JSON.stringify({ action: 'trace', source: source, args: args, time: Date.now() })); sendcount++; } catch (ex) { }
3986 }
3987 }
3988 }
3989 if (sendcount == 0) { obj.debugRemoteSources = null; } // If there are no listeners, remove debug sources.
3990 }
3991 };
3992
3993 // Update server state. Writes a server state file.
3994 const meshServerState = {};
3995 obj.updateServerState = function (name, val) {
3996 //console.log('updateServerState', name, val);
3997 try {
3998 if ((name != null) && (val != null)) {
3999 var changed = false;
4000 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; } } }
4001 if (changed == false) return;
4002 }
4003 var r = 'time=' + Date.now() + '\r\n';
4004 for (var i in meshServerState) { r += (i + '=' + meshServerState[i] + '\r\n'); }
4005 try {
4006 obj.fs.writeFileSync(obj.getConfigFilePath('serverstate.txt'), r); // Try to write the server state, this may fail if we don't have permission.
4007 } catch (ex) { obj.serverSelfWriteAllowed = false; }
4008 } catch (ex) { } // Do nothing since this is not a critical feature.
4009 };
4010
4011 // Read a list of IP addresses from a file
4012 function readIpListFromFile(arg) {
4013 if ((typeof arg != 'string') || (!arg.startsWith('file:'))) return arg;
4014 var lines = null;
4015 try { lines = obj.fs.readFileSync(obj.path.join(obj.datapath, arg.substring(5))).toString().split(/\r?\n/).join('\r').split('\r'); } catch (ex) { }
4016 if (lines == null) return null;
4017 const validLines = [];
4018 for (var i in lines) {
4019 const line = lines[i].trim();
4020 if (line.length === 0) continue;
4021 if (line.charAt(0) === '#') continue;
4022 const parts = line.split('#');
4023 const candidate = parts[0].trim();
4024 if (candidate.length > 0 && candidate.indexOf('@') === -1 && (candidate.indexOf('.') > -1 || candidate.charAt(0) === ':')) {
4025 validLines.push(candidate);
4026 }
4027 }
4028 return validLines;
4029 }
4030
4031 // Logging funtions
4032 function logException(e) { e += ''; logErrorEvent(e); }
4033 function logInfoEvent(msg) { if (obj.servicelog != null) { obj.servicelog.info(msg); } console.log(msg); }
4034 function logWarnEvent(msg) { if (obj.servicelog != null) { obj.servicelog.warn(msg); } console.log(msg); }
4035 function logErrorEvent(msg) { if (obj.servicelog != null) { obj.servicelog.error(msg); } console.error(msg); }
4036 obj.getServerWarnings = function () { return serverWarnings; }
4037 // TODO: migrate from other addServerWarning function and add timestamp
4038 obj.addServerWarning = function (msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
4039
4040 // auth.log functions
4041 obj.authLog = function (server, msg, args) {
4042 if (typeof msg != 'string') return;
4043 var str = msg;
4044 if (args != null) {
4045 if (typeof args.sessionid == 'string') { str += ', SessionID: ' + args.sessionid; }
4046 if (typeof args.useragent == 'string') { const userAgentInfo = obj.webserver.getUserAgentInfo(args.useragent); str += ', Browser: ' + userAgentInfo.browserStr + ', OS: ' + userAgentInfo.osStr; }
4047 }
4048 obj.debug('authlog', str);
4049 if (obj.syslogauth != null) { try { obj.syslogauth.log(obj.syslogauth.LOG_INFO, str); } catch (ex) { } }
4050 if (obj.authlogfile != null) { // Write authlog to file
4051 try {
4052 const d = new Date(), month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()];
4053 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');
4054 obj.fs.write(obj.authlogfile, str, function (err, written, string) { if (err) { console.error(err); } });
4055 } catch (ex) { console.error(ex); }
4056 }
4057 }
4058
4059 // Return the path of a file into the meshcentral-data path
4060 obj.getConfigFilePath = function (filename) {
4061 if ((obj.config != null) && (obj.config.configfiles != null) && (obj.config.configfiles[filename] != null) && (typeof obj.config.configfiles[filename] == 'string')) {
4062 //console.log('getConfigFilePath(\"' + filename + '\") = ' + obj.config.configfiles[filename]);
4063 return obj.config.configfiles[filename];
4064 }
4065 //console.log('getConfigFilePath(\"' + filename + '\") = ' + obj.path.join(obj.datapath, filename));
4066 return obj.path.join(obj.datapath, filename);
4067 };
4068
4069 return obj;
4070 }
4071
4072 // Resolve a list of names, call back with list of failed resolves.
4073 function checkResolveAll(names, func) {
4074 const dns = require('dns'), state = { func: func, count: names.length, err: null };
4075 for (var i in names) {
4076 dns.lookup(names[i], { all: true }, function (err, records) {
4077 if (err != null) { if (this.state.err == null) { this.state.err = [this.name]; } else { this.state.err.push(this.name); } }
4078 if (--this.state.count == 0) { this.state.func(this.state.err); }
4079 }.bind({ name: names[i], state: state }))
4080 }
4081 }
4082
4083 // Resolve a list of domains to IP addresses, return a flat array of IPs.
4084 async function resolveDomainsToIps(originalArray) {
4085 if (!Array.isArray(originalArray)) { return undefined; }
4086 const flatResult = [];
4087 for (const item of originalArray) {
4088 if (new require('ipcheck')(item).valid) {
4089 flatResult.push(item);
4090 continue;
4091 }
4092 try {
4093 const results = await require('dns').promises.lookup(item, { all: true });
4094 flatResult.push(...results.map(r => r.address));
4095 } catch (err) {
4096 console.log(`Could not resolve ${item}`);
4097 }
4098 }
4099 if (flatResult.length == 0) { return undefined; }
4100 return flatResult;
4101 }
4102
4103 // Return the server configuration
4104 function getConfig(createSampleConfig) {
4105 // Figure out the datapath location
4106 var i, datapath = null;
4107 const fs = require('fs'), path = require('path'), args = require('minimist')(process.argv.slice(2));
4108 if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
4109 datapath = path.join(__dirname, '../../meshcentral-data');
4110 } else {
4111 datapath = path.join(__dirname, '../meshcentral-data');
4112 }
4113 if (args.datapath) { datapath = args.datapath; }
4114 try { fs.mkdirSync(datapath); } catch (ex) { }
4115
4116 // Read configuration file if present and change arguments.
4117 var config = {}, configFilePath = path.join(datapath, 'config.json');
4118 if (args.configfile) { configFilePath = common.joinPath(datapath, args.configfile); }
4119 if (fs.existsSync(configFilePath)) {
4120 // Load and validate the configuration file
4121 try { config = require(configFilePath); } catch (ex) { console.log('ERROR: Unable to parse ' + configFilePath + '.'); return null; }
4122 if (config.domains == null) { config.domains = {}; }
4123 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; } }
4124 } else {
4125 if (createSampleConfig === true) {
4126 // Copy the "sample-config.json" to give users a starting point
4127 const sampleConfigPath = path.join(__dirname, 'sample-config.json');
4128 if (fs.existsSync(sampleConfigPath)) { fs.createReadStream(sampleConfigPath).pipe(fs.createWriteStream(configFilePath)); }
4129 }
4130 }
4131
4132 // Set the command line arguments to the config file if they are not present
4133 if (!config.settings) { config.settings = {}; }
4134 for (i in args) { config.settings[i] = args[i]; }
4135
4136 // Lower case all keys in the config file
4137 try {
4138 require('./common.js').objKeysToLower(config, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
4139 } catch (ex) {
4140 console.log('CRITICAL ERROR: Unable to access the file \"./common.js\".\r\nCheck folder & file permissions.');
4141 process.exit();
4142 }
4143
4144 return config;
4145 }
4146
4147 // Check if a list of modules are present and install any missing ones
4148 function InstallModules(modules, args, func) {
4149 var missingModules = [];
4150 if (modules.length > 0) {
4151 const dependencies = require('./package.json').dependencies;
4152 for (var i in modules) {
4153 // Modules may contain a version tag (foobar@1.0.0), remove it so the module can be found using require
4154 const moduleNameAndVersion = modules[i];
4155 const moduleInfo = moduleNameAndVersion.split('@', 3);
4156 var moduleName = null;
4157 var moduleVersion = null;
4158 if(moduleInfo.length == 1){ // normal package without version
4159 moduleName = moduleInfo[0];
4160 } else if (moduleInfo.length == 2) { // normal package with a version OR custom repo package with no version
4161 moduleName = moduleInfo[0] === '' ? moduleNameAndVersion : moduleInfo[0];
4162 moduleVersion = moduleInfo[0] === '' ? null : moduleInfo[1];
4163 } else if (moduleInfo.length == 3) { // custom repo package and package with a version
4164 moduleName = "@" + moduleInfo[1];
4165 moduleVersion = moduleInfo[2];
4166 }
4167 try {
4168 // Does the module need a specific version?
4169 if (moduleVersion) {
4170 var versionMatch = false;
4171 var modulePath = null;
4172 // This is the first way to test if a module is already installed.
4173 try { versionMatch = (require(`${moduleName}/package.json`).version == moduleVersion) } catch (ex) {
4174 if (ex.code == "ERR_PACKAGE_PATH_NOT_EXPORTED") { modulePath = ("" + ex).split(' ').at(-1); } else { throw new Error(); }
4175 }
4176 // If the module is not installed, but we get the ERR_PACKAGE_PATH_NOT_EXPORTED error, try a second way.
4177 if ((versionMatch == false) && (modulePath != null)) {
4178 if (JSON.parse(require('fs').readFileSync(modulePath, 'utf8')).version != moduleVersion) { throw new Error(); }
4179 } else if (versionMatch == false) {
4180 throw new Error();
4181 }
4182 } else {
4183 // For all other modules, do the check here.
4184 // Is the module in package.json? Install exact version.
4185 if (typeof dependencies[moduleName] != null) { moduleVersion = dependencies[moduleName]; }
4186 require(moduleName);
4187 }
4188 } catch (ex) {
4189 missingModules.push(moduleNameAndVersion);
4190 }
4191 }
4192
4193 if (missingModules.length > 0) { if (args.debug) { console.log('Missing Modules: ' + missingModules.join(', ')); } InstallModuleEx(missingModules, args, func); } else { func(); }
4194 }
4195 }
4196
4197 // 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,
4198 // 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.
4199 function InstallModuleEx(modulenames, args, func) {
4200 var names = modulenames.join(' ');
4201 console.log('Installing modules', modulenames);
4202 const child_process = require('child_process');
4203 var parentpath = __dirname;
4204 function getCurrentVersion() { try { return JSON.parse(require('fs').readFileSync(require('path').join(__dirname, 'package.json'), 'utf8')).version; } catch (ex) { } return null; } // Fetch server version
4205 //const meshCentralVersion = getCurrentVersion();
4206 //if ((meshCentralVersion != null) && (args.dev == null)) { names = 'meshcentral@' + getCurrentVersion() + ' ' + names; }
4207
4208 // Get the working directory
4209 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, '../..'); }
4210
4211 if (args.debug) { console.log('NPM Command Line: ' + npmpath + ` install --save-exact --no-audit --omit=optional --no-fund ${names}`); }
4212 // always use --save-exact - https://stackoverflow.com/a/64507176/1210734
4213 child_process.exec(npmpath + ` install --save-exact --no-audit --no-optional --omit=optional ${names}`, { maxBuffer: 512000, timeout: 300000, cwd: parentpath }, function (error, stdout, stderr) {
4214 if ((error != null) && (error != '')) {
4215 var mcpath = __dirname;
4216 if (mcpath.endsWith('\\node_modules\\meshcentral') || mcpath.endsWith('/node_modules/meshcentral')) { mcpath = require('path').join(mcpath, '..', '..'); }
4217 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');
4218 process.exit();
4219 return;
4220 }
4221 func();
4222 return;
4223 });
4224 }
4225
4226 // Detect CTRL-C on Linux and stop nicely
4227 process.on('SIGINT', function () { if (meshserver != null) { meshserver.Stop(); meshserver = null; } console.log('Server Ctrl-C exit...'); process.exit(); });
4228
4229 // Add a server warning, warnings will be shown to the administrator on the web application
4230 // TODO: migrate to obj.addServerWarning?
4231 const serverWarnings = [];
4232 function addServerWarning(msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
4233
4234 /*
4235 var ServerWarnings = {
4236 1: "",
4237 2: "Missing WebDAV parameters.",
4238 3: "Unrecognized configuration option \"{0}\".",
4239 4: "WebSocket compression is disabled, this feature is broken in NodeJS v11.11 to v12.15 and v13.2",
4240 5: "Unable to load Intel AMT TLS root certificate for default domain.",
4241 6: "Unable to load Intel AMT TLS root certificate for domain {0}.",
4242 7: "CIRA local FQDN's ignored when server in LAN-only or WAN-only mode.",
4243 8: "Can't have more than 4 CIRA local FQDN's. Ignoring value.",
4244 9: "Agent hash checking is being skipped, this is unsafe.",
4245 10: "Missing Let's Encrypt email address.",
4246 11: "Invalid Let's Encrypt host names.",
4247 12: "Invalid Let's Encrypt names, can't contain a *.",
4248 13: "Unable to setup Let's Encrypt module.",
4249 14: "Invalid Let's Encrypt names, unable to resolve: {0}",
4250 15: "Invalid Let's Encrypt email address, unable to resolve: {0}",
4251 16: "Unable to load CloudFlare trusted proxy IPv6 address list.",
4252 17: "SendGrid server has limited use in LAN mode.",
4253 18: "SMTP server has limited use in LAN mode.",
4254 19: "SMS gateway has limited use in LAN mode.",
4255 20: "Invalid \"LoginCookieEncryptionKey\" in config.json.",
4256 21: "Backup path can't be set within meshcentral-data folder, backup settings ignored.",
4257 22: "Failed to sign agent {0}: {1}",
4258 23: "Unable to load agent icon file: {0}.",
4259 24: "Unable to load agent logo file: {0}.",
4260 25: "This NodeJS version does not support OpenID.",
4261 26: "This NodeJS version does not support Discord.js.",
4262 27: "Firebase now requires a service account JSON file, Firebase disabled."
4263 };
4264 */
4265
4266 // Load the really basic modules
4267 var npmpath = 'npm';
4268 var meshserver = null;
4269 var childProcess = null;
4270 var previouslyInstalledModules = {};
4271 function mainStart() {
4272 // Check the NodeJS is version 16 or better.
4273 if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) < 16) { console.log("MeshCentral requires Node v16 or above, current version is " + process.version + "."); return; }
4274
4275 // If running within the node_modules folder, move working directory to the parent of the node_modules folder.
4276 if (__dirname.endsWith('\\node_modules\\meshcentral') || __dirname.endsWith('/node_modules/meshcentral')) { process.chdir(require('path').join(__dirname, '..', '..')); }
4277
4278 // Check for any missing modules.
4279 InstallModules(['minimist'], {}, function () {
4280 // Parse inbound arguments
4281 const args = require('minimist')(process.argv.slice(2));
4282
4283 // Setup the NPM path
4284 if (args.npmpath == null) {
4285 try {
4286 var xnodepath = process.argv[0];
4287 var xnpmpath = require('path').join(require('path').dirname(process.argv[0]), 'npm');
4288 if (require('fs').existsSync(xnodepath) && require('fs').existsSync(xnpmpath)) {
4289 if (xnodepath.indexOf(' ') >= 0) { xnodepath = '"' + xnodepath + '"'; }
4290 if (xnpmpath.indexOf(' ') >= 0) { xnpmpath = '"' + xnpmpath + '"'; }
4291 if (require('os').platform() == 'win32') { npmpath = xnpmpath; } else { npmpath = (xnodepath + ' ' + xnpmpath); }
4292 }
4293 } catch (ex) { console.log(ex); }
4294 } else {
4295 npmpath = args.npmpath;
4296 }
4297
4298 // Get the server configuration
4299 var config = getConfig(false);
4300 if (config == null) { process.exit(); }
4301
4302 // Lowercase the auth value if present
4303 for (var i in config.domains) { if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); } }
4304
4305 // Get the current node version
4306 const verSplit = process.version.substring(1).split('.');
4307 var nodeVersion = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
4308
4309 // Check if RDP support if present
4310 var mstsc = true;
4311 try { require('./rdp') } catch (ex) { mstsc = false; }
4312
4313 // Check if Windows SSPI, LDAP, Passport and YubiKey OTP will be used
4314 var sspi = false;
4315 var ldap = false;
4316 var passport = [];
4317 var allsspi = true;
4318 var yubikey = false;
4319 var ssh = false;
4320 var sessionRecording = false;
4321 var domainCount = 0;
4322 var wildleek = false;
4323 var nodemailer = false;
4324 var sendgrid = false;
4325 var captcha = false;
4326 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; }
4327 if (domainCount == 0) { allsspi = false; }
4328 for (var i in config.domains) {
4329 if (i.startsWith('_')) continue;
4330 if (((config.domains[i].smtp != null) && (config.domains[i].smtp.name != 'console')) || (config.domains[i].sendmail != null)) { nodemailer = true; }
4331 if (config.domains[i].sendgrid != null) { sendgrid = true; }
4332 if (config.domains[i].yubikey != null) { yubikey = true; }
4333 if (config.domains[i].auth == 'ldap') { ldap = true; }
4334 if (mstsc == false) { config.domains[i].mstsc = false; }
4335 if (config.domains[i].ssh == true) { ssh = true; }
4336 if ((typeof config.domains[i].authstrategies == 'object')) {
4337 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
4338 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.push('passport-twitter@1.0.4'); }
4339 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.push('passport-google-oauth20@2.0.0'); }
4340 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.push('passport-github2@0.1.12'); }
4341 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.push('passport-azure-oauth2@0.1.0'); passport.push('jwt-simple@0.5.6'); }
4342 if (typeof config.domains[i].authstrategies.oidc == 'object') { passport.push('openid-client@5.7.1'); }
4343 if ((typeof config.domains[i].authstrategies.saml == 'object') || (typeof config.domains[i].authstrategies.jumpcloud == 'object')) { passport.push('passport-saml@3.2.4'); }
4344 }
4345 if (config.domains[i].sessionrecording != null) { sessionRecording = true; }
4346 if ((config.domains[i].passwordrequirements != null) && (config.domains[i].passwordrequirements.bancommonpasswords == true)) { wildleek = true; }
4347 if ((config.domains[i].newaccountscaptcha != null) && (config.domains[i].newaccountscaptcha !== false)) { captcha = true; }
4348 if ((typeof config.domains[i].duo2factor == 'object') && (passport.indexOf('@duosecurity/duo_universal') == -1)) { passport.push('@duosecurity/duo_universal@2.1.0'); }
4349 }
4350
4351 // Build the list of required modules
4352 // NOTE: ALL MODULES MUST HAVE A VERSION NUMBER AND THE VERSION MUST MATCH THAT USED IN Dockerfile
4353 var modules = ['archiver@7.0.1', 'cbor@5.2.0', 'compression@1.8.1', 'cookie-session@2.1.1', 'express@4.22.2', 'express-handlebars@7.1.3', 'express-ws@5.0.2', 'ipcheck@0.1.0', 'minimist@1.2.8', 'multiparty@4.3.0', '@seald-io/nedb@4.1.2', 'node-forge@1.4.0', 'ua-parser-js@1.0.40', 'ua-client-hints-js@0.1.2', 'ws@8.21.0', 'yauzl@2.10.0'];
4354 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
4355 if (ldap == true) { modules.push('ldapauth-fork@5.0.5'); }
4356 if (ssh == true) { modules.push('ssh2@1.17.0'); }
4357 if (passport != null) { passport = passport.filter((value, index, self) => self.indexOf(value) === index); modules.push(...passport); }
4358 if (captcha == true) { modules.push('svg-captcha@1.4.0'); }
4359
4360 if (sessionRecording == true) { modules.push('image-size@2.0.2'); } // Need to get the remote desktop JPEG sizes to index the recording file.
4361 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.
4362 if (config.settings.mqtt != null) { modules.push('aedes@0.51.3'); } // Add MQTT Modules
4363 if (config.settings.mysql != null) { modules.push('mysql2@3.15.1'); } // Add MySQL.
4364 //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/)
4365 if (config.settings.mongodb != null) { modules.push('mongodb@4.17.2'); modules.push('@mongodb-js/saslprep@1.3.1')} // Add MongoDB, official driver.
4366 if (config.settings.postgres != null) { modules.push('pg@8.16.3') } // Add Postgres, official driver.
4367 if (config.settings.mariadb != null) { modules.push('mariadb@3.4.5'); } // Add MariaDB, official driver.
4368 if (config.settings.acebase != null) { modules.push('acebase@1.29.5'); } // Add AceBase, official driver.
4369 if (config.settings.sqlite3 != null) { modules.push('sqlite3@5.1.7'); } // Add sqlite3, official driver.
4370 if (config.settings.vault != null) { modules.push('node-vault@0.10.5'); } // Add official HashiCorp's Vault module.
4371 const hasExistingProxy = process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy'];
4372 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
4373 else if (config.settings.xmongodb != null) { modules.push('mongojs@3.1.0'); } // Add MongoJS, old driver.
4374 if (nodemailer || ((config.smtp != null) && (config.smtp.name != 'console')) || (config.sendmail != null)) { modules.push('nodemailer@6.10.1'); } // Add SMTP support
4375 if (sendgrid || (config.sendgrid != null)) { modules.push('@sendgrid/mail@8.1.6'); } // Add SendGrid support
4376 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
4377 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)
4378 if (config.settings.prometheus != null) { modules.push('prom-client@15.1.3'); } // Add Prometheus Metrics support
4379
4380 if (typeof config.settings.autobackup == 'object') {
4381 // Setup encrypted zip support if needed
4382 if (config.settings.autobackup.zippassword) { modules.push('archiver-zip-encrypted@2.0.0'); }
4383 // Enable Google Drive Support
4384 if (typeof config.settings.autobackup.googledrive == 'object') { modules.push('googleapis@128.0.0'); }
4385 // Enable WebDAV Support
4386 if (typeof config.settings.autobackup.webdav == 'object') {
4387 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.9.0'); }
4388 }
4389 // Enable S3 Support
4390 if (typeof config.settings.autobackup.s3 == 'object') { modules.push('minio@8.0.6'); }
4391 }
4392
4393 // Setup common password blocking
4394 if (wildleek == true) { modules.push('wildleek@2.0.0'); }
4395
4396 // Setup 2nd factor authentication
4397 if (config.settings.no2factorauth !== true) {
4398 // Setup YubiKey OTP if configured
4399 if (yubikey == true) { modules.push('yub@0.11.1'); } // Add YubiKey OTP support (replaced yubikeyotp due to form-data issues)
4400 if (allsspi == false) { modules.push('otplib@13.4.1'); } // Google Authenticator support (v10 supports older NodeJS versions).
4401 }
4402
4403 // Desktop multiplexor support
4404 if (config.settings.desktopmultiplex === true) { modules.push('image-size@2.0.2'); }
4405
4406 // SMS support
4407 if (config.sms != null) {
4408 if (config.sms.provider == 'twilio') { modules.push('twilio@4.23.0'); }
4409 if (config.sms.provider == 'plivo') { modules.push('plivo@4.75.1'); }
4410 if (config.sms.provider == 'telnyx') { modules.push('telnyx@1.25.5'); }
4411 }
4412
4413 // Messaging support
4414 if (config.messaging != null) {
4415 if (config.messaging.telegram != null) { modules.push('telegram@2.26.22'); modules.push('input@1.0.1'); }
4416 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); } }
4417 if (config.messaging.xmpp != null) { modules.push('@xmpp/client@0.13.6'); }
4418 if (config.messaging.pushover != null) { modules.push('node-pushover@1.0.0'); }
4419 if (config.messaging.zulip != null) { modules.push('zulip@0.1.0'); }
4420 }
4421
4422 // Setup web based push notifications
4423 if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) { modules.push('web-push@3.6.7'); }
4424
4425 // Firebase Support
4426 if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) { modules.push('firebase-admin@12.7.0'); }
4427
4428 // Syslog support
4429 if ((require('os').platform() != 'win32') && (config.settings.syslog || config.settings.syslogjson)) { modules.push('modern-syslog@1.2.0'); }
4430 if (config.settings.syslogtcp) { modules.push('syslog@0.1.1-1'); }
4431
4432 // Setup heapdump support if needed, useful for memory leak debugging
4433 // https://www.arbazsiddiqui.me/a-practical-guide-to-memory-leaks-in-nodejs/
4434 if (config.settings.heapdump === true) { modules.push('heapdump@0.3.15'); }
4435
4436 // Install any missing modules and launch the server
4437 InstallModules(modules, args, function () {
4438 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; } }
4439 meshserver = CreateMeshCentralServer(config, args);
4440 meshserver.Start();
4441 });
4442
4443 // On exit, also terminate the child process if applicable
4444 process.on('exit', function () { if (childProcess) { childProcess.kill(); childProcess = null; } });
4445
4446 // If our parent exits, we also exit
4447 if (args.launch) {
4448 process.stderr.on('end', function () { process.exit(); });
4449 process.stdout.on('end', function () { process.exit(); });
4450 process.stdin.on('end', function () { process.exit(); });
4451 process.stdin.on('data', function (data) { });
4452 }
4453 });
4454 }
4455
4456 if (require.main === module) {
4457 mainStart(); // Called directly, launch normally.
4458 } else {
4459 module.exports.mainStart = mainStart; // Required as a module, useful for winservice.js
4460 }