| 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 | } |